#include <exception>
#include <stdexcept>
#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 "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. */

/* 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;

/* View settings. */
int g_bViewWatched = 1;
int g_bViewTVOriginal = 1;
char g_limitScreenwriter[64];

/* 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);
/* Process context menu commands. */
static void HandleContextMenu(HWND, WORD);
/* Call Prolog predicate in other thread, if available. */
static void WaitFor(const char*, const char*);
/* 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();

static void OnTerminate() noexcept
{
	const wchar_t* what = L"an exception";
	WcharPtr why;

	try {
		std::rethrow_exception(std::current_exception());
        } catch (const term_t& t) {
		what = L"a Prolog exception";
		try { why = PlString(t); } catch (...) {}
	} catch (const Win32Error& e) {
		what = L"a Windows error";
		try { why = WcharPtr::Copy(e.What()); } catch (...) {}
	} catch (const std::exception& e) {
		try { why = WcharPtr::FromNarrow(e.what()); } catch (...) {}
	} catch (...) {}

	wchar_t msg[256] = {0};
	if (why)
		Swprintf(msg, L"Episode Browser was terminated due to %s: %s",
		    what, static_cast<wchar_t*>(why));
	else
		Swprintf(msg, L"Episode Browser was terminated due to %s.", what);

	MessageBox(g_hWnd, msg, L"Fatal Error", MB_ICONERROR);
	_Exit(1);
}

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

	/* Exit gracefully on uncaught exception. */
	std::set_terminate(OnTerminate);

	/* 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));

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

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

	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->GetProcAddress<UINT(HWND)>("GetDpiForWindow"))
		g_dpi = GetDpiForWindow(hWnd);

	/* Load normal font. */
	if (auto lib = Library::Maybe(L"User32.dll");
	    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");
	}

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

	/* Get saved view settings. */
	Pl("cfg","get_view_watched",&g_bViewWatched);
	Pl("cfg","get_view_tv_original",&g_bViewTVOriginal);
	{
		Mark m;
		char* s;
		if (Pl("cfg","get_limit_screenwriter",&s))
			Strcpy(g_limitScreenwriter, s);
	}
	{
		int dlvHeight = 0;
		Pl("cfg","get_dlv_height",&dlvHeight);
		g_dlv->SetHeight(dlvHeight);
	}

	/* 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_bViewWatched? MF_CHECKED: MF_UNCHECKED);
		CheckMenuItem(GetMenu(hWnd), IDM_VIEW_TV_ORIGINAL, g_bViewTVOriginal? MF_CHECKED: MF_UNCHECKED);
		CheckMenuItem(GetMenu(hWnd), IDM_VIEW_OTHERS, g_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:
			HandleContextMenu(hWnd, 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_bViewWatched?
			    L"Click to hide watched episodes.":
			    L"Click to show watched episodes."),
			/*IDM_VIEW_TV_ORIGINAL*/(g_bViewTVOriginal?
			    L"Click to hide TV original episodes.":
			    L"Click to show TV original episodes."),
			/*IDM_VIEW_OTHERS*/(g_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("episode_data","update_episode_data");
		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_bViewWatched? MF_UNCHECKED: MF_CHECKED);
		g_bViewWatched = !g_bViewWatched;
		Pl("cfg","set_view_watched",g_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_bViewTVOriginal? MF_UNCHECKED: MF_CHECKED);
		g_bViewTVOriginal = !g_bViewTVOriginal;
		Pl("cfg","set_view_tv_original",g_bViewTVOriginal);
		g_elv->Update();
		g_elv->EnsureFocusVisible();
		break;

	case IDM_VIEW_OTHERS:
		if (g_limitScreenwriter[0]) { /* Show episodes by all screenwriters. */
			CheckMenuItem(GetMenu(hWnd), IDM_VIEW_OTHERS, MF_CHECKED);
			g_limitScreenwriter[0] = 0;
		} else { /* Hide episodes by other screenwriters than current. */
			Mark m;
			char* s;
			LVITEM lvi = {LVIF_PARAM, -1};
			if (g_elv->FindNextItem(&lvi, LVNI_FOCUSED)
			    && Pl("episode_data","episode_datum",lvi.lParam,"Screenwriter",&s)) {
				Strcpy(g_limitScreenwriter, s);
				CheckMenuItem(GetMenu(hWnd), IDM_VIEW_OTHERS, MF_UNCHECKED);
			}
		}
		Pl("cfg","set_limit_screenwriter",g_limitScreenwriter);
		g_elv->Update();
		g_elv->EnsureFocusVisible();
		break;
	}
}

