#include <exception>
#include <stdexcept>
#include <thread>
#include <windows.h>
#include <commctrl.h>
#include <SWI-Prolog.h>

#include "debug.h"
#include "resource.h"
#include "datalistview.h"
#include "episodelistview.h"
#include "layout.h"
#include "pl.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

/* Exit gracefully on uncaught exception. */
static void OnTerminate() noexcept;
static auto UNUSED = std::set_terminate(OnTerminate);

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

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

/* Cursors. */
HCURSOR g_hcArrow = LoadCursor(nullptr, IDC_ARROW);
HCURSOR g_hcSizeNs = LoadCursor(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);
/* Process main menu commands. */
static void HandleMainMenu(HWND, WORD);
/* Wait for thread. */
void WaitFor(void (*f)(bool*));
/* 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();

void OnTerminate() noexcept
{
	ShowException(L"Episode Browser was terminated due to an error: %s", L"Fatal Error", MB_ICONERROR);
	_Exit(1);
}

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

	/* Initialize Prolog. */
	const char* argv[] = {"EpisodeBrowser", nullptr};
	if (!PL_initialise(1, const_cast<char**>(argv)))
		throw std::runtime_error("Could not initialize Prolog.");
	if (!Pl("track_episodes","attach") || !Pl("episode_data","attach"))
		throw std::runtime_error("Could not attach databases.");

	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	 = LoadIcon(nullptr, IDI_APPLICATION);
	wc.hCursor	 = g_hcArrow;
	wc.hbrBackground = reinterpret_cast<HBRUSH>(COLOR_WINDOW);
	wc.lpszMenuName	 = MAKEINTRESOURCE(IDR_MENU);
	wc.lpszClassName = L"Episode Browser";
	wc.hIconSm	 = LoadIcon(nullptr, IDI_APPLICATION);
	Require(RegisterClassEx(&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(CreateWindowEx(
	    0,
	    L"Episode Browser",
	    L"Episode Browser",
	    WS_OVERLAPPEDWINDOW|WS_CLIPCHILDREN,
	    XMAIN, YMAIN, 0, 0,
	    nullptr, nullptr, hInstance, nullptr));

	g_hWndStatus = Require(CreateWindowEx(
	    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. */
	Pl("track_episodes","update_tracked_episodes");
	g_elv->Update();
	g_elv->RestoreFocus();

#ifdef DEBUG
	RunTests();
#endif

	MSG msg;
	while (GetMessage(&msg, nullptr, 0, 0) > 0) {
		if (IsDialogMessage(hWnd, &msg)) continue;
		TranslateMessage(&msg);
		DispatchMessage(&msg);
	}

	PL_halt(0);
	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(SystemParametersInfo(SPI_GETNONCLIENTMETRICS, sizeof(NONCLIENTMETRICS), &m, 0));
		g_hfNormal = Require(CreateFontIndirect(&m.lfMessageFont));
	} else
		g_hfNormal = static_cast<HFONT>(Require(GetStockObject(DEFAULT_GUI_FONT)));

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

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

	/* Load context menu. */
	g_hMenuPopup = Require(LoadMenu(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)
{
	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.limitScreenwriter[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);
			Pl("track_episodes","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_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.limitScreenwriter?
			    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_FORGET*/L"Reset watched/unwatched status.",
			/*IDM_LOOKUP*/L"Fetch episode data from the web, such as date, source and hint.",
			/*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)];
		}
		SendMessage(g_hWndStatus, SB_SETTEXT, MAKEWPARAM(0,0), reinterpret_cast<LPARAM>(tip));
		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(WaitFetchData);
		break;
	    }

	case IDM_FILE_FETCH_SCREENWRITERS:
		//WaitFor("episode_data","update_screenwriters");
		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.limitScreenwriter[0]) { /* Show episodes by all screenwriters. */
			CheckMenuItem(GetMenu(hWnd), IDM_VIEW_OTHERS, MF_CHECKED);
			g_cfg.limitScreenwriter[0] = 0;
		} else { /* Hide episodes by other screenwriters than current. */
			Mark m;
			WcharPtr s;
			LVITEM lvi = {LVIF_PARAM, -1};
			if (g_elv->FindNextItem(&lvi, LVNI_FOCUSED)
			    && Pl("episode_data","episode_datum",lvi.lParam,"Screenwriter",&s)) {
				Wcscpy(g_cfg.limitScreenwriter, s);
				CheckMenuItem(GetMenu(hWnd), IDM_VIEW_OTHERS, MF_UNCHECKED);
			}
		}
		g_elv->Update();
		g_elv->EnsureFocusVisible();
		break;
	}
}

void WaitFor(void (*f)(bool*))
{
	static bool bActive = false;
	static bool bDone = false;
	static UINT_PTR iTimer;

	/* Ensure that only a single thread is waited on. */
	if (bActive) {
		if (EBMessageBox(L"Another task is active. "
		    L"Do you want to cancel the existing task and start a new one?",
		    L"Error", MB_YESNO|MB_ICONWARNING) == IDYES) {
			KillTimer(nullptr, iTimer);
			bActive = false;
			g_elv->Update();
		} else
			return;
	}

	/* The timer procedure animates an ellipsis in the status bar
	 * while the thread is running. */
	static auto proc = [](HWND, UINT, UINT_PTR, DWORD) -> void
	{
		static int i = 0;
		static const wchar_t* text[] = {L".", L"..", L"...", L""};

		if (bDone) {
			KillTimer(nullptr, iTimer);
			i = 0;
			bActive = 0;
			g_elv->Update();
		} else {
			i = (i+1)%(sizeof(text)/sizeof(*text));
			SendMessage(g_hWndStatus, SB_SETTEXT, MAKEWPARAM(1,0),
			    reinterpret_cast<LPARAM>(text[i]));
		}
	};

	/* The waited-on function signals its completion by setting a
	 * shared boolean value to true. */
	bDone = false;
	bActive = true;
	std::thread{f, &bDone}.detach();
	SendMessage(g_hWndStatus, SB_SETTEXT, MAKEWPARAM(1,0), reinterpret_cast<LPARAM>(L"."));
	Prefer(iTimer = SetTimer(nullptr, -1, 500, proc));
}

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);
	}
}