{*******************************************************}
{                                                       }
{       Tocsg.Keyboard                                   }
{                                                       }
{       Copyright (C) 2022 sunk                         }
{                                                       }
{*******************************************************}

unit Tocsg.Keyboard;

interface

uses
  Winapi.Windows, System.Classes, System.SysUtils;

function IsCapsLockOn: Boolean;
function IsNumLockOn: Boolean;
function IsScrollLockOn: Boolean;
function GetInputKeyToStr(nInput: Integer): String; inline;

function EngCharToHanChar(const cEng: Char): Char;
function EngStrToHanStr(const sEng: String; bIgnoreSPKey: Boolean = false): String;

procedure DeleteSPKey(var sContexts: String);

function ConvHotkeyToStr(dwHotkey: DWORD): String;

procedure PressKeys(const sPress: String; bCtrl: Boolean = false);
procedure PressKey(const dwCode: DWORD);

function IsWinKeyDown: Boolean;

implementation

uses
  Vcl.Forms;

const
  ChoSung  : WideString = 'ㄱㄲㄴㄷㄸㄹㅁㅂㅃㅅㅆㅇㅈㅉㅊㅋㅌㅍㅎ';
  JungSung : WideString = 'ㅏㅐㅑㅒㅓㅔㅕㅖㅗㅘㅙㅚㅛㅜㅝㅞㅟㅠㅡㅢㅣ';
  JongSung : WideString = ' ㄱㄲㄳㄴㄵㄶㄷㄹㄺㄻㄼㄽㄾㄿㅀㅁㅂㅄㅅㅆㅇㅈㅊㅋㅌㅍㅎ';

function IsCapsLockOn: Boolean;
begin
  Result := GetKeyState(VK_CAPITAL) = 1;
end;

function IsNumLockOn: Boolean;
begin
  Result := GetKeyState(VK_NUMLOCK) = 1;
end;

function IsScrollLockOn: Boolean;
begin
  Result := GetKeyState(VK_SCROLL) = 1;
end;

function GetInputKeyToStr(nInput: Integer): String;
var
  ShiftState: TShiftState;
begin
  ShiftState := KeyDataToShiftState(0);
  if GetKeyState(VK_LMENU) < 0 then
    Include(ShiftState, ssAlt);

