{
 * Keyman is copyright (C) SIL International. MIT License.
 *
 * Wrapper around Core debug events and actions, interleaved into a single list.
}
unit Keyman.System.Debug.DebugEvent;

interface

uses
  System.Classes,
  System.Generics.Collections,
  System.SysUtils,
  Winapi.Windows,

  debugkeyboard,
  Keyman.System.KeymanCore,
  Keyman.System.KeymanCoreDebug;

const
  MAXSTOREOFFSETS=20;

type
  TAIDebugKeyInfo = record
    VirtualKey: UINT;
	  Modifiers: DWORD;
  end;

  PAIDebugKeyInfo = ^TAIDebugKeyInfo;

  { DebugEvent structure -- an event has occurred }

  TDebugEventActionData = class
    ActionType: km_kbp_action_type;
    dwData: Integer;
    Text: WideString;
  end;

  TDebugEventRuleData = class
    ItemType, Line: Integer;
    Flags: DWord;
    Rule: TKeymanKeyEx;
    Group: TKeymanGroupEx;
    OptionStoreName: string;
    OptionValue: string;
    Key: TAIDebugKeyInfo;
    Context: WideString;
    StoreOffsets: array[0..20] of Word; //TKeymanStoreEx;
    nStores: Integer;
    procedure FillStoreList(event: pkm_kbp_state_debug_item; KeyboardMemory: PChar);
  end;

  TDebugEventType = (etAction, etRuleMatch);

  TDebugEvent = class
  private
    FEventType: TDebugEventType;
    FAction: TDebugEventActionData;
    FRule: TDebugEventRuleData;
    procedure SetEventType(const Value: TDebugEventType);
  public
    constructor Create;
    destructor Destroy; override;
    property Action: TDebugEventActionData read FAction;
    property Rule: TDebugEventRuleData read FRule;
    property EventType: TDebugEventType read FEventType write SetEventType;
  end;

  TDebugEventList = class(TObjectList<TDebugEvent>)
  private
    procedure Action_Char(const character: km_kbp_usv);
    procedure Action_DeleteBack(expected_type: uint8_t;
      expected_value: uintptr_t);
    procedure Action_EmitKeystroke(const key: Word);
    procedure Action_Marker(marker: uintptr_t);
    function AddActionItem(key: Word; action: pkm_kbp_action_item): Boolean;
    procedure AddDebugItem(
      debug: pkm_kbp_state_debug_item;
      debugkeyboard: TDebugKeyboard;
      vk: uint16_t;
      modifier_state: uint16_t
    );
  public
    function AddStateItems(
      state: pkm_kbp_state;
      vk: uint16_t;
      modifier_state: uint16_t;
      debugkeyboard: TDebugKeyboard
    ): Boolean; overload;

    function AddStateItems(
      state: pkm_kbp_state;
      vk: uint16_t;
      modifier_state: uint16_t
    ): Boolean; overload;
  end;

implementation

uses
  kmxfile,
  Unicode;

{ TDebugEvent }

constructor TDebugEvent.Create;
begin
  inherited Create;
  FEventType := etAction;
  FAction := TDebugEventActionData.Create;
end;

destructor TDebugEvent.Destroy;
begin
  FreeAndNil(FAction);
  FreeAndNil(FRule);
  inherited;
end;

procedure TDebugEvent.SetEventType(const Value: TDebugEventType);
begin
  if FEventType <> Value then
  begin
    case FEventType of
      etAction:    FreeAndNil(FAction);
      etRuleMatch: FreeAndNil(FRule);
    end;
    FEventType := Value;
    case FEventType of
      etAction:    FAction := TDebugEventActionData.Create;
      etRuleMatch: FRule := TDebugEventRuleData.Create;
    end;
  end;
end;

function TDebugEventList.AddActionItem(key: Word; action: pkm_kbp_action_item): Boolean;
begin
  Result := True;
  case action._type of
    KM_KBP_IT_CHAR:           Action_Char(action.character);
    KM_KBP_IT_MARKER:         Action_Marker(action.marker); // Correlates to kmn's "deadkey" markers.
    KM_KBP_IT_ALERT:          ; // TODO: The keyboard has triggered a alert/beep/bell.
    KM_KBP_IT_BACK:           Action_DeleteBack(action.backspace.expected_type, action.backspace.expected_value);
    KM_KBP_IT_PERSIST_OPT:    ; // TODO: The indicated option needs to be stored.
    KM_KBP_IT_EMIT_KEYSTROKE: Action_EmitKeystroke(key);
    KM_KBP_IT_CAPSLOCK:       ; // TODO: Caps lock state needs to be updated
    KM_KBP_IT_INVALIDATE_CONTEXT: ;
    else Assert(False, 'Action type '+IntToStr(Ord(action._type))+' is unexpected.');
  end;
