{
    This file is part of the Free Pascal run time library.
    Copyright (c) 2006 by Karoly Balogh

    Keyboard unit for MorphOS and Amiga and AROS

    See the file COPYING.FPC, included in this distribution,
    for details about the copyright.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

 **********************************************************************}
unit Keyboard;
interface

{$i keybrdh.inc}

{
  Amiga specific function, waits for a system event to occur on the
  message port of the window. This is mainly used in Free Vision to
  give up the Task's timeslice instead of dos.library/Delay() which
  blocks the event handling and ruins proper window refreshing among
  others 
  input: specify a timeout to wait for an event to arrive. this is the
         maximum timeout. the function might return earlier or even
         immediately if there's an event. it's specified in milliseconds
  result: boolean if there is an incoming system event. false otherwise
}

function WaitForSystemEvent(millisec: Integer): boolean;

implementation

uses
   video, exec, intuition, inputevent, mouse, sysutils, keymap, timer;

{$i keyboard.inc}
{$i keyscan.inc}
var
   LastShiftState : Byte;               {set by handler for PollShiftStateEvent}
   OldMouseX : LongInt;
   OldmouseY : LongInt;
   OldButtons: Word;

procedure SysInitKeyboard;
begin
//  writeln('sysinitkeyboard');
{$IFDEF MORPHOS}
  InitKeyMapLibrary;
{$ENDIF}
  LastShiftState := 0;
  OldMouseX := -1;
  OldmouseY := -1;
  OldButtons := 0;
end;

procedure SysDoneKeyboard;
begin

end;

function IsMsgPortEmpty(Port: PMsgPort): Boolean; inline;
begin
  IsMsgPortEmpty := (Port^.mp_MsgList.lh_TailPred = @(Port^.mp_MsgList));
end;

var
  KeyQueue: TKeyEvent;

type 
  RawCodeEntry = record
    rc,n,s,c,a : Word; { raw code, normal, shift, ctrl, alt }
  end;

const
  RCTABLE_MAXIDX = 24;
  RawCodeTable : array[0..RCTABLE_MAXIDX] of RawCodeEntry =
    (
     (rc: 66; n: $0F09; s: $0F00; c: $9400; a: $A500; ), // TAB
     (rc: 68; n: $1C0D; s: $1C0D; c: $1C0A; a: $1C0D; ), // Enter  // shift, alt?
     (rc: 69; n: $011B; s: $011B; c: $011B; a: $0100; ), // ESC    // shift?
     (rc: 70; n: $5300; s: $0700; c: $A300; a: $A200; ), // Delete
     (rc: 71; n: $5200; s: $0500; c: $0400; a: $A200; ), // Insert
     (rc: 72; n: $4900; s: $4900; c: $8400; a: $9900; ), // PgUP   // shift?
     (rc: 73; n: $5100; s: $5100; c: $7600; a: $A100; ), // PgDOWN // shift?

     (rc: 76; n: $4800; s: $4800; c: $8D00; a: $9800; ), // UP     // shift?
     (rc: 77; n: $5000; s: $5000; c: $9100; a: $A000; ), // DOWN   // shift?
     (rc: 78; n: $4D00; s: $4D00; c: $7400; a: $9D00; ), // RIGHT  // shift?
     (rc: 79; n: $4B00; s: $4B00; c: $7300; a: $9B00; ), // LEFT   // shift?
 
     (rc: 80; n: $3B00; s: $5400; c: $5E00; a: $6800; ), // F1
     (rc: 81; n: $3C00; s: $5500; c: $5F00; a: $6900; ), // F2
     (rc: 82; n: $3D00; s: $5600; c: $6000; a: $6A00; ), // F3
     (rc: 83; n: $3E00; s: $5700; c: $6100; a: $6B00; ), // F4
     (rc: 84; n: $3F00; s: $5800; c: $6200; a: $6C00; ), // F5
     (rc: 85; n: $4000; s: $5900; c: $6300; a: $6D00; ), // F6
     (rc: 86; n: $4100; s: $5A00; c: $6400; a: $6E00; ), // F7
     (rc: 87; n: $4200; s: $5B00; c: $6500; a: $6F00; ), // F8
     (rc: 88; n: $4300; s: $5C00; c: $6600; a: $7000; ), // F9
     (rc: 89; n: $4400; s: $5D00; c: $6700; a: $7100; ), // F10
     (rc: 75; n: $8500; s: $8700; c: $8900; a: $8B00; ), // F11
     (rc: 76; n: $8600; s: $8800; c: $8A00; a: $8C00; ), // F12

     (rc: 112; n: $4700; s: $4700; c: $7700; a: $9700; ),// Home    // shift?
     (rc: 113; n: $4F00; s: $4F00; c: $7500; a: $9F00; ) // End     // shift?
    );

