#include <vector>
#include <windows.h>
#include <commctrl.h>
#include <SWI-Prolog.h>

#include "resource.h"
#include "data.h"
#include "datalistview.h"
#include "episodelistview.h"
#include "listview.h"
#include "pl.h"
#include "util.h"
#include "win.h"

extern CfgA& g_cfg;
extern FileView<ElvDataA> g_fvElv;

EpisodeListView::EpisodeListView(const HWND hWndParent)
	: ListView(hWndParent, reinterpret_cast<HMENU>(IDC_EPISODELISTVIEW), 0)
{
	LVCOLUMN lvc;

	lvc.mask = LVCF_WIDTH|LVCF_TEXT|LVCF_SUBITEM;
	lvc.iSubItem = ELVSIEPISODE;
	lvc.pszText = const_cast<wchar_t*>(L"#");
	lvc.cx = Dpi(42);
	ListView_InsertColumn(hWnd, ELVSIEPISODE, &lvc);

	lvc.iSubItem = ELVSITITLE;
	lvc.pszText = const_cast<wchar_t*>(L"Title");
	lvc.cx = 500;
	ListView_InsertColumn(hWnd, ELVSITITLE, &lvc);

	lvc.iSubItem = ELVSIRATING;
	lvc.pszText = const_cast<wchar_t*>(L"/");
	lvc.cx = Dpi(30);
	ListView_InsertColumn(hWnd, ELVSIRATING, &lvc);

	/* Get saved sort-by-column setting. */
	m_iSortCol = g_cfg.iSortCol;
}

void EpisodeListView::EnsureFocusVisible()
{
	const int iEpFocus = ListView_GetNextItem(hWnd, -1, LVNI_FOCUSED);
	if (iEpFocus != -1)
		ListView_EnsureVisible(hWnd, iEpFocus, TRUE);
}

void EpisodeListView::HandleContextMenu(const WORD command)
{
	int cNotFound = 0;

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

	LVITEM lvi = {LVIF_PARAM, -1};
	while (FindNextItem(&lvi, LVNI_SELECTED)) {
		ElvDataA& e = g_fvElv.At(lvi.lParam-1);

		if (ID_SUBGROUP(command) == IDG_CTX_RATE) {
			/* Process rate commands. */
			if (e.rating = ID_RATING(command))
				Swprintf(e.sRating, L"%d", e.rating);
			else
				e.sRating[0] = 0;
		} 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:
				e.bWatched = !e.bWatched;
				break;

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

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

	Redraw();

	if (ID_SUBGROUP(command) == IDG_CTX_RATE) {
		/* If ratings changed, the episodes may need to be resorted. */
		Sort();
		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);
		}
	}
}

