/********************************************************************************************
 * apps/graphics/nxwm/src/ckeyboard.cxx
 *
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.  The
 * ASF licenses this file to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance with the
 * License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
 * License for the specific language governing permissions and limitations
 * under the License.
 *
 ********************************************************************************************/

/********************************************************************************************
 * Included Files
 ********************************************************************************************/

#include <nuttx/config.h>

#include <cerrno>

#include <fcntl.h>
#include <sched.h>
#include <pthread.h>
#include <assert.h>
#include <debug.h>
#include <unistd.h>

#include "graphics/nxwm/nxwmconfig.hxx"
#include "graphics/nxwm/ckeyboard.hxx"

/********************************************************************************************
 * Pre-Processor Definitions
 ********************************************************************************************/

/********************************************************************************************
 * CKeyboard Method Implementations
 ********************************************************************************************/

using namespace NxWM;

/**
 * CKeyboard Constructor
 *
 * @param server. An instance of the NX server.  This will be needed for
 *   injecting keyboard data.
 */

CKeyboard::CKeyboard(NXWidgets::CNxServer *server)
{
  m_server      = server;              // Save the NX server
  m_kbdFd     = -1;                    // Device driver is not opened
  m_state       = LISTENER_NOTRUNNING; // The listener thread is not running yet

  // Initialize the semaphore used to synchronize with the listener thread

  sem_init(&m_waitSem, 0, 0);
}

/**
 * CKeyboard Destructor
 */

CKeyboard::~CKeyboard(void)
{
  // Stop the listener thread

  m_state = LISTENER_STOPREQUESTED;

  // Wake up the listener thread so that it will use our buffer
  // to receive data
  // REVISIT:  Need wait here for the listener thread to terminate

  pthread_kill(m_thread, CONFIG_NXWM_KEYBOARD_SIGNO);

  // Close the keyboard device (or should these be done when the thread exits?)

  if (m_kbdFd >= 0)
    {
      close(m_kbdFd);
    }
}

/**
 * Start the keyboard listener thread.
 *
 * @return True if the keyboard listener thread was correctly started.
 */

bool CKeyboard::start(void)
{
  pthread_attr_t attr;

  ginfo("Starting listener\n");

  // Start a separate thread to listen for keyboard events

  pthread_attr_init(&attr);

  struct sched_param param;
  param.sched_priority = CONFIG_NXWM_KEYBOARD_LISTENERPRIO;
  pthread_attr_setschedparam(&attr, &param);

  pthread_attr_setstacksize(&attr, CONFIG_NXWM_KEYBOARD_LISTENERSTACK);

  m_state  = LISTENER_STARTED; // The listener thread has been started, but is not yet running

  int ret = pthread_create(&m_thread, &attr, listener, (FAR void *)this);
  if (ret != 0)
    {
      gerr("ERROR: CKeyboard::start: pthread_create failed: %d\n", ret);
      return false;
    }

  // Detach from the thread

  pthread_detach(m_thread);

  // Don't return until we are sure that the listener thread is running
  // (or until it reports an error).

  while (m_state == LISTENER_STARTED)
    {
      // Wait for the listener thread to wake us up when we really
      // are connected.

      sem_wait(&m_waitSem);
    }

  // Then return true only if the listener thread reported successful
  // initialization.

  ginfo("Listener m_state=%d\n", (int)m_state);
  return m_state == LISTENER_RUNNING;
}

/**
 * Open the keyboard device.  Not very interesting for the case of
 * standard device but much more interesting for a USB keyboard device
 * that may disappear when the keyboard is disconnect but later reappear
 * when the keyboard is reconnected.  In this case, this function will
 * not return until the keyboard device was successfully opened (or
 * until an irrecoverable error occurs.
 *
 * Opens the keyboard device specified by CONFIG_NXWM_KEYBOARD_DEVPATH.
 *
 * @return On success, then method returns a valid file descriptor that
 * can be used to redirect stdin.  A negated errno value is returned
 * if an irrecoverable error occurs.
 */

int CKeyboard::open(void)
{
  int fd;

  // Loop until we have successfully opened the USB keyboard (or until some
  // irrecoverable error occurs).

  do
    {
      // Try to open the keyboard device

      fd = ::open(CONFIG_NXWM_KEYBOARD_DEVPATH, O_RDONLY);
      if (fd < 0)
        {
          int errcode = errno;
          DEBUGASSERT(errcode > 0);

          // EINTR should be ignored because it is not really an error at
          // all.  We should retry immediately

          if (errcode != EINTR)
            {
#ifdef CONFIG_NXWM_KEYBOARD_USBHOST
              // ENOENT means that the USB device is not yet connected and,
              // hence, has no entry under /dev.  If the USB driver still
              // exists under /dev (because other threads still have the driver
              // open), then we might also get ENODEV.

              if (errcode == ENOENT || errcode == ENODEV)
                {
                  // REVIST: Can we inject a constant string here to let the
                  // user know that we are waiting for a USB keyboard to be
                  // connected?

                  // Sleep a bit and try again

                  ginfo("WAITING for a USB device\n");
                  sleep(2);
                }

              // Anything else would be really bad.

              else
#endif
                {
                  // Let the top-level logic decide what it wants to do
                  // about all really bad things

                  gerr("ERROR: Failed to open %s for reading: %d\n",
                       CONFIG_NXWM_KEYBOARD_DEVPATH, errcode);
                  return -errcode;
                }
            }
        }
    }
  while (fd < 0);

  return fd;
}

