#define MAX_INPUT_FRONTEND_SETS 8

static uint32_t _input_backend_pair_count = 0;
static const struct engine_input_BackendPair * _input_backend_pairs = NULL;

static uint32_t _input_binding_count = 0;
static alias_R * _input_bindings = NULL;

void engine_input_setPlayerBackend(uint32_t player_index, const struct engine_input_BackendPair * pairs, uint32_t pair_count) {
  (void)player_index;
  _input_backend_pair_count = pair_count;
  _input_backend_pairs = pairs;

  uint32_t max_binding_index = 0;
  for(uint32_t i = 0; i < pair_count; i++) {
    if(pairs[i].binding > max_binding_index) {
      max_binding_index = pairs[i].binding;
    }
  }

  if(max_binding_index + 1 > _input_binding_count) {
    _input_bindings = alias_realloc(
        alias_default_MemoryCB()
      , _input_bindings
      , sizeof(*_input_bindings) * _input_binding_count
      , sizeof(*_input_bindings) * (max_binding_index + 1)
      , alignof(*_input_bindings)
      );
    _input_binding_count = max_binding_index + 1;
  }
}

static struct {
  uint32_t count;
  struct engine_input_Signal * signals;
} _input_frontends[MAX_INPUT_FRONTEND_SETS] = { 0 };

uint32_t engine_input_addFrontend(uint32_t player_index, struct engine_input_Signal * signals, uint32_t signal_count) {
  (void)player_index;

  uint32_t index = 0;
  for(; index < MAX_INPUT_FRONTEND_SETS; index++) {
    if(_input_frontends[index].count == 0) {
      break;
    }
  }
  if(index < MAX_INPUT_FRONTEND_SETS) {
    _input_frontends[index].count = signal_count;
    _input_frontends[index].signals = signals;
    return index;
  }
  return -1;
}

void engine_input_removeFrontend(uint32_t player_index, uint32_t index) {
  (void)player_index;

  if(index < MAX_INPUT_FRONTEND_SETS) {
    _input_frontends[index].count = 0;
    _input_frontends[index].signals = NULL;
  }
}

