'OHRRPGCE - gfx_sdl/gfx_sdl2 shared routines
'(C) Copyright 1997-2020 James Paige, Ralph Versteegen, and the OHRRPGCE Developers
'Dual licensed under the GNU GPL v2+ and MIT Licenses. Read LICENSE.txt for terms and disclaimer of liability.
'
' This file contains code common to gfx_sdl and gfx_sdl2 graphics backends.
' Note that it is included directly into gfx_sdl.bas and gfx_sdl2.bas and compiled twice
' with macros SDL12 and SDL2.


#ifdef USE_SDL2
 DECLARE FUNCTION sdl2_update_gamepad(joynum as integer, state as IOJoystickState ptr) as integer
#endif


SUB GFX_SDL(close)()
  IF libsdl_handle THEN
    dylibfree(libsdl_handle)
    libsdl_handle = NULL
  END IF

  'debug "quit_joystick_subsystem"
  IF SDL_WasInit(SDL_INIT_JOYSTICK) THEN
    quit_joystick_subsystem()
  END IF

  'debug "quit_video_subsystem"
  IF SDL_WasInit(SDL_INIT_VIDEO) THEN
    quit_video_subsystem()

    IF SDL_WasInit(0) = 0 THEN
      SDL_Quit()
    END IF
  END IF
END SUB


'==============================================================================
'                                   Joysticks

DIM SHARED joystickhandles(maxJoysticks - 1) as SDL_Joystick ptr
DIM joystickinfo(maxJoysticks - 1) as JoystickInfo
'The following is only used by gfx_sdl2, for button press events, to track
'fast presses that are released between ticks.
DIM SHARED joystickbuttons(maxJoysticks - 1) as uinteger

LOCAL SUB quit_joystick_subsystem()
  FOR i as integer = 0 TO small(SDL_NumJoysticks(), maxJoysticks) - 1
    IF joystickhandles(i) <> NULL THEN SDL_JoystickClose(joystickhandles(i))
    joystickhandles(i) = NULL
    joystickinfo(i).instance_id = -1
  NEXT
  SDL_QuitSubSystem(SDL_INIT_JOYSTICK)
END SUB

'Return -1 if not found
LOCAL FUNCTION instance_to_joynum(instance_id as integer) as integer
  FOR joynum as integer = 0 TO UBOUND(joystickinfo)
    IF joystickinfo(joynum).instance_id = instance_id THEN RETURN joynum
  NEXT
  RETURN -1
END FUNCTION

'Check a joystick is valid, and open it if not open yet, reading info
'Same return values as io_get_joystick_state in gfx.bi
LOCAL FUNCTION get_joystick(byval joynum as integer) as integer
  IF joynum < 0 ORELSE joynum >= maxJoysticks THEN RETURN 1
  IF joynum > SDL_NumJoysticks() - 1 THEN RETURN 1
  'FIXME: this doesn't handle joysticks getting renumbered as they are plugged/unplugged
  '(SDL 1.2 doesn't support that, though)
  DIM byref joy as SDL_Joystick ptr = joystickhandles(joynum)
  IF joy THEN RETURN 0  'Success

  joy = SDL_JoystickOpen(joynum)
  IF joy = NULL THEN
    debug "Couldn't open joystick " & joynum & ": " & *SDL_GetError
    RETURN 1
  END IF

  STATIC joystick_counter as integer

  WITH joystickinfo(joynum)
    .structsize = JOYSTICKINFO_SZ
    DIM joyname as const zstring ptr
    #ifdef USE_SDL2
      joyname = SDL_JoystickNameForIndex(joynum)
    #else
      joyname = SDL_JoystickName(joynum)
    #endif
    IF joyname = NULL THEN joyname = @"(NULL)"
    .name = *joyname

    .num_buttons = SDL_JoystickNumButtons(joy)
    .num_axes = SDL_JoystickNumAxes(joy)
    .num_hats = SDL_JoystickNumHats(joy)
    .num_balls = SDL_JoystickNumBalls(joy)
    #ifdef USE_SDL2
      .instance_id = SDL_JoystickInstanceID(joy)

      DIM GUID as SDL_JoystickGUID = SDL_JoystickGetGUID(joy)
      memcpy(@.model_guid(0), @GUID, sizeof(SDL_JoystickGUID))

      DIM GUIDstr as zstring * 33
      SDL_JoystickGetGUIDString(GUID, @GUIDstr[0], 32)
      debuginfo "Joystick " & joynum & " GUID " & GUIDstr & " instance_id " & .instance_id

      'Open the controller, but no need to store the pointer because we can look it up again
      .have_bindings = SDL_IsGameController(joynum)
      IF .have_bindings THEN
        VAR controller = SDL_GameControllerOpen(joynum)
        IF controller THEN
          'This name may vary from SDL_JoystickNameForIndex
          debuginfo " Opened as gamecontroller " & *SDL_GameControllerName(controller)
          IF SDL_GameControllerTypeForIndex THEN  'Dynamically loaded (SDL 2.0.12+)
            debuginfo " Type: " & SDL_GameControllerTypeForIndex(joynum)
          END IF
          DIM mappingstr as zstring ptr = SDL_GameControllerMapping(controller)
          debuginfo mappingstr
          SDL_free(mappingstr)
        ELSE
          debug "Couldn't open gamecontroller " & joynum & ": " & *SDL_GetError
          .have_bindings = NO
        END IF
      END IF

    #else
      joystick_counter += 1
      .instance_id = joystick_counter
      'Can't even retrieve guid under SDL1.2
    #endif

    debuginfo strprintf("Opened joystick %d %s (id %d) -- %d buttons %d axes %d hats %d balls", _
                        joynum, joyname, .instance_id, .num_buttons, .num_axes, .num_hats, .num_balls)

    IF .have_bindings THEN
      .num_buttons = large(joyLASTGAMEPAD, .num_buttons)
      .num_axes = large(axisLASTGAMEPAD, .num_buttons)
    END IF
    .num_buttons = small(32, .num_buttons)
    .num_axes = small(8, .num_axes)
    .num_hats = small(4, .num_hats)
  END WITH
  RETURN -1  'Acquired joystick
