#include "stdafx.h"

#include "InPlaceEdit.h"
#include "wtl-pp.h"
#include "win32_op.h"

#include "AutoComplete.h"
#include "CWindowCreateAndDelete.h"
#include "win32_utility.h"
#include "listview_helper.h" // ListView_GetColumnCount
#include "clipboard.h"
#include "DarkMode.h"

#include <forward_list>

using namespace InPlaceEdit;


namespace {

	enum {
		MSG_COMPLETION = WM_USER,
		MSG_DISABLE_EDITING
	};


	// Rationale: more than one HWND on the list is extremely uncommon, hence forward_list
	static std::forward_list<HWND> g_editboxes;
	static HHOOK g_hook = NULL /*, g_keyHook = NULL*/;

	static void GAbortEditing(HWND edit, t_uint32 code) {
		CWindow parent = ::GetParent(edit);
		parent.SendMessage(MSG_DISABLE_EDITING);
		parent.PostMessage(MSG_COMPLETION, code, 0);
	}

#if 0
	static void GAbortEditing(t_uint32 code) {
		for (auto walk = g_editboxes.begin(); walk != g_editboxes.end(); ++walk) {
			GAbortEditing(*walk, code);
		}
	}
#endif

	static bool IsSamePopup(CWindow wnd1, CWindow wnd2) {
		return pfc::findOwningPopup(wnd1) == pfc::findOwningPopup(wnd2);
	}

	static void MouseEventTest(HWND target, CPoint pt, bool isWheel) {
		for (auto walk = g_editboxes.begin(); walk != g_editboxes.end(); ++walk) {
			CWindow edit(*walk);
			bool cancel = false;
			if (target != edit && IsSamePopup(target, edit)) {
				cancel = true;
			} else if (isWheel) {
				CWindow target2 = WindowFromPoint(pt);
				if (target2 != edit && IsSamePopup(target2, edit)) {
					cancel = true;
				}
			}

			if (cancel) GAbortEditing(edit, KEditLostFocus);
		}
	}

	static LRESULT CALLBACK GMouseProc(int nCode, WPARAM wParam, LPARAM lParam) {
		if (nCode == HC_ACTION) {
			const MOUSEHOOKSTRUCT * mhs = reinterpret_cast<const MOUSEHOOKSTRUCT *>(lParam);
			switch (wParam) {
			case WM_NCLBUTTONDOWN:
			case WM_NCRBUTTONDOWN:
			case WM_NCMBUTTONDOWN:
			case WM_NCXBUTTONDOWN:
			case WM_LBUTTONDOWN:
			case WM_RBUTTONDOWN:
			case WM_MBUTTONDOWN:
			case WM_XBUTTONDOWN:
				MouseEventTest(mhs->hwnd, mhs->pt, false);
				break;
			case WM_MOUSEWHEEL:
			case WM_MOUSEHWHEEL:
				MouseEventTest(mhs->hwnd, mhs->pt, true);
				break;
			}
		}
		return CallNextHookEx(g_hook, nCode, wParam, lParam);
	}

	static void on_editbox_creation(HWND p_editbox) {
		// PFC_ASSERT(core_api::is_main_thread());
		g_editboxes.push_front(p_editbox);
		if (g_hook == NULL) {
			g_hook = SetWindowsHookEx(WH_MOUSE, GMouseProc, NULL, GetCurrentThreadId());
		}
		/*if (g_keyHook == NULL) {
			g_keyHook = SetWindowsHookEx(WH_KEYBOARD, GKeyboardProc, NULL, GetCurrentThreadId());
		}*/
	}
	static void UnhookHelper(HHOOK & hook) {
		HHOOK v = pfc::replace_null_t(hook);
		if (v != NULL) UnhookWindowsHookEx(v);
	}
	static void on_editbox_destruction(HWND p_editbox) {
		// PFC_ASSERT(core_api::is_main_thread());
		g_editboxes.remove(p_editbox);
		if (g_editboxes.empty()) {
			UnhookHelper(g_hook); /*UnhookHelper(g_keyHook);*/
		}
	}

	class CInPlaceComboBox : public CWindowImpl<CInPlaceComboBox, CComboBox> {
	public:
		BEGIN_MSG_MAP_EX(CInPlaceComboBox)
			//MSG_WM_CREATE(OnCreate)
			MSG_WM_DESTROY(OnDestroy)
			MSG_WM_GETDLGCODE(OnGetDlgCode)
			// MSG_WM_KILLFOCUS(OnKillFocus)
			MSG_WM_KEYDOWN(OnKeyDown)
		END_MSG_MAP()

