//Disables console window
#if !_DEBUG
#pragma comment(linker, "/SUBSYSTEM:windows /ENTRY:mainCRTStartup")
#endif

// we need commctrl v6 for LoadIconMetric()
// see https://github.com/microsoft/Windows-classic-samples/blob/main/Samples/Win7Samples/winui/shell/appshellintegration/NotificationIcon/NotificationIcon.cpp
#pragma comment(linker,"/manifestdependency:\"type='win32' name='Microsoft.Windows.Common-Controls' version='6.0.0.0' processorArchitecture='*' publicKeyToken='6595b64144ccf1df' language='*'\"")
#pragma comment(lib, "comctl32.lib")

#include <windows.h>

#include <functional>
#include <iostream>
#include <sstream>
#include <shellapi.h>
#include <commctrl.h>
#include <strsafe.h>

#include "Util.h"
#include "AudioApi.h"
#include "Settings.h"
#include "resource.h"
#include "AsuroTool.h"

const size_t MAX_FONT_PATH_LENGTH = 2048;
const UINT TRAY_ID = 420;
const UINT WMAPP_NOTIFYCALLBACK = WM_APP + 1;

// Globals for use in callbacks
DrawData* gDrawData;
ApplicationData* gAppData;
bool justDocked = false;
bool isHidden = false;

int main()
{
	std::wstring appDir;
	getAppDir(appDir);
	if (_wchdir(appDir.c_str()) != 0)
	{
		std::cout << "Failed to set working dir." << std::endl;
	}

	ApplicationData applicationData{};
	ImGuiCallbacks callbacks{};

	callbacks.initFunc = std::bind(init, std::placeholders::_1, std::ref(applicationData));
	callbacks.drawFunc = std::bind(draw, std::placeholders::_1, std::ref(applicationData));
	callbacks.cleanupFunc = std::bind(cleanup, std::placeholders::_1, std::ref(applicationData));

	startImgui(callbacks, "Audio Thingy", 600, 400);
}

void init(DrawData& drawData, ApplicationData& appData)
{
	std::wstring appPath;
	getAppDir(appPath);
	char appPathStr[MAX_FONT_PATH_LENGTH];
	HRESULT convResult = WideCharToMultiByte(CP_UTF8, NULL, &appPath[0], -1, appPathStr, MAX_FONT_PATH_LENGTH, NULL, nullptr);
	isError(convResult, "Failed to convert path: ");

	// sad :(
	gDrawData = &drawData;
	gAppData = &appData;

	// Load text font
	ImGuiIO& io = ImGui::GetIO();
	std::string fontPath = std::string(appPathStr);
	fontPath.append("\\Montserrat-Regular.ttf");
	io.Fonts->AddFontFromFileTTF(fontPath.c_str(), 18.0f);

	// Load icon font
	static const ImWchar icons_ranges[] = { 0xEA01, 0xF2DF, 0 };
	ImFontConfig icons_config;
	icons_config.MergeMode = true;
	icons_config.PixelSnapH = true;
	std::string iconFontPath = std::string(appPathStr);
	iconFontPath.append("\\remixicon.ttf");
	io.Fonts->AddFontFromFileTTF(iconFontPath.c_str(), 14.0f, &icons_config, icons_ranges);

	// Set window icon
	HINSTANCE instance = GetModuleHandle(NULL);
	LPWSTR iconId = MAKEINTRESOURCE(IDI_ICON1);
	HANDLE iconLarge = LoadImageW(instance, iconId, IMAGE_ICON, 64, 64, 0);
	HANDLE iconSmall = LoadImageW(instance, iconId, IMAGE_ICON, 32, 32, 0);
	SendMessage(drawData.window_handle, WM_SETICON, ICON_BIG, (LPARAM)iconLarge);
	SendMessage(drawData.window_handle, WM_SETICON, ICON_SMALL, (LPARAM)iconSmall);
	SendMessage(drawData.window_handle, WM_SETICON, ICON_SMALL2, (LPARAM)iconSmall);

	// Set tray icon
	NOTIFYICONDATA nid = { sizeof(nid) };
	nid.hWnd = drawData.window_handle;
	nid.uFlags = NIF_ICON | NIF_TIP | NIF_MESSAGE | NIF_SHOWTIP;
	nid.uID = TRAY_ID;
	nid.uCallbackMessage = WMAPP_NOTIFYCALLBACK;

	HRESULT iconResult;
	iconResult = LoadIconMetric(instance, iconId, LIM_SMALL, &nid.hIcon);
	isError(iconResult, "Failed to load tray icon image: ");

	iconResult = LoadString(instance, IDS_STRING_TOOLTIP, nid.szTip, ARRAYSIZE(nid.szTip));
	isError(iconResult, "Failed to load tray icon text: ");

	Shell_NotifyIcon(NIM_ADD, &nid);

	nid.uVersion = NOTIFYICON_VERSION_4;
	Shell_NotifyIcon(NIM_SETVERSION, &nid);

	if (!SetWindowsHookEx(WH_CALLWNDPROC, trayIconEventHandler, instance, GetCurrentThreadId()))
	{
		std::cout << "Failed to hook tray icon events: " << std::hex << GetLastError() << std::endl;
	}

	// Set window minimize behavior
	glfwSetWindowIconifyCallback(drawData.window, [](GLFWwindow* window, int isIconified) {
		if (isIconified && gAppData->settings.docked)
		{
			glfwHideWindow(window);
		}
		isHidden = isIconified;
	});

	glfwSetWindowFocusCallback(drawData.window, [](GLFWwindow* window, int isFocused) {
		if (!isFocused && gAppData->settings.docked && !justDocked)
		{
			glfwIconifyWindow(window);
		}
	});

	// Load settings
	initSettings(drawData, appData);

	// Set up audio device api
	initAudio(appData);
}