// 단순 캐스팅으로 뭘 쳤는지 알수 없는 것들은 아래처럼 처리한다.
  case nInput of
     8  : Result := '[Back Space]';
     9  : Result := '[Tab]';
    13  : Result := '[Enter]';
    19  : Result := '[Pause]';
    21  : Result := '[HanYoung]';
    22  : Result := '[Hangul]';
    23,
    25  : Result := '[Hanja]';
    27  : Result := '[Esc]';
    32  : Result := ' ';
    33  : Result := '[Page Up]';
    34  : Result := '[Page Down]';
    35  : Result := '[End]';
    36  : Result := '[Home]';
    37  : Result := '[Left]';
    38  : Result := '[Up]';
    39  : Result := '[Right]';
    40  : Result := '[Down]';
    44  : Result := '[Print Screen]';
    45  : Result := '[Insert]';
    46  : Result := '[Delete]';
    91  : Result := '[Win Right]';
    92  : Result := '[Win Left]';
    93  : Result := '[Win Menu]';
    96  : Result := '[Num 0]';
    97  : Result := '[Num 1]';
    98  : Result := '[Num 2]';
    99  : Result := '[Num 3]';
    100 : Result := '[Num 4]';
    101 : Result := '[Num 5]';
    102 : Result := '[Num 6]';
    103 : Result := '[Num 7]';
    104 : Result := '[Num 8]';
    105 : Result := '[Num 9]';
    106 : Result := '*';
    107 : Result := '+';
    109 : Result := '-';
    110 : Result := '[. Del]';
    111 : Result := '/';
    112 : Result := '[F1]';
    113 : Result := '[F2]';
    114 : Result := '[F3]';
    115 : Result := '[F4]';
    116 : Result := '[F5]';
    117 : Result := '[F6]';
    118 : Result := '[F7]';
    119 : Result := '[F8]';
    120 : Result := '[F9]';
    121 : Result := '[F10]';
    122 : Result := '[F11]';
    123 : Result := '[F12]';
    144 : Result := '[Num Lock]';
    145 : Result := '[Scroll Lock]';
    186 :
      if ssShift in ShiftState then
        Result := ':'
      else
        Result := ';';
    187 :
      if ssShift in ShiftState then
        Result := '+'
      else
        Result := '=';
    188 :
      if ssShift in ShiftState then
        Result := '<'
      else
        Result := ',';
    189 :
      if ssShift in ShiftState then
        Result := '_'
      else
        Result := '-';
    190 :
      if ssShift in ShiftState then
        Result := '>'
      else
        Result := '.';
    191 :
      if ssShift in ShiftState then
        Result := '?'
      else
        Result := '/';
    192 :
      if ssShift in ShiftState then
        Result := '~'
      else
        Result := '`';
    219 :
      if ssShift in ShiftState then
        Result := '{'
      else
        Result := '"["';
    220 :
      if ssShift in ShiftState then
        Result := '|'
      else
        Result := '\';
    221 :
      if ssShift in ShiftState then
        Result := '}'
      else
        Result := '"]"';
    222 :
      if ssShift in ShiftState then
        Result := '"'
      else
        Result := '''';
    48..57 :
      if ssShift in ShiftState then
      begin
        case nInput of
          48 : Result := ')';
          49 : Result := '!';
          50 : Result := '@';
          51 : Result := '#';
          52 : Result := '$';
          53 : Result := '%';
          54 : Result := '^';
          55 : Result := '&';
          56 : Result := '*';
          57 : Result := '(';
        end;
      end else
        Result := IntToStr(nInput - 48); // 숫자 0 ~ 9
    65..90 :
      begin
        if GetKeyState(VK_CAPITAL) = 0 then
        begin
          if ssShift in ShiftState then
            Result := Char(nInput)
          else
            Result := Char(nInput + 32);
        end else
          if ssShift in ShiftState then
            Result := Char(nInput + 32)
          else
            Result := Char(nInput);
      end;
    end;

  if ssAlt in ShiftState then
    Result := '[Alt]+' + Result;

  if ssCtrl in ShiftState then
    Result := '[Ctrl]+' + Result;

  if ssShift in ShiftState then
    Result := '[Shift]+' + Result;
end;

function GetChoSungIdx(const cJaso: Char): WORD;
begin
  Result := Pos(cJaso, ChoSung);
end;

function GetJungSungIdx(const cJaso: Char): WORD;
begin
  Result := Pos(cJaso, JungSung);
end;

function GetJongSungIdx(const cJaso: Char): WORD;
begin
  Result := Pos(cJaso, JongSung);
end;

function EngCharToHanChar(const cEng: Char): Char;
begin
  Result := cEng;
  case cEng of
    'A',
    'a' : Result := 'ㅁ';
    'B',
    'b' : Result := 'ㅠ';
    'C',
    'c' : Result := 'ㅊ';
    'D',
    'd' : Result := 'ㅇ';
    'e' : Result := 'ㄷ';
    'F',
    'f' : Result := 'ㄹ';
    'G',
    'g' : Result := 'ㅎ';
    'H',
    'h' : Result := 'ㅗ';
    'I',
    'i' : Result := 'ㅑ';
    'J',
    'j' : Result := 'ㅓ';
    'K',
    'k' : Result := 'ㅏ';
    'L',
    'l' : Result := 'ㅣ';
    'M',
    'm' : Result := 'ㅡ';
    'N',
    'n' : Result := 'ㅜ';
    'o' : Result := 'ㅐ';
    'p' : Result := 'ㅔ';
    'q' : Result := 'ㅂ';
    'r' : Result := 'ㄱ';
    'S',
    's' : Result := 'ㄴ';
    't' : Result := 'ㅅ';
    'U',
    'u' : Result := 'ㅕ';
    'V',
    'v' : Result := 'ㅍ';
    'w' : Result := 'ㅈ';
    'X',
    'x' : Result := 'ㅌ';
    'Y',
    'y' : Result := 'ㅛ';
    'Z',
    'z' : Result := 'ㅋ';

    'R' : Result := 'ㄲ';
    'E' : Result := 'ㄸ';
    'Q' : Result := 'ㅃ';
    'T' : Result := 'ㅆ';
    'W' : Result := 'ㅉ';

    'O' : Result := 'ㅒ';
    'P' : Result := 'ㅖ';
  end;
end;

function EngStrToHanStr(const sEng: String; bIgnoreSPKey: Boolean = false): String;
const
  STEP_CHOSUNG   = 0;
  STEP_JUNGSUNG  = 1;
  STEP_JUNGSUNG2 = 2;
  STEP_JONGSUNG  = 3;
  STEP_JONGSUNG2 = 4;

  RESULT_SUCCESS   = 0;
  RESULT_FAIL      = 1;
  RESULT_RETRY     = 2;
  RESULT_ADD_RETRY = 3;

var
  i, nLen,
  nStep: Integer;
  wCombine,
  wJasoTemp,
  wCombineTemp: WORD;
  cJaso,
  cNextJaso: Char;
  bFilterSP: Boolean;

  function ProcessCombine: Integer;
  var
    wTemp: WORD;
  Label LB_EXIT_STEP_JUNGSUNG2;
  Label LB_EXIT_STEP_JONGSUNG2;
  begin
    Result := RESULT_FAIL;

    case nStep of
      STEP_CHOSUNG :
        begin
          wTemp := GetChoSungIdx(cJaso);
          if wTemp > 0 then
          begin
            Result := RESULT_SUCCESS;
            wCombineTemp := wTemp;
            nStep := STEP_JUNGSUNG;
          end;
        end;
      STEP_JUNGSUNG :
        begin
          wTemp := GetJungSungIdx(cJaso);
          if wTemp > 0 then
          begin
            wCombine := ((wCombineTemp - 1) * 21 * 28) + $AC00;
            Result := RESULT_SUCCESS;
            case wTemp of
              9,       // ㅗ
              14,      // ㅜ
              19 :     // ㅡ
                begin
                  wCombineTemp := wTemp;
                  nStep := STEP_JUNGSUNG2;
                end;
              else begin
                Inc(wCombine, (wTemp-1) * 28);
                nStep := STEP_JONGSUNG;
              end;
            end;
          end;
        end;
      STEP_JUNGSUNG2 :
        begin
          nStep := STEP_JONGSUNG;
        // ㅘ, ㅚ, ㅙ, ㅞ,ㅟ... 등등을 처리해 준다 13_1216 16:37:29 sunk
          wJasoTemp := GetJungSungIdx(cJaso);
          case wJasoTemp of
            1 :          // ㅏ
              case wCombineTemp of
                9  : Inc(wCombine, (10-1) * 28); // ㅘ
                14 : Goto LB_EXIT_STEP_JUNGSUNG2;
                19 : Goto LB_EXIT_STEP_JUNGSUNG2;
              end;
            2 :         // ㅐ
              case wCombineTemp of
                9  : Inc(wCombine, (11-1) * 28); // ㅙ
                14 : Goto LB_EXIT_STEP_JUNGSUNG2;
                19 : Goto LB_EXIT_STEP_JUNGSUNG2;
              end;
            5 :
              case wCombineTemp of
                9  : Goto LB_EXIT_STEP_JUNGSUNG2;
                14 : Inc(wCombine, (15-1) * 28); // ㅝ
                19 : Goto LB_EXIT_STEP_JUNGSUNG2;
              end;
            6 :          // ㅔ
              case wCombineTemp of
                9  : Goto LB_EXIT_STEP_JUNGSUNG2;
                14 : Inc(wCombine, (16-1) * 28); // ㅞ
                19 : Goto LB_EXIT_STEP_JUNGSUNG2;
              end;
            21 :        // ㅣ
              case wCombineTemp of
                9  : Inc(wCombine, (12-1) * 28); // ㅚ
                14 : Inc(wCombine, (17-1) * 28); // ㅟ
                19 : Inc(wCombine, (20-1) * 28); // ㅢ
              end;
            else Goto LB_EXIT_STEP_JUNGSUNG2;
          end;
          Result := RESULT_SUCCESS;
          exit;

        LB_EXIT_STEP_JUNGSUNG2 :
          Inc(wCombine, (wCombineTemp-1) * 28);
          Result := RESULT_RETRY;
        end;
      STEP_JONGSUNG :
        begin
          if cJaso = ' ' then
            exit;

          if (cNextJaso <> #0) and
             (GetJungSungIdx(EngCharToHanChar(cNextJaso)) > 0) then
          begin
          // 종성처리 부분이지만 다음 부분이 중성이면
          // 종성처리 하지 않고 초성처리부터 조합하도록 넘긴다 13_1216 16:38:03 sunk
            nStep := STEP_CHOSUNG;
            Result := RESULT_ADD_RETRY;
            exit;
          end;

          wTemp := GetJongSungIdx(cJaso);
          if wTemp > 0 then
          begin
            Result := RESULT_SUCCESS;
            case wTemp of
              2,      // ㄱ
              5,      // ㄴ
              9,      // ㄹ
              18 :    // ㅂ
                begin
                  wCombineTemp := wTemp;
                  nStep := STEP_JONGSUNG2;
                end;
              else begin
                Inc(wCombine, wTemp-1);
                nStep := STEP_CHOSUNG;
              end;
            end;
          end;
        end;
      STEP_JONGSUNG2 :
        if ((cNextJaso <> #0) and
           (GetJungSungIdx(EngCharToHanChar(cNextJaso)) > 0)) or
           (GetJungSungIdx(cJaso) > 0) then
        begin
          Inc(wCombine, wCombineTemp-1);
          nStep := STEP_CHOSUNG;
          Result := RESULT_ADD_RETRY;
        end else begin
      // 종성처리 - ㄳ ㄵ ㄶ ㄺ... 등등을 처리해준다.
          nStep := STEP_CHOSUNG;
          wJasoTemp := GetJongSungIdx(cJaso);
          case wJasoTemp of
            2 :          // ㄱ
              case wCombineTemp of
                2  : Goto LB_EXIT_STEP_JONGSUNG2; // ㄱㄱ
                5  : Goto LB_EXIT_STEP_JONGSUNG2; // ㄴㄱ
                9  : Inc(wCombine, 10-1);         // ㄹㄱ
                18 : Goto LB_EXIT_STEP_JONGSUNG2; // ㅂㄱ
              end;
            17 :          // ㅁ
              case wCombineTemp of
                2  : Goto LB_EXIT_STEP_JONGSUNG2; // ㄱㅁ
                5  : Goto LB_EXIT_STEP_JONGSUNG2; // ㄴㅁ
                9  : Inc(wCombine, 11-1);         // ㄹㅁ
                18 : Goto LB_EXIT_STEP_JONGSUNG2; // ㅂㅁ
              end;
            18 :         // ㅂ
              case wCombineTemp of
                2  : Goto LB_EXIT_STEP_JONGSUNG2; // ㄱㅂ
                5  : Goto LB_EXIT_STEP_JONGSUNG2; // ㄴㅂ
                9  : Inc(wCombine, 12-1);         // ㄹㅂ
                18 : Goto LB_EXIT_STEP_JONGSUNG2; // ㅂㅂ
              end;
            20 :        // ㅅ
              case wCombineTemp of
                2  : Inc(wCombine, 4-1);          // ㄱㅅ
                5  : Goto LB_EXIT_STEP_JONGSUNG2; // ㄴㅅ
                9  : Inc(wCombine, 13-1);         // ㄹㅅ
                18 : Inc(wCombine, 19-1);         // ㅂㅅ
              end;
            23 :          // ㅈ
              case wCombineTemp of
                2  : Goto LB_EXIT_STEP_JONGSUNG2; // ㄱㅈ
                5  : Inc(wCombine, 6-1);          // ㄴㅈ
                9  : Goto LB_EXIT_STEP_JONGSUNG2; // ㄹㅈ
                18 : Goto LB_EXIT_STEP_JONGSUNG2; // ㅂㅈ
              end;
            26 :         // ㅌ
              case wCombineTemp of
                2  : Goto LB_EXIT_STEP_JONGSUNG2; // ㄱㅌ
                5  : Goto LB_EXIT_STEP_JONGSUNG2; // ㄴㅌ
                9  : Inc(wCombine, 14-1);         // ㄹㅌ
                18 : Goto LB_EXIT_STEP_JONGSUNG2; // ㅂㅌ
              end;
            27 :         // ㅍ
              case wCombineTemp of
                2  : Goto LB_EXIT_STEP_JONGSUNG2; // ㄱㅍ
                5  : Goto LB_EXIT_STEP_JONGSUNG2; // ㄴㅍ
                9  : Inc(wCombine, 15-1);         // ㄹㅍ
                18 : Goto LB_EXIT_STEP_JONGSUNG2; // ㅂㅍ
              end;
            28 :        // ㅎ
              case wCombineTemp of
                2  : Goto LB_EXIT_STEP_JONGSUNG2; // ㄱㅎ
                5  : Inc(wCombine, 7-1);          // ㄴㅎ
                9  : Inc(wCombine, 16-1);         // ㄹㅎ
                18 : Goto LB_EXIT_STEP_JONGSUNG2; // ㅂㅎ
              end;
            else Goto LB_EXIT_STEP_JONGSUNG2;
          end;
          Result := RESULT_SUCCESS;
          exit;

        LB_EXIT_STEP_JONGSUNG2 :
          Inc(wCombine, wCombineTemp-1);
          Result := RESULT_ADD_RETRY;
        end;
      else ASSERT(false);
    end;
  end;

  procedure ProcessAfterCombine;
  begin
  // 특수키 제외 시키는 부분에서도 필요해서 따로 뺌 13_1217 16:25:27 sunk
    case nStep of
      STEP_JUNGSUNG  : Result := Result + ChoSung[wCombineTemp]; // 중성 조합시기에 끝났다면 초성찍고 마무리 13_1217 13:42:40 sunk
      STEP_JUNGSUNG2 :
        begin
          Inc(wCombine, (wCombineTemp-1) * 28);
          Result := Result + Char(wCombine);
        end;
      STEP_JONGSUNG2 :
        begin
          Inc(wCombine, wCombineTemp-1);
          Result := Result + Char(wCombine);
        end;
      else
        if wCombine <> 0 then
            Result := Result + Char(wCombine); // 조합중인거 있다면 포함시켜서 마무리 13_1217 13:43:31 sunk
    end;
    wCombine := 0;
    nStep := STEP_CHOSUNG;
  end;

begin
  Result := '';
  bFilterSP := false;
  nStep := 0;
  nLen := Length(sEng);
  wCombine := 0;
  i := 1;

  while i <= nLen do
  begin
    cJaso := EngCharToHanChar(sEng[i]);
    if (i+1) <= nLen then
      cNextJaso := EngCharToHanChar(sEng[i+1])
    else
      cNextJaso := #0;

    if bIgnoreSPKey and not bFilterSP and (cJaso = '[') and (cNextJaso <> '"') then
    begin
      ProcessAfterCombine;

      bFilterSP := true;
      Result := Result + sEng[i];
      Inc(i);
      continue;
    end else
    if bIgnoreSPKey and bFilterSP and (cJaso <> '"') and (cNextJaso = ']') then
    begin
      bFilterSP := false;
      Result := Result + sEng[i] + sEng[i+1];
      Inc(i, 2);
      continue;
    end else
    if bFilterSP then
    begin
      Result := Result + sEng[i];
      Inc(i);
      continue;
    end;

    case ProcessCombine of
      RESULT_SUCCESS   :
        case nStep of
          STEP_CHOSUNG :
            begin
              Result := Result + Char(wCombine);
              wCombine := 0;
            end;
        end;
      RESULT_FAIL      :
        if nStep <> STEP_CHOSUNG then
        begin
          if nStep = STEP_JUNGSUNG then
          begin
            Result := Result + ChoSung[wCombineTemp];
            nStep := STEP_CHOSUNG;
            continue;
          end else
          if wCombine <> 0 then
          begin
            Result := Result + Char(wCombine) + cJaso;
          end else
            Result := Result + cJaso;
          wCombine := 0;
          nStep := STEP_CHOSUNG;
        end else
          Result := Result + cJaso;
      RESULT_RETRY     : continue;
      RESULT_ADD_RETRY :
        begin
          Result := Result + Char(wCombine);
          wCombine := 0;
          continue;
        end;
    end;
    Inc(i);
  end;

// 조합 도중 끝났을때 마무리
  ProcessAfterCombine;
end;

// [Shift], [Ctrl] ... 이렇게 [ ]에 감싼건 다 없애준다. 13_1217 15:19:14 sunk
procedure DeleteSPKey(var sContexts: String);
var
  i, nDelBegin: Integer;
  cOne, cTwo: Char;
begin
  sContexts := StringReplace(sContexts, ']+', ']', [rfReplaceAll]);
  i := Length(sContexts);
  nDelBegin := 0;
  while i > 0 do
  begin
    cOne := sContexts[i];
    if (i-1) > 0 then
      cTwo := sContexts[i-1]
    else
      cTwo := #0;

    if (nDelBegin = 0) and (cOne = ']') and (cTwo <> '"') then
    begin
      nDelBegin := i;
    end else
    if (nDelBegin > 0) and (cTwo = '[') and (cOne <> '"') then
    begin
      Delete(sContexts, i-1, nDelBegin - i + 2);
      Dec(i);
      nDelBegin := 0;
    end;

    Dec(i);
  end;
end;

function ConvHotkeyToStr(dwHotkey: DWORD): String;
var
  nHk: Integer;
  arrChar: array [0..1] of Char;
begin
  Result := '';
  ZeroMemory(@arrChar, SizeOf(arrChar));
  try
    if dwHotkey = 0 then
      exit;

    if (dwHotkey and $FF) >= $70 then // F1 - F24
    begin
      if ((dwHotkey and $FF) >= $90) then
      begin
        case dwHotKey and $FF of
          $90 : Result := 'Num Lock';
          $91 : Result := 'Scroll Lock';
        end;
      end else begin
        nHk := ((dwHotkey and $FF) - $70) + 1;
        Result := 'F' + IntToStr(nHk);
      end;
    end else begin
      Result := GetInputKeyToStr(dwHotKey and $FF);
    end;

//      if (Result <> '') and ((dwHotKey and $700) > 0) then
//        Result := ' + ' + Result;

  // 아래 순서는 일부러 이렇게 함 17_1011 15:29:21 sunk
    if ((dwHotKey and $400) > 0) then
      Result := 'Alt + ' + Result;
    if ((dwHotKey and $100) > 0) then
      Result := 'Shift + ' + Result;
    if ((dwHotKey and $200) > 0) then
      Result := 'Ctrl + ' + Result;
  finally
    if Result = '' then
      Result := 'None';
  end;
end;

procedure PressKeys(const sPress: String; bCtrl: Boolean = false);
var
  i: Integer;
  u: Byte;
begin
  if bCtrl then
    keybd_event(VK_CONTROL, 0, 0, 0);
  try
    for i := 1 to Length(sPress) do
    begin
      u := Byte(sPress[i]);

      keybd_event(u, MapVirtualKey(u, 0),0, 0);
      keybd_event(u, MapVirtualKey(u, 0), KEYEVENTF_KEYUP, 0);
    end;
  finally
    if bCtrl then
      keybd_event(VK_CONTROL, 0, KEYEVENTF_KEYUP, 0);
  end;
end;

procedure PressKey(const dwCode: DWORD);
begin
  keybd_event(dwCode, 0, 0, 0);
  keybd_event(dwCode, 0, KEYEVENTF_KEYUP, 0);
end;

function IsWinKeyDown: Boolean;
begin
  Result := (GetKeyState(VK_LWIN) and $8000 <> 0) or (GetKeyState(VK_RWIN) and $8000 <> 0);
end;

end.
