#include "CDSearch.h"

#include <ctype.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>

#include "WimpLib:PtrArray.h"
#include "WimpLib:CIcon.h"
#include "WimpLib:Desktop.h"
#include "WimpLib:Display.h"
#include "WimpLib:Exception.h"
#include "WimpLib:Hourglass.h"
#include "WimpLib:Mem.h"
#include "WimpLib:Menu.h"
#include "WimpLib:Message.h"
#include "WimpLib:Task.h"
#include "WimpLib:Template.h"
#include "WimpLib:Window.h"

#include "DigitalCD.h"
#include "CDDesc.h"
#include "CDTrack.h"
#include "Options.h"

typedef enum
{ Action_List = 0
, Action_AddMark
, Action_RemoveMark
, Action_AddSelect
, Action_RemoveSelect
} Action;

typedef enum
{ Scope_Database
, Scope_Marked
, Scope_Unmarked
, Scope_Selected
, Scope_NotSelected
} Scope;

static struct
{
	void*       m_pView;
	CDSearch_GetTrackCount GetTrackCount;
	CDSearch_GetTrack      GetTrack;
	CDSearch_GetState      GetState;
	CDSearch_SetState      SetState;
	CDSearch_ShowTrack     ShowTrack;
	HWind       m_wnd;
	HMenu       m_menuaction;
	HMenu       m_menuscope;
	Action      m_action;
	Scope       m_scope;
} CDSearch = {NULL, NULL, NULL, NULL, NULL, NULL, HWind_None, NULL, NULL, Action_List, Scope_Database};

#define Icon_Search             0
#define Icon_Cancel             1
#define Icon_Action             3
#define Icon_ActionMenu         4
#define Icon_Scope              6
#define Icon_ScopeMenu          7
#define Icon_FieldAll           8
#define Icon_HideStart          9
#define Icon_FieldSearchString 12
#define Icon_HideEnd           12

static EListenerAction CDSearch_EventHandler(void* handle, const Event* e);


/**
 * Returns where the pointer should be located in the list of pointers
 * if it is/should be present, that is in range [0, count].
 *
 * @param  pi      Pointer to locate.
 * @param  bExact  if true return -1 if not present.
 *
 * @returns  Index in pointers array or -1.
 */
static int CollectionMatch_Find(const PtrArray* pArray, const void* pi, bool bExact)
{
	int i = 0;
	int k = PtrArray_Count(pArray) - 1;
	int j = (i + k) / 2;

	if (k >= 0)
	{
		int val = ((char*) PtrArray_Get(pArray, j)) - ((char*) pi);

		while (i < j)
		{
			if (val >= 0) k = j; // Allow for duplicates
			else i = j;

			j = (i + k) / 2;
			val = ((char*) PtrArray_Get(pArray, j)) - ((char*) pi);
		}

		if (bExact)
		{
			if (!val) return j;

			val = ((char*) PtrArray_Get(pArray, k)) - ((char*) pi);
			if (!val) return k;

			return -1;
		}
		else
		{
			if (val >= 0) return j;

			val = ((char*) PtrArray_Get(pArray, k)) - ((char*) pi);
			if (val >= 0) return k;

			return k + 1;
		}
	}

	return bExact ? -1 : 0;
}