function rcTableIdx(rc: LongInt): LongInt;
var
  Counter: LongInt;
begin
  rcTableIdx := -1;
  Counter := 0;
  while (RawCodeTable[Counter].rc <> rc) and (Counter <= RCTABLE_MAXIDX) do
    Inc(Counter);
  if (Counter <= RCTABLE_MAXIDX) then
    rcTableIdx := Counter;
end;

function HasShift(IQual: Word): Boolean; inline;
begin
  HasShift := ((IQual and IEQUALIFIER_LSHIFT) <> 0) or
     ((IQual and IEQUALIFIER_RSHIFT) <> 0);
end;

function HasCtrl(IQual: Word): Boolean; inline;
begin
  HasCtrl := ((IQual and IEQUALIFIER_CONTROL) <> 0);
end;

function HasAlt(IQual: Word): Boolean; inline;
begin
  HasAlt := ((IQual and IEQUALIFIER_LALT) <> 0) or
     ((IQual and IEQUALIFIER_RALT) <> 0);
end;

function rcTableCode(IQual: Word; Idx: LongInt): LongInt;
begin
  if (Idx < 0) or (Idx > RCTABLE_MAXIDX) then
  begin
    rcTableCode := -1;
    Exit;
  end;

  if HasShift(IQual) then
    rcTableCode:=RawCodeTable[Idx].s
  else
    if HasCtrl(IQual) then
      rcTableCode:=RawCodeTable[Idx].c
    else
      if HasAlt(IQual) then
        rcTableCode:=RawCodeTable[Idx].a
      else
        rcTableCode:=RawCodeTable[Idx].n;
end;

procedure setShiftState(IQual: Word);
begin
  LastShiftState := 0;
  if ((IQual and IEQUALIFIER_LSHIFT) <> 0) then
    LastShiftState := LastShiftState or $01;
  if ((IQual and IEQUALIFIER_RSHIFT) <> 0) then
    LastShiftState := LastShiftState or $02;
  if HasCtrl(IQual) then
    LastShiftState := LastShiftState or $04;
  if HasAlt(IQual) then
    LastShiftState := LastShiftState or $08;
  if ((IQual and IEQUALIFIER_NUMERICPAD) <> 0) then
    LastShiftState := LastShiftState or $20;
  if ((IQual and IEQUALIFIER_CAPSLOCK) <> 0) then
    LastShiftState := LastShiftState or $40;
end;

function SysPollKeyEvent: TKeyEvent;
var
  MouseEvent: Boolean;   // got a mouseevent -> do not leave cycle
  SendMouse: Boolean;    // we got a (or many) mouse move  send the last one
  mes: TMouseEvent;      // save mouse message send after cycle -> prevent mouse move stacking
  me: TMouseEvent;
  KeyCode: LongInt;
  OldKeyCode: LongInt;
  KeySet: ^TKeyRecord;   // points to result to set fields directly
  Ret: LongInt;
  //
  iMsg: PIntuiMessage;
  ICode: Word;           // save items from Message
  IQual: Word;
  IClass: Longword;
  MouseX: Integer;
  MouseY: Integer;
  KeyUp: Boolean;        // Event is a key up event
  Buff: array[0..19] of Char;
  ie: TInputEvent;       // for mapchar
