Option Strict On
Option Explicit On

Imports System
Imports System.Runtime.InteropServices
Imports System.Text
Imports SkiaSharp

Namespace Nexamas.UI.TextInput

    ' CONTRACT:
    ' MASWin32IMEBridge is the only Win32/IMM32 integration point for TextInput.
    '
    ' Responsibilities:
    ' - Translate WM_IME_* messages into MASTextCompositionCommand
    ' - Read composition/result strings from IMM32
    ' - Position the IME candidate/composition window near the caret
    '
    ' It must not:
    ' - Mutate MASTextInputState directly
    ' - Know UI controls
    ' - Render
    ' - Store committed text
    Friend NotInheritable Class MASWin32IMEBridge

        Private Const WM_IME_STARTCOMPOSITION As Integer = &H10D
        Private Const WM_IME_ENDCOMPOSITION As Integer = &H10E
        Private Const WM_IME_COMPOSITION As Integer = &H10F

        Private Const GCS_COMPSTR As Integer = &H8
        Private Const GCS_CURSORPOS As Integer = &H80
        Private Const GCS_RESULTSTR As Integer = &H800

        Private Const CFS_POINT As Integer = &H2

        Private Sub New()
        End Sub

        Friend Shared Function ProcessMessage(hwnd As IntPtr,
                                              message As Integer,
                                              wParam As IntPtr,
                                              lParam As IntPtr,
                                              dispatch As Func(Of MASTextCompositionCommand, Boolean),
                                              candidatePointProvider As Func(Of SKPoint)) As Boolean

            If hwnd = IntPtr.Zero Then Return False
            If dispatch Is Nothing Then Return False

            Select Case message
                Case WM_IME_STARTCOMPOSITION
                    SetCandidateWindow(hwnd, candidatePointProvider)
                    Return dispatch.Invoke(MASTextCompositionCommand.Begin(String.Empty, 0, 0))

                Case WM_IME_COMPOSITION
                    SetCandidateWindow(hwnd, candidatePointProvider)
                    Return ProcessCompositionMessage(hwnd, lParam, dispatch)

                Case WM_IME_ENDCOMPOSITION
                    Return dispatch.Invoke(MASTextCompositionCommand.Cancel())
            End Select

            Return False
        End Function

        Private Shared Function ProcessCompositionMessage(hwnd As IntPtr,
                                                         lParam As IntPtr,
                                                         dispatch As Func(Of MASTextCompositionCommand, Boolean)) As Boolean

            Dim flags As Integer = lParam.ToInt32()

            If (flags And GCS_RESULTSTR) = GCS_RESULTSTR Then
                Dim committed As String = GetCompositionString(hwnd, GCS_RESULTSTR)
                Return dispatch.Invoke(MASTextCompositionCommand.Commit(committed))
            End If

            If (flags And GCS_COMPSTR) = GCS_COMPSTR Then
                Dim compositionText As String = GetCompositionString(hwnd, GCS_COMPSTR)
                Dim cursorPos As Integer = GetCompositionCursorPosition(hwnd)

                If cursorPos < 0 Then cursorPos = compositionText.Length
                If cursorPos > compositionText.Length Then cursorPos = compositionText.Length

                Return dispatch.Invoke(MASTextCompositionCommand.Update(compositionText, cursorPos, 0))
            End If

            Return False
        End Function

        Private Shared Function GetCompositionString(hwnd As IntPtr,
                                                     index As Integer) As String

            Dim himc As IntPtr = ImmGetContext(hwnd)
            If himc = IntPtr.Zero Then Return String.Empty

            Try
                Dim byteCount As Integer = ImmGetCompositionStringW(himc, index, IntPtr.Zero, 0)
                If byteCount <= 0 Then Return String.Empty

                Dim buffer As IntPtr = Marshal.AllocHGlobal(byteCount)

                Try
                    Dim copied As Integer = ImmGetCompositionStringW(himc, index, buffer, byteCount)
                    If copied <= 0 Then Return String.Empty

                    Dim bytes(copied - 1) As Byte
                    Marshal.Copy(buffer, bytes, 0, copied)

                    Return Encoding.Unicode.GetString(bytes).TrimEnd(ControlChars.NullChar)
                Finally
                    Marshal.FreeHGlobal(buffer)
                End Try
            Finally
                ImmReleaseContext(hwnd, himc)
            End Try
        End Function

        Private Shared Function GetCompositionCursorPosition(hwnd As IntPtr) As Integer
            Dim himc As IntPtr = ImmGetContext(hwnd)
            If himc = IntPtr.Zero Then Return -1

            Try
                Return ImmGetCompositionStringW(himc, GCS_CURSORPOS, IntPtr.Zero, 0)
            Finally
                ImmReleaseContext(hwnd, himc)
            End Try
        End Function

        Private Shared Sub SetCandidateWindow(hwnd As IntPtr,
                                             candidatePointProvider As Func(Of SKPoint))

            If candidatePointProvider Is Nothing Then Return

            Dim point As SKPoint

            Try
                point = candidatePointProvider.Invoke()
            Catch masCaughtException1 As Exception
                Nexamas.UI.Diagnostics.MASExceptionSilencer.SwallowNativeInterop(masCaughtException1)
                Return
            End Try

            Dim himc As IntPtr = ImmGetContext(hwnd)
            If himc = IntPtr.Zero Then Return

            Try
                Dim form As New COMPOSITIONFORM() With {
                    .dwStyle = CFS_POINT,
                    .ptCurrentPos = New POINT() With {
                        .x = CInt(Math.Round(point.X)),
                        .y = CInt(Math.Round(point.Y))
                    },
                    .rcArea = New RECT()
                }

                ImmSetCompositionWindow(himc, form)
            Finally
                ImmReleaseContext(hwnd, himc)
            End Try
        End Sub

#Region "Win32"

        <StructLayout(LayoutKind.Sequential)>
        Private Structure POINT
            Friend x As Integer
            Friend y As Integer
        End Structure

        <StructLayout(LayoutKind.Sequential)>
        Private Structure RECT
            Friend left As Integer
            Friend top As Integer
            Friend right As Integer
            Friend bottom As Integer
        End Structure

        <StructLayout(LayoutKind.Sequential)>
        Private Structure COMPOSITIONFORM
            Friend dwStyle As Integer
            Friend ptCurrentPos As POINT
            Friend rcArea As RECT
        End Structure

        <DllImport("imm32.dll")>
        Private Shared Function ImmGetContext(hwnd As IntPtr) As IntPtr
        End Function

        <DllImport("imm32.dll")>
        Private Shared Function ImmReleaseContext(hwnd As IntPtr,
                                                  himc As IntPtr) As Boolean
        End Function

        <DllImport("imm32.dll", CharSet:=CharSet.Unicode)>
        Private Shared Function ImmGetCompositionStringW(himc As IntPtr,
                                                         dwIndex As Integer,
                                                         lpBuf As IntPtr,
                                                         dwBufLen As Integer) As Integer
        End Function

        <DllImport("imm32.dll")>
        Private Shared Function ImmSetCompositionWindow(himc As IntPtr,
                                                        ByRef lpCompForm As COMPOSITIONFORM) As Boolean
        End Function

#End Region

    End Class

End Namespace
