#include <exception>
#include <stdexcept>
#include <windows.h>
#include <commctrl.h>
#include <libxml/xmlversion.h>

#include "debug.h"
#include "resource.h"
#include "datalistview.h"
#include "episodelistview.h"
#include "layout.h"
#include "test.h"
#include "util.h"

#ifdef _DEBUG
#define XMAIN 30
#define YMAIN 30
#else
#define XMAIN CW_USEDEFAULT
#define YMAIN CW_USEDEFAULT
#endif

/* main.cpp defines all global (non-template) variables used in the
 * program. `extern' is used to access them from other files, when
 * need be. */

/* Exit gracefully on uncaught exception. */
static auto _ = SET_TERMINATE;

/* Looked-up constants. */
int g_dpi = 96;

/* Cursors. */
HCURSOR g_hcArrow = LoadCursorW(nullptr, IDC_ARROW);
HCURSOR g_hcSizeNs = LoadCursorW(nullptr, IDC_SIZENS);

/* Fonts. */
HFONT g_hfNormal;
HFONT g_hfBold;

/* Menus. */
HMENU g_hMenuPopup;

/* Windows. */
HWND g_hWndFocus;
HWND g_hWnd;
HWND g_hWndStatus;

/* Child window objects. */
DataListView* g_dlv;
EpisodeListView* g_elv;

/* Layout handlers. */
DlvDragger g_dragDlv;

/* File views. */
FileView<CfgA> g_fvCfg = FileView<CfgA>::Initialized(L"cfg.dat", 1);
CfgA& g_cfg = g_fvCfg.At(0);
FileView<ElvDataA> g_fvElv(L"elvdata.dat", g_cfg.cEp+128u);
FileView<DlvDataA> g_fvDlv(L"dlvdata.dat", g_cfg.cEp+128u);

/* Optional Windows functions. */
BOOL (*IsThemeActive)();
BOOL (*SetWindowTheme)(HWND, const wchar_t*, const wchar_t*);

/* Initialize important global state on parent window creation. */
static void InitializeMainWindow(HWND);
/* Process parent window commands. */
static LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM);
static LRESULT CALLBACK HandleMsg(HWND, UINT, WPARAM, LPARAM);
/* Process main menu commands. */
static void HandleMainMenu(HWND, WORD);
/* Handle messages to Help > About dialog. */
static INT_PTR CALLBACK AboutDlgProc(HWND, UINT, WPARAM, LPARAM);
/* Try to style application according to current Windows theme. */
static void UpdateTheme();

int WINAPI WinMain(
	_In_ const HINSTANCE hInstance,
	_In_opt_ const HINSTANCE,
	_In_ char* const,
	_In_ const int nCmdShow)
{
	setbuf(stdout, nullptr);
	LIBXML_TEST_VERSION;

	INITCOMMONCONTROLSEX icc;
	icc.dwSize = sizeof(icc);
	icc.dwICC = ICC_WIN95_CLASSES;
	Require(InitCommonControlsEx(&icc));

	WNDCLASSEX wc;
	memset(&wc, 0, sizeof(WNDCLASSEX));
	wc.cbSize	 = sizeof(WNDCLASSEX);
	wc.lpfnWndProc	 = WndProc;
	wc.hInstance	 = hInstance;
	wc.hIcon	 = LoadIconW(nullptr, IDI_APPLICATION);
	wc.hCursor	 = g_hcArrow;
	wc.hbrBackground = reinterpret_cast<HBRUSH>(COLOR_WINDOW);
	wc.lpszMenuName	 = MAKEINTRESOURCEW(IDR_MENU);
	wc.lpszClassName = L"Episode Browser";
	wc.hIconSm	 = LoadIconW(nullptr, IDI_APPLICATION);
	Require(RegisterClassExW(&wc));

	/* InitializeMainWindow is called before the first message is
	 * sent to WndProc. This is important, as it initializes
	 * global state on which WndProc relies. */
	WithNextWindow(InitializeMainWindow);
	const HWND hWnd = Require(CreateWindowExW(
	    0,
	    L"Episode Browser",
	    L"Episode Browser",
	    WS_OVERLAPPEDWINDOW|WS_CLIPCHILDREN,
	    XMAIN, YMAIN, 0, 0,
	    nullptr, nullptr, hInstance, nullptr));

	g_hWndStatus = Require(CreateWindowExW(
	    0,
	    STATUSCLASSNAME,
	    nullptr,
	    WS_CHILD|WS_VISIBLE|SBARS_SIZEGRIP,
	    0, 0, 0, 0,
	    hWnd, reinterpret_cast<HMENU>(IDR_STATUS), hInstance, nullptr));

	ShowWindow(hWnd, nCmdShow);

	/* Populate episode list view. */
	/* TODO: Update tracked episodes. */
	g_elv->Update();
	g_elv->RestoreFocus();

#ifdef _DEBUG
	RunTests();
#endif

	MSG msg;
	while (GetMessageW(&msg, nullptr, 0, 0) > 0) {
		if (IsDialogMessageW(hWnd, &msg)) continue;
		TranslateMessage(&msg);
		DispatchMessageW(&msg);
	}

	return 0;
}