void CDSearch_Open(void* pView
		, CDSearch_GetTrackCount GetTrackCount
		, CDSearch_GetTrack GetTrack
		, CDSearch_GetState GetState
		, CDSearch_SetState SetState
		, CDSearch_ShowTrack ShowTrack)
{
	int i;

	if (!CDSearch.m_menuscope)
	{
		try
		{
			CDSearch.m_menuaction = throw_New_Menu(Msg_Lookup("Action"), Msg_Lookup("CDScAMenu"));
			CDSearch.m_menuscope = throw_New_Menu(Msg_Lookup("Scope"), Msg_Lookup("CDScSMenu"));
		}
		catch
		{
			App_ReportException();
			return;
		}
		catch_end
	}

	if (CDSearch.m_wnd == HWind_None)
	{
		try
		{
			CDSearch.m_wnd = throw_Window_CreateFrom("CDSearch", "CDSrcH");
			throw_Window_RegisterEventHandler(CDSearch.m_wnd, CDSearch_EventHandler, &CDSearch, false);
		}
		catch
		{
			Window_Delete(CDSearch.m_wnd);
			CDSearch.m_wnd = HWind_None;
			App_ReportException();
			return;
		}
		catch_end
	}

	CDSearch.m_pView = pView;
	CDSearch.GetTrackCount = GetTrackCount;
	CDSearch.GetTrack = GetTrack;
	CDSearch.GetState = GetState;
	CDSearch.SetState = SetState;
	CDSearch.ShowTrack = ShowTrack;

	Icon_SetData(CDSearch.m_wnd, Icon_FieldSearchString, NULL);
	Icon_SetHighlight(CDSearch.m_wnd, Icon_FieldAll, false);
	Icon_SetData(CDSearch.m_wnd, Icon_Action, Menu_ItemGetText(CDSearch.m_menuaction, CDSearch.m_action));
	Icon_SetData(CDSearch.m_wnd, Icon_Scope, Menu_ItemGetText(CDSearch.m_menuscope, CDSearch.m_scope));
	Icon_SetDimmed(CDSearch.m_wnd, Icon_Action, false);
	Icon_SetDimmed(CDSearch.m_wnd, Icon_ActionMenu, false);
	for (i = Icon_HideStart; i <= Icon_HideEnd; i++)
		Icon_SetDimmed(CDSearch.m_wnd, i, 0);

	Window_OpenBehind(CDSearch.m_wnd, -1);
	Icon_SetFocus(CDSearch.m_wnd, Icon_FieldSearchString, -1);
}

void CDSearch_Close(void* pView)
{
	if (CDSearch.m_pView == pView)
		Window_Close(CDSearch.m_wnd);
}

void CDSearch_NotCDSearch(void)
{
	if (CDSearch.m_wnd != HWind_None)
	{
		Window_DeRegisterEventHandler(CDSearch.m_wnd, CDSearch_EventHandler, &CDSearch);
		Window_Delete(CDSearch.m_wnd);
		CDSearch.m_wnd = HWind_None;
	}
	Delete_Menu(CDSearch.m_menuaction, false);
	CDSearch.m_menuaction = NULL;
	Delete_Menu(CDSearch.m_menuscope, false);
	CDSearch.m_menuscope = NULL;
}

static bool IsSelected(HIcon i)
{
	return (Icon_GetState(CDSearch.m_wnd, i, EIcon_Selected) == EIcon_Selected);
}

static bool Str_Find(const char* in, const char* ref)
{
	int lin = strlen(in);
	int lref = strlen(ref);
	const char* ein = in + (lin - lref);
	int i;

	for (; in <= ein; in++)
	{
		for (i = 0; i < lref; i++)
		{
			if (toupper(in[i]) != toupper(ref[i]))
				break;
		}

		if (i == lref)
			return true;
	}

	return false;
}

#define fieldflag_metadata  0x01

static bool CDSearch_IsWithinScope(const CDTrack* pTrack, Scope scope)
{
	switch(scope)
	{
		case Scope_Database:
		{
			return true;
		}
		break;
		case Scope_Marked:
		{
			return ((CDSearch.GetState(CDSearch.m_pView, pTrack) & EWItem_Marked) != 0);
		}
		break;
		case Scope_Unmarked:
		{
			return ((CDSearch.GetState(CDSearch.m_pView, pTrack) & EWItem_Marked) == 0);
		}
		break;
		case Scope_Selected:
		{
			return ((CDSearch.GetState(CDSearch.m_pView, pTrack) & EWItem_Selected) != 0);
		}
		break;
		case Scope_NotSelected:
		{
			return ((CDSearch.GetState(CDSearch.m_pView, pTrack) & EWItem_Selected) == 0);
		}
		break;
	}

	return false;
}

static bool CDSearch_IsMatch(const CDTrack* pTrack, unsigned int flags, const char* psearch, PtrArray* pMatchCol)
{
	bool bsel = false;

	if (IsSelected(Icon_FieldAll))
		bsel = true;
	else
	{
		if (*psearch)
		{
			const CDDesc* pDesc = CDTrack_GetCDDesc(pTrack);
			// Search matching strings
			if (flags & fieldflag_metadata)
			{
				if (Str_Find(CDTrack_GetMetaString(pTrack, EMetaId_StreamTitle), psearch)
				||  (CollectionMatch_Find(pMatchCol, CDTrack_GetMetaString(pTrack, EMetaId_StreamCollective), true) >= 0)
				||  (CollectionMatch_Find(pMatchCol, CDTrack_GetMetaString(pTrack, EMetaId_StreamArtist), true) >= 0)
				||  (CollectionMatch_Find(pMatchCol, CDDesc_GetMetaString(pDesc, EMetaId_StreamCollective), true) >= 0)
				||  (CollectionMatch_Find(pMatchCol, CDDesc_GetMetaString(pDesc, EMetaId_StreamArtist), true) >= 0)
				||  (CollectionMatch_Find(pMatchCol, CDDesc_GetMetaString(pDesc, EMetaId_StreamBox), true) >= 0)
				||  (CollectionMatch_Find(pMatchCol, CDDesc_GetMetaString(pDesc, EMetaId_StreamAlbum), true) >= 0)
				   )
					bsel = true;
			}
		}
	}

	return bsel;
}

