{
    This file is part of Chentrah,
    Copyright (C) 2004-2012 Anton Rzheshevski (chebmaster@mail.ru).

    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation, either version 2 of the License, or
    (at your option) any later version.

    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.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with this program.  If not, see http://www.gnu.org/licenses/

 **********************************************************************}

{$ifndef windows}
  //Stubs for Linux
  constructor TGamepadManager.Create;
  begin
    inherited;
    with MotherState do begin
      GamepadPresent:= false;
      GamepadName:= RuEn('отсутствует','not present');
      GamepadAbsenceReason:= RuEn('Поддерживается только для платформы Windows','Only supported on the Windows platform')
    end;
  end;
  destructor TGamepadManager.Destroy;
  begin
    inherited;
  end;
  procedure TGamepadManager.Pulse; begin end;
{$else}

  function TGamepadmanagerThread.Poll: boolean;
  var i: integer;
    k: TKey;
    BitMask: word;
  begin
    if MotherState.GamepadPresent then begin
      FillChar(State, Sizeof(State), 0);
      if HasFocus or WindowManager.Focus then begin
        if 0 <> XInputGetState (0, State) then begin
          AddLog(RuEn('Геймпад "%0" был отсоединён!','Controller "%0" detached!'),[MotherState.GamepadName]);
          MotherState.GamepadPresent:= false;
          MotherState.GamepadRumble[0]:= 0;
          MotherState.GamepadRumble[1]:= 0;
          MotherState.GamepadRumble[2]:= 0;
          MotherState.GamepadRumble[3]:= 0;
          Exit(false);
        end;
//AddLog('--%0',[State.dwPacketNumber]);
      end else begin
        if HasFocus and not WindowManager.Focus then begin
          if assigned(XInputEnable) then XInputEnable(false); //not supported in vanilla Vista and 7
          Exit(false);
        end;
        if not HasFocus and WindowManager.Focus then begin
          if assigned(XInputEnable) then XInputEnable(true);
          Exit(false);
        end;
      end;

      //the actual analysis
      with State.Gamepad do begin
        ProcessKey(KEY_GAMEPAD_LEFT_TRIGGER, (bLeftTrigger / 255.0) > parent.LeftTriggerThreshold);
        ProcessKey(KEY_GAMEPAD_RIGHT_TRIGGER, (bRightTrigger / 255.0) > parent.RightTriggerThreshold);
        for k:= KEY_GAMEPAD_DPAD_UP to KEY_GAMEPAD_Y do begin
          BitMask:= XinputButtons[k];
          ProcessKey(k, (BitMask and wButtons) = BitMask);
        end;
        ProcessJoystick(JoytypePrimary, sThumbLX, sThumbLY, 32767);
        ProcessJoystick(JoytypeRightThumb, sThumbRX, sThumbRY, 32767);
        ProcessJoystick(JoytypeLeftTrigger, 0, bLeftTrigger, 255);
        ProcessJoystick(JoytypeRightTrigger, 0, bRightTrigger, 255);
      end;
      Result:= true;
    end else begin
       for i:=0 to GAMEPAD_PRESENCE_CHECK_PERIOD div 100 do begin
         Sleep(100);
         if Terminated then Exit(false);
       end;
       parent.Init;
       Result:= false;
    end;
  end;

{   TjoyState = record
    ix, iy, ixmax,ixmin,iymax,iymin: integer;
    x, y: float;
    rdtstamp: qword;
  end;
 }

  procedure TGamepadmanagerThread.ProcessJoystick(t: TJoystickType; rawix, rawiy, range: integer);
    var
      delta: double;
      newx, newy: float;
{    function RawToFloat(var vmax, vmin: integer; v, range: integer): float;
    begin
      if v > 0 then begin
        vmax:= Math.max(vmax, Math.min(range, v)); //dynamic range correction
        Exit(v / vmax);
      end;
      if v < 0 then begin
        vmin:= Math.min(vmin, Math.max(-range,v));
        Exit(- v / vmin);
      end;
      Result:= 0.0;
    end; }
  begin
    with JoyState[t] do begin
{      ixmax:= max(ixmax, round(0.9 * range));
      ixmin:= min(ixmin, round(-0.9 * range));
      iymax:= max(iymax, round(0.9 * range));
      iymin:= min(iymin, round(-0.9 * range));
}
      if (rawix <> ix) or (rawiy <> iy) then begin
         newx:= rawix/range;//RawToFloat(ixmax, ixmin, rawix, range);
         newy:= rawiy/range;//RawToFloat(iymax, iymin, rawiy, range);
         delta:= Math.max(0.001, 0.001*UsecByTsc(@rdtstamp));
         parent.AddEvent(t, newx, newy, (newx - x) / delta, (newy - y) /delta);
