/*
 *  GRUB  --  GRand Unified Bootloader
 *  Copyright (C) 2022  Free Software Foundation, Inc.
 *
 *  GRUB 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 3 of the License, or
 *  (at your option) any later version.
 *
 *  GRUB 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 GRUB.  If not, see <http://www.gnu.org/licenses/>.
 */

#include <grub/dl.h>
#include <grub/term.h>
#include <grub/misc.h>
#include <grub/types.h>
#include <grub/command.h>
#include <grub/i18n.h>
#include <grub/err.h>
#include <grub/efi/efi.h>
#include <grub/efi/api.h>

GRUB_MOD_LICENSE ("GPLv3+");

#define GRUB_EFI_SIMPLE_POINTER_GUID  \
  { 0x31878c87, 0x0b75, 0x11d5, \
    { 0x9a, 0x4f, 0x00, 0x90, 0x27, 0x3f, 0xc1, 0x4d } \
  }

typedef struct
{
  grub_efi_int32_t x;
  grub_efi_int32_t y;
  grub_efi_int32_t z;
  grub_efi_boolean_t left;
  grub_efi_boolean_t right;
} grub_efi_mouse_state;

grub_efi_mouse_state no_move = {0, 0, 0, 0, 0};

typedef struct
{
  grub_efi_uint64_t x;
  grub_efi_uint64_t y;
  grub_efi_uint64_t z;
  grub_efi_boolean_t left;
  grub_efi_boolean_t right;
} grub_efi_mouse_mode;

struct grub_efi_simple_pointer_protocol
{
  grub_efi_status_t (*reset) (struct grub_efi_simple_pointer_protocol *this,
                              grub_efi_boolean_t extended_verification);
  grub_efi_status_t (*get_state) (struct grub_efi_simple_pointer_protocol *this,
                                  grub_efi_mouse_state *state);
  grub_efi_event_t *wait_for_input;
  grub_efi_mouse_mode *mode;
};
typedef struct grub_efi_simple_pointer_protocol grub_efi_simple_pointer_protocol_t;

typedef struct
{
  grub_efi_uintn_t count;
  grub_efi_simple_pointer_protocol_t **mouse;
} grub_efi_mouse_prot_t;

static grub_int32_t
mouse_div (grub_int32_t a, grub_uint64_t b)
{
  grub_int32_t s = 1, q, ret;
  grub_uint64_t n = a;
  if (!b)
    return 0;
  if (a < 0)
  {
    s = -1;
    n = -a;
  }
  q = grub_divmod64 (n, b, NULL);
  ret = s * (q > 0 ? q : -q);
  return ret;
}

static grub_efi_mouse_prot_t *
grub_efi_mouse_prot_init (void)
{
  grub_efi_status_t status;
  grub_efi_guid_t mouse_guid = GRUB_EFI_SIMPLE_POINTER_GUID;
  grub_efi_mouse_prot_t *mouse_input = NULL;
  grub_efi_boot_services_t *b = grub_efi_system_table->boot_services;
  grub_efi_handle_t *buf;
  grub_efi_uintn_t count;
  grub_efi_uintn_t i;

  status = efi_call_5 (b->locate_handle_buffer, GRUB_EFI_BY_PROTOCOL,
                       &mouse_guid, NULL, &count, &buf);
  if (status != GRUB_EFI_SUCCESS)
  {
#ifdef MOUSE_DEBUG
    grub_printf ("ERROR: SimplePointerProtocol not found.\n");
#endif
    return NULL;
  }

  mouse_input = grub_malloc (sizeof (grub_efi_mouse_prot_t));
  if (!mouse_input)
    return NULL;
  mouse_input->mouse = grub_malloc (count
            * sizeof (grub_efi_simple_pointer_protocol_t *));
  if (!mouse_input->mouse)
  {
    grub_free (mouse_input);
    return NULL;
  }
  mouse_input->count = count;
  for (i = 0; i < count; i++)
  {
    efi_call_3 (b->handle_protocol,
                buf[i], &mouse_guid, (void **)&mouse_input->mouse[i]);
#ifdef MOUSE_DEBUG
    grub_printf ("%d %p ", (int)i, mouse_input->mouse[i]);
#endif
    efi_call_2 (mouse_input->mouse[i]->reset, mouse_input->mouse[i], 1);
#ifdef MOUSE_DEBUG
    grub_printf
      ("[%"PRIuGRUB_UINT64_T"] [%"PRIuGRUB_UINT64_T"] [%"PRIuGRUB_UINT64_T"]\n",
       mouse_input->mouse[i]->mode->x,
       mouse_input->mouse[i]->mode->y, mouse_input->mouse[i]->mode->z);
#endif
  }
  return mouse_input;
}

