#include <exception>
#include <sstream>
#include <windows.h>
#include <commctrl.h>
#include <uxtheme.h>
#include <SWI-Prolog.h>

#include "resource.h"
#include "common.h"
#include "datalistview.h"
#include "episodelistview.h"
#include "main.h"
#include "pl.h"

/* Application state. */
atom_t g_aThread;
int g_bThread;

/* Looked-up constants. */
int g_bThemes;
int g_cxVScroll;
int g_iDPI = 96;

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

/* Menus. */
HMENU g_hPopupMenu;

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

/* Child window objects. */
DataListView* g_pDlv;
EpisodeListView* g_pElv;

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

static LRESULT CALLBACK CBTProc(int, WPARAM, LPARAM);
static LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM);
static INT_PTR CALLBACK AboutDlgProc(HWND, UINT, WPARAM, LPARAM);
static void UpdateTheme();

template <typename T>
void TerminateMsg(const T* sz1, const T* sz2) noexcept
{
	std::basic_ostringstream<T> ss;
	ss << AWTEXT(T, "Episode Browser was terminated due to ");
	ss << sz1;
	if (sz2)
		ss << ": " << sz2;
	else
		ss << ".";
	AWFUN(T, MessageBox)((HWND)NULL, ss.str().c_str(), AWTEXT(T, "Fatal Error"), MB_ICONERROR);
}

void OnTerminate() noexcept
{
	try {
		std::rethrow_exception(std::current_exception());
        } catch (const term_t& t) {
		TCHAR* tsz;
		if (PL_get_tchars(t, &tsz, CVT_WRITE))
			TerminateMsg<TCHAR>(TEXT("a Prolog exception"), tsz);
		else
			TerminateMsg<char>("a Prolog exception", NULL);
	} catch (Win32Error& e) {
		TerminateMsg<TCHAR>(TEXT("a Windows error"), e.what<TCHAR>());
	} catch (std::exception& e) {
		TerminateMsg<char>("an exception", e.what());
	} catch (...) {
		TerminateMsg<char>("an exception", NULL);
	}
	_Exit(1);
}