/**
 * This is the heart of the keyboard listener thread.  It contains the
 * actual logic that listeners for and dispatches keyboard events to the
 * NX server.
 *
 * @return  If the session terminates gracefully (i.e., because >m_state
 * is no longer equal to  LISTENER_RUNNING), then method returns OK.  A
 * negated errno value is returned if an error occurs while reading from
 * the keyboard device.  A read error, depending upon the type of the
 * error, may simply indicate that a USB keyboard was removed and we
 * should wait for the keyboard to be connected.
 */

int CKeyboard::session(void)
{
  ginfo("Session started\n");

  // Loop, reading and dispatching keyboard data

  while (m_state == LISTENER_RUNNING)
    {
      // Read one keyboard sample

      ginfo("Listening for keyboard input\n");

      uint8_t rxbuffer[CONFIG_NXWM_KEYBOARD_BUFSIZE];
      ssize_t nbytes = read(m_kbdFd, rxbuffer,
                            CONFIG_NXWM_KEYBOARD_BUFSIZE);

      // Check for errors

      if (nbytes < 0)
        {
          int errcode = errno;
          DEBUGASSERT(errcode > 0);

          // EINTR is not really an error, it simply means that something is
          // trying to get our attention.  We need to check m_state to see
          // if we were asked to terminate

          if (errcode != EINTR)
            {
              // Let the top-level listener logic decide what to do about
              // the read failure.

              gerr("ERROR: read %s failed: %d\n",
                   CONFIG_NXWM_KEYBOARD_DEVPATH, errcode);
              return -errcode;
            }

          fwarn("WARNING: Awakened with EINTR\n");
        }

      // Give the keyboard input to NX

      else if (nbytes > 0)
        {
          // Looks like good keyboard input... process it.
          // First, get the server handle

          NXHANDLE handle = m_server->getServer();

          // Then inject the keyboard input into NX

          int ret = nx_kbdin(handle, (uint8_t)nbytes, rxbuffer);
          if (ret < 0)
            {
              gerr("ERROR: nx_kbdin failed: %d\n", ret);
              //break;  ignore the error
            }
        }
    }

  return OK;
}

/**
 * The keyboard listener thread.  This is the entry point of a thread
 * that listeners for and dispatches keyboard events to the NX server.
 * It simply opens the keyboard device (using CKeyboard::open()) and
 * executes the session (via CKeyboard::session()).
 *
 * If an errors while reading from the keyboard device AND we are
 * configured to use a USB keyboard, then this function will wait for
 * the USB keyboard to be re-connected.
 *
 * @param arg.  The CKeyboard 'this' pointer cast to a void*.
 * @return This function normally does not return but may return NULL on
 *   error conditions.
 */

FAR void *CKeyboard::listener(FAR void *arg)
{
  CKeyboard *This = (CKeyboard *)arg;

  ginfo("Listener started\n");

#ifdef CONFIG_NXWM_KEYBOARD_USBHOST
  // Indicate that we have successfully started.  We might be stuck waiting
  // for a USB keyboard to be connected, but we are technically running

  This->m_state = LISTENER_RUNNING;
  sem_post(&This->m_waitSem);

  // Loop until we are told to quit

  while (This->m_state == LISTENER_RUNNING)
#endif
    {
      // Open/Re-open the keyboard device

      This->m_kbdFd = This->open();
      if (This->m_kbdFd < 0)
        {
          gerr("ERROR: open failed: %d\n", This->m_kbdFd);
          This->m_state = LISTENER_FAILED;
          sem_post(&This->m_waitSem);
          return (FAR void *)0;
        }

#ifndef CONFIG_NXWM_KEYBOARD_USBHOST
      // Indicate that we have successfully initialized

      This->m_state = LISTENER_RUNNING;
      sem_post(&This->m_waitSem);
#endif

      // Now execute the session.  The session will run until either (1) we
      // were asked to terminate gracefully (with m_state !=LISTENER_RUNNING),
      // of if an error occurred while reading from the keyboard device.  If
      // we are configured to use a USB keyboard, then this error, depending
      // upon what the error is, may indicate that the USB keyboard has been
      // removed.  In that case, we need to continue looping and, hopefully,
      // the USB keyboard will be reconnected.

      int ret = This->session();
#ifdef CONFIG_NXWM_KEYBOARD_USBHOST
      if (ret < 0)
        {
          ferr("ERROR: CKeyboard::session() returned %d\n", ret);
        }
#else
      // No errors from session() are expected

      DEBUGASSERT(ret == OK);
      UNUSED(ret);
#endif

      // Close the keyboard device

      close(This->m_kbdFd);
      This->m_kbdFd = -1;
    }

  // We should get here only if we were asked to terminate via
  // m_state = LISTENER_STOPREQUESTED (or perhaps if some irrecoverable
  // error has occurred).

  ginfo("Listener exiting\n");
  This->m_state = LISTENER_TERMINATED;
  return (FAR void *)0;
}