void InitializeMainWindow(const HWND hWnd)
{
	/* This code is run ONCE, at the creation of the top-level
	 * window -- before WndProc! This is important, as it
	 * initializes global variables that are used by WndProc. */

	/* Look up DPI. */
	if (auto lib = Library::Maybe(L"User32.dll");
	    auto GetDpiForWindow = lib? lib->GetProcAddress<UINT(HWND)>("GetDpiForWindow"): nullptr)
		g_dpi = GetDpiForWindow(hWnd);

	/* Load normal font. */
	if (auto lib = Library::Maybe(L"User32.dll");
	    lib && lib->GetProcAddress<void>("SystemParametersInfoW")) {
		NONCLIENTMETRICS m = {sizeof(NONCLIENTMETRICS)};
		Require(SystemParametersInfoW(SPI_GETNONCLIENTMETRICS, sizeof(NONCLIENTMETRICS), &m, 0));
		g_hfNormal = Require(CreateFontIndirectW(&m.lfMessageFont));
	} else
		g_hfNormal = static_cast<HFONT>(Require(GetStockObject(DEFAULT_GUI_FONT)));

	/* Load bold font. */
	{
		LOGFONT lf;
		Require(GetObjectW(g_hfNormal, sizeof(LOGFONT), &lf));
		lf.lfWeight = FW_BOLD;
		g_hfBold = Require(CreateFontIndirectW(&lf));
	}

	/* Load theme functions, if available. */
	if (HMODULE hModule = LoadLibraryW(L"uxtheme.dll")) {
		IsThemeActive = (decltype(IsThemeActive))(void*)GetProcAddress(hModule, "IsThemeActive");
		SetWindowTheme = (decltype(SetWindowTheme))(void*)GetProcAddress(hModule, "SetWindowTheme");
	}

	/* Load context menu. */
	g_hMenuPopup = Require(LoadMenuW(nullptr, MAKEINTRESOURCE(IDR_POPUPMENU)));
	g_hMenuPopup = Require(GetSubMenu(g_hMenuPopup, 0));

	/* Create child windows. */
	g_dlv = new DataListView(hWnd);
	g_elv = new EpisodeListView(hWnd);

	/* The global main window handle must only be set AFTER
	 * successful initialization. */
	g_hWnd = hWnd;
}

LRESULT CALLBACK WndProc(const HWND hWnd, const UINT uMsg, const WPARAM wParam, const LPARAM lParam)
{
	try {
		return HandleMsg(hWnd, uMsg, wParam, lParam);
	} catch (...) {
		ShowException(L"The action was cancelled due to an error: %s");
	}
	return DefWindowProc(hWnd, uMsg, wParam, lParam);
}