static bool CDSearch_Act(CDTrack* pTrack, Action action)
{
	bool bret = false;

	switch(action)
	{
		case Action_AddMark:
		{
			CDSearch.SetState(CDSearch.m_pView, pTrack, EWItem_Marked, EWItem_Marked);
		}
		break;
		case Action_RemoveMark:
		{
			CDSearch.SetState(CDSearch.m_pView, pTrack, 0, EWItem_Marked);
		}
		break;
		case Action_AddSelect:
		{
			CDSearch.SetState(CDSearch.m_pView, pTrack, EWItem_Selected, EWItem_Selected);
		}
		break;
		case Action_RemoveSelect:
		{
			CDSearch.SetState(CDSearch.m_pView, pTrack, 0, EWItem_Selected);
		}
		break;
	}

	return bret;
}

static void CDSearch_OnSearch(void)
{
	char* psearch = Icon_GetData(CDSearch.m_wnd, Icon_FieldSearchString);
	unsigned int flags = 0;
	unsigned int i;
	CDTrack* pTrack;
	const CDTrack* pShowTrack = NULL;
	CDDescList* pResultList = NULL;
	PtrArray* MatchCol = NULL;
	bool off = !WLib_Hourglass_IsOn();

	try
	{
		OrderedSet* pSet;
		OrderedNode* pNode;
		const char* pData;
		int pos;

	   	MatchCol = New_PtrArray(256);

		flags |= fieldflag_metadata;

		// Create view if required
		if (CDSearch.m_action == Action_List)
		{
			pResultList = throw_New_CDSearchList();
			if (off) WLib_Hourglass_On();
			CDDescList_StartUpdate(pResultList);
		}

		// Locate matches in text collections for faster searchs
	    if (flags & fieldflag_metadata)
    	{
    		pSet = DigitalCD.pCollectivesCol;
		    for ( pNode = OrderedSet_GetSuccessor(pSet, NULL)
    		    ; pNode
		        ; pNode = OrderedSet_GetSuccessor(pSet, pNode)
    		    )
		    {
		    	pData = OrderedSet_GetNodeData(pSet, pNode);

				if (Str_Find(pData, psearch))
				{
					pos = CollectionMatch_Find(MatchCol, pData, false);

					PtrArray_Insert(MatchCol, pos, (char*) pData);
				}
		    }

    		pSet = DigitalCD.pArtistsCol;
		    for ( pNode = OrderedSet_GetSuccessor(pSet, NULL)
    		    ; pNode
		        ; pNode = OrderedSet_GetSuccessor(pSet, pNode)
    		    )
		    {
		    	pData = OrderedSet_GetNodeData(pSet, pNode);

				if (Str_Find(pData, psearch))
				{
					pos = CollectionMatch_Find(MatchCol, pData, false);

					PtrArray_Insert(MatchCol, pos, (char*) pData);
				}
		    }

    		pSet = DigitalCD.pBoxesCol;
		    for ( pNode = OrderedSet_GetSuccessor(pSet, NULL)
    		    ; pNode
		        ; pNode = OrderedSet_GetSuccessor(pSet, pNode)
    		    )
		    {
		    	pData = OrderedSet_GetNodeData(pSet, pNode);

				if (Str_Find(pData, psearch))
				{
					pos = CollectionMatch_Find(MatchCol, pData, false);

					PtrArray_Insert(MatchCol, pos, (char*) pData);
				}
		    }

    		pSet = DigitalCD.pAlbumsCol;
		    for ( pNode = OrderedSet_GetSuccessor(pSet, NULL)
    		    ; pNode
		        ; pNode = OrderedSet_GetSuccessor(pSet, pNode)
    		    )
		    {
		    	pData = OrderedSet_GetNodeData(pSet, pNode);

				if (Str_Find(pData, psearch))
				{
					pos = CollectionMatch_Find(MatchCol, pData, false);

					PtrArray_Insert(MatchCol, pos, (char*) pData);
				}
		    }
    	}

		for (i = 0; i < CDSearch.GetTrackCount(CDSearch.m_pView); i++)
		{
			pTrack = CDSearch.GetTrack(CDSearch.m_pView, i);

			if ((CDSearch_IsWithinScope(pTrack, CDSearch.m_scope))
			&&  (CDSearch_IsMatch(pTrack, flags, psearch, MatchCol)))
			{
				if (pResultList)
				{
					throw_CDSearchList_Add(pResultList, pTrack);
				}
				else
				{
					if (!pShowTrack) pShowTrack = pTrack;

					if (CDSearch_Act(pTrack, CDSearch.m_action))
					{
						CDTrack_RefreshViews(pTrack, &CDSearch, false);
					}
				}
			}
		}
	}
	catch
	{
		App_ReportException();
	}
	catch_end

	if (pResultList)
	{
		CDDescList_EndUpdate(pResultList);
		CDDescList_Show(pResultList, NULL);

		if (off) WLib_Hourglass_Off();
	}
	else
	{
		CDSearch.ShowTrack(CDSearch.m_pView, pShowTrack);
	}

  	Delete_PtrArray(MatchCol);
}

