// Copyright (c) 2014 AlphaSierraPapa for the SharpDevelop Team
// 
// Permission is hereby granted, free of charge, to any person obtaining a copy of this
// software and associated documentation files (the "Software"), to deal in the Software
// without restriction, including without limitation the rights to use, copy, modify, merge,
// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons
// to whom the Software is furnished to do so, subject to the following conditions:
// 
// The above copyright notice and this permission notice shall be included in all copies or
// substantial portions of the Software.
// 
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
// DEALINGS IN THE SOFTWARE.

using System;
using System.Linq;
using System.Runtime.InteropServices;
using System.Windows;
using System.Windows.Interop;
using System.Windows.Media.TextFormatting;

using UyghurEditPP.Rendering;
using UyghurEditPP.Utils;

namespace UyghurEditPP.Editing
{
	/// <summary>
	/// Native API required for IME support.
	/// </summary>
	static class ImeNativeWrapper
	{
		[StructLayout(LayoutKind.Sequential)]
		struct CompositionForm
		{
			public int dwStyle;
			public POINT ptCurrentPos;
			public RECT rcArea;
		}

		[StructLayout(LayoutKind.Sequential)]
		struct POINT
		{
			public int x;
			public int y;
		}

		[StructLayout(LayoutKind.Sequential)]
		struct RECT
		{
			public int left;
			public int top;
			public int right;
			public int bottom;
		}

		[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
		struct LOGFONT
		{
			public int lfHeight;
			public int lfWidth;
			public int lfEscapement;
			public int lfOrientation;
			public int lfWeight;
			public byte lfItalic;
			public byte lfUnderline;
			public byte lfStrikeOut;
			public byte lfCharSet;
			public byte lfOutPrecision;
			public byte lfClipPrecision;
			public byte lfQuality;
			public byte lfPitchAndFamily;
			[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 32)] public string lfFaceName;
		}

		const int CPS_CANCEL = 0x4;
		const int NI_COMPOSITIONSTR = 0x15;
		const int GCS_COMPSTR = 0x0008;

		public const int WM_IME_COMPOSITION = 0x10F;
		public const int WM_IME_SETCONTEXT = 0x281;
		public const int WM_INPUTLANGCHANGE = 0x51;

		[DllImport("imm32.dll")]
		public static extern IntPtr ImmAssociateContext(IntPtr hWnd, IntPtr hIMC);
		[DllImport("imm32.dll")]
		internal static extern IntPtr ImmGetContext(IntPtr hWnd);
		[DllImport("imm32.dll")]
		internal static extern IntPtr ImmGetDefaultIMEWnd(IntPtr hWnd);
		[DllImport("imm32.dll")]
		[return: MarshalAs(UnmanagedType.Bool)]
		internal static extern bool ImmReleaseContext(IntPtr hWnd, IntPtr hIMC);
		[DllImport("imm32.dll")]
		[return: MarshalAs(UnmanagedType.Bool)]
		static extern bool ImmNotifyIME(IntPtr hIMC, int dwAction, int dwIndex, int dwValue = 0);
		[DllImport("imm32.dll")]
		[return: MarshalAs(UnmanagedType.Bool)]
		static extern bool ImmSetCompositionWindow(IntPtr hIMC, ref CompositionForm form);
		[DllImport("imm32.dll", CharSet = CharSet.Unicode)]
		[return: MarshalAs(UnmanagedType.Bool)]
		static extern bool ImmSetCompositionFont(IntPtr hIMC, ref LOGFONT font);
		[DllImport("imm32.dll")]
		[return: MarshalAs(UnmanagedType.Bool)]
		static extern bool ImmGetCompositionFont(IntPtr hIMC, out LOGFONT font);

		[DllImport("msctf.dll")]
		static extern int TF_CreateThreadMgr(out ITfThreadMgr threadMgr);

		[ThreadStatic] static bool textFrameworkThreadMgrInitialized;
		[ThreadStatic] static ITfThreadMgr textFrameworkThreadMgr;

		public static ITfThreadMgr GetTextFrameworkThreadManager()
		{
			if (!textFrameworkThreadMgrInitialized) {
				textFrameworkThreadMgrInitialized = true;
				TF_CreateThreadMgr(out textFrameworkThreadMgr);
			}
			return textFrameworkThreadMgr;
		}

		public static bool NotifyIme(IntPtr hIMC)
		{
			return ImmNotifyIME(hIMC, NI_COMPOSITIONSTR, CPS_CANCEL);
		}