LRESULT CALLBACK HandleMsg(const HWND hWnd, const UINT uMsg, const WPARAM wParam, const LPARAM lParam)
{
	switch (uMsg) {
	case WM_CREATE:
		UpdateTheme();
		SetWindowPos(hWnd, nullptr, -1, -1, Dpi(510), Dpi(412), SWP_NOZORDER|SWP_NOMOVE|SWP_NOACTIVATE);
		SetFocus(g_elv->hWnd);

		/* Set menu item checkmarks according to saved settings. */
		CheckMenuItem(GetMenu(hWnd), IDM_VIEW_WATCHED, g_cfg.bViewWatched? MF_CHECKED: MF_UNCHECKED);
		CheckMenuItem(GetMenu(hWnd), IDM_VIEW_TV_ORIGINAL, g_cfg.bViewTVOriginal? MF_CHECKED: MF_UNCHECKED);
		CheckMenuItem(GetMenu(hWnd), IDM_VIEW_OTHERS, g_cfg.limitToScreenwriter[0]? MF_UNCHECKED: MF_CHECKED);
		return 0;

	case WM_CLOSE:
		DestroyWindow(hWnd);
		return 0;

	case WM_DESTROY:
		g_elv->SaveFocus();
		PostQuitMessage(0);
		return 0;

	case WM_SIZE:
		SendMessage(g_hWndStatus, WM_SIZE, wParam, lParam);
		UpdateLayout(LOWORD(lParam), HIWORD(lParam));
		return 0;

	case WM_GETMINMAXINFO:
		reinterpret_cast<MINMAXINFO*>(lParam)->ptMinTrackSize.x = Dpi(220);
		reinterpret_cast<MINMAXINFO*>(lParam)->ptMinTrackSize.y = Dpi(220);
		return 0;

	case WM_THEMECHANGED:
		UpdateTheme();
		UpdateLayout();
		return 0;

	case 0x02E0: /* WM_DPICHANGED */

		/* TODO: What does GetSystemMetrics return depending
		 * on the DPI? Do the cached values need to be updated
		 * when the DPI changes? */

		/* Get new DPI. */
		g_dpi = HIWORD(wParam);

		/* Get new window position/size. */
		{
			const RECT* const r = reinterpret_cast<RECT*>(lParam);
			Prefer(SetWindowPos(hWnd, nullptr,
			    r->left, r->top,
			    r->right-r->left,
			    r->bottom-r->top,
			    SWP_NOZORDER|SWP_NOACTIVATE));
			UpdateLayout(r->right-r->left, r->bottom-r->top);
		}
		return 0;

	case WM_ACTIVATE:
		if (wParam == WA_INACTIVE)
			g_hWndFocus = GetFocus();
		else {
			SetFocus(g_hWndFocus);
			/* TODO: Update tracked episodes. */
			g_elv->Redraw();
		}
		return 0;

	case WM_NOTIFY:
		switch (reinterpret_cast<NMHDR*>(lParam)->idFrom) {
		case IDC_EPISODELISTVIEW:
			return g_elv->HandleNotify(lParam);
		}
		return 0;

	case WM_COMMAND:
	    {
		const WORD command = LOWORD(wParam);
		switch (ID_GROUP(command)) {
		case IDG_MENU:
			HandleMainMenu(hWnd, command);
			return 0;
		case IDG_CTX:
			g_elv->HandleContextMenu(command);
			return 0;
		default:
			return 0;
		}
	    }

	case WM_MENUSELECT:
	    {
		/* Look up status bar tip for menu command. The tip
		 * strings are stored in arrays, whose indices
		 * correspond to the IDM_ values (see resource.h). */

		const wchar_t* vTipMenu[] = {
			/*IDM_FILE_EXIT*/L"Close Episode Browser.",
			/*IDM_FILE_REFRESH*/L"Quickly refresh episode list.",
			/*IDM_FILE_FETCH_DATA*/L"Fetch episode data from the web (may take a few seconds).",
			/*IDM_FILE_FETCH_SCREENWRITERS*/L"Fetch screenwriters from the web (may take a minute).",
			/*IDM_FILE_FETCH_CANCEL*/L"Stop fetching data from the web.",
			/*IDM_FILE_ABOUT*/L"Show information about Episode Browser.",
			/*IDM_VIEW_WATCHED*/(g_cfg.bViewWatched?
			    L"Click to hide watched episodes.":
			    L"Click to show watched episodes."),
			/*IDM_VIEW_TV_ORIGINAL*/(g_cfg.bViewTVOriginal?
			    L"Click to hide TV original episodes.":
			    L"Click to show TV original episodes."),
			/*IDM_VIEW_OTHERS*/(g_cfg.limitToScreenwriter?
			    L"Click to hide episodes by other screenwriters.":
			    L"Click to show episodes by other screenwriters.")
		};
		const wchar_t* vTipCtx[] = {
			/*IDM_WATCH_LOCALLY*/L"Open local copy of episode, if available.",
			/*IDM_WATCH_ONLINE*/L"Open episode in the web browser.",
			/*IDM_TOGGLE*/L"Toggle watched/unwatched status.",
			/*IDM_WIKI*/L"Show Detective Conan Wiki entry for episode.",
			/*IDM_RATE0*/L"Remove episode rating.",
			/*IDM_RATE1*/L"Rate episode 1/10.",
			/*IDM_RATE2*/L"Rate episode 2/10.",
			/*IDM_RATE3*/L"Rate episode 3/10.",
			/*IDM_RATE4*/L"Rate episode 4/10.",
			/*IDM_RATE5*/L"Rate episode 5/10.",
			/*IDM_RATE6*/L"Rate episode 6/10.",
			/*IDM_RATE7*/L"Rate episode 7/10.",
			/*IDM_RATE8*/L"Rate episode 8/10.",
			/*IDM_RATE9*/L"Rate episode 9/10.",
			/*IDM_RATE10*/L"Rate episode 10/10."
		};

		const WORD command = LOWORD(wParam);
		const WORD group = ID_GROUP(command);
		const wchar_t* tip = {0};
		if (group) {
			const wchar_t** const vTip = group == IDG_MENU? vTipMenu: vTipCtx;
			tip = vTip[ID_INDEX(command)];
		}
		Status(tip, 0);
		return 0;
	    }

	case WM_LBUTTONDOWN:
		g_dragDlv.HandleLButtonDown();
		return 0;

	case WM_SETCURSOR:
		if (g_dragDlv.HandleSetCursor())
			return 1;
		else {
			/* Use default cursor. */
			if (reinterpret_cast<HWND>(wParam) == hWnd)
				return DefWindowProc(hWnd, uMsg, wParam, lParam);
			else
				return 0;
		}

	default:
		return DefWindowProc(hWnd, uMsg, wParam, lParam);
	}
}