//         AddLog('%0   |   %1   |   %2',[delta, (newx - x) / delta, (newy - y) /delta]);
         x:= newx;
         y:= newy;
         ix:= rawix;
         iy:= rawiy;
      end;
    end;
  end;

  procedure TGamepadmanagerThread.ProcessKey(k: TKey; pressed: boolean);
  begin
//if pressed then addlog('++ %0',[ord(k)]);
    if (ord(k) < 0) or (ord(k) > 255) then Die('Invalid pressed key, scan code = %0', [ord(k)]);
    if pressed and (not KeyState[k]) then begin
      parent.AddEvent(CGM_PRESS, k);
      Self.KeyState[k]:= true;
//addlog('++ %0',[IntToHex(ord(k),2)]);
      Exit;
    end;
    if (not pressed) and KeyState[k] then begin
      parent.AddEvent(CGM_RELEASE, k);
      Self.KeyState[k]:= false;
//addlog('-- %0',[IntToHex(ord(k),2)]);
    end;
  end;

  procedure TGamepadmanagerThread.ProcessRumble(seconds_passed: float);
  var
    i, j: integer;
    changed: boolean = No;
    Vibro: T_XINPUT_VIBRATION;
  begin
    for i:=0 to 1 do begin
      Vibro.Amp[i]:= trunc(65535 * Math.max(0.0, Math.min(1.0, MotherState.GamepadRumble[i * 2])));
      RumbleTime[i]+= seconds_passed;
      if MotherState.GamepadRumble[i * 2] <> RumbleAmp[i] then begin
        RumbleAmp[i]:= MotherState.GamepadRumble[i * 2];
        RumbleTime[i]:= 0;
        changed:= Yes;
      end;
      if RumbleTime[i] >= Math.min(GAMEPAD_RUMBLE_DURATION_LIMIT, MotherState.GamepadRumble[i * 2 + 1]) then begin
        changed:= Yes;
        Vibro.Amp[i]:= 0;
      end;
    end;
    if changed then XInputSetState(0, Vibro);
  end;

  constructor TGamepadmanagerThread.Create(_parent:  TGamepadManager);
  begin
    if MotherState.DebugMode then AddLog('  Creating the background polling thread...');
    inherited Create(Yes, 100000);
    parent:= _parent;
    FreeOnTerminate:= No;
    Priority:= tpHigher;
    Resume;
    if MotherState.DebugMode then AddLogOk();
  end;

  procedure TGamepadmanagerThread.Execute;
  var delta: double;
  begin
    try
      try
        repeat
          ProcessRumble(1.0 / GAMEPAD_POLLING_FREQUENCE);
          if Poll then begin
            delta:= UsecByTsc(@PreviousRateCullTsc) - 1000000.0 / GAMEPAD_POLLING_FREQUENCE;
            delta:= 1.0 * Sign(delta) * Math.min(abs(delta), 1000000.0 / 30);
            if delta < 0 then begin
              CgeSleep(-Round(delta / 1000.0));
              delta:= 0;
            end;
            UsecByTsc(@PreviousRateCullTsc);
            PreviousRateCullTsc+= round(delta);
          end else begin
            Sleep(100);
          end;
        until Terminated;
      except
        Die(RuEn('Крах в подпрограмме фонового опроса геймпада','Game controller background polling routine crashed'));
      end;
    except
      parent.ThreadCrashed:= StopDying;
