/*
 * UPIWIN - Micro Pi Windowing Framework Kernel
 * Copyright (C) 2019 Amy Bowersox/Erbosoft Metaverse Design Solutions
 *
 * 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, write to the Free Software Foundation, Inc.,
 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
 *-------------------------------------------------------------------------
 */
#include <stddef.h>
#include <signal.h>
#include <string.h>
#include <errno.h>
#include <fcntl.h>
#include <unistd.h>
#include <pthread.h>
#include <linux/input.h>
#include "scode.h"
#include "config.h"
#include "log.h"
#include "msg_queue.h"
#include "gpio.h"
#include "fbinit.h"
#include "time_func.h"

#define INPUT_EVENT_BATCH 16   /* number of events to retrieve from touchscreen at once */

PMSG_QUEUE Sys_Queue = NULL;   /* system message queue */
INT32 Sys_Exit_Code = -1;      /* system exit code, set on WM_QUIT */
static int ts_fd = 0;          /* file descriptor for touchscreen */

static pthread_t ithread;                    /* input thread handle */
static volatile sig_atomic_t running = 1;    /* "running" flag for input thread */

static pthread_mutex_t wait_mutex = PTHREAD_MUTEX_INITIALIZER;  /* mutex for waiting on input */
static pthread_cond_t wait_cond = PTHREAD_COND_INITIALIZER;     /* condition for waiting on input */

/* Local data for poll_buttons() */
static UINT32 last_bstate = 0;                        /* previous button state */
static TIMESTAMP button_event_ok[GPIO_BUTTON_COUNT];  /* timestamps to debounce events */
static TIMESTAMP button_down_time[GPIO_BUTTON_COUNT];  /* timestamps for click detection */

/* Local data for poll_touchscreen() */
static UINT_PTR touch_x = 0;                  /* X coordinate to send with next message */
static UINT_PTR touch_y = 0;                  /* Y coordinate to send with next message */
static UINT32 touch_nextmsg = WM_TOUCHMOVE;   /* identifier of next message to send */
static UINT_PTR touch_down_x, touch_down_y;   /* location of the touch-down event for click detection */
static TIMESTAMP touch_down_time;             /* timestamp for click detection */

static BOOL poll_buttons(void)
{
  BOOL posted = FALSE;
  UINT32 st, down, up, mask;
  UINT_PTR attr;
  TIMESTAMP now;

  /* poll hardware buttons */
  st = Gpio_read_buttons();
  if (st != last_bstate)
  {
    now = Time_since_start();
    up = last_bstate & ~st;
    down = st & ~last_bstate;
    for (attr = 1, mask = GRB_STATE_BUTTON1; attr <= GPIO_BUTTON_COUNT; attr++, mask <<= 1)
    {
      if (now < button_event_ok[attr - 1])
        continue;  /* this is a "contact bounce" event, don't bother */
      if (up & mask)
      {
        /* reset contact bounce timer - only seems to happen after button releases */
        button_event_ok[attr - 1] = now + Gconfig.button_debounce;
        Mq_post1(Sys_Queue, 0, WM_HWBUTTONUP, attr);
        if ((now - button_down_time[attr - 1]) <= Gconfig.click_time)
          Mq_post1(Sys_Queue, 0, WM_HWBUTTONCLICK, attr);
        posted = TRUE;
      }
      else if (down & mask)
      {
        Mq_post1(Sys_Queue, 0, WM_HWBUTTONDOWN, attr);
        button_down_time[attr - 1] = now;
        posted = TRUE;
      }
    }
    last_bstate = st;
  }
  return posted;
}