void HandleMainMenu(const HWND hWnd, const WORD command)
{
	switch (command) {
	case IDM_FILE_EXIT:
		PostMessage(hWnd, WM_CLOSE, 0, 0);
		break;

	case IDM_FILE_REFRESH:
		g_elv->Update();
		break;

	case IDM_FILE_FETCH_DATA:
	    {
		WaitFor(FetchData);
		break;
	    }

	case IDM_FILE_FETCH_SCREENWRITERS:
		WaitFor(FetchScreenwriters);
		break;

	case IDM_FILE_FETCH_CANCEL:
		WaitFor(nullptr);
		break;

	case IDM_FILE_ABOUT:
		DialogBox(
		    GetModuleHandle(nullptr),
		    MAKEINTRESOURCE(IDD_ABOUT),
		    hWnd,
		    AboutDlgProc);
		break;

	case IDM_VIEW_WATCHED:
		CheckMenuItem(GetMenu(hWnd), IDM_VIEW_WATCHED, g_cfg.bViewWatched? MF_UNCHECKED: MF_CHECKED);
		g_cfg.bViewWatched = !g_cfg.bViewWatched;
		g_elv->Update();
		g_elv->EnsureFocusVisible();
		/* TODO: Remember last valid focus. In case of
		 * non-existing focus, use the last valid focus. */
		break;

	case IDM_VIEW_TV_ORIGINAL:
		CheckMenuItem(GetMenu(hWnd), IDM_VIEW_TV_ORIGINAL, g_cfg.bViewTVOriginal? MF_UNCHECKED: MF_CHECKED);
		g_cfg.bViewTVOriginal = !g_cfg.bViewTVOriginal;
		g_elv->Update();
		g_elv->EnsureFocusVisible();
		break;

	case IDM_VIEW_OTHERS:
		if (g_cfg.limitToScreenwriter[0]) { /* Show episodes by all screenwriters. */
			CheckMenuItem(GetMenu(hWnd), IDM_VIEW_OTHERS, MF_CHECKED);
			g_cfg.limitToScreenwriter[0] = 0;
		} else { /* Hide episodes by other screenwriters than current. */
			LVITEM lvi = {LVIF_PARAM, -1};
			if (g_elv->FindNextItem(&lvi, LVNI_FOCUSED)
			    && g_fvDlv.At(lvi.lParam-1).screenwriter[0]) {
				Wcscpy(g_cfg.limitToScreenwriter, g_fvDlv.At(lvi.lParam-1).screenwriter);
				CheckMenuItem(GetMenu(hWnd), IDM_VIEW_OTHERS, MF_UNCHECKED);
			}
		}
		g_elv->Update();
		g_elv->EnsureFocusVisible();
		break;
	}
}

INT_PTR CALLBACK AboutDlgProc(const HWND hWnd, const UINT uMsg, const WPARAM wParam, const LPARAM)
{
	switch (uMsg) {
	case WM_CLOSE:
		EndDialog(hWnd, IDOK);
		return TRUE;

	case WM_COMMAND:
		if (LOWORD(wParam) == IDOK)
			EndDialog(hWnd, IDOK);
		return TRUE;

	default:
		return FALSE;
	}
}

void UpdateTheme()
{
	if (IsThemeActive) {
		const bool bThemeActive = IsThemeActive();
		g_dlv->UpdateTheme(bThemeActive);
		g_elv->UpdateTheme(bThemeActive);
	}
}