static grub_err_t
grub_efi_mouse_input_init (struct grub_term_input *term)
{
  grub_efi_mouse_prot_t *mouse_input = NULL;
  if (term->data)
    return 0;
  mouse_input = grub_efi_mouse_prot_init ();
  if (!mouse_input)
    return GRUB_ERR_BAD_OS;

  term->data = (void *)mouse_input;

  return 0;
}

static int
grub_mouse_getkey (struct grub_term_input *term)
{
  grub_efi_mouse_state cur;
  grub_efi_mouse_prot_t *mouse = term->data;
  //int x;
  int y;
  grub_efi_uintn_t i;
  if (!mouse)
    return GRUB_TERM_NO_KEY;
  for (i = 0; i < mouse->count; i++)
  {
    efi_call_2 (mouse->mouse[i]->get_state, mouse->mouse[i], &cur);
    if (grub_memcmp (&cur, &no_move, sizeof (grub_efi_mouse_state)) != 0)
    {
      y = mouse_div (cur.y, mouse->mouse[i]->mode->y);
      if (cur.left)
        return 0x0d;
      if (cur.right)
        return GRUB_TERM_ESC;
      if (y > 0)
        return GRUB_TERM_KEY_DOWN;
      if (y < 0)
        return GRUB_TERM_KEY_UP;
    }
  }
  return GRUB_TERM_NO_KEY;
}

#ifdef MOUSE_DEBUG
static grub_err_t
grub_cmd_mouse_test (grub_command_t cmd __attribute__ ((unused)),
                    int argc __attribute__ ((unused)),
                    char **args __attribute__ ((unused)))

{
  grub_efi_mouse_state cur;
  int x = 0, y = 0, z = 0;
  grub_efi_uintn_t i;
  grub_efi_mouse_prot_t *mouse = NULL;

  mouse = grub_efi_mouse_prot_init ();
  if (!mouse)
    return grub_error (GRUB_ERR_BAD_OS, "mouse not found.\n");
  grub_printf ("Press [1] to exit.\n");
  while (1)
  {
    if (grub_getkey_noblock () == '1')
      break;
    for (i = 0; i < mouse->count; i++)
    {
      efi_call_2 (mouse->mouse[i]->get_state, mouse->mouse[i], &cur);
      if (grub_memcmp (&cur, &no_move, sizeof (grub_efi_mouse_state)) != 0)
      {
        x = mouse_div (cur.x, mouse->mouse[i]->mode->x);
        y = mouse_div (cur.y, mouse->mouse[i]->mode->y);
        z = mouse_div (cur.z, mouse->mouse[i]->mode->z);
        grub_printf ("[ID=%d] X=%d Y=%d Z=%d L=%d R=%d\n",
                     (int)i, x, y, z, cur.left, cur.right);
      }
    }
    grub_refresh ();
  }
  grub_free (mouse->mouse);
  grub_free (mouse);
  return GRUB_ERR_NONE;
}
static grub_command_t cmd;
#endif

static struct grub_term_input grub_mouse_term_input =
{
  .name = "mouse",
  .getkey = grub_mouse_getkey,
  .init = grub_efi_mouse_input_init,
};

GRUB_MOD_INIT(mouse)
{
  grub_term_register_input ("mouse", &grub_mouse_term_input);
#ifdef MOUSE_DEBUG
  cmd = grub_register_command ("mouse_test", grub_cmd_mouse_test, 0,
                               N_("UEFI mouse test."));
#endif
}

GRUB_MOD_FINI(mouse)
{
  grub_term_unregister_input (&grub_mouse_term_input);
#ifdef MOUSE_DEBUG
  grub_unregister_command (cmd);
#endif
}