END FUNCTION

'Returns -1 to 3; see gfx.bi
FUNCTION IO_SDL(get_joystick_state)(byval joynum as integer, byval state as IOJoystickState ptr) as integer
  DIM ret as integer = get_joystick(joynum)
  IF ret > 0 THEN RETURN ret  'Failure

  DIM byref joy as SDL_Joystick ptr = joystickhandles(joynum)

  'Fixed joystick info
  state->info = @joystickinfo(joynum)

  'SDL1.2 (not SDL 2) reports joystick state even when the app isn't focused (under both
  'Linux and Windows), be consistent and ignore it
  #ifdef USE_SDL2
    'IF (SDL_GetWindowFlags(mainwindow) AND SDL_WINDOW_INPUT_FOCUS) = 0 THEN RETURN 3
  #else
    IF (SDL_GetAppState() AND SDL_APPINPUTFOCUS) = 0 THEN RETURN 3
  #endif

  'We can assume that state has already been cleared.
  DIM idx as integer
  WITH *state

    .buttons_new = joystickbuttons(joynum)
    joystickbuttons(joynum) = 0

    IF .info->have_bindings THEN
      #ifdef USE_SDL2
        ret = sdl2_update_gamepad(joynum, state)
      #endif
    ELSE
      '?"no binding"
      FOR idx = 0 TO .info->num_buttons - 1
        IF SDL_JoystickGetButton(joy, idx) THEN .buttons_down OR= 1 SHL idx
      NEXT

      FOR idx = 0 TO .info->num_axes - 1
        'Has range -32768 (SDL_JOYSTICK_AXIS_MIN) to 32767 (SDL_JOYSTICK_AXIS_MAX), which is pretty odd...
        .axes(idx) = large(-1000, SDL_JoystickGetAxis(joy, idx) * 1000 \ 32767)
      NEXT

      FOR idx = 0 TO .info->num_hats - 1
        DIM vec as integer = SDL_JoystickGetHat(joy, idx)
        IF vec AND SDL_HAT_LEFT  THEN .hats(idx) OR= 1
        IF vec AND SDL_HAT_RIGHT THEN .hats(idx) OR= 2
        IF vec AND SDL_HAT_UP    THEN .hats(idx) OR= 4
        IF vec AND SDL_HAT_DOWN  THEN .hats(idx) OR= 8
      NEXT

    END IF

    IF debugging_io THEN
      STATIC last_state(maxJoysticks - 1) as string
      DIM temp as string = lpad(BIN(.buttons_down), "0", .info->num_buttons)
      DIM msg as string = strprintf("joy %d buttons: 0b%s", joynum, STRPTR(temp))
      FOR idx = 0 TO .info->num_axes - 1
        msg &= strprintf(" axis%d: %6d", idx, .axes(idx))
      NEXT
      FOR idx = 0 TO .info->num_hats - 1
        msg &= strprintf(" hat%d: 0x%x", idx, .hats(idx))  'Value from 0-15
      NEXT
      FOR idx = 0 TO .info->num_balls - 1
        DIM as integer bx, by
        'NOTE: This is relative movement since last call, so will break if we ever support balls!
        SDL_JoystickGetBall(joy, idx, @bx, @by)
        msg &= strprintf(" ball%d: %d,%d", idx, bx, by)
      NEXT
      IF last_state(joynum) <> msg THEN
        debuginfo msg
        last_state(joynum) = msg
      END IF
    END IF
  END WITH

  RETURN ret
END FUNCTION

#ifdef USE_SDL2

#include "scancodes.bi"

END EXTERN ' Can't put assignment statements in an extern block

DIM SHARED ohr_gamepad_axes(0 to SDL_CONTROLLER_AXIS_TRIGGERRIGHT) as JoyAxis
ohr_gamepad_axes(SDL_CONTROLLER_AXIS_LEFTX) = axisX
ohr_gamepad_axes(SDL_CONTROLLER_AXIS_LEFTY) = axisY
ohr_gamepad_axes(SDL_CONTROLLER_AXIS_RIGHTX) = axisRightX
ohr_gamepad_axes(SDL_CONTROLLER_AXIS_RIGHTY) = axisRightY
ohr_gamepad_axes(SDL_CONTROLLER_AXIS_TRIGGERLEFT) = axisL2
ohr_gamepad_axes(SDL_CONTROLLER_AXIS_TRIGGERRIGHT) = axisR2
'Other axes up to SDL_CONTROLLER_AXIS_MAX ignored

DIM SHARED ohr_gamepad_buttons(0 to SDL_CONTROLLER_BUTTON_MAX) as JoyButton
ohr_gamepad_buttons(SDL_CONTROLLER_BUTTON_A) = joyA
ohr_gamepad_buttons(SDL_CONTROLLER_BUTTON_B) = joyB
ohr_gamepad_buttons(SDL_CONTROLLER_BUTTON_X) = joyX
ohr_gamepad_buttons(SDL_CONTROLLER_BUTTON_Y) = joyY
ohr_gamepad_buttons(SDL_CONTROLLER_BUTTON_BACK) = joyBack
ohr_gamepad_buttons(SDL_CONTROLLER_BUTTON_GUIDE) = joyGuide
ohr_gamepad_buttons(SDL_CONTROLLER_BUTTON_START) = joyStart
ohr_gamepad_buttons(SDL_CONTROLLER_BUTTON_LEFTSTICK) = joyLeftStick
ohr_gamepad_buttons(SDL_CONTROLLER_BUTTON_RIGHTSTICK) = joyRightStick
ohr_gamepad_buttons(SDL_CONTROLLER_BUTTON_LEFTSHOULDER) = joyL1
ohr_gamepad_buttons(SDL_CONTROLLER_BUTTON_RIGHTSHOULDER) = joyR1
ohr_gamepad_buttons(SDL_CONTROLLER_BUTTON_DPAD_UP) = joyUp
ohr_gamepad_buttons(SDL_CONTROLLER_BUTTON_DPAD_DOWN) = joyDown
ohr_gamepad_buttons(SDL_CONTROLLER_BUTTON_DPAD_LEFT) = joyLeft
ohr_gamepad_buttons(SDL_CONTROLLER_BUTTON_DPAD_RIGHT) = joyRight
'Other buttons (which are equal to 0 aka joyNone) ignored

EXTERN "C"

'Return value: see io_get_joystick_state
FUNCTION sdl2_update_gamepad(joynum as integer, state as IOJoystickState ptr) as integer
  WITH *state
    '.attached = SDL_ControllerGetAttached(controller)

    DIM controller as SDL_GameController ptr
    'This returns NULL if not already opened, and it doesn't increment refcount
    'FIXME: if you unplug a controller and plug it back in it won't work, but
    'starting unplugged and plugging in does work.
    controller = SDL_GameControllerFromInstanceID(state->info->instance_id)
    'This works even after unplugging and replugging, but doing so causes this
    'and the SDL_GameControllerClose below (also needs uncommenting!) to lag
    'massively on open() and close() calls.
    'controller = SDL_GameControllerOpen(joynum)
    IF controller = NULL THEN
      'FIXME: not really handling renumbering properly
      'SDL_GetError doesn't report anything
      debug "Lost game controller " & state->info->name
      state->info->instance_id = -1
      state->info->have_bindings = NO
      RETURN 2  'Lost joystick
    END IF

    DIM buttons as uinteger = 0

    FOR idx as integer = 0 TO UBOUND(ohr_gamepad_buttons)
      IF ohr_gamepad_buttons(idx) = joyNone THEN CONTINUE FOR
      IF SDL_GameControllerGetButton(controller, idx) THEN
        'io_get_joystick_state returns gamepad buttons starting with joyButton1 in the first bit
        '? "raw SDL button " & idx & " " & *SDL_GameControllerGetStringForButton(idx)
        buttons OR= 1 SHL (ohr_gamepad_buttons(idx) - 1)
      END IF
    NEXT

    .buttons_down = buttons

    FOR idx as integer = 0 TO UBOUND(ohr_gamepad_axes)
      DIM off as integer = SDL_GameControllerGetAxis(controller, idx)  'Range -32768 to 32767
      IF off THEN
        '? "raw SDL axis " & idx & " ohrax=" & ohr_gamepad_axes(idx) &  " " & off
      END IF
      off = (off * 1000.0) / 32767.
      .axes(ohr_gamepad_axes(idx)) = off 'bound(CINT(off), -1000, 1000)
    NEXT

    'Uncomment if uncommenting the SDL_GameControllerOpen above
    'SDL_GameControllerClose(controller)

  END WITH
  RETURN 0  'Success
END FUNCTION

'Record a button press (when a button down event happens)
FUNCTION sdl2_joy_button_press(btn as integer, instance_id as integer) as bool
  DIM joynum as integer = instance_to_joynum(instance_id)
  IF joynum < 0 ORELSE ohr_gamepad_buttons(btn) = joyNone THEN RETURN NO
  joystickbuttons(joynum) OR= 1 SHL (ohr_gamepad_buttons(btn) - 1)
  RETURN YES
END FUNCTION

#endif  'USE_SDL2