void HandleContextMenu(const HWND, const WORD command)
{
	int cNotFound = 0;

	/* Look through selected items, applying the
	 * selected command to each one. */

	LVITEM lvi = {LVIF_PARAM, -1};
	while (g_elv->FindNextItem(&lvi, LVNI_SELECTED)) {
		if (ID_SUBGROUP(command) == IDG_CTX_RATE) {
			/* Process rate commands. */
			Pl("episode_data","rate_episode",lvi.lParam,ID_RATING(command));
			g_elv->UpdateItem(lvi.iItem, lvi.lParam);
		} else {
			/* Process other commands. */
			switch (command) {
			case IDM_WATCH_LOCALLY:
				if (!Pl("local_episode","open_episode_locally",lvi.lParam))
					cNotFound++;
				break;

			case IDM_WATCH_ONLINE:
				Pl("local_episode","open_episode_online",lvi.lParam);
				break;

			case IDM_TOGGLE:
				Pl("track_episodes","toggle_episode",lvi.lParam);
				break;

			case IDM_FORGET:
				Pl("track_episodes","forget_episode",lvi.lParam);
				Pl("track_episodes","update_tracked_episodes");
				break;

			case IDM_LOOKUP:
				Pl("episode_data","retract_episode",lvi.lParam);
				g_elv->UpdateItem(lvi.iItem, lvi.lParam);
				g_dlv->ShowEpisode(lvi.lParam);
				break;

			case IDM_WIKI:
				Pl("episode_data","open_episode_wiki",lvi.lParam);
				break;
			}
		}
	}

	g_elv->Redraw();

	if (ID_SUBGROUP(command) == IDG_CTX_RATE) {
		/* If ratings changed, the episodes may need to be resorted. */
		g_elv->Sort();
		g_elv->ShowFocus();
	} else if (cNotFound) {
		/* Show warning if local episodes were not found. */
		if (cNotFound == 1)
			EBMessageBox(L"Episode could not be opened locally.", L"Error", MB_ICONWARNING);
		else {
			wchar_t msg[64] = {0};
			Swprintf(msg, L"%d episodes could not be opened locally.", cNotFound);
			EBMessageBox(msg, L"Error", MB_ICONWARNING);
		}
	}
}

void WaitFor(const char* mod, const char* pred)
{
	/* WaitFor uses a thread on the Prolog side to execute a
	 * predicate asynchronously. */

	static WcharPtr activePred;
	static UINT_PTR iTimer;
	static atom_t aThread;

	if (activePred) {
		wchar_t msg[256] = {0};
		Swprintf(msg,
		    L"Another task (%s) is active. "
		    L"Do you want to cancel the existing task and start a new one?",
		    static_cast<wchar_t*>(activePred));
		if (EBMessageBox(msg, L"Error", MB_YESNO|MB_ICONWARNING) != IDYES)
			return;
		KillTimer(nullptr, iTimer);
		activePred = nullptr;
		g_elv->Update();
	}

	/* 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 (Pl("episode_data","thread_running",aThread)) {
			i = (i+1)%(sizeof(text)/sizeof(*text));
			SendMessage(g_hWndStatus, SB_SETTEXT, MAKEWPARAM(1,0),
			    reinterpret_cast<LPARAM>(text[i]));
		} else {
			KillTimer(nullptr, iTimer);
			i = 0;
			activePred = nullptr;
			g_elv->Update();
		}
	};

	Plx(mod,"thread_create",pred,&aThread);
	SendMessage(g_hWndStatus, SB_SETTEXT, MAKEWPARAM(1,0), reinterpret_cast<LPARAM>(L"."));
	if (Prefer(iTimer = SetTimer(nullptr, -1, 500, proc)))
		activePred = WcharPtr::FromNarrow(pred);
}

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:
		switch (LOWORD(wParam)) {
		case IDOK:
			EndDialog(hWnd, IDOK);
			break;
		}
		return TRUE;

	default:
		return FALSE;
	}
}

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