void draw(DrawData& drawData, ApplicationData& appData)
{
	justDocked = false;

	// Actual Drawing
	if (isHidden)
	{
		glfwWaitEvents();
		return;
	}

	float customYCursor = 0;
	ImVec2 viewportSize = ImGui::GetMainViewport()->Size;

	// Menu Bar
	customYCursor += menuBar(drawData, appData).y;

	// Playback Devices
	ImGui::SetNextWindowPos(ImVec2(0, customYCursor));
	ImGui::SetNextWindowSize(ImVec2(viewportSize.x, 0));
	customYCursor += audioDeviceWindow(appData, appData.audioData->playbackDevices, " \xEE\xB8\x84 Playback").y;

	customYCursor += 5.;

	// Recording devices
	ImGui::SetNextWindowPos(ImVec2(0, customYCursor));
	ImGui::SetNextWindowSize(ImVec2(viewportSize.x, 0));
	customYCursor += audioDeviceWindow(appData, appData.audioData->recordingDevices, " \xEE\xBD\x8F Recording").y;

	// Resize viewport
	drawData.window_size.y = customYCursor;

	if (appData.settings.docked)
	{
		int monitorX, monitorY, monitorW, monitorH;
		glfwGetMonitorWorkarea(glfwGetPrimaryMonitor(), &monitorX, &monitorY, &monitorW, &monitorH);
		glfwSetWindowPos(drawData.window, monitorX + monitorW - drawData.window_size.x, monitorY + monitorH - drawData.window_size.y);
	}
}

void cleanup(DrawData& drawData, ApplicationData& appData)
{
	// Remove tray icon
	NOTIFYICONDATA nid = { sizeof(nid) };
	nid.hWnd = drawData.window_handle;
	nid.uID = TRAY_ID;
	Shell_NotifyIcon(NIM_DELETE, &nid);
}

ImVec2 menuBar(DrawData& drawData, ApplicationData& appData)
{
	ImVec2 size{};
	bool closeMenu = false;

	if (ImGui::BeginMainMenuBar())
	{
		if (ImGui::BeginMenu("Settings"))
		{
			if (ImGui::Checkbox("Docked", &appData.settings.docked))
			{
				closeMenu = true;
				updateDocked(drawData, appData);
			}

			ImGui::Checkbox("Show Disabled Devices", &appData.settings.showDisabledDevices);

			if (ImGui::Checkbox("Autostart", &appData.settings.autostart))
			{
				setAutostart(appData.settings.autostart);
			}

			ImGui::EndMenu();
		}

		if (ImGui::BeginMenu("Debug"))
		{
			if (ImGui::Button("Manual Refresh"))
			{
				reloadDeviceLists(*appData.audioData);
			}
			ImGui::EndMenu();
		}

		if (appData.settings.docked)
		{
			ImVec2 availableSpace = ImGui::GetContentRegionAvail();
			ImVec2 cursorPos = ImGui::GetCursorPos();
			ImGui::SetCursorPosX(cursorPos.x + availableSpace.x - 33.);

			if (ImGui::SmallButton("-"))
			{
				glfwIconifyWindow(drawData.window);
			}
			if (ImGui::SmallButton("x"))
			{
				glfwSetWindowShouldClose(drawData.window, GLFW_TRUE);
			}
		}

		size = ImGui::GetWindowSize();
		ImGui::EndMainMenuBar();
	}

	if (closeMenu)
	{
		ImGui::SetWindowFocus();
	}

	return size;
}

bool customButton(const char* id_start, const char* id_end, const char* title, bool visible)
{
	std::string buttonId(id_start);
	buttonId.append(id_end);

	bool result = false;
	if (visible)
	{
		ImGui::PushID(buttonId.c_str());
		result = ImGui::SmallButton(title);
		ImGui::PopID();
	}
	else
	{
		ImGui::InvisibleButton(buttonId.c_str(), ImGui::CalcTextSize(title));
	}

	return result;
}