//AddLog('----%0',[parent.ThreadCrashed]);
    end;
  end;

  constructor TGamepadManager.Create;
    var
      i: integer;
    function _GetProcAddress(ProcName: ansistring; ignore_error: boolean = false): Pointer;
    var
      baseaddr: pointer;
      exename: string;
    begin
      Result:=GetProcAddress(Dll, PChar(ProcName));
      if Assigned(Result) and MotherState.DebugMode then begin
        {$ifdef cpu64}
        AddLog('  %0() at %1', [ProcName, Result]);
        {$else}
        GetModuleByAddr(Result, baseaddr, exename);
        if (DllName <> exename) and (DllName <> '')
          then AddLog('  %0() at %1 in %2 (wrapper %3)', [ProcName, Result, exename, DllName])
          else AddLog('  %0() at %1 in %2', [ProcName, Result, exename]);
        {$endif}
      end;
      if not Assigned(Result) then begin
        if ignore_error then begin
          if MotherState.DebugMode then AddLog(MI_ERROR_FUNCTION_NOT_FOUND_IN_DLL, [ProcName, DllName]);
        end
        else MotherState.GamepadAbsenceReason:= PervertedFormat(
          MessageContainer[MI_ERROR_FUNCTION_NOT_FOUND_IN_DLL], [ProcName, DllName]);
      end;
    end;

    function LoadDll: boolean;
    begin
      if MotherState.DebugMode then AddLog('  Loading %0...', [DllName]);
      Dll:= LoadLibrary(PChar(DllName));
      Result:= Dll <> 0;
      if MotherState.DebugMode then
        if not result then AddLogComment('FAILED.')
                      else AddLogComment('Ok');
    end;

  begin
    AddLog(RuEn('Геймпад:','Сontroller:'));
    inherited;

    InitCriticalSection(CriticalSection);

    LeftTriggerThreshold:= 0.3;
    RightTriggerThreshold:= 0.3;
    LeftThumbDeadZone:=0;
    RightThumbDeadZone:=0;

    MotherState.GamepadPresent:= false;
    MotherState.GamepadName:= RuEn('отсутствует','not present');

    MotherState.GCToMouseSpeedMultiplier:= Config.IntChk['game_controller', 'mouse_emul_speed_multiplier', 100, 3000];
    MotherState.GCToMouseAccelThreshold:= Config.IntChk['game_controller', 'mouse_emul_acceleration_threshold', 0, 90];
    MotherState.GCToMouseAccelMultiplier:= Config.IntChk['game_controller', 'mouse_emul_acceleration_multiplier', 0, 3000];


    i:=0;
     repeat
       DllName:= Config.Str['microsoft_xinput', 'dll_' + IntToStr(i)];
       if DllName='' then break;
       if LoadDll() then break;
       inc(i);
     until false;

    if DllName = '' then DllName:= Config.Str['microsoft_xinput','dll_0'];
    if Dll = 0 then begin
      MotherState.GamepadAbsenceReason:= RuEn(
        'не удалось загрузить '+DllName+', убедитесь что DirectX установлен правильно',
        'failed to load '+DllName+', make sure latest DirectX is installed');
    end else begin;
      DllName:= GetDLLFileName(Dll);
      pointer(XInputGetCapabilities):= _GetProcAddress('XInputGetCapabilities');
    end;
    if Assigned(XInputGetCapabilities) then begin
      Init(true);
      pointer(XInputGetState):= _GetProcAddress('XInputGetState');
      pointer(XInputSetState):= _GetProcAddress('XInputSetState');
      pointer(XInputEnable):= _GetProcAddress('XInputEnable', true);
    end;
    if MotherState.GamepadAbsenceReason <> '' then begin
      AddLogComment('%0 (%1)',[MotherState.GamepadName, MotherState.GamepadAbsenceReason]);
      Exit;
    end;
    Thread:= TGamepadmanagerThread.Create(Self);
    if not MotherState.GamepadPresent then begin
      MotherState.GamepadAbsenceReason:= RuEn(
        'драйвер загрузился нормально, но устройство отсутствует, не является Xbox360-совместимым или переключено на канал отличный от №1.',
        'driver loaded Ok but no controller is present or it isn''t Xbox360-compatible or is set to a channel other than #1');
      AddLogComment('%0 (%1)',[MotherState.GamepadName, MotherState.GamepadAbsenceReason]);
    end
    else AddLogComment(MotherState.GamepadName);
  end;

  function TGamepadManager.Init(just_starting: boolean = false): boolean;
    var
      caps: T_XINPUT_CAPABILITIES;
  begin
    FillChar(caps, sizeof(caps), 0);
    MotherState.GamepadPresent:= 0 = XInputGetCapabilities(0, XINPUT_FLAG_GAMEPAD, caps);
    if MotherState.GamepadPresent then begin
      MotherState.GamepadName:= RuEn('Совместимый с Xbox 360','Xbox 360 compatible');
      if not just_starting then AddLog(RuEn('Обнаружен геймпад: "%0"','Controller detected: "%0"'),[MotherState.GamepadName]);
      MotherState.GamepadRumble[0]:= 0.15; //a test shake
      MotherState.GamepadRumble[1]:= 0.8;
      MotherState.GamepadRumble[2]:= 0.7;
      MotherState.GamepadRumble[3]:= 0.1;
    end;
  end;

  destructor TGamepadManager.Destroy;
  var Vibro: T_XINPUT_VIBRATION;
  begin
    if Assigned(Thread) then Thread.Free;
    if Assigned(XInputSetState) then begin
      if assigned(XInputEnable) then XInputEnable(true);
      Vibro.wLeftMotorSpeed:=0;
      Vibro.wRightMotorSpeed:=0;
      XInputSetState(0, Vibro);
      if assigned(XInputEnable) then XInputEnable(false);
    end;
    if Dll <> 0 then begin
      FreeLibrary(Dll);
    end;
    DoneCriticalsection(CriticalSection);
  end;

  procedure TGamepadManager.Pulse;
  var i, k, n, ne: integer;
  begin
    if ThreadCrashed <> '' then Die(ThreadCrashed); //Relay the error message
    //Dumping the local event queue into the global one.