LRESULT EpisodeListView::HandleNotify(const LPARAM lParam)
{
	const NMLISTVIEW* const nm = reinterpret_cast<NMLISTVIEW*>(lParam);

	switch (nm->hdr.code) {
	case LVN_GETDISPINFO:	/* Display item. */
	    {
		NMLVDISPINFO* const nm = reinterpret_cast<NMLVDISPINFO*>(lParam);
		ElvDataA& e = g_fvElv.At(nm->item.lParam-1);
		wchar_t* vs[] = {e.siEp, e.title, e.sRating}; /* ELVSIEPISODE, ELVSITITLE, ELVSIRATING */
		nm->item.pszText = vs[nm->item.iSubItem];
		return 0;
	    }

	case LVN_ITEMCHANGED:	/* Select/focus episode. */
		if ((nm->uChanged & LVIF_STATE) && (nm->uNewState & LVIS_FOCUSED)) {
			extern DataListView* const g_dlv;
			g_dlv->ShowEpisode(nm->lParam);
		}
		return 0;

	case LVN_COLUMNCLICK:	/* Sort by column. */
	    {
		const int iCol = nm->iSubItem+1;

		/* The sign of m_iSortCol decides the sort order. */
		m_iSortCol = abs(m_iSortCol) == iCol? -m_iSortCol: iCol;

		g_cfg.iSortCol = m_iSortCol;
		Sort();
		ShowFocus();
		return 0;
	    }

	case LVN_KEYDOWN:	/* Navigate episodes by keyboard. */
	    {
		const NMLVKEYDOWN *const nm = reinterpret_cast<NMLVKEYDOWN*>(lParam);
		switch (nm->wVKey) {
		case VK_LEFT:
			SelectUnwatched(-1);
			break;
		case VK_RIGHT:
			SelectUnwatched(1);
			break;
		}
		return 0;
	    }

	case NM_CUSTOMDRAW:	/* Make unwatched episodes bold. */
	    {
		const NMLVCUSTOMDRAW* const nm = reinterpret_cast<NMLVCUSTOMDRAW*>(lParam);
		switch (nm->nmcd.dwDrawStage) {
		case CDDS_PREPAINT:
			return CDRF_NOTIFYITEMDRAW;
			break;
		case CDDS_ITEMPREPAINT:
		    {
			const ElvDataA& e = g_fvElv.At(nm->nmcd.lItemlParam-1);
			if (!e.bWatched) {
				extern HFONT g_hfBold;
				Require(SelectObject(nm->nmcd.hdc, g_hfBold));
				return CDRF_NEWFONT;
			}
			break;
		    }
		}
		return 0;
	    }

	case NM_DBLCLK:		/* Open clicked episode. */
	    {
		LVITEM lvi = {LVIF_PARAM, -1};
		if (FindNextItem(&lvi, LVNI_FOCUSED))
			Pl("local_episodes","open_episode_locally",lvi.lParam)
			|| Pl("local_episodes","open_episode_online",lvi.lParam);
		return 0;
	    }

	case NM_RETURN:		/* Open all selected episodes. */
	    {
		LVITEM lvi = {LVIF_PARAM, -1};
		while (FindNextItem(&lvi, LVNI_SELECTED))
			Pl("local_episodes","open_episode_locally",lvi.lParam)
			|| Pl("local_episodes","open_episode_online",lvi.lParam);
		return 0;
	    }

	case NM_RCLICK:
	    {
		extern HMENU g_hMenuPopup;
		const DWORD pos = GetMessagePos();
		Require(TrackPopupMenu(g_hMenuPopup, TPM_RIGHTBUTTON,
		    LOWORD(pos), HIWORD(pos), 0, m_hWndParent, nullptr));
		return 0;
	    }

	default:
		return 0;
	}
}

void EpisodeListView::Redraw()
{
	RedrawWindow(hWnd, nullptr, nullptr,
	    RDW_ERASE|RDW_FRAME|RDW_INVALIDATE|RDW_ALLCHILDREN);
}

void EpisodeListView::ResizeColumns(int w)
{
	ListView_SetColumnWidth(hWnd, ELVSIEPISODE, LVSCW_AUTOSIZE);
	int cx = ListView_GetColumnWidth(hWnd, ELVSIEPISODE)+Dpi(4);
	ListView_SetColumnWidth(hWnd, ELVSIEPISODE, cx);
	cx += ListView_GetColumnWidth(hWnd, ELVSIRATING);
	ListView_SetColumnWidth(hWnd, ELVSITITLE, w-cx-Metric<SM_CXVSCROLL>-Dpi(4));
}

/* Select previously focused episode. */
void EpisodeListView::RestoreFocus()
{
	int i, iEpisode, iItem;
	LVFINDINFO lvfi;
	extern DataListView* const g_dlv;

	iItem = 0;
	iEpisode = g_cfg.iFocus;

	lvfi.flags = LVFI_PARAM;
	lvfi.lParam = iEpisode;
	i = 0;
	while ((iItem = ListView_FindItem(hWnd, -1, &lvfi)) == -1 && i++ < 100)
		lvfi.lParam = ++iEpisode;
	if (iItem != -1) goto s;

	iEpisode -= 100;
	lvfi.lParam = iEpisode;
	i = 0;
	while ((iItem = ListView_FindItem(hWnd, -1, &lvfi)) == -1 && i++ < 100)
		lvfi.lParam = --iEpisode;
	if (iItem != -1) goto s;
	return;

s:	g_dlv->ShowEpisode(iEpisode);
	ListView_SetItemState(hWnd, -1, LVIF_STATE, LVIS_SELECTED);
	SetTop(iItem > 5? iItem-5: 0);
	ListView_SetItemState(hWnd, iItem,
	    LVIS_SELECTED|LVIS_FOCUSED, LVIS_SELECTED|LVIS_FOCUSED);
}

void EpisodeListView::SaveFocus()
{
	LVITEM lvi = {LVIF_PARAM, -1};
	if (FindNextItem(&lvi, LVNI_FOCUSED))
		g_cfg.iFocus = lvi.lParam;
}