		void OnCreation() {
			on_editbox_creation(m_hWnd);
		}
	private:
		void OnDestroy() {
			m_selfDestruct = true;
			on_editbox_destruction(m_hWnd);
			SetMsgHandled(FALSE);
		}
		int OnCreate(LPCREATESTRUCT) {
			OnCreation();
			SetMsgHandled(FALSE);
			return 0;
		}
		UINT OnGetDlgCode(LPMSG lpMsg) {
			if (lpMsg == NULL) {
				SetMsgHandled(FALSE); return 0;
			} else {
				switch (lpMsg->message) {
				case WM_KEYDOWN:
				case WM_SYSKEYDOWN:
					switch (lpMsg->wParam) {
					case VK_TAB:
					case VK_ESCAPE:
					case VK_RETURN:
						return DLGC_WANTMESSAGE;
					default:
						SetMsgHandled(FALSE); return 0;
					}
				default:
					SetMsgHandled(FALSE); return 0;

				}
			}
		}
		void OnKillFocus(CWindow wndFocus) {
			if ( wndFocus != NULL ) ForwardCompletion(KEditLostFocus);
			SetMsgHandled(FALSE);
		}

		void OnKeyDown(UINT nChar, UINT nRepCnt, UINT nFlags) {
			(void)nRepCnt;
			m_suppressChar = nFlags & 0xFF;
			switch (nChar) {
			case VK_TAB:
				ForwardCompletion(IsKeyPressed(VK_SHIFT) ? KEditShiftTab : KEditTab);
				return;
			case VK_ESCAPE:
				ForwardCompletion(KEditAborted);
				return;
			}
			m_suppressChar = 0;
			SetMsgHandled(FALSE);
		}

		void ForwardCompletion(t_uint32 code) {
			if (IsWindowEnabled()) {
				CWindow owner = GetParent();
				owner.SendMessage(MSG_DISABLE_EDITING);
				owner.PostMessage(MSG_COMPLETION, code, 0);
				EnableWindow(FALSE);
			}
		}

		bool m_selfDestruct = false;
		UINT m_suppressChar = 0;
	};

	class InPlaceComboContainer : public CWindowImpl<InPlaceComboContainer> {
	public:
		DECLARE_WND_CLASS_EX(_T("{18D85006-0CDB-49AB-A563-6A42014309A3}"), 0, -1);

		const pfc::string_list_const * m_initData;
		const unsigned m_iDefault;

		HWND Create(CWindow parent) {

			RECT rect_cropped;
			{
				RECT client;
				WIN32_OP_D(parent.GetClientRect(&client));
				IntersectRect(&rect_cropped, &client, &m_initRect);
			}
			const DWORD containerStyle = WS_BORDER | WS_CHILD;
			AdjustWindowRect(&rect_cropped, containerStyle, FALSE);



			WIN32_OP(__super::Create(parent, rect_cropped, NULL, containerStyle) != NULL);

			try {
				CRect rcClient;
				WIN32_OP_D(GetClientRect(rcClient));


				DWORD style = WS_CHILD | WS_VISIBLE;//parent is invisible now

				style |= CBS_DROPDOWNLIST;


				CComboBox edit;

				WIN32_OP(edit.Create(*this, rcClient, NULL, style, 0, ID_MYEDIT) != NULL);
				edit.SetFont(parent.GetFont());

				if ((m_flags & KFlagDark) != 0) DarkMode::DarkenComboLite(edit);

				m_edit.SubclassWindow(edit);
				m_edit.OnCreation();


#if 0 // doesn't quite work
				if (m_flags & (KFlagAlignCenter | KFlagAlignRight)) {
					COMBOBOXINFO info = {sizeof(info)};
					if (m_edit.GetComboBoxInfo(&info)) {
						CEdit edit2 = info.hwndList;
						if (edit2) {
							if (m_flags & KFlagAlignCenter) edit2.ModifyStyle(0, ES_CENTER);
							else if (m_flags & KFlagAlignRight) edit2.ModifyStyle(0, ES_RIGHT);
						}
					}
				}
#endif


				if (m_initData != nullptr) {
					const size_t count = m_initData->get_count();
					for (size_t walk = 0; walk < count; ++walk) {
						m_edit.AddString(pfc::stringcvt::string_os_from_utf8(m_initData->get_item(walk)));
					}
					if (m_iDefault < count) m_edit.SetCurSel(m_iDefault);
				}
			} catch (...) {
				PostMessage(MSG_COMPLETION, InPlaceEdit::KEditAborted, 0);
				return m_hWnd;
			}

			ShowWindow(SW_SHOW);
			m_edit.SetFocus();

			m_initialized = true;

			m_edit.ShowDropDown();

			PFC_ASSERT(m_hWnd != NULL);

			return m_hWnd;
		}