static int _input_get_key_down(enum engine_input_Device device) {
  static int source_to_key[] = {
    [engine_input_Device_keyboardApostrophe] = GLFW_KEY_APOSTROPHE,
    [engine_input_Device_keyboardComma] = GLFW_KEY_COMMA,
    [engine_input_Device_keyboardMinus] = GLFW_KEY_MINUS,
    [engine_input_Device_keyboardPeriod] = GLFW_KEY_PERIOD,
    [engine_input_Device_keyboardSlash] = GLFW_KEY_SLASH,
    [engine_input_Device_keyboard0] = GLFW_KEY_0,
    [engine_input_Device_keyboard1] = GLFW_KEY_1,
    [engine_input_Device_keyboard2] = GLFW_KEY_2,
    [engine_input_Device_keyboard3] = GLFW_KEY_3,
    [engine_input_Device_keyboard4] = GLFW_KEY_4,
    [engine_input_Device_keyboard5] = GLFW_KEY_5,
    [engine_input_Device_keyboard6] = GLFW_KEY_6,
    [engine_input_Device_keyboard7] = GLFW_KEY_7,
    [engine_input_Device_keyboard8] = GLFW_KEY_8,
    [engine_input_Device_keyboard9] = GLFW_KEY_9,
    [engine_input_Device_keyboardSemicolon] = GLFW_KEY_SEMICOLON,
    [engine_input_Device_keyboardEqual] = GLFW_KEY_EQUAL,
    [engine_input_Device_keyboardA] = GLFW_KEY_A,
    [engine_input_Device_keyboardB] = GLFW_KEY_B,
    [engine_input_Device_keyboardC] = GLFW_KEY_C,
    [engine_input_Device_keyboardD] = GLFW_KEY_D,
    [engine_input_Device_keyboardE] = GLFW_KEY_E,
    [engine_input_Device_keyboardF] = GLFW_KEY_F,
    [engine_input_Device_keyboardG] = GLFW_KEY_G,
    [engine_input_Device_keyboardH] = GLFW_KEY_H,
    [engine_input_Device_keyboardI] = GLFW_KEY_I,
    [engine_input_Device_keyboardJ] = GLFW_KEY_J,
    [engine_input_Device_keyboardK] = GLFW_KEY_K,
    [engine_input_Device_keyboardL] = GLFW_KEY_L,
    [engine_input_Device_keyboardM] = GLFW_KEY_M,
    [engine_input_Device_keyboardN] = GLFW_KEY_N,
    [engine_input_Device_keyboardO] = GLFW_KEY_O,
    [engine_input_Device_keyboardP] = GLFW_KEY_P,
    [engine_input_Device_keyboardQ] = GLFW_KEY_Q,
    [engine_input_Device_keyboardR] = GLFW_KEY_R,
    [engine_input_Device_keyboardS] = GLFW_KEY_S,
    [engine_input_Device_keyboardT] = GLFW_KEY_T,
    [engine_input_Device_keyboardU] = GLFW_KEY_U,
    [engine_input_Device_keyboardV] = GLFW_KEY_V,
    [engine_input_Device_keyboardW] = GLFW_KEY_W,
    [engine_input_Device_keyboardX] = GLFW_KEY_X,
    [engine_input_Device_keyboardY] = GLFW_KEY_Y,
    [engine_input_Device_keyboardZ] = GLFW_KEY_Z,
    [engine_input_Device_keyboardSpace] = GLFW_KEY_SPACE,
    [engine_input_Device_keyboardEscape] = GLFW_KEY_ESCAPE,
    [engine_input_Device_keyboardEnter] = GLFW_KEY_ENTER,
    [engine_input_Device_keyboardTab] = GLFW_KEY_TAB,
    [engine_input_Device_keyboardBackspace] = GLFW_KEY_BACKSPACE,
    [engine_input_Device_keyboardInsert] = GLFW_KEY_INSERT,
    [engine_input_Device_keyboardDelete] = GLFW_KEY_DELETE,
    [engine_input_Device_keyboardRight] = GLFW_KEY_RIGHT,
    [engine_input_Device_keyboardLeft] = GLFW_KEY_LEFT,
    [engine_input_Device_keyboardDown] = GLFW_KEY_DOWN,
    [engine_input_Device_keyboardUp] = GLFW_KEY_UP,
    [engine_input_Device_keyboardPageUp] = GLFW_KEY_PAGE_UP,
    [engine_input_Device_keyboardPageDown] = GLFW_KEY_PAGE_DOWN,
    [engine_input_Device_keyboardHome] = GLFW_KEY_HOME,
    [engine_input_Device_keyboardEnd] = GLFW_KEY_END,
    [engine_input_Device_keyboardCapsLock] = GLFW_KEY_CAPS_LOCK,
    [engine_input_Device_keyboardScrollLock] = GLFW_KEY_SCROLL_LOCK,
    [engine_input_Device_keyboardNumLock] = GLFW_KEY_NUM_LOCK,
    [engine_input_Device_keyboardPrintScreen] = GLFW_KEY_PRINT_SCREEN,
    [engine_input_Device_keyboardPause] = GLFW_KEY_PAUSE,
    [engine_input_Device_keyboardF1] = GLFW_KEY_F1,
    [engine_input_Device_keyboardF2] = GLFW_KEY_F2,
    [engine_input_Device_keyboardF3] = GLFW_KEY_F3,
    [engine_input_Device_keyboardF4] = GLFW_KEY_F4,
    [engine_input_Device_keyboardF5] = GLFW_KEY_F5,
    [engine_input_Device_keyboardF6] = GLFW_KEY_F6,
    [engine_input_Device_keyboardF7] = GLFW_KEY_F7,
    [engine_input_Device_keyboardF8] = GLFW_KEY_F8,
    [engine_input_Device_keyboardF9] = GLFW_KEY_F9,
    [engine_input_Device_keyboardF10] = GLFW_KEY_F10,
    [engine_input_Device_keyboardF11] = GLFW_KEY_F11,
    [engine_input_Device_keyboardF12] = GLFW_KEY_F12,
    [engine_input_Device_keyboardLeftShift] = GLFW_KEY_LEFT_SHIFT,
    [engine_input_Device_keyboardLeftControl] = GLFW_KEY_LEFT_CONTROL,
    [engine_input_Device_keyboardLeftAlt] = GLFW_KEY_LEFT_ALT,
    [engine_input_Device_keyboardLeftMeta] = GLFW_KEY_LEFT_SUPER,
    [engine_input_Device_keyboardRightShift] = GLFW_KEY_RIGHT_SHIFT,
    [engine_input_Device_keyboardRightControl] = GLFW_KEY_RIGHT_CONTROL,
    [engine_input_Device_keyboardRightAlt] = GLFW_KEY_RIGHT_ALT,
    [engine_input_Device_keyboardRightMeta] = GLFW_KEY_RIGHT_SUPER,
    [engine_input_Device_keyboardMenu] = GLFW_KEY_MENU,
    [engine_input_Device_keyboardLeftBracket] = GLFW_KEY_LEFT_BRACKET,
    [engine_input_Device_keyboardBackslash] = GLFW_KEY_BACKSLASH,
    [engine_input_Device_keyboardRightBracket] = GLFW_KEY_RIGHT_BRACKET,
    [engine_input_Device_keyboardGrave] = GLFW_KEY_GRAVE_ACCENT,
    [engine_input_Device_keyboardPad0] = GLFW_KEY_UNKNOWN,
    [engine_input_Device_keyboardPad1] = GLFW_KEY_UNKNOWN,
    [engine_input_Device_keyboardPad2] = GLFW_KEY_UNKNOWN,
    [engine_input_Device_keyboardPad3] = GLFW_KEY_UNKNOWN,
    [engine_input_Device_keyboardPad4] = GLFW_KEY_UNKNOWN,
    [engine_input_Device_keyboardPad5] = GLFW_KEY_UNKNOWN,
    [engine_input_Device_keyboardPad6] = GLFW_KEY_UNKNOWN,
    [engine_input_Device_keyboardPad7] = GLFW_KEY_UNKNOWN,
    [engine_input_Device_keyboardPad8] = GLFW_KEY_UNKNOWN,
    [engine_input_Device_keyboardPad9] = GLFW_KEY_UNKNOWN,
    [engine_input_Device_keyboardPadPeriod] = GLFW_KEY_UNKNOWN,
    [engine_input_Device_keyboardPadDivide] = GLFW_KEY_UNKNOWN,
    [engine_input_Device_keyboardPadMultiply] = GLFW_KEY_UNKNOWN,
    [engine_input_Device_keyboardPadMinus] = GLFW_KEY_UNKNOWN,
    [engine_input_Device_keyboardPadAdd] = GLFW_KEY_UNKNOWN,
    [engine_input_Device_keyboardPadEnter] = GLFW_KEY_UNKNOWN,
    [engine_input_Device_keyboardPadEqual] = GLFW_KEY_UNKNOWN
  };
  int state = glfwGetKey(_r_window, source_to_key[device]);
  return state == GLFW_PRESS;
}