begin
  KeyCode := 0;
  SysPollKeyEvent := 0;
  KeySet := @SysPollKeyEvent;
  FillChar(me, SizeOf(TMouseEvent), 0);

  if KeyQueue <> 0 then
  begin
    SysPollKeyEvent := KeyQueue;
    Exit;
  end;
  SendMouse := False;
  repeat
    MouseEvent := False;
    if VideoWindow <> nil then
    begin
      if IsMsgPortEmpty(videoWindow^.UserPort) then
        Break;
    end else
      Exit;
    PMessage(iMsg) := GetMsg(VideoWindow^.UserPort);
    if (iMsg <> nil) then
    begin
      ICode := iMsg^.Code;
      IQual := iMsg^.Qualifier;
      IClass := iMsg^.iClass;
      MouseX := iMsg^.MouseX;
      MouseY := iMsg^.MouseY;
      ReplyMsg(PMessage(iMsg)); // fast reply to system
      SetShiftState(IQual); // set Shift state qualifiers. do this for all messages we get.
      // main event case
      case (IClass) of
        IDCMP_ACTIVEWINDOW: begin
            GotActiveWindow;
          end;
        IDCMP_INACTIVEWINDOW: begin
            // force cursor off. we stop getting IntuiTicks when 
            // the window is inactive, so the blinking stops.
            ToggleCursor(true);
            GotInactiveWindow;
          end;
        IDCMP_INTUITICKS: begin
            ToggleCursor(false);
            MouseX := (MouseX - VideoWindow^.BorderLeft) div 8;
            MouseY := (MouseY - VideoWindow^.BorderTop) div 16;
            if (MouseX >= 0) and (MouseY >= 0) and
               (MouseX < Video.ScreenWidth) and (MouseY < Video.ScreenHeight) and
               ((MouseX <> OldMouseX) or (MouseY <> OldmouseY))
              then begin
//              //writeln('mousemove:',Mousex,'/',Mousey,' oldbutt:',OldButtons);
              // Drawing is very slow so when moving window it will drag behind
              // because the mouse events stack in the messageport
              // -> so we override move until messageport is empty or keyevent is fired
              SendMouse := True;
              MouseEvent := True;
              mes.Action := MouseActionMove;
              mes.Buttons := OldButtons;
              mes.X := MouseX;
              mes.Y := MouseY;
              //PutMouseEvent(me);
            end;
          end;
        IDCMP_CLOSEWINDOW: begin
            //writeln('got close');
            GotCloseWindow;
          end;
        IDCMP_CHANGEWINDOW: begin
            GotResizeWindow;
          end;
        IDCMP_REFRESHWINDOW: begin
            GotRefreshWindow;
          end;
        IDCMP_MOUSEBUTTONS: begin
            MouseEvent := True;
            me.x := (MouseX - videoWindow^.BorderLeft) div 8;  // calculate char position
            me.y := (MouseY - videoWindow^.BorderTop) div 16;
            case ICode of
              SELECTDOWN: begin
                  //writeln('left down!');
                  me.Action := MouseActionDown;
                  OldButtons := OldButtons or MouseLeftButton;
                  me.Buttons := OldButtons;
                  PutMouseEvent(me);
                end;
              SELECTUP: begin
                  //writeln('left up!');
                  me.Action := MouseActionUp;
                  OldButtons := OldButtons and (not MouseLeftButton);
                  me.Buttons := OldButtons;
                  PutMouseEvent(me);
                end;
              MENUDOWN: begin
                  //writeln('right down!');
                  me.Action := MouseActionDown;
                  OldButtons := OldButtons or MouseRightButton;
                  me.Buttons := OldButtons;
                  PutMouseEvent(me);
                end;
              MENUUP: begin
                  //writeln('right up!');
                  me.Action := MouseActionUp;
                  OldButtons := OldButtons and (not MouseRightButton);
                  me.Buttons := OldButtons;
                  PutMouseEvent(me);
                end;
            end;
            //writeln('Buttons: ' , me.Buttons);
          end;
        IDCMP_MOUSEMOVE: begin
            { IDCMP_MOUSEMOVE is disabled now in the video unit,
              according to autodocs INTUITICKS should be enough
              to handle most moves, esp. in a "textmode" app }
            MouseX := (MouseX - VideoWindow^.BorderLeft) div 8;
            MouseY := (MouseY - VideoWindow^.BorderTop) div 16;
            if (MouseX >= 0) and (MouseY >= 0) and
               (MouseX < Video.ScreenWidth) and (MouseY < Video.ScreenHeight) and
               ((MouseX <> OldMouseX) or (MouseY <> OldmouseY))
              then begin
