/****************************************************************************
 * drivers/ioexpander/skeleton.c
 *
 * 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 <assert.h>
#include <errno.h>
#include <debug.h>

#include <nuttx/kmalloc.h>
#include <nuttx/mutex.h>
#include <nuttx/ioexpander/ioexpander.h>
#include <nuttx/ioexpander/skeleton.h>

#include "skeleton.h"

#if defined(CONFIG_IOEXPANDER_skeleton)

/****************************************************************************
 * Pre-processor Definitions
 ****************************************************************************/

/****************************************************************************
 * Private Types
 ****************************************************************************/

#ifdef CONFIG_IOEXPANDER_INT_ENABLE
/* This type represents on registered pin interrupt callback */

struct skel_callback_s
{
  ioe_pinset_t pinset;          /* Set of pin interrupts that will generate the callback */
  ioe_callback_t cbfunc;        /* The saved callback function pointer */
};
#endif

/* This structure represents the state of the I/O Expander driver */

struct skel_dev_s
{
  struct ioexpander_dev_s dev;  /* Nested structure to allow casting as public gpio expander */
#ifdef CONFIG_skeleton_MULTIPLE
  FAR struct skel_dev_s *flink; /* Supports a singly linked list of drivers */
#endif
  mutex_t lock;                 /* Mutual exclusion */

#ifdef CONFIG_IOEXPANDER_INT_ENABLE
  struct work_s work;           /* Supports the interrupt handling "bottom half" */

  /* Saved callback information for each I/O expander client */

  struct skel_callback_s cb[CONFIG_SKELETON_INT_NCALLBACKS];
#endif
};

/****************************************************************************
 * Private Function Prototypes
 ****************************************************************************/

static int skel_lock(FAR struct skel_dev_s *priv);

static int skel_direction(FAR struct ioexpander_dev_s *dev, uint8_t pin,
                          int dir);
static int skel_option(FAR struct ioexpander_dev_s *dev, uint8_t pin,
                       int opt, void *val);
static int skel_writepin(FAR struct ioexpander_dev_s *dev, uint8_t pin,
                         bool value);
static int skel_readpin(FAR struct ioexpander_dev_s *dev, uint8_t pin,
                        FAR bool *value);
static int skel_readbuf(FAR struct ioexpander_dev_s *dev, uint8_t pin,
                        FAR bool *value);
#ifdef CONFIG_IOEXPANDER_MULTIPIN
static int skel_multiwritepin(FAR struct ioexpander_dev_s *dev,
                              FAR const uint8_t *pins,
                              FAR const bool *values, int count);
static int skel_multireadpin(FAR struct ioexpander_dev_s *dev,
                             FAR const uint8_t *pins,
                             FAR bool *values, int count);
static int skel_multireadbuf(FAR struct ioexpander_dev_s *dev,
                             FAR const uint8_t *pins,
                             FAR bool *values, int count);
#endif
#ifdef CONFIG_IOEXPANDER_INT_ENABLE
static int skel_attach(FAR struct ioexpander_dev_s *dev,
                       ioe_pinset_t pinset, ioe_callback_t callback);
static int skel_detach(FAR struct ioexpander_dev_s *dev,
                       FAR void *handle);

static void skel_irqworker(void *arg);
static void skel_interrupt(FAR void *arg);
#endif

/****************************************************************************
 * Private Data
 ****************************************************************************/

#ifndef CONFIG_skeleton_MULTIPLE
/* If only a single device is supported, then the driver state structure may
 * as well be pre-allocated.
 */

static struct skel_dev_s g_skel;
#endif

/* I/O expander vtable */

static const struct ioexpander_ops_s g_skel_ops =
{
  skel_direction,
  skel_option,
  skel_writepin,
  skel_readpin,
  skel_readbuf
#ifdef CONFIG_IOEXPANDER_MULTIPIN
  , skel_multiwritepin
  , skel_multireadpin
  , skel_multireadbuf
#endif
#ifdef CONFIG_IOEXPANDER_INT_ENABLE
  , skel_attach
  , skel_detach
#endif
};

/****************************************************************************
 * Private Functions
 ****************************************************************************/

/****************************************************************************
 * Name: skel_direction
 *
 * Description:
 *   Set the direction of an ioexpander pin. Required.
 *
 * Input Parameters:
 *   dev - Device-specific state data
 *   pin - The index of the pin to alter in this call
 *   dir - One of the IOEXPANDER_DIRECTION_ macros
 *
 * Returned Value:
 *   0 on success, else a negative error code
 *
 ****************************************************************************/