int WINAPI WinMain(const HINSTANCE hInstance, const HINSTANCE, char* const, const int nCmdShow)
{
	/* Exit gracefully on uncaught exception. */
	std::set_terminate(OnTerminate);

	/* Initialize Prolog. */
	char* argv[] = { (char*)"EpisodeBrowser", NULL };
	if (!PL_initialise(1, 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.");

	/* Initialize common controls, load menu and register window class. */
	INITCOMMONCONTROLSEX icc;
	icc.dwSize = sizeof(icc);
	icc.dwICC = ICC_WIN95_CLASSES;
	require(InitCommonControlsEx(&icc));

	g_hPopupMenu = require(LoadMenu((HINSTANCE)NULL, MAKEINTRESOURCE(IDR_POPUPMENU)));
	g_hPopupMenu = require(GetSubMenu(g_hPopupMenu, 0));

	WNDCLASSEX wc;
	memset(&wc, 0, sizeof(WNDCLASSEX));
	wc.cbSize	 = sizeof(WNDCLASSEX);
	wc.lpfnWndProc	 = WndProc;
	wc.hInstance	 = hInstance;
	wc.hIcon	 = LoadIcon(NULL, IDI_APPLICATION);
	wc.hCursor	 = LoadCursor(NULL, IDC_ARROW);
	wc.hbrBackground = (HBRUSH)(COLOR_WINDOW+1);
	wc.lpszMenuName	 = MAKEINTRESOURCE(IDR_MENU);
	wc.lpszClassName = TEXT("Episode Browser");
	wc.hIconSm	 = LoadIcon(NULL, IDI_APPLICATION);
	require(RegisterClassEx(&wc));

	/* Create window. A CBT hook is used to initialize important
	 * global variables before any messages are sent to the new
	 * window. It is vital that the hook is set up correctly. */
	const HHOOK hHook = require(SetWindowsHookEx(WH_CBT, CBTProc,
	    (HINSTANCE)NULL, GetCurrentThreadId()));
	const HWND hWnd = require(CreateWindowEx(
	    0,
	    TEXT("Episode Browser"),
	    TEXT("Episode Browser"),
	    WS_OVERLAPPEDWINDOW,
	    CW_USEDEFAULT, CW_USEDEFAULT, 0, 0,
	    (HWND)NULL, (HMENU)NULL, hInstance, (void*)NULL));
	require(UnhookWindowsHookEx(hHook));

	g_hWndStatus = require(CreateWindowEx(
	    0,
	    STATUSCLASSNAME,
	    (const TCHAR*)NULL,
	    WS_CHILD|WS_VISIBLE|SBARS_SIZEGRIP,
	    0, 0, 0, 0,
	    hWnd, (HMENU)ID_STATUS, hInstance, (void*)NULL));

	ShowWindow(hWnd, nCmdShow);

	/* Populate episode list view. */
	Pl("track_episodes","update_tracked_episodes");
	g_pElv->Update();
	g_pElv->RestoreFocus();

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

	PL_halt(0);
	return 0;
}

static LRESULT CALLBACK CBTProc(const int nCode, const WPARAM wParam, const LPARAM lParam)
{
	if (nCode < 0 || nCode != HCBT_CREATEWND || g_hWnd)
		return CallNextHookEx(0, nCode, wParam, lParam);

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

	g_hWnd = (HWND)wParam;

	/* Look up constants. */
	g_cxVScroll = GetSystemMetrics(SM_CXVSCROLL);

	if (auto opLib = maybe_make<Library>(TEXT("User32.dll")))
		if (auto GetDpiForWindow = opLib->GetProcAddress<UINT(HWND)>("GetDpiForWindow"))
			g_iDPI = GetDpiForWindow(g_hWnd);

	if (auto opLib = maybe_make<Library>(TEXT("uxtheme.dll")))
		if (opLib->GetProcAddress<void>("SetWindowTheme"))
			g_bThemes = 1;

	if (auto opLib = maybe_make<Library>(TEXT("User32.dll"))) {
		if (opLib->GetProcAddress<void>("SystemParametersInfo" WA)) {
			NONCLIENTMETRICS m;
			m.cbSize = 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)));

	LOGFONT lf;
	require(GetObject(g_hfNormal, sizeof(LOGFONT), &lf));
	lf.lfWeight = FW_BOLD;
	g_hfBold = require(CreateFontIndirect(&lf));

	/* Create child windows. */
	g_pDlv = new DataListView(g_hWnd);
	g_pElv = new EpisodeListView(g_hWnd);

	/* Get saved view settings. */
	char* sz;
	Pl("cfg","get_view_watched",&g_bViewWatched);
	Pl("cfg","get_view_tv_original",&g_bViewTVOriginal);
	if (Pl("cfg","get_limit_screenwriter",&sz))
		strcpy_s(g_szLimitScreenwriter, sizeof(g_szLimitScreenwriter), sz);
	return 0;
}

LRESULT CALLBACK WndProc(const HWND hWnd, const UINT uMsg, const WPARAM wParam, LPARAM lParam)
{
	switch (uMsg) {
	case WM_CREATE:
		UpdateTheme();
		SetWindowPos(hWnd, NULL, -1, -1, Dpi(510), Dpi(400), SWP_NOZORDER|SWP_NOMOVE|SWP_NOACTIVATE);
		SetFocus(g_pElv->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_szLimitScreenwriter[0]? MF_UNCHECKED: MF_CHECKED);
		break;
	case WM_CLOSE:
		DestroyWindow(hWnd);
		break;
	case WM_DESTROY:
		g_pElv->SaveFocus();
		PostQuitMessage(0);
		break;
	case WM_SIZE:
		SendMessage(g_hWndStatus, WM_SIZE, wParam, lParam);
		UpdateLayout();
		break;
	case WM_GETMINMAXINFO:
	    {
		MINMAXINFO* const pMMI = (MINMAXINFO*)lParam;
		pMMI->ptMinTrackSize.x = Dpi(220);
		pMMI->ptMinTrackSize.y = Dpi(220);
		break;
	    }
	case WM_THEMECHANGED:
		UpdateTheme();
		break;
	case 0x02E0: /* WM_DPICHANGED */
	    {
		const RECT* const lpr = (RECT*)lParam;
		g_iDPI = HIWORD(wParam);
		prefer(SetWindowPos(hWnd, (HWND)NULL,
		    lpr->left, lpr->top,
		    lpr->right-lpr->left,
		    lpr->bottom-lpr->top,
		    SWP_NOZORDER|SWP_NOACTIVATE));
		UpdateLayout();
		break;
	    }
	case WM_ACTIVATE:
		switch (wParam) {
		case WA_INACTIVE:
			g_hFocus = GetFocus();
			break;
		case WA_ACTIVE:
		case WA_CLICKACTIVE:
			SetFocus(g_hFocus);
			Pl("track_episodes","update_tracked_episodes");
			g_pElv->Redraw();
		}
		break;
	case WM_NOTIFY:
		switch (((NMHDR*)lParam)->idFrom) {
		case IDC_EPISODELISTVIEW:
			return g_pElv->HandleNotify(lParam);
		}
		break;
	case WM_TIMER:
		switch (wParam) {
		case IDT_TIMER:
		    {
			static int i = 0;

			/* Animate ellipsis in status bar while doing
			 * work in other thread. */
			if (Pl("episode_data","thread_running",g_aThread)) {
				i = (i+1)%4;
				SendMessage(g_hWndStatus, SB_SETTEXT, MAKEWPARAM(1,0),
				    (LPARAM)(i==0? TEXT("."):
					i==1? TEXT(".."):
					i==2? TEXT("..."):
					TEXT("")));
			} else {
				i = 0;
				g_bThread = 0;
				prefer(KillTimer(hWnd, IDT_TIMER));
				g_pElv->Update();
			}
			break;
		    }
		}
		break;
	case WM_COMMAND:
		switch (LOWORD(wParam)) {
		case IDM_FILE_EXIT:
			PostMessage(hWnd, WM_CLOSE, 0, 0);
			break;
		case IDM_FILE_REFRESH:
			g_pElv->Update();
			break;
		case IDM_FILE_FETCH_DATA:
			if (g_bThread) break;
			Pl("episode_data","thread_create","update_episode_data",&g_aThread);
			goto t;
		case IDM_FILE_FETCH_SCREENWRITERS:
			if (g_bThread) break;
			Pl("episode_data","thread_create","update_screenwriters",&g_aThread);
		t:	KillTimer(hWnd, IDT_TIMER);
			if (!prefer(SetTimer(hWnd, IDT_TIMER, 500, (TIMERPROC)NULL))) break;
			SendMessage(g_hWndStatus, SB_SETTEXT, MAKEWPARAM(1,0), (LPARAM)TEXT("."));
			g_bThread = 1;
			break;
		case IDM_FILE_ABOUT:
			DialogBox(
			    GetModuleHandle(NULL),
			    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_pElv->Update();
			g_pElv->EnsureFocusVisible();
			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_pElv->Update();
			g_pElv->EnsureFocusVisible();
			break;
		case IDM_VIEW_OTHERS:	/* Show/hide other screenwriters. */
			if (g_szLimitScreenwriter[0]) {
				CheckMenuItem(GetMenu(hWnd), IDM_VIEW_OTHERS,
				    MF_CHECKED);
				g_szLimitScreenwriter[0] = 0;
			} else {
				const int iEpFocus = ListView_GetNextItem(g_pElv->hWnd, -1, LVNI_FOCUSED);
				if (iEpFocus == -1) break;

				LVITEM lvi;
				lvi.iItem = iEpFocus;
				lvi.mask = LVIF_PARAM;
				if (!ListView_GetItem(g_pElv->hWnd, &lvi)) break;

				char* sz;
				if (!Pl("episode_data","episode_datum",lvi.lParam,"Screenwriter",&sz))
					break;
				strcpy_s(g_szLimitScreenwriter,
				    sizeof(g_szLimitScreenwriter), sz);
				CheckMenuItem(GetMenu(hWnd), IDM_VIEW_OTHERS,
				    MF_UNCHECKED);
			}
			Pl("cfg","set_limit_screenwriter",g_szLimitScreenwriter);
			g_pElv->Update();
			g_pElv->EnsureFocusVisible();
			break;
		case IDM_WATCH_LOCALLY:
		case IDM_WATCH_ONLINE:
		case IDM_TOGGLE:
		case IDM_FORGET:
		case IDM_LOOKUP:
		case IDM_WIKI:
		case IDM_RATE10:
		case IDM_RATE9:
		case IDM_RATE8:
		case IDM_RATE7:
		case IDM_RATE6:
		case IDM_RATE5:
		case IDM_RATE4:
		case IDM_RATE3:
		case IDM_RATE2:
		case IDM_RATE1:
		case IDM_RATE0:
		    {
			int iRating;
			LVITEM lvi;

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

			lvi.mask = LVIF_PARAM;
			lvi.iItem = -1;
			while ((lvi.iItem = ListView_GetNextItem(
				    g_pElv->hWnd, lvi.iItem, LVNI_SELECTED)) != -1) {
				if (!ListView_GetItem(g_pElv->hWnd, &lvi)) goto b;

				switch (LOWORD(wParam)) {
				case IDM_WATCH_LOCALLY:
					if (!Pl("local_episode","open_episode_locally",lvi.lParam))
						EBMessageBox(TEXT("Local episode could not be opened."),
						    TEXT("Error"), MB_ICONWARNING);
					break;
				case IDM_WATCH_ONLINE:
					Pl("local_episode","open_episode_online",lvi.lParam);
					break;
				case IDM_TOGGLE:
					Pl("track_episodes","toggle_episode",lvi.lParam);
					g_pElv->Redraw();
					break;
				case IDM_FORGET:
					Pl("track_episodes","forget_episode",lvi.lParam);
					Pl("track_episodes","update_tracked_episodes");
					g_pElv->Redraw();
					break;
				case IDM_LOOKUP:
					Pl("episode_data","retract_episode",lvi.lParam);
					g_pElv->UpdateItem(&lvi);
					g_pElv->Redraw();
					g_pDlv->ShowEpisode(lvi.lParam);
					break;
				case IDM_WIKI:
					Pl("episode_data","open_episode_wiki",lvi.lParam);
					break;
				case IDM_RATE10:
					iRating = 10;
					goto r;
				case IDM_RATE9:
					iRating = 9;
					goto r;
				case IDM_RATE8:
					iRating = 8;
					goto r;
				case IDM_RATE7:
					iRating = 7;
					goto r;
				case IDM_RATE6:
					iRating = 6;
					goto r;
				case IDM_RATE5:
					iRating = 5;
					goto r;
				case IDM_RATE4:
					iRating = 4;
					goto r;
				case IDM_RATE3:
					iRating = 3;
					goto r;
				case IDM_RATE2:
					iRating = 2;
					goto r;
				case IDM_RATE1:
					iRating = 1;
					goto r;
				case IDM_RATE0:
					iRating = 0;
				r:	Pl("episode_data","rate_episode",lvi.lParam,iRating);
					g_pElv->UpdateItem(&lvi);
					break;
				}
			}
			switch (LOWORD(wParam)) {
			case IDM_RATE10:
			case IDM_RATE9:
			case IDM_RATE8:
			case IDM_RATE7:
			case IDM_RATE6:
			case IDM_RATE5:
			case IDM_RATE4:
			case IDM_RATE3:
			case IDM_RATE2:
			case IDM_RATE1:
			case IDM_RATE0:
				g_pElv->Redraw();
				g_pElv->Sort();
				g_pElv->ShowFocus();
			}
		    b:	break;
		    }
		}
		break;
	default:
		return DefWindowProc(hWnd, uMsg, wParam, lParam);
	}

	return 0;
}

/* Handle messages to Help > About dialog. */
INT_PTR CALLBACK AboutDlgProc(const HWND hWnd, const UINT uMsg, const WPARAM wParam, const LPARAM)
{
	switch (uMsg) {
	case WM_CLOSE:
		EndDialog(hWnd, IDOK);
		break;
	case WM_COMMAND:
		switch (LOWORD(wParam)) {
		case IDOK:
			EndDialog(hWnd, IDOK);
			break;
		}
		break;
	default:
		return FALSE;
	}

	return TRUE;
}

void UpdateLayout()
{
	if (!g_hWndStatus) return;

	RECT rc, rrStatus;
	require(GetClientRect(g_hWnd, &rc));
	require(GetRelativeRect(g_hWndStatus, &rrStatus));

	/* Resize list views. */
	SendMessage(g_pDlv->hWnd, WM_SETREDRAW, FALSE, 0);
	SendMessage(g_pElv->hWnd, WM_SETREDRAW, FALSE, 0);

	const long cyDlv = rrStatus.top-g_pDlv->Height();
	require(SetWindowRect(g_pDlv->hWnd, 0, cyDlv, rc.right, rrStatus.top));
	require(SetWindowRect(g_pElv->hWnd, 0, 0, rc.right, cyDlv+IsThemeActive()));
	g_pDlv->ResizeColumns(rc);
	g_pElv->ResizeColumns(rc);

	SendMessage(g_pElv->hWnd, WM_SETREDRAW, TRUE, 0);
	SendMessage(g_pDlv->hWnd, WM_SETREDRAW, TRUE, 0);

	/* Resize status bar parts. */
	const int aParts[] = {rc.right-Dpi(55), rc.right};
	SendMessage(g_hWndStatus, SB_SETPARTS, (WPARAM)sizeof(aParts), (LPARAM)aParts);
}

/* Try to style application according to current Windows theme. */
void UpdateTheme()
{
	if (!g_bThemes) return;
	const BOOL bThemeActive = IsThemeActive();
	g_pDlv->UpdateTheme(bThemeActive);
	g_pElv->UpdateTheme(bThemeActive);
}