//              //writeln('mousemove:',Mousex,'/',Mousey,' oldbutt:',OldButtons);
              // Drawing is very slow so when moving window it will drag behind
              // because the mouse events stack in the messageport
              // -> so we override move until messageport is empty or keyevent is fired
              SendMouse := True;
              MouseEvent := True;
              mes.Action := MouseActionMove;
              mes.Buttons := OldButtons;
              mes.X := MouseX;
              mes.Y := MouseY;
              //PutMouseEvent(me);
            end;
          end;
        IDCMP_RAWKEY: begin
          // mouse wheel up or down -> pgup and pgdown
          if ICode = 122 then
            ICode := 72;
          if ICode = 123 then
            ICode := 73;
          // get char from rawkey
          KeyUp := (ICode and IECODE_UP_PREFIX) <> 0;   // is key up
          ICode := ICode and not IECODE_UP_PREFIX;      // remove key up from ICode
          ie.ie_Class := IECLASS_RAWKEY;
          ie.ie_SubClass := 0;
          ie.ie_Code := ICode;
          ie.ie_Qualifier := IQual;
          ie.ie_NextEvent := nil;
          Buff[0] := #0;
          Ret := MapRawKey(@ie, @Buff[0], 1, nil);
          KeyCode := Ord(Buff[0]);
          KeySet^.KeyCode := Ord(Buff[0]);         // if maprawkey does not work it still is 0
          KeySet^.ShiftState := LastShiftState;    // shift state set before the case
          KeySet^.Flags := 0;
          if keyup then                            // we do not need key up events up to now
          begin
            KeySet^.Flags := KeySet^.Flags or kbReleased; // kbReleased does work but make strange effects
            SysPollKeyEvent := 0;
            Exit;
          end;
          // check our hard coed list if there is an entry -> leave it must be right ;)
          // F-keys, cursor, esc, del, ins, del, pgup, pgdown, pos, end, enter, tab
          if rcTableCode(IQual,rcTableIdx(ICode)) >= 0 then
          begin
            KeyCode := rcTableCode(IQual,rcTableIdx(ICode));
            KeySet^.KeyCode := KeyCode;
            KeySet^.Flags := kbPhys;
          end else
          begin
            // left alt or ctrl is pressed -> check for alternative Scancode -> commando
            if ((IQual and IEQUALIFIER_LALT) <> 0) or  HasCtrl(IQual) then
            begin
              OldKeyCode := KeyCode;  // save keycode if nothing found
              KeyCode := 0;
              ie.ie_Class := IECLASS_RAWKEY;    // get keycode without qualifier easier case
              ie.ie_SubClass := 0;
              ie.ie_Code := ICode;
              ie.ie_Qualifier := 0;
              ie.ie_NextEvent := nil;
              Buff[0] := #0;
              Ret := MapRawKey(@ie, @Buff[0], 1, nil);
              if Ret > 0 then
              begin
                if ((IQual and IEQUALIFIER_LALT) <> 0) then   // check left alt keycodes
                begin
                  case Buff[0] of        // Alt - keys already defined
                    'a': KeyCode := kbAltA shl 8;
                    'b': KeyCode := kbAltB shl 8;
                    'c': KeyCode := kbAltC shl 8;
                    'd': KeyCode := kbAltD shl 8;
                    'e': KeyCode := kbAltE shl 8;
                    'f': KeyCode := kbAltF shl 8;
                    'g': KeyCode := kbAltG shl 8;
                    'h': KeyCode := kbAltH shl 8;
                    'i': KeyCode := kbAltI shl 8;
                    'j': KeyCode := kbAltJ shl 8;
                    'k': KeyCode := kbAltK shl 8;
                    'l': KeyCode := kbAltL shl 8;
                    'm': KeyCode := kbAltM shl 8;
                    'n': KeyCode := kbAltN shl 8;
                    'o': KeyCode := kbAltO shl 8;
                    'p': KeyCode := kbAltP shl 8;
                    'q': KeyCode := kbAltQ shl 8;
                    'r': KeyCode := kbAltR shl 8;
                    's': KeyCode := kbAltS shl 8;
                    't': KeyCode := kbAltT shl 8;
                    'u': KeyCode := kbAltU shl 8;
                    'v': KeyCode := kbAltV shl 8;
                    'w': KeyCode := kbAltW shl 8;
                    'x': KeyCode := kbAltX shl 8;
                    'y': KeyCode := kbAltY shl 8;
                    'z': KeyCode := kbAltZ shl 8;
                  end;
                end else
                begin
                  case Buff[0] of      // ctrl - keys defined in FreeVision/drivers.pas -> so here direct numbers
                    'a': KeyCode := $1E01;
                    'b': KeyCode := $3002;
                    'c': KeyCode := $2E03;
                    'd': KeyCode := $2004;
                    'e': KeyCode := $1205;
                    'f': KeyCode := $2106;
                    'g': KeyCode := $2207;
                    'h': KeyCode := $2308;
                    'i': KeyCode := $1709;
                    'j': KeyCode := $240a;
                    'k': KeyCode := $250b;
                    'l': KeyCode := $260c;
                    'm': KeyCode := $320d;
                    'n': KeyCode := $310e;
                    'o': KeyCode := $180f;
                    'p': KeyCode := $1910;
                    'q': KeyCode := $1011;
                    'r': KeyCode := $1312;
                    's': KeyCode := $1F13;
                    't': KeyCode := $1414;
                    'u': KeyCode := $1615;
                    'v': KeyCode := $2F16;
                    'w': KeyCode := $1117;
                    'x': KeyCode := $2D18;
                    'y': KeyCode := $1519;
                    'z': KeyCode := $2C1A;
                  end;
                end;
              end;
              if KeyCode <= 0 then      // nothing found restore keycode
                KeyCode := OldKeyCode;
              KeySet^.KeyCode := KeyCode;
              KeySet^.Flags := kbPhys;
            end;
          end;
          if keycode <= 0 then
          begin
            KeySet^.KeyCode := 0;
            KeyCode := 0;
          end;
          //writeln('raw keycode: ',iMsg^.code, ' -> $', IntToHex(keycode,4), ' ret: ', ret);
        end;
        else begin
          KeyCode := 0;
        end;
      end;
    end else
      Break;
  until (not MouseEvent);
  //
  if SendMouse then
  begin
    PutMouseEvent(mes);
    OldMouseX:=Mousex;
    OldmouseY:=Mousey;
  end;
  if KeyCode <= 0 then     // no keycode found then also delete flags and shiftstate
    SysPollKeyEvent := 0
  else
    KeyQueue:=SysPollKeyEvent;