static void CDSearch_OnCancel(void)
{
	Window_Close(CDSearch.m_wnd);
}

static EListenerAction CDSearch_ActionMenuHandler(void* handle, const int* hit, const Event* e)
{
	IGNORE(handle);

	switch(e->Type)
	{
		case EEvent_Menu:
		{
			CDSearch.m_action = (Action) hit[0];
			Icon_SetData(CDSearch.m_wnd, Icon_Action, Menu_ItemGetText(CDSearch.m_menuaction, CDSearch.m_action));

			return EListenerAction_StopEvent;
		}
		break;
	}

	return EListenerAction_ContinueEvent;
}

static EListenerAction CDSearch_ScopeMenuHandler(void* handle, const int* hit, const Event* e)
{
	IGNORE(handle);

	switch(e->Type)
	{
		case EEvent_Menu:
		{
			CDSearch.m_scope = (Scope) hit[0];
			Icon_SetData(CDSearch.m_wnd, Icon_Scope, Menu_ItemGetText(CDSearch.m_menuscope, CDSearch.m_scope));

			return EListenerAction_StopEvent;
		}
		break;
	}

	return EListenerAction_ContinueEvent;
}

static EListenerAction CDSearch_EventHandler(void* handle, const Event* e)
{
	IGNORE(handle);

	switch(e->Type)
	{
		case EEvent_Mouse:
		{
			const Mouse* m = e->pData;

			if (m->but & EBut_Menu)
				return EListenerAction_StopEvent;

			switch(m->i)
			{
				case Icon_Search:
				{
					CDSearch_OnSearch();
				}
				break;
				case Icon_Cancel:
				{
					CDSearch_OnCancel();
				}
				break;
				case Icon_ActionMenu:
				{
					Menu_Popup(CDSearch.m_wnd, Icon_ActionMenu, CDSearch.m_menuaction, CDSearch_ActionMenuHandler, handle);
				}
				break;
				case Icon_ScopeMenu:
				{
					Menu_Popup(CDSearch.m_wnd, Icon_ScopeMenu, CDSearch.m_menuscope, CDSearch_ScopeMenuHandler, handle);
				}
				break;
				case Icon_FieldAll:
				{
					bool dim = IsSelected(Icon_FieldAll);
					int i;

					for (i = Icon_HideStart; i <= Icon_HideEnd; i++)
						Icon_SetDimmed(CDSearch.m_wnd, i, dim);
				}
				break;
			}

			return EListenerAction_StopEvent;
		}
		break;
		case EEvent_Key:
		{
			const Event_Key* pkey = e->pData;

			switch(pkey->code)
			{
				case 0x1b: // Esc
				{
					CDSearch_OnCancel();
					return EListenerAction_StopEvent;
				}
				break;
				case 0x0d: // Return
				{
					CDSearch_OnSearch();
					return EListenerAction_StopEvent;
				}
				break;
				case 0x181: // F1
				{
					App_StrongHelp("CDs_Search");
					return EListenerAction_StopEvent;
				}
				break;
			}
		}
		break;
	}

	return EListenerAction_ContinueEvent;
}