void EpisodeListView::SetTop(const int iItem)
{
	const int iItemLast = ListView_GetItemCount(hWnd)-1;
	ListView_EnsureVisible(hWnd, iItemLast, TRUE);
	ListView_EnsureVisible(hWnd, iItem, TRUE);
}

void EpisodeListView::SelectUnwatched(int dir)
{
	/* Get focused episode. */
	LVITEM lviFocus = {LVIF_PARAM, -1};
	if (!FindNextItem(&lviFocus, LVNI_FOCUSED))
		return;

	LVFINDINFO lvfi;
	int i, iEpNew, iItemNew;
	i = 0;
	lvfi.flags = LVFI_PARAM;
	lvfi.lParam = lviFocus.lParam;

	do {
		if (!Pl("track_episodes",dir > 0? "next_unwatched": "previous_unwatched",
			lvfi.lParam,&iEpNew))
			return;

		lvfi.lParam = iEpNew;
		if ((iItemNew = ListView_FindItem(hWnd, -1, &lvfi)) != -1) {
			ListView_SetItemState(hWnd,-1,LVIF_STATE,LVIS_SELECTED);
			ListView_SetSelectionMark(hWnd, iItemNew);
			ListView_SetItemState(hWnd, iItemNew,
			    LVIS_SELECTED|LVIS_FOCUSED,
			    LVIS_SELECTED|LVIS_FOCUSED);
			Redraw();
			ListView_EnsureVisible(hWnd, iItemNew, TRUE);
			return;
		}
	} while (i++ < 1000);
}

void EpisodeListView::ShowFocus()
{
	const int iEpFocus = ListView_GetNextItem(hWnd, -1, LVNI_FOCUSED);
	if (iEpFocus == -1) return;
	ListView_EnsureVisible(hWnd, iEpFocus, TRUE);
}

void EpisodeListView::Sort()
{
	ListView_SortItemsEx(hWnd, EpisodeListView::SortProc, reinterpret_cast<LPARAM>(this));
}

int CALLBACK EpisodeListView::SortProc(const LPARAM iItem1, const LPARAM iItem2, const LPARAM extra)
{
	EpisodeListView* const elv = reinterpret_cast<EpisodeListView*>(extra);

	LVITEM lvi1 = {LVIF_PARAM, static_cast<int>(iItem1)};
	if (!ListView_GetItem(elv->hWnd, &lvi1)) return 0;

	LVITEM lvi2 = {LVIF_PARAM, static_cast<int>(iItem2)};
	if (!ListView_GetItem(elv->hWnd, &lvi2)) return 0;

	/* abs(m_iSortCol) is the 1-based index of the column to sort by.
	 * If m_iSortCol is negative, the order is descending. */
	const int order = Cmp(elv->m_iSortCol, 0);

	const ElvDataA& e1 = g_fvElv[lvi1.lParam-1];
	const ElvDataA& e2 = g_fvElv.At(lvi2.lParam-1);

	switch (abs(elv->m_iSortCol)-1) {
	case ELVSIEPISODE:
		return order*Cmp(lvi1.lParam, lvi2.lParam);
	case ELVSIRATING:
	    {
		int rating1 = e1.rating;
		int rating2 = e2.rating;
		if (!rating1)
			rating1 = elv->m_iSortCol > 0? 99: -1;
		if (!rating2)
			rating2 = elv->m_iSortCol > 0? 99: -1;
		if (rating1 == rating2)
			return Cmp(lvi1.lParam, lvi2.lParam);
		return order*Cmp(rating1, rating2);
	    }
	case ELVSITITLE:
		return order*_wcsicmp(e1.title, e2.title);
	default:
		return 0;
	}
}