		public static bool SetCompositionWindow(HwndSource source, IntPtr hIMC, TextArea textArea)
		{
			if (textArea == null)
				throw new ArgumentNullException("textArea");
			Rect textViewBounds = textArea.TextView.GetBounds(source);
			Rect characterBounds = textArea.TextView.GetCharacterBounds(textArea.Caret.Position, source);
			CompositionForm form = new CompositionForm();
			form.dwStyle = 0x0020;
			form.ptCurrentPos.x = (int)Math.Max(characterBounds.Left, textViewBounds.Left);
			form.ptCurrentPos.y = (int)Math.Max(characterBounds.Top, textViewBounds.Top);
			form.rcArea.left = (int)textViewBounds.Left;
			form.rcArea.top = (int)textViewBounds.Top;
			form.rcArea.right = (int)textViewBounds.Right;
			form.rcArea.bottom = (int)textViewBounds.Bottom;
			return ImmSetCompositionWindow(hIMC, ref form);
		}

		public static bool SetCompositionFont(HwndSource source, IntPtr hIMC, TextArea textArea)
		{
			if (textArea == null)
				throw new ArgumentNullException("textArea");
			LOGFONT lf = new LOGFONT();
			Rect characterBounds = textArea.TextView.GetCharacterBounds(textArea.Caret.Position, source);
			lf.lfFaceName = textArea.FontFamily.Source;
			lf.lfHeight = (int)characterBounds.Height;
			return ImmSetCompositionFont(hIMC, ref lf);
		}

		static Rect GetBounds(this TextView textView, HwndSource source)
		{
			// this may happen during layout changes in AvalonDock, so we just return an empty rectangle
			// in those cases. It should be refreshed immediately.
			if (source.RootVisual == null || !source.RootVisual.IsAncestorOf(textView))
				return EMPTY_RECT;
			Rect displayRect = new Rect(0, 0, textView.ActualWidth, textView.ActualHeight);
			return textView
				.TransformToAncestor(source.RootVisual).TransformBounds(displayRect) // rect on root visual
				.TransformToDevice(source.RootVisual); // rect on HWND
		}

		static readonly Rect EMPTY_RECT = new Rect(0, 0, 0, 0);

		static Rect GetCharacterBounds(this TextView textView, TextViewPosition pos, HwndSource source)
		{
			VisualLine vl = textView.GetVisualLine(pos.Line);
			if (vl == null)
				return EMPTY_RECT;
			// this may happen during layout changes in AvalonDock, so we just return an empty rectangle
			// in those cases. It should be refreshed immediately.
			if (source.RootVisual == null || !source.RootVisual.IsAncestorOf(textView))
				return EMPTY_RECT;
			TextLine line = vl.GetTextLine(pos.VisualColumn, pos.IsAtEndOfLine);
			Rect displayRect;
			// calculate the display rect for the current character
			if (pos.VisualColumn < vl.VisualLengthWithEndOfLineMarker) {
				displayRect = line.GetTextBounds(pos.VisualColumn, 1).First().Rectangle;
				displayRect.Offset(0, vl.GetTextLineVisualYPosition(line, VisualYPosition.LineTop));
			} else {
				// if we are in virtual space, we just use one wide-space as character width
				displayRect = new Rect(vl.GetVisualPosition(pos.VisualColumn, VisualYPosition.TextTop),
									   new Size(textView.WideSpaceWidth, textView.DefaultLineHeight));
			}
			// adjust to current scrolling
			displayRect.Offset(-textView.ScrollOffset);
			return textView
				.TransformToAncestor(source.RootVisual).TransformBounds(displayRect) // rect on root visual
				.TransformToDevice(source.RootVisual); // rect on HWND
		}
	}

	[ComImport, Guid("aa80e801-2021-11d2-93e0-0060b067b86e"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
	interface ITfThreadMgr
	{
		void Activate(out int clientId);
		void Deactivate();
		void CreateDocumentMgr(out IntPtr docMgr);
		void EnumDocumentMgrs(out IntPtr enumDocMgrs);
		void GetFocus(out IntPtr docMgr);
		void SetFocus(IntPtr docMgr);
		void AssociateFocus(IntPtr hwnd, IntPtr newDocMgr, out IntPtr prevDocMgr);
		void IsThreadFocus([MarshalAs(UnmanagedType.Bool)] out bool isFocus);
		void GetFunctionProvider(ref Guid classId, out IntPtr funcProvider);
		void EnumFunctionProviders(out IntPtr enumProviders);
		void GetGlobalCompartment(out IntPtr compartmentMgr);
	}
}