static int _input_get_mouse_button_down(enum engine_input_Device device) {
  int source_to_mouse[] = {
    [engine_input_Device_mouseLeftButton] = GLFW_MOUSE_BUTTON_1,
    [engine_input_Device_mouseRightButton] = GLFW_MOUSE_BUTTON_2
  };
  int state = glfwGetMouseButton(_r_window, source_to_mouse[device]);
  return state == GLFW_PRESS;
}

static void _input_frame(void) {
  double xpos, ypos;
  glfwGetCursorPos(_r_window, &xpos, &ypos);

  alias_memory_clear(_input_bindings, sizeof(*_input_bindings) * _input_binding_count);

  for(uint32_t i = 0; i < _input_backend_pair_count; i++) {
    enum engine_input_Device device = _input_backend_pairs[i].device;
    alias_R * binding = &_input_bindings[_input_backend_pairs[i].binding];

    switch(device) {
    case engine_input_Device_keyboardApostrophe ... engine_input_Device_keyboardPadEqual:
      if(_input_get_key_down(device)) {
        *binding = alias_R_ONE;
      }
      break;
    case engine_input_Device_mouseLeftButton ... engine_input_Device_mouseRightButton:
      if(_input_get_mouse_button_down(device)) {
        *binding = alias_R_ONE;
      }
      break;
    case engine_input_Device_mousePositionX:
      *binding = xpos;
      break;
    case engine_input_Device_mousePositionY:
      *binding = ypos;
      break;
    case engine_input_Device_COUNT:
      break;
    }
  }

  for(uint32_t i = 0; i < MAX_INPUT_FRONTEND_SETS; i++) {
    for(uint32_t j = 0; j < _input_frontends[i].count; j++) {
      struct engine_input_Signal * signal = &_input_frontends[i].signals[j];
      switch(signal->type) {
      case engine_input_Signal_pass:
        {
          signal->boolean = _input_bindings[signal->bindings[0]] > alias_R_ZERO;
          break;
        }
      case engine_input_Signal_up:
        {
          bool value = _input_bindings[signal->bindings[0]] > alias_R_ZERO;
          signal->boolean = !signal->internal.up && value;
          signal->internal.up = value;
          break;
        }
      case engine_input_Signal_down:
        {
          bool value = _input_bindings[signal->bindings[0]] > alias_R_ZERO;
          signal->boolean = signal->internal.down && !value;
          signal->internal.down = value;
          break;
        }
      case engine_input_Signal_direction:
        {
          signal->direction = alias_pga2d_direction(_input_bindings[signal->bindings[0]], _input_bindings[signal->bindings[1]]);
          break;
        }
      case engine_input_Signal_point:
        {
          signal->point = alias_pga2d_point(_input_bindings[signal->bindings[0]], _input_bindings[signal->bindings[1]]);
          break;
        }
      case engine_input_Signal_viewportPoint:
        {
          alias_R px = _input_bindings[signal->bindings[0]];
          alias_R py = _input_bindings[signal->bindings[1]];

          const struct engine_Camera * camera = engine_Camera_read(signal->click_camera);
          const struct alias_LocalToWorld2D * transform = alias_LocalToWorld2D_read(signal->click_camera);

          alias_R minx = alias_pga2d_point_x(camera->viewport_min) * _r_width;
          alias_R miny = alias_pga2d_point_y(camera->viewport_min) * _r_height;
          alias_R maxx = alias_pga2d_point_x(camera->viewport_max) * _r_width;
          alias_R maxy = alias_pga2d_point_y(camera->viewport_max) * _r_height;
          alias_R width = maxx - minx;
          alias_R height = maxy - miny;

          px -= minx;
          py -= miny;
          if(px < 0 || py < 0 || px > width || py > height) {
            break;
          }

          px -= width / 2;
          py -= height / 2;
          
          alias_R cx = alias_pga2d_point_x(transform->position);
          alias_R cy = alias_pga2d_point_y(transform->position);

          px += cx;
          py += cy;

          signal->point = alias_pga2d_point(px, py);
          
          break;
        }
      }
    }
  }
}