static int skel_direction(FAR struct ioexpander_dev_s *dev, uint8_t pin,
                          int direction)
{
  FAR struct skel_dev_s *priv = (FAR struct skel_dev_s *)dev;
  int ret;

  if (direction != IOEXPANDER_DIRECTION_IN &&
      direction != IOEXPANDER_DIRECTION_OUT)
    {
      return -EINVAL;
    }

  gpioinfo("pin=%u direction=%s\n",
           pin, (direction == IOEXPANDER_DIRECTION_IN) ? "IN" : "OUT");

  DEBUGASSERT(priv != NULL && pin < CONFIG_IOEXPANDER_NPINS);

  /* Get exclusive access to the I/O Expander */

  ret = nxmutex_lock(&priv->lock);
  if (ret < 0)
    {
      return ret;
    }

  /* Set the pin direction in the I/O Expander */
#warning Missing logic

  nxmutex_unlock(&priv->lock);
  return ret;
}

/****************************************************************************
 * Name: skel_option
 *
 * Description:
 *   Set pin options. Required.
 *   Since all IO expanders have various pin options, this API allows setting
 *     pin options in a flexible way.
 *
 * Input Parameters:
 *   dev - Device-specific state data
 *   pin - The index of the pin to alter in this call
 *   opt - One of the IOEXPANDER_OPTION_ macros
 *   val - The option's value
 *
 * Returned Value:
 *   0 on success, else a negative error code
 *
 ****************************************************************************/

static int skel_option(FAR struct ioexpander_dev_s *dev, uint8_t pin,
                       int opt, FAR void *value)
{
  FAR struct skel_dev_s *priv = (FAR struct skel_dev_s *)dev;
  int ret = -ENOSYS;

  gpioinfo("addr=%02x pin=%u option=%u\n",  priv->addr, pin, opt);

  DEBUGASSERT(priv != NULL);

  /* Check for pin polarity inversion. */

  if (opt == IOEXPANDER_OPTION_INVERT)
    {
      /* Get exclusive access to the I/O Expander */

      ret = nxmutex_lock(&priv->lock);
      if (ret < 0)
        {
          return ret;
        }

      /* Set the pin option */
#warning Missing logic

      nxmutex_unlock(&priv->lock);
    }

  return ret;
}

/****************************************************************************
 * Name: skel_writepin
 *
 * Description:
 *   Set the pin level. Required.
 *
 * Input Parameters:
 *   dev - Device-specific state data
 *   pin - The index of the pin to alter in this call
 *   val - The pin level. Usually TRUE will set the pin high,
 *         except if OPTION_INVERT has been set on this pin.
 *
 * Returned Value:
 *   0 on success, else a negative error code
 *
 ****************************************************************************/

static int skel_writepin(FAR struct ioexpander_dev_s *dev, uint8_t pin,
                         bool value)
{
  FAR struct skel_dev_s *priv = (FAR struct skel_dev_s *)dev;
  int ret;

  gpioinfo("pin=%u value=%u\n", pin, value);

  DEBUGASSERT(priv != NULL && pin < CONFIG_IOEXPANDER_NPINS);

  /* Get exclusive access to the I/O Expander */

  ret = nxmutex_lock(&priv->lock);
  if (ret < 0)
    {
      return ret;
    }

  /* Write the pin value */
#warning Missing logic

  nxmutex_unlock(&priv->lock);
  return ret;
}

/****************************************************************************
 * Name: skel_readpin
 *
 * Description:
 *   Read the actual PIN level. This can be different from the last value
 *   written to this pin. Required.
 *
 * Input Parameters:
 *   dev    - Device-specific state data
 *   pin    - The index of the pin
 *   valptr - Pointer to a buffer where the pin level is stored. Usually TRUE
 *            if the pin is high, except if OPTION_INVERT has been set on
 *            this pin.
 *
 * Returned Value:
 *   0 on success, else a negative error code
 *
 ****************************************************************************/

static int skel_readpin(FAR struct ioexpander_dev_s *dev, uint8_t pin,
                        FAR bool *value)
{
  FAR struct skel_dev_s *priv = (FAR struct skel_dev_s *)dev;
  int ret;

  gpioinfo("pin=%u\n", priv->addr);

  DEBUGASSERT(priv != NULL && pin < CONFIG_IOEXPANDER_NPINS &&
              value != NULL);

  /* Get exclusive access to the I/O Expander */

  ret = nxmutex_lock(&priv->lock);
  if (ret < 0)
    {
      return ret;
    }

  /* Read the pin value */
#warning Missing logic

  /* Return the pin value via the value pointer */
#warning Missing logic

  nxmutex_unlock(&priv->lock);
  return ret;
}