end;

procedure TDebugEventList.Action_EmitKeystroke(const key: Word);
var
  event: TDebugEvent;
begin
  case key of
    VK_TAB:    Action_Char(9);  // Emit a tab character
    VK_RETURN: Action_Char(13); // New line character
    VK_BACK:   Action_DeleteBack(Ord(KM_KBP_BT_UNKNOWN), 0);
    else
    begin
      event := TDebugEvent.Create;
      event.EventType := etAction;
      event.Action.ActionType := KM_KBP_IT_EMIT_KEYSTROKE;
      event.Action.dwData := key;
      Add(event);
    end;
  end;
end;

///
/// Insert a UTF-32 character at the insertion point.
/// TODO: define behaviour around selection
///
procedure TDebugEventList.Action_Char(const character: km_kbp_usv);
var
  event: TDebugEvent;
begin
  event := TDebugEvent.Create;
  event.EventType := etAction;
  event.Action.ActionType := KM_KBP_IT_CHAR;
  event.Action.Text := Uni_UTF32CharToUTF16(character);
  Add(event);
end;

///
/// Delete the codepoint preceding the insertion point.
/// TODO: define behaviour around selection
///
procedure TDebugEventList.Action_DeleteBack(
  expected_type: uint8_t;    /// one of KM_KBP_BT_CHAR, KM_KBP_BT_MARKER, KM_KBP_BT_UNKNOWN
  expected_value: uintptr_t  /// used mainly in unit tests
);
var
  event: TDebugEvent;
begin
  event := TDebugEvent.Create;
  event.EventType := etAction;
  event.Action.ActionType := KM_KBP_IT_BACK;
  event.Action.dwData := expected_type;
  Add(event);
end;

///
/// Insert a deadkey marker into the output
///
procedure TDebugEventList.Action_Marker(marker: uintptr_t);
var
  event: TDebugEvent;
begin
  // kmx requires that markers are between 1 and $FFFF
  // ($FFFD in practical terms)
  Assert((marker > 0) and (marker <= High(WORD)));

  event := TDebugEvent.Create;
  event.EventType := etAction;
  event.Action.ActionType := KM_KBP_IT_MARKER;
  event.Action.dwData := marker;
  Add(event);
end;

procedure TDebugEventList.AddDebugItem(
  debug: pkm_kbp_state_debug_item;
  debugkeyboard: TDebugKeyboard;
  vk: uint16_t;
  modifier_state: uint16_t);

  function DiscoverRuleLine: Integer;
  var
    grp, i: Integer;
    group: PKeymanGroup;
  begin
    grp := -1;
    Result := 0;

    if Assigned(debug.kmx_info.group) then
    begin
      group := PKeymanGroup(debug.kmx_info.group);
      for i := 0 to debugkeyboard.Groups.Count - 1 do
        if group.dpName = debugkeyboard.Groups[i].Name then
        begin
          grp := i;
          Break;
        end;
    end;

    case debug._type of
      KM_KBP_DEBUG_END:           Result := debugkeyboard.BeginUnicodeLine;
      KM_KBP_DEBUG_BEGIN:         Result := debugkeyboard.BeginUnicodeLine;
      KM_KBP_DEBUG_GROUP_ENTER,
      KM_KBP_DEBUG_GROUP_EXIT:    if grp > -1 then Result := debugkeyboard.Groups[grp].Line;
      KM_KBP_DEBUG_MATCH_ENTER,
      KM_KBP_DEBUG_MATCH_EXIT:    if grp > -1 then Result := debugkeyboard.Groups[grp].MatchLine;
      KM_KBP_DEBUG_NOMATCH_ENTER,
      KM_KBP_DEBUG_NOMATCH_EXIT:  if grp > -1 then Result := debugkeyboard.Groups[grp].NomatchLine;
    end;
  end;

var
  ev: TDebugEvent;
  rule: PKeymanKey;
  group: PKeymanGroup;
  store: PKeymanStore;