		InPlaceComboContainer(const RECT & p_rect, unsigned p_flags, pfc::string_list_const * initData, unsigned iDefault, comboReply_t p_notify) : m_notify(p_notify), m_initData(initData), m_iDefault(iDefault), m_initRect(p_rect), m_flags(p_flags) { }

		enum { ID_MYEDIT = 666 };

		BEGIN_MSG_MAP_EX(InPlaceEditContainer)
			MESSAGE_HANDLER_EX(WM_CTLCOLOREDIT, MsgForwardToParent)
			MESSAGE_HANDLER_EX(WM_CTLCOLORSTATIC, MsgForwardToParent)
			MESSAGE_HANDLER_EX(WM_MOUSEWHEEL, MsgLostFocus)
			MESSAGE_HANDLER_EX(WM_MOUSEHWHEEL, MsgLostFocus)
			MESSAGE_HANDLER_SIMPLE(MSG_DISABLE_EDITING, OnMsgDisableEditing)
			MESSAGE_HANDLER_EX(MSG_COMPLETION, OnMsgCompletion)
			COMMAND_ID_HANDLER_EX(ID_MYEDIT, OnComboMsg)
			MSG_WM_DESTROY(OnDestroy)
		END_MSG_MAP()

		HWND GetEditBox() const { return m_edit; }

	private:
		void OnDestroy() { m_selfDestruct = true; }

		LRESULT MsgForwardToParent(UINT msg, WPARAM wParam, LPARAM lParam) {
			return GetParent().SendMessage(msg, wParam, lParam);
		}
		LRESULT MsgLostFocus(UINT, WPARAM, LPARAM) {
			PostMessage(MSG_COMPLETION, InPlaceEdit::KEditLostFocus, 0);
			return 0;
		}
		void OnMsgDisableEditing() {
			ShowWindow(SW_HIDE);
			GetParent().UpdateWindow();
		}
		LRESULT OnMsgCompletion(UINT, WPARAM wParam, LPARAM) {
			PFC_ASSERT(m_initialized);
			if ((wParam & KEditMaskReason) != KEditLostFocus) {
				GetParent().SetFocus();
			}
			OnCompletion((unsigned) wParam );
			if (!m_selfDestruct) {
				m_selfDestruct = true;
				DestroyWindow();
			}
			return 0;
		}
		void OnComboMsg(UINT code, int, CWindow) {
			if (m_initialized && (code == CBN_SELENDOK || code == CBN_SELENDCANCEL) ) {
				PostMessage(MSG_COMPLETION, InPlaceEdit::KEditLostFocus, 0);
			}
		}

	private:

		void OnCompletion(unsigned p_status) {
			if (!m_completed) {
				m_completed = true;
				if (m_notify) m_notify( p_status, m_edit.GetCurSel() );
			}
		}

		const comboReply_t m_notify;
		bool m_completed = false;
		bool m_initialized = false;
		bool m_selfDestruct = false;
		const CRect m_initRect;
		CInPlaceComboBox m_edit;
		const unsigned m_flags;
	};

}

static void fail(comboReply_t p_notify) {
	p_notify(KEditAborted, UINT_MAX);
}

HWND InPlaceEdit::StartCombo(HWND p_parentwnd, const RECT & p_rect, unsigned p_flags, pfc::string_list_const & data, unsigned iDefault, comboReply_t p_notify) {
	try {
		PFC_ASSERT((CWindow(p_parentwnd).GetWindowLong(GWL_STYLE) & WS_CLIPCHILDREN) != 0);
		return (new CWindowCreateAndDelete<InPlaceComboContainer>(p_parentwnd, p_rect, p_flags, &data, iDefault, p_notify))->GetEditBox();
	} catch (...) {
		fail(p_notify);
		return NULL;
	}
}