/****************************************************************************
 * Name: skel_readbuf
 *
 * Description:
 *   Read the buffered pin level.
 *   This can be different from the actual pin state. Required.
 *
 * Input Parameters:
 *   dev    - Device-specific state data
 *   pin    - The index of the pin
 *   valptr - Pointer to a buffer where the level is stored.
 *
 * Returned Value:
 *   0 on success, else a negative error code
 *
 ****************************************************************************/

static int skel_readbuf(FAR struct ioexpander_dev_s *dev, uint8_t pin,
                        FAR bool *value)
{
  FAR struct skel_dev_s *priv = (FAR struct skel_dev_s *)dev;
  int ret;

  /* Get exclusive access to the I/O Expander */

  ret = nxmutex_lock(&priv->lock);
  if (ret < 0)
    {
      return ret;
    }

  /* Read the buffered pin level */
#warning Missing logic

  nxmutex_unlock(&priv->lock);
  return ret;
}

/****************************************************************************
 * Name: skel_getmultibits
 *
 * Description:
 *  Read multiple bits from I/O Expander registers.
 *
 ****************************************************************************/

#ifdef CONFIG_IOEXPANDER_MULTIPIN
static int skel_getmultibits(FAR struct skel_dev_s *priv, FAR uint8_t *pins,
                             FAR bool *values, int count)
{
  ioe_pinset_t pinset;
  int pin;
  int ret = OK;
  int i;

  /* Read the pinset from the IO-Expander hardware */
#warning Missing logic

  /* Read the requested bits */

  for (i = 0; i < count; i++)
    {
      pin = pins[i];
      if (pin >= CONFIG_IOEXPANDER_NPINS)
        {
          return -ENXIO;
        }

      values[i] = (((pinset >> pin) & 1) != 0);
    }

  return OK;
}
#endif

/****************************************************************************
 * Name: skel_multiwritepin
 *
 * Description:
 *   Set the pin level for multiple pins. This routine may be faster than
 *   individual pin accesses. Optional.
 *
 * Input Parameters:
 *   dev - Device-specific state data
 *   pins - The list of pin indexes to alter in this call
 *   val - The list of pin levels.
 *
 * Returned Value:
 *   0 on success, else a negative error code
 *
 ****************************************************************************/

#ifdef CONFIG_IOEXPANDER_MULTIPIN
static int skel_multiwritepin(FAR struct ioexpander_dev_s *dev,
                              FAR const uint8_t *pins,
                              FAR const bool *values, int count)
{
  FAR struct skel_dev_s *priv = (FAR struct skel_dev_s *)dev;
  ioe_pinset_t pinset;
  int pin;
  int ret;
  int i;

  /* Get exclusive access to the I/O Expander */

  ret = nxmutex_lock(&priv->lock);
  if (ret < 0)
    {
      return ret;
    }

  /* Read the pinset from the IO-Expander hardware */
#warning Missing logic

  /* Apply the user defined changes */

  for (i = 0; i < count; i++)
    {
      pin = pins[i];
      if (pin >= CONFIG_IOEXPANDER_NPINS)
        {
          nxmutex_unlock(&priv->lock);
          return -ENXIO;
        }

      if (values[i])
        {
          pinset |= ((ioe_pinset_t)1 << pin);
        }
      else
        {
          pinset &= ~((ioe_pinset_t)1 << pin);
        }
    }

  /* Now write back the new pins states */
#warning Missing logic

  nxmutex_unlock(&priv->lock);
  return ret;
}
#endif

/****************************************************************************
 * Name: skel_multireadpin
 *
 * Description:
 *   Read the actual level for multiple pins. This routine may be faster than
 *   individual pin accesses. Optional.
 *
 * Input Parameters:
 *   dev    - Device-specific state data
 *   pin    - The list of pin indexes to read
 *   valptr - Pointer to a buffer where the pin levels are stored.
 *
 * Returned Value:
 *   0 on success, else a negative error code
 *
 ****************************************************************************/