ImVec2 audioDeviceWindow(ApplicationData& appData, std::vector<AudioDevice>& deviceList, const char* title)
{
	if (ImGui::Begin(title, 0, ImGuiWindowFlags_NoResize))
	{
		if (ImGui::BeginTable("DeviceTable", 3, ImGuiTableFlags_Resizable | ImGuiTableFlags_Borders | ImGuiTableFlags_NoSavedSettings))
		{
			ImGui::TableSetupColumn("Devices", ImGuiTableColumnFlags_WidthStretch, 3.);
			ImGui::TableSetupColumn("Volume", ImGuiTableColumnFlags_WidthStretch, 1.);
			ImGui::TableSetupColumn("Defaults", ImGuiTableColumnFlags_WidthFixed, 55.);
			ImGui::TableHeadersRow();

			for (auto& dev : deviceList)
			{
				std::string deviceIdUtf8 = utf8Encode(dev.id);

				if (dev.state == DEVICE_STATE_ACTIVE)
				{
					ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1., 1., 1., 1.));
				}
				else if (!appData.settings.showDisabledDevices)
				{
					continue;
				}
				else
				{
					ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(.7, .7, .7, 1.));
				}

				// Device Name
				ImGui::TableNextRow();
				ImGui::TableNextColumn();

				ImGui::Text(dev.name.c_str());

				// Volume
				ImGui::TableNextColumn();
				if (dev.state == DEVICE_STATE_ACTIVE)
				{
					// Log scale because it looks better (no actual reason for these exact numbers)
					float meterValue = log10f(getMeterValue(dev.meterInterface) * 9. + 1.);

					auto drawList = ImGui::GetWindowDrawList();
					ImVec2 windowPos = ImGui::GetWindowPos();
					ImVec2 cursorPos = ImGui::GetCursorScreenPos();
					ImVec2 space = ImGui::GetContentRegionAvail();
					float lineY = cursorPos.y + ImGui::GetTextLineHeight() / 2. + 2.;

					const float linePaddingX = 3.;
					cursorPos.x += linePaddingX;

					drawList->AddLine(ImVec2(cursorPos.x, lineY), ImVec2(cursorPos.x + (space.x - 2. * linePaddingX), lineY), IM_COL32(120, 120, 120, 255), 2.);
					drawList->AddLine(ImVec2(cursorPos.x, lineY), ImVec2(cursorPos.x + (space.x - 2. * linePaddingX) * meterValue, lineY), IM_COL32(200, 200, 255, 255), 3.);

					float volume = getVolume(dev.volumeInterface);
					float prevVolume = volume;
					ImGui::SetNextItemWidth(space.x);
					ImGui::PushID(dev.device);
					ImGui::PushStyleColor(ImGuiCol_FrameBg, ImVec4(0, 0, 0, 0));
					ImGui::SliderFloat("", &volume, 0., 1., "", ImGuiSliderFlags_NoRoundToFormat | ImGuiSliderFlags_AlwaysClamp);
					ImGui::PopStyleColor();
					ImGui::PopID();
					if (prevVolume != volume)
					{
						setVolume(dev.volumeInterface, volume);
					}
				}

				// Defaults
				ImGui::TableNextColumn();
				if (dev.state == DEVICE_STATE_ACTIVE)
				{
					if (dev.isDefaultConsole)
					{
						drawCircle(5, IM_COL32(50, 50, 222, 255));
					}
					if (customButton("bn_d_", deviceIdUtf8.c_str(), "\xEE\xBE\x82", !dev.isDefaultConsole))
					{
						setDefaultAudioDevice(*appData.audioData, dev.id.c_str(), ERole::eConsole);
					}

					ImGui::SameLine();
					if (dev.isDefaultCommunication)
					{
						drawCircle(5, IM_COL32(222, 50, 50, 255));
					}
					if (customButton("bn_c_", deviceIdUtf8.c_str(), "\xEE\xBF\xA9", !dev.isDefaultCommunication))
					{
						setDefaultAudioDevice(*appData.audioData, dev.id.c_str(), ERole::eCommunications);
					}
				}

				ImGui::PopStyleColor();
			}

			ImGui::EndTable();
		}
	}

	ImVec2 size = ImGui::GetWindowSize();

	ImGui::End();
	return size;
}

void drawCircle(float radius, ImU32 color)
{
	ImGui::Dummy(ImVec2(0, 0));
	ImGui::SameLine();
	auto drawList = ImGui::GetWindowDrawList();
	ImVec2 cursorPos = ImGui::GetCursorPos();
	ImVec2 windowPos = ImGui::GetWindowPos();
	drawList->AddCircleFilled(ImVec2(cursorPos.x + windowPos.x, cursorPos.y + windowPos.y + ImGui::GetTextLineHeight() / 2.), radius, color);
}

LRESULT CALLBACK trayIconEventHandler(int code, WPARAM wParam, LPARAM lParam)
{
	if (code >= 0)
	{
		CWPSTRUCT* data = (CWPSTRUCT*)lParam;

		if (data->message == WMAPP_NOTIFYCALLBACK)
		{
			auto id = HIWORD(data->lParam);
			auto trayEvent = LOWORD(data->lParam);

			if (id == TRAY_ID && trayEvent == WM_LBUTTONUP)
			{
				glfwShowWindow(gDrawData->window);
				glfwRestoreWindow(gDrawData->window);
			}
		}
	}

	return CallNextHookEx(NULL, code, wParam, lParam);
}