end;

function SysGetKeyEvent: TKeyEvent;
var
  Res: TKeyEvent;
  me: TMouseEvent;
begin
  Res := 0;
  if VideoWindow <> nil then
  begin
    if KeyQueue <> 0 then
    begin
      SysGetKeyEvent := KeyQueue;
      KeyQueue := 0;
      Exit;
    end;
    repeat
      WaitPort(VideoWindow^.UserPort);
      Res := SysPollKeyEvent;
    until Res <> 0;
  end else
  begin
    me.Action := MouseActionDown;
    me.Buttons := MouseRightButton;
    PutMouseEvent(me);
  end;
  SysGetKeyEvent := Res;
end;

{function SysTranslateKeyEvent(KeyEvent: TKeyEvent): TKeyEvent;
begin
end;}

function SysGetShiftState: Byte;
begin
  //writeln('SysgetShiftState:',hexstr(LastShiftState,2));
  SysGetShiftState := LastShiftState;
end;

var
  waitTPort:  PMsgPort;
  waitTimer: PTimeRequest;
  waitTimerFired: boolean;

function WaitForSystemEvent(millisec: Integer): boolean;
var
  windowbit: PtrUInt;
  timerbit: PtrUInt;
  recvbits: PtrUInt;
begin
  WaitForSystemEvent:=false;
  if waitTPort = nil then
  begin
    { this really shouldn't happen, but it's enough to avoid a
      crash if the timer init failed during startup }
    if VideoWindow <> nil then
      WaitPort(VideoWindow^.UserPort);
    exit;
  end;

  windowbit:=0;
  if VideoWindow <> nil then
  begin
    if not IsMsgPortEmpty(VideoWindow^.UserPort) then
    begin
      WaitForSystemEvent:=true;
      exit;
    end;
    windowbit:=1 shl (VideoWindow^.UserPort^.mp_SigBit);
  end;
  timerbit:=0;
  if waitTPort <> nil then
    timerbit:=1 shl (waitTPort^.mp_SigBit);
  if (windowbit or timerbit) = 0 then exit;

  if not waitTimerFired then
  begin
    waitTimer^.tr_node.io_Command:=TR_ADDREQUEST;
    waitTimer^.tr_time.tv_secs:=millisec div 1000;
    waitTimer^.tr_time.tv_micro:=(millisec mod 1000) * 1000;
    SendIO(PIORequest(waitTimer));
    waitTimerFired:=true;
  end;

  recvbits:=Wait(windowbit or timerbit);
  if (recvbits and windowbit) > 0 then
    WaitForSystemEvent:=true;

  if waitTimerFired then 
  begin
    AbortIO(PIORequest(waitTimer));
    WaitIO(PIORequest(waitTimer));
    SetSignal(0,timerbit);
    waitTimerFired:=false;
  end;