#ifdef CONFIG_IOEXPANDER_MULTIPIN
static int skel_multireadpin(FAR struct ioexpander_dev_s *dev,
                             FAR const uint8_t *pins,
                             FAR bool *values, int count)
{
  FAR struct skel_dev_s *priv = (FAR struct skel_dev_s *)dev;
  int ret;

  gpioinfo("count=%u\n", count);

  DEBUGASSERT(priv != NULL && pins != NULL && values != NULL && count > 0);

  /* Get exclusive access to the I/O Expander */

  ret = nxmutex_lock(&priv->lock);
  if (ret < 0)
    {
      return ret;
    }

  ret = skel_getmultibits(priv, pins, values, count);
  nxmutex_unlock(&priv->lock);
  return ret;
}
#endif

/****************************************************************************
 * Name: skel_multireadbuf
 *
 * Description:
 *   Read the buffered level of multiple pins. This routine may be faster
 *   than individual pin accesses. Optional.
 *
 * Input Parameters:
 *   dev    - Device-specific state data
 *   pin    - The index of the pin
 *   valptr - Pointer to a buffer where the buffered levels are stored.
 *
 * Returned Value:
 *   0 on success, else a negative error code
 *
 ****************************************************************************/

#ifdef CONFIG_IOEXPANDER_MULTIPIN
static int skel_multireadbuf(FAR struct ioexpander_dev_s *dev,
                             FAR const uint8_t *pins,
                             FAR bool *values, int count)
{
  FAR struct skel_dev_s *priv = (FAR struct skel_dev_s *)dev;
  int ret;

  gpioinfo("count=%u\n", count);

  DEBUGASSERT(priv != NULL && pins != NULL && values != NULL && count > 0);

  /* Get exclusive access to the I/O Expander */

  ret = nxmutex_lock(&priv->lock);
  if (ret < 0)
    {
      return ret;
    }

  ret = skel_getmultibits(priv, pins, values, count);
  nxmutex_unlock(&priv->lock);
  return ret;
}
#endif

/****************************************************************************
 * Name: skel_attach
 *
 * Description:
 *   Attach a pin interrupt callback function.
 *
 * Input Parameters:
 *   dev      - Device-specific state data
 *   pinset   - The set of pin events that will generate the callback
 *   callback - The pointer to callback function.  NULL will detach the
 *              callback.
 *
 * Returned Value:
 *   0 on success, else a negative error code
 *
 ****************************************************************************/

#ifdef CONFIG_IOEXPANDER_INT_ENABLE
static int skel_attach(FAR struct ioexpander_dev_s *dev, ioe_pinset_t pinset,
                       ioe_callback_t callback)
{
  FAR struct skel_dev_s *priv = (FAR struct skel_dev_s *)dev;
  int ret;
  int i;

  /* Get exclusive access to the I/O Expander */

  ret = nxmutex_lock(&priv->lock);
  if (ret < 0)
    {
      return ret;
    }

  /* Find and available in entry in the callback table */

  ret = -ENOSPC;
  for (i = 0; i < CONFIG_SKELETON_INT_NCALLBACKS; i++)
    {
      /* Is this entry available (i.e., no callback attached) */

      if (priv->cb[i].cbfunc == NULL)
        {
          /* Yes.. use this entry */

          priv->cb[i].pinset = pinset;
          priv->cb[i].cbfunc = callback;
          ret = OK;
        }
    }

  /* Add this callback to the table */

  nxmutex_unlock(&priv->lock);
  return ret;
}
#endif

/****************************************************************************
 * Name: skel_detach_detach
 *
 * Description:
 *   Detach and disable a pin interrupt callback function.
 *
 * Input Parameters:
 *   dev      - Device-specific state data
 *   handle   - The non-NULL opaque value return by skel_attach_attch()
 *
 * Returned Value:
 *   0 on success, else a negative error code
 *
 ****************************************************************************/

#ifdef CONFIG_IOEXPANDER_INT_ENABLE
static int skel_detach(FAR struct ioexpander_dev_s *dev, FAR void *handle)
{
  FAR struct skel_dev_s *priv = (FAR struct skel_dev_s *)dev;
  FAR struct skel_callback_s *cb =
    (FAR struct skel_callback_s *)handle;

  DEBUGASSERT(priv != NULL && cb != NULL);
  DEBUGASSERT((uintptr_t)cb >= (uintptr_t)&priv->cb[0] &&
              (uintptr_t)cb <=
              (uintptr_t)&priv->cb[CONFIG_SKELETON_INT_NCALLBACKS - 1]);
  UNUSED(priv);

  cb->pinset = 0;
  cb->cbfunc = NULL;
  cb->cbarg  = NULL;
  return OK;
}
#endif