begin
  if not Assigned(debugkeyboard) then Exit;

  ev := TDebugEvent.Create;
  ev.EventType := etRuleMatch;
  ev.Rule.ItemType := debug._type;
  ev.Rule.Flags := debug.flags; // TODO: are these flags right?

  if Assigned(debug.kmx_info.rule) then
  begin
    rule := PKeymanKey(debug.kmx_info.rule);
    ev.Rule.Line := rule.Line;
    ev.Rule.Rule.Key := rule.Key;
    ev.Rule.Rule.Line := rule.Line;
    ev.Rule.Rule.ShiftFlags := rule.ShiftFlags;
    ev.Rule.Rule.dpOutput := rule.dpOutput;
    ev.Rule.Rule.dpContext := rule.dpContext;
    ev.Rule.FillStoreList(debug, debugkeyboard.Memory.Memory);
  end
  else
    ev.Rule.Line := DiscoverRuleLine;

  ev.Rule.Context := debug.kmx_info.Context;
  if Assigned(debug.kmx_info.group) then
  begin
    group := PKeymanGroup(debug.kmx_info.group);
    ev.Rule.Group.dpName := group.dpName;
    ev.Rule.Group.dpMatch := group.dpMatch;
    ev.Rule.Group.dpNoMatch := group.dpNoMatch;
    ev.Rule.Group.fUsingKeys := group.fUsingKeys;
  end;

  Add(ev);

  { Update user interface }

  if ev.Rule.ItemType = KM_KBP_DEBUG_BEGIN then
  begin
    ev.Rule.Key.VirtualKey := vk;
    ev.Rule.Key.Modifiers := modifier_state;
  end;

  if ev.Rule.ItemType = KM_KBP_DEBUG_SET_OPTION then
  begin
    store := PKeymanStore(debug.kmx_info.option.store);
    ev.Rule.OptionStoreName := store.dpName;
    ev.Rule.OptionValue := debug.kmx_info.option.value;
  end;
end;

function TDebugEventList.AddStateItems(
  state: pkm_kbp_state;
  vk: uint16_t;
  modifier_state: uint16_t
): Boolean;
var
  action: pkm_kbp_action_item;
begin
  Result := True;
  action := km_kbp_state_action_items(state, nil);
  while (action._type <> KM_KBP_IT_END) do
  begin
    Result := Result and AddActionItem(vk, action);
    Inc(action);
  end;
end;

function TDebugEventList.AddStateItems(
  state: pkm_kbp_state;
  vk: uint16_t;
  modifier_state: uint16_t;
  debugkeyboard: TDebugKeyboard
): Boolean;
var
  action: pkm_kbp_action_item;
  debug: pkm_kbp_state_debug_item;
  action_index: Integer;
begin
  Result := True;
  debug := km_kbp_state_debug_items(state, nil);
  action := km_kbp_state_action_items(state, nil);
  action_index := 0;
  while debug._type <> KM_KBP_DEBUG_END do
  begin
    if debug.kmx_info.first_action > action_index then
    begin
      while (action._type <> KM_KBP_IT_END) and (action_index < debug.kmx_info.first_action) do
      begin
        Result := Result and AddActionItem(vk, action);
        Inc(action);
        Inc(action_index);
      end;
    end;
    AddDebugItem(debug, debugkeyboard, vk, modifier_state);
    Inc(debug);
  end;

  AddDebugItem(debug, debugkeyboard, vk, modifier_state);

  if action._type = KM_KBP_IT_EMIT_KEYSTROKE then
  begin
    // The EMIT_KEYSTROKE action comes after all rules have completed processing
    Result := Result and AddActionItem(vk, action);
    Inc(action);
  end;

  // By the time we get to the end of rule processing, all actions should have
  // already been undertaken
  Assert(action._type = KM_KBP_IT_END);
end;

{ TDebugEventRuleData }

procedure TDebugEventRuleData.FillStoreList(event: pkm_kbp_state_debug_item; KeyboardMemory: PChar);
  function StoreOffset(kfh: PKeyboardFileHeader; i: Word): PChar;
  begin
    Result := KeyboardMemory;
    Inc(Result, kfh.dpStoreArray);
    while i > 0 do
    begin
      Inc(Result, SizeOf(TKeyboardFileStore));
      Dec(i);
    end;
  end;
begin
  nStores := 0;
//  kfh := PKeyboardFileHeader(KeyboardMemory);
  while event.kmx_info.store_offsets[nStores*2] <> $FFFF do
  begin
    StoreOffsets[nStores] := event.kmx_info.store_offsets[nStores*2+1];
//    Stores[nStores].Store := PKeyboardFileStore(StoreOffset(kfh, di.StoreOffsets[nStores*2]))^;
//    Stores[nStores].MatchPosition := di.StoreOffsets[nStores*2+1];
    Inc(nStores);
  end;
end;

end.