void EpisodeListView::Update()
{
	if (!Pl("episode_data","ensure_episode_data"))
		return;

	/* Save scrolling position. */
	int iEpTop = 0;
	{
		LVITEM lviTop = {LVIF_PARAM, ListView_GetTopIndex(hWnd)};
		ListView_GetItem(hWnd, &lviTop);
		iEpTop = lviTop.lParam;
	}

	/* Save selected episodes. */
	int iItemMark;
	static std::vector<int> vEpSel;
	{
		vEpSel.clear();
		LVITEM lvi = {LVIF_PARAM, -1};
		while (FindNextItem(&lvi, LVNI_SELECTED))
			vEpSel.push_back(lvi.lParam);
		iItemMark = ListView_GetSelectionMark(hWnd);
	}

	/* Save focus. */
	int iEpFocus = 0;
	{
		LVITEM lvi = {LVIF_PARAM, -1};
		if (FindNextItem(&lvi, LVNI_FOCUSED))
			iEpFocus = lvi.lParam;
	}

	{
		int cItem = 0;
		LVITEM lviEpisode = {LVIF_TEXT|LVIF_PARAM};

		/* Retrieve episode data and add list view items. */
		SendMessage(hWnd, WM_SETREDRAW, FALSE, 0);
		ListView_DeleteAllItems(hWnd);
		for (int iEp = 1; iEp <= g_cfg.cEp; iEp++) {
			ElvDataA e = g_fvElv.At(iEp-1);

			if (!e.siEp[0])
				continue;

			//if (g_cfg.limitScreenwriter[0] && !Pl("episode_data","episode_datum",iEp,
			//    "Screenwriter",g_cfg.limitScreenwriter))
			//	continue;

			if (!g_cfg.bViewWatched && e.bWatched)
				continue;

			if (!g_cfg.bViewTVOriginal && e.bTVOriginal)
				continue;

			/* Insert item. */
			lviEpisode.iItem = cItem++;
			lviEpisode.iSubItem = ELVSIEPISODE;
			lviEpisode.pszText = LPSTR_TEXTCALLBACK;
			lviEpisode.lParam = iEp;
			ListView_InsertItem(hWnd, &lviEpisode);

			ListView_SetItemText(hWnd, lviEpisode.iItem, ELVSITITLE, LPSTR_TEXTCALLBACK);
			ListView_SetItemText(hWnd, lviEpisode.iItem, ELVSIRATING, LPSTR_TEXTCALLBACK);
		}

		/* Show number of displayed items in status bar. */
		extern HWND g_hWndStatus;
		wchar_t disp[16];
		Swprintf(disp, L"%d", cItem);
		SendMessage(g_hWndStatus, SB_SETTEXT, MAKEWPARAM(1,0), reinterpret_cast<LPARAM>(disp));
	}

	Sort();

	LVFINDINFO lvfi;
	lvfi.flags = LVFI_PARAM;

	/* Reset selection. */
	for (const int iEpSel : vEpSel) {
		int iItemSel;
		lvfi.lParam = iEpSel;
		if ((iItemSel = ListView_FindItem(hWnd, -1, &lvfi)) != -1)
			ListView_SetItemState(hWnd, iItemSel, LVIS_SELECTED, LVIS_SELECTED);
	}
	if (iItemMark != -1)
		ListView_SetSelectionMark(hWnd, iItemMark);

	/* Reset focus. */
	if (iEpFocus) {
		int iItemFocus;
		int i = 0;
		do
			lvfi.lParam = iEpFocus+i;
		while ((iItemFocus = ListView_FindItem(hWnd, -1, &lvfi)) == -1 && i++ < 100);
		if (iItemFocus != -1)
			ListView_SetItemState(hWnd, iItemFocus, LVIS_FOCUSED, LVIS_FOCUSED);
	}

	/* Try to reset scrolling position. Note that this must be
	 * done last, as focusing an item scrolls it into view. */
	{
		int iItemTopNew;
		int i = 0;
		do
			lvfi.lParam = iEpTop+i;
		while ((iItemTopNew = ListView_FindItem(hWnd, -1, &lvfi)) == -1 && i++ < 100);
		if (iItemTopNew != -1)
			SetTop(iItemTopNew);
	}

	SendMessage(hWnd, WM_SETREDRAW, TRUE, 0);
}

LRESULT CALLBACK EpisodeListView::WndProc(const HWND hWnd, const UINT uMsg,
    const WPARAM wParam, const LPARAM lParam)
{
	switch (uMsg) {
	case WM_GETDLGCODE:
	    {
		/* For the episode list view, the Enter key should not
		 * be handled by the dialog manager, but instead be sent
		 * along to the main window procedure, so that it may be
		 * handled by the NM_RETURN case in HandleNotify. */

		const LRESULT lResult = CallWindowProc(m_proc0, hWnd, uMsg, wParam, lParam);
		MSG* const msg = reinterpret_cast<MSG*>(lParam);
		if (lParam && msg->message == WM_KEYDOWN && msg->wParam == VK_RETURN)
			return DLGC_WANTMESSAGE;
		else
			return lResult;
	    }
	}

	return ListView::WndProc(hWnd, uMsg, wParam, lParam);
}