/****************************************************************************
 * Name: skel_irqworker
 *
 * Description:
 *   Handle GPIO interrupt events (this function actually executes in the
 *   context of the worker thread).
 *
 ****************************************************************************/

#ifdef CONFIG_IOEXPANDER_INT_ENABLE
static void skel_irqworker(void *arg)
{
  FAR struct skel_dev_s *priv = (FAR struct skel_dev_s *)arg;
  ioe_pinset_t pinset;
  int ret;
  int i;

  /* Read the pinset from the IO-Expander hardware */
#warning Missing logic

  /* Perform pin interrupt callbacks */

  for (i = 0; i < CONFIG_SKELETON_INT_NCALLBACKS; i++)
    {
      /* Is this entry valid (i.e., callback attached)?  If so, did andy of
       * the requested pin interrupts occur?
       */

      if (priv->cb[i].cbfunc != NULL)
        {
          /* Did any of the requested pin interrupts occur? */

          ioe_pinset_t match = pinset & priv->cb[i].pinset;
          if (match != 0)
            {
              /* Yes.. perform the callback */

              priv->cb[i].cbfunc(&priv->dev, match);
            }
        }
    }

  /* Re-enable interrupts */
#warning Missing logic
}
#endif

/****************************************************************************
 * Name: skel_interrupt
 *
 * Description:
 *   Handle GPIO interrupt events (this function executes in the
 *   context of the interrupt).
 *
 *   NOTE: A more typical prototype for an interrupt handler would be:
 *
 *     int skel_interrupt(int irq, FAR void *context, FAR void *arg)
 *
 *   However, it is assume that the lower half, board specific interface
 *   can provide intercept the actual interrupt, and call this function with
 *   the arg that can be mapped to the provide driver structure instance.
 *
 *   Presumably the lower level interface provides an attach() method that
 *   provides both the address of skel_interrupt() as well as the arg value.
 *
 ****************************************************************************/

#ifdef CONFIG_IOEXPANDER_INT_ENABLE
static void skel_interrupt(FAR void *arg)
{
  FAR struct skel_dev_s *priv = (FAR struct skel_dev_s *)arg;

  DEBUGASSERT(priv != NULL);

  /* Defer interrupt processing to the worker thread.  This is not only
   * much kinder in the use of system resources but is probably necessary
   * to access the I/O expander device.
   *
   * Notice that further GPIO interrupts are disabled until the work is
   * actually performed.  This is to prevent overrun of the worker thread.
   * Interrupts are re-enabled in skel_irqworker() when the work is
   * completed.
   */

  if (work_available(&priv->work))
    {
      /* Disable interrupts */
#warning Missing logic

      /* Schedule interrupt related work on the high priority worker
       * thread.
       */

      work_queue(HPWORK, &priv->work, skel_irqworker,
                 (FAR void *)priv, 0);
    }

  return OK;
}
#endif

/****************************************************************************
 * Public Functions
 ****************************************************************************/

/****************************************************************************
 * Name: skel_initialize
 *
 * Description:
 *   Initialize a I/O Expander device.
 *
 * NOTE: There are no arguments to the initialization function this
 * skeleton example.  Typical implementations take two arguments:
 *
 * 1) A reference to an I2C or SPI interface used to interactive with the
 *    device, and
 * 2) A read-only configuration structure that provides things like:  I2C
 *    or SPI characteristics and callbacks to attach, enable, and disable
 *    interrupts.
 *
 ****************************************************************************/

FAR struct ioexpander_dev_s *skel_initialize(void)
{
  FAR struct skel_dev_s *priv;

#ifdef CONFIG_skeleton_MULTIPLE
  /* Allocate the device state structure */

  priv = kmm_zalloc(sizeof(struct skel_dev_s));
  if (!priv)
    {
      gpioerr("ERROR: Failed to allocate driver instance\n");
      return NULL;
    }
#else
  /* Use the one-and-only I/O Expander driver instance */

  priv = &g_skel;
#endif

  /* Initialize the device state structure
   * NOTE: Normally you would also save the I2C/SPI device interface and
   * any configuration information here as well.
   */

  priv->dev.ops = &g_skel_ops;

#ifdef CONFIG_IOEXPANDER_INT_ENABLE
  /* Attach the I/O expander interrupt handler and enable interrupts */
#warning Missing logic

#endif

  nxmutex_init(&priv->lock);
  return &priv->dev;
}

#endif /* CONFIG_IOEXPANDER_skeleton */