static BOOL poll_touchscreen(void)
{
  BOOL posted = FALSE;
  int nb, nev, xerrno, i;
  TIMESTAMP now;
  struct input_event buffer[INPUT_EVENT_BATCH];

  nb = read(ts_fd, buffer, INPUT_EVENT_BATCH * sizeof(struct input_event));
  if (nb == -1)
  {
    xerrno = errno;
    if ((xerrno != EAGAIN) && (xerrno != EWOULDBLOCK))
      Log(LERROR, "Error reading from touchscreen device (%d)", xerrno);
    return FALSE;
  }
  else if (nb == 0)
  {
    Log(LERROR, "Unexpected end of file reading from touchscreen device");
    return FALSE;
  }

  nev = nb / sizeof(struct input_event);
  xerrno = nev * sizeof(struct input_event);
  if (nb > xerrno)
    Log(LERROR, "read %d bytes from touchscreen but we can only use %d", nb, xerrno);

  for (i=0; i<nev; i++)
  {
    switch (buffer[i].type)
    {
      case EV_SYN:
        if (buffer[i].code == SYN_REPORT)
        {
    		  now = Time_since_start();
    		  Mq_post2(Sys_Queue, 0, touch_nextmsg, touch_x, touch_y);
    		  if (touch_nextmsg == WM_TOUCHDOWN)
    		  {
            touch_down_x = touch_x;
            touch_down_y = touch_y;
            touch_down_time = now;
    		  }
    		  else if (touch_nextmsg == WM_TOUCHUP)
    		  {
            if (   ((now - touch_down_time) <= Gconfig.click_time)
                && (ABS((INT32)touch_x - (INT32)touch_down_x) <= Gconfig.click_radius)
                && (ABS((INT32)touch_y - (INT32)touch_down_y) <= Gconfig.click_radius))
              Mq_post2(Sys_Queue, 0, WM_TOUCHCLICK, touch_x, touch_y);
    		  }
  	      touch_nextmsg = WM_TOUCHMOVE;
  	      posted = TRUE;
    		}
    		break;

      case EV_ABS:
        /* The screen driver thinks the screen is horizontal with origin at upper left and max at lower right. */
    		if (buffer[i].code == ABS_X)
    		  touch_x = buffer[i].value;
    		else if (buffer[i].code == ABS_Y)
    		  touch_y = buffer[i].value;
    		break;

      case EV_KEY:
        if (buffer[i].code == BTN_TOUCH)
          touch_nextmsg = (buffer[i].value ? WM_TOUCHDOWN : WM_TOUCHUP);
        break;

      default:
    		break;
    }
  }
  return posted;
}

static void *input_thread(void *arg)
{
  BOOL gotinput;

  /* clear all state at startup */
  last_bstate = 0;
  memset(button_event_ok, 0, GPIO_BUTTON_COUNT * sizeof(TIMESTAMP));
  touch_x = touch_y = 0;
  touch_nextmsg = WM_TOUCHMOVE;

  while (running)
  {
  	gotinput = poll_buttons();
    gotinput = poll_touchscreen() || gotinput;
  	if (gotinput)
  	{
  	  pthread_mutex_lock(&wait_mutex);
  	  pthread_cond_signal(&wait_cond);
  	  pthread_mutex_unlock(&wait_mutex);
  	}
  }
  return NULL;
}

static void do_disable_input(void)
{
  running = 0;
  pthread_join(ithread, NULL);
  close(ts_fd);
  ts_fd = -1;
  Mq_destroy(Sys_Queue);
  Sys_Queue = NULL;
}

HRESULT Sys_enable_input(void)
{
  HRESULT rc = S_OK;
  int threadrc;

  Sys_Queue = Mq_alloc(Gconfig.sys_mq_length);
  if (!Sys_Queue)
  {
    Log(LFATAL, "Unable to allocate system message queue.");
    return E_OUTOFMEMORY;
  }

  ts_fd = open(Gconfig.touchscreen_device, O_RDONLY|O_NONBLOCK);
  if (ts_fd < 0)
  {
    rc = ERRNO_AS_SCODE;
    Log(LFATAL, "Unable to open touchscreen device %s (%08X).", Gconfig.touchscreen_device, rc);
    goto error_0;
  }

  running = 1;
  threadrc = pthread_create(&ithread, NULL, input_thread, NULL);
  if (threadrc != 0)
  {
    rc = SCODE_FROM_ERRNO(threadrc);
    Log(LFATAL, "Unable to start system input thread (%08X).", rc);
  	goto error_1;
  }

  rc = Config_exitfunc(do_disable_input);
  if (FAILED(rc))
    do_disable_input();
  return rc;

error_1:
  close(ts_fd);
  ts_fd = -1;
error_0:
  Mq_destroy(Sys_Queue);
  return rc;
}

void Sys_wait_for_input(void)
{
  pthread_mutex_lock(&wait_mutex);
  while (Mq_is_empty(Sys_Queue))
  	pthread_cond_wait(&wait_cond, &wait_mutex);
  pthread_mutex_unlock(&wait_mutex);
}