//exit;
    if MaxEvent > 0 then begin
      EnterCriticalSection(CriticalSection);
      try
{
Yeah, I screwed myself good here.
Forgot that module has MotherState.InputEvents defined as a chain of pointers, _not_ as an array.
So, unless it's plugged with an extra empty one (generated in NewInputEvent()),
module's ProcessMessages goes past the array end merrily exploring random memory :((((((
        k:= length(MotherState.InputEvents);
        SetLength(MotherState.InputEvents, k + MaxEvent);
        for i:= 1 to MaxEvent do MotherState.InputEvents[k + i - 1]:= Events[i];
}

        for i:= 1 to MaxEvent do begin
          ne:= NewInputEvent();
          MotherState.InputEvents[ne]:= Events[i];
        end;
        MaxEvent:= 0;
      finally
        LeaveCriticalSection(CriticalSection);
      end;
    end;
  end;

  procedure TGamepadManager.AddEvent(_evtype: integer; _key: TKey);
  begin
//    if MaxEvent = (GAMEPAD_EVENT_QUEUE_LIMIT - 1) then AddLog(RuEn('Переполнение очереди событий геймпада!','Controller event queue full!'));
    if MaxEvent = GAMEPAD_EVENT_QUEUE_LIMIT then begin
//      AddLog(RuEn('Переполнение очереди событий геймпада!','Controller event queue full!'));
      Exit;
    end;
    if (ord(_key) < 0) or (ord(_key) > 255) then Die('Invalid pressed key, scan code = %0', [ord(_key)]);
    EnterCriticalSection(CriticalSection);
      if MaxEvent < GAMEPAD_EVENT_QUEUE_LIMIT then begin
        inc(MaxEvent);
        with Events[MaxEvent] do begin
          EventType:= _evtype;
          key:= _key;
          mx:= WindowManager.MouseX;
          my:= WindowManager.MouseY;
        end;
      end;
    LeaveCriticalSection(CriticalSection);
  end;

  procedure TGamepadManager.AddEvent(joy_type: TJoystickType; _x, _y, _dx, _dy: float); overload;
  begin
//    if MaxEvent = (GAMEPAD_EVENT_QUEUE_LIMIT - 1) then AddLog(RuEn('Переполнение очереди событий геймпада!','Controller event queue full!'));
    if MaxEvent = GAMEPAD_EVENT_QUEUE_LIMIT then begin
//      AddLog(RuEn('Переполнение очереди событий геймпада!','Controller event queue full!'));
      Exit;
    end;
//addlog('  тип %0, x:y = %1:%2, dx:dy = %3:%4',[joy_type, round(_x*100),round(_y*100),round(_dx*100),round(_dy*100)]);
    EnterCriticalSection(CriticalSection);
    if MaxEvent < GAMEPAD_EVENT_QUEUE_LIMIT then begin
      inc(MaxEvent);
      with Events[MaxEvent] do begin
        EventType:= CGM_JOYSTICK;
        JoyType:= joy_type;
        joyx:= _x;
        joyy:= _y;
        joydx:= _dx;
        joydy:= _dy;
      end;
      if joy_type = JoytypeRightThumb then WindowManager.AddRightThumbToMouseMovement(_x, _y);
    end;
    LeaveCriticalSection(CriticalSection);
  end;


{$endif}