end;

procedure DoneSystemEventWait;
begin
  if assigned(waitTimer) then
  begin
    if waitTimerFired then 
    begin
      AbortIO(PIORequest(waitTimer));
      WaitIO(PIORequest(waitTimer));
      waitTimerFired:=false;
    end;
    CloseDevice(PIORequest(waitTimer));
    DeleteIORequest(PIORequest(waitTimer));
    waitTimer:=nil;
  end;
  if assigned(waitTPort) then
  begin
    DeleteMsgPort(waitTPort);
    waitTPort:=nil;
  end;
end;

procedure InitSystemEventWait;
var
  initOK: boolean;
begin
  waitTimerFired:=false;
  waitTPort:=CreateMsgPort();
  if assigned(waitTPort) then
  begin
    waitTimer:=PTimeRequest(CreateIORequest(waitTPort,sizeof(TTimeRequest)));
    if assigned(waitTimer) then
    begin
      if OpenDevice(TIMERNAME,UNIT_VBLANK,PIORequest(waitTimer),0) = 0 then
      begin
        initOK:=true;
        waitTimerFired:=false;
      end;
    end;
  end;
  if not initOK then begin
    {* this really shouldn't happen if everything is OK with the system *}
    SysDebugLn('FPC RTL-Console: SystemEventWait Initialization failed!');
    DoneSystemEventWait;
  end;
end;


const
  SysKeyboardDriver : TKeyboardDriver = (
    InitDriver : @SysInitKeyBoard;
    DoneDriver : @SysDoneKeyBoard;
    GetKeyevent : @SysGetKeyEvent;
    PollKeyEvent : @SysPollKeyEvent;
    GetShiftState : @SysGetShiftState;
//    TranslateKeyEvent : @SysTranslateKeyEvent;
    TranslateKeyEvent : Nil;
    TranslateKeyEventUnicode : Nil;
  );


initialization
  SetKeyBoardDriver(SysKeyBoardDriver);
  InitSystemEventWait;
finalization
  DoneSystemEventWait;
end.
