/****************************************************************************
 * drivers/ioexpander/icjx.c
 *
 * SPDX-License-Identifier: Apache-2.0
 *
 * 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 <sys/param.h>

#include <nuttx/kmalloc.h>
#include <nuttx/arch.h>
#include <nuttx/spi/spi.h>
#include <nuttx/ioexpander/ioexpander.h>
#include <nuttx/ioexpander/icjx.h>

#include "icjx.h"

#ifdef CONFIG_IOEXPANDER_ICJX

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

#define ICJX_NOP     0x00
#define ICJX_RNW     0x01
#define ICJX_NOB1    0x0f
#define ICJX_NOB2    0x1e
#define ICJX_CONTROL 0x59

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

struct icjx_callback_s
{
  ioe_pinset_t pinset;
  ioe_callback_t function;
  FAR void *arg;
};

struct icjx_dev_s
{
  struct ioexpander_dev_s dev;         /* Nested structure to allow casting
                                        * as public gpio expander. */
  FAR struct icjx_config_s *config;    /* Board configuration data */
  FAR struct spi_dev_s *spi;           /* Saved SPI driver instance */

  uint16_t outpins;
  uint16_t outstate;
  uint16_t irqpins;
  mutex_t lock;

#ifdef CONFIG_IOEXPANDER_INT_ENABLE
  struct work_s work;
  struct icjx_callback_s callback;
#endif
};

/****************************************************************************
 * Private Function Protototypes
 ****************************************************************************/

/* SPI helpers */

static void icjx_select(FAR struct spi_dev_s *spi,
                        FAR struct icjx_config_s *config, int bits);
static void icjx_deselect(FAR struct spi_dev_s *spi,
                          FAR struct icjx_config_s *config);

/* Read/Write helpers */

static int icjx_read(FAR struct icjx_dev_s *priv, uint8_t reg,
                     uint16_t *data, int nob);
static int icjx_write(FAR struct icjx_dev_s *priv, uint8_t reg,
                      uint16_t data, int nob);

/* I/O Expander Methods */

static int icjx_direction(FAR struct ioexpander_dev_s *dev, uint8_t pin,
                          int dir);
static int icjx_option(FAR struct ioexpander_dev_s *dev, uint8_t pin,
                       int opt, void *regval);
static int icjx_writepin(FAR struct ioexpander_dev_s *dev, uint8_t pin,
                         bool value);
static int icjx_readpin(FAR struct ioexpander_dev_s *dev, uint8_t pin,
                        FAR bool *value);
#ifdef CONFIG_IOEXPANDER_MULTIPIN
static int icjx_multiwritepin(FAR struct ioexpander_dev_s *dev,
                              FAR const uint8_t *pins,
                              FAR const bool *values, int count);
static int icjx_multireadpin(FAR struct ioexpander_dev_s *dev,
                             FAR const uint8_t *pins, FAR bool *values,
                             int count);
#endif
#ifdef CONFIG_IOEXPANDER_INT_ENABLE
static FAR void *icjx_attach(FAR struct ioexpander_dev_s *dev,
                             ioe_pinset_t pinset, ioe_callback_t callback,
                             FAR void *arg);
static int icjx_detach(FAR struct ioexpander_dev_s *dev, FAR void *handle);
#endif

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

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

static struct icjx_dev_s g_icjx;
#endif

/* I/O expander vtable */

static const struct ioexpander_ops_s g_icjx_ops =
{
  icjx_direction,
  icjx_option,
  icjx_writepin,
  icjx_readpin,
  icjx_readpin
#ifdef CONFIG_IOEXPANDER_MULTIPIN
  , icjx_multiwritepin
  , icjx_multireadpin
  , icjx_multireadpin
#endif
#ifdef CONFIG_IOEXPANDER_INT_ENABLE
  , icjx_attach
  , icjx_detach
#endif
};

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

/****************************************************************************
 * Name: icjx_select
 *
 * Description:
 *   Select the SPI, locking and  re-configuring if necessary
 *
 * Input Parameters:
 *   spi    - Reference to the SPI driver structure
 *   config - Reference to iC-JX configuration structure
 *   bits   - Number of SPI bits
 *
 * Returned Value:
 *   None
 *
 ****************************************************************************/

static void icjx_select(FAR struct spi_dev_s *spi,
                        FAR struct icjx_config_s *config, int bits)
{
  /* Select iC-JX chip (locking the SPI bus in case there are multiple
   * devices competing for the SPI bus
   */

  SPI_LOCK(spi, true);
  SPI_SELECT(spi, SPIDEV_EXPANDER(config->id), true);

  /* Now make sure that the SPI bus is configured for the iC-JX (it
   * might have gotten configured for a different device while unlocked)
   */

  SPI_SETMODE(spi, config->mode);
  SPI_SETBITS(spi, bits);
  SPI_SETFREQUENCY(spi, config->frequency);
}

/****************************************************************************
 * Name: icjx_deselect
 *
 * Description:
 *   De-select the SPI
 *
 * Input Parameters:
 *   spi    - Reference to the SPI driver structure
 *   config - Reference to iC-JX configuration structure
 *
 * Returned Value:
 *   None
 *
 ****************************************************************************/

static void icjx_deselect(FAR struct spi_dev_s *spi,
                          FAR struct icjx_config_s *config)
{
  /* De-select iC-JX chip and relinquish the SPI bus. */

  SPI_SELECT(spi, SPIDEV_EXPANDER(config->id), false);
  SPI_LOCK(spi, false);
}

/****************************************************************************
 * Name: icjx_read
 *
 * Description:
 *   Helper function for register read operation.
 *
 * Input Parameters:
 *   priv  - Pointer to icjx_dev_s structure
 *   reg   - Register offset (see icjx.h)
 *   data  - Pointer to read data
 *
 * Returned Value:
 *   0 on success, else a negative error code
 *
 ****************************************************************************/

static int icjx_read(FAR struct icjx_dev_s *priv, uint8_t reg,
                     uint16_t *data, int nob)
{
  uint8_t startaddr;
  uint8_t tx_buffer[6];
  uint8_t rx_buffer[6];

  startaddr =  (priv->config->addr << 6) | (reg << 1) | ICJX_RNW;
  tx_buffer[0] = startaddr;
  tx_buffer[1] = ICJX_NOP;
  tx_buffer[2] = nob;

  icjx_select(priv->spi, priv->config, 8);
  SPI_EXCHANGE(priv->spi, tx_buffer, rx_buffer, 3);

  *data = rx_buffer[2];

  if (priv->config->verification)
    {
      if (nob == ICJX_NOB2)
        {
          tx_buffer[0] = rx_buffer[2];
          SPI_EXCHANGE(priv->spi, tx_buffer, rx_buffer, 1);
          *data |= rx_buffer[0] << 8;
          tx_buffer[0] = rx_buffer[0];
          startaddr =  (priv->config->addr << 6) |
                       ((reg + 1) << 1) | ICJX_RNW;
        }
      else
        {
          tx_buffer[0] = rx_buffer[2];
        }

      tx_buffer[1] = ICJX_CONTROL;
      SPI_EXCHANGE(priv->spi, tx_buffer, rx_buffer, 2);
    }

  icjx_deselect(priv->spi, priv->config);

  if (priv->config->verification)
    {
      if (rx_buffer[0] != startaddr)
        {
          gpioerr("ERROR: Data verification error for register 0x%x!\n",
                  reg);
          return -EIO;
        }

      if (rx_buffer[1] != ICJX_CONTROL)
        {
          gpioerr("ERROR: Control byte verification error for register"
                  "0x%x!\n", reg);
          return -EIO;
        }
    }

  return OK;
}

/****************************************************************************
 * Name: icjx_write
 *
 * Description:
 *   Helper function for register write operation.
 *
 * Input Parameters:
 *   priv  - Pointer to icjx_dev_s structure
 *   reg   - Register offset (see icjx.h)
 *   data  - Data to be written
 *
 * Returned Value:
 *   0 on success, else a negative error code
 *
 ****************************************************************************/

static int icjx_write(FAR struct icjx_dev_s *priv, uint8_t reg,
                      uint16_t data, int nob)
{
  uint8_t startaddr;
  uint8_t bytes_to_exchange;
  uint8_t tx_buffer[6];
  uint8_t rx_buffer[6];
  int ver_idx;
  int data_len;

  data_len = 1;
  ver_idx = 2;
  bytes_to_exchange = 0;
  startaddr =  (priv->config->addr << 6) | (reg << 1);
  tx_buffer[bytes_to_exchange++] = startaddr;
  tx_buffer[bytes_to_exchange++] = nob;
  tx_buffer[bytes_to_exchange++] = data & 0xff;
  if (nob == ICJX_NOB2)
    {
      data_len = 2;
      tx_buffer[bytes_to_exchange++] = (data >> 8) & 0xff;
    }

  if (priv->config->verification)
    {
      tx_buffer[bytes_to_exchange++] = (priv->config->addr << 6) |
                                       ((reg + (data_len - 1)) << 1);
      tx_buffer[bytes_to_exchange++] = ICJX_CONTROL;
    }

  icjx_select(priv->spi, priv->config, 8);
  SPI_EXCHANGE(priv->spi, tx_buffer, rx_buffer, bytes_to_exchange);
  icjx_deselect(priv->spi, priv->config);

  if (priv->config->verification)
    {
      if (rx_buffer[ver_idx++] != nob)
        {
          gpioerr("ERROR: Start address verification error for register"
                  "0x%x!\n", reg);
          return -EIO;
        }

      for (int i = 0; i < data_len; i++)
        {
          if (rx_buffer[ver_idx++] != ((data >> (i * 8)) & 0xff))
            {
              gpioerr("ERROR: Data verification error for register 0x%x!\n",
                      reg);
              return -EIO;
            }
        }

      if (rx_buffer[ver_idx++] != ICJX_CONTROL)
        {
          gpioerr("ERROR: Control verification error for register 0x%x!\n",
                  reg);
          return -EIO;
        }
    }

  return OK;
}

/****************************************************************************
 * Name: icjx_direction
 *
 * Description:
 *   iC-JX is only input pin. However interface is provided in order
 *   to avoid system falls if called.
 *
 * 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 icjx_direction(FAR struct ioexpander_dev_s *dev, uint8_t pin,
                          int dir)
{
  FAR struct icjx_dev_s *priv = (FAR struct icjx_dev_s *)dev;
  uint8_t outpins;
  uint16_t out_set;
  uint8_t regaddr;
  int ret;

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

  DEBUGASSERT(priv != NULL && priv->config != NULL && pin < 16);

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

  if (dir == IOEXPANDER_DIRECTION_OUT)
    {
      priv->outpins |= (1 << pin);
    }
  else
    {
      priv->outpins &= ~(1 << pin);
    }

  if (pin < 8)
    {
      regaddr = ICJX_CTRL_WORD_2_A;
      outpins = priv->outpins & 0xff;
    }
  else
    {
      regaddr = ICJX_CTRL_WORD_2_B;
      outpins = (priv->outpins >> 8) & 0xff;
    }

  ret = icjx_read(priv, regaddr, &out_set, ICJX_NOB1);
  if (ret < 0)
    {
      nxmutex_unlock(&priv->lock);
      return ret;
    }

  /* Enable output generation */

  if ((outpins & 0xf) != 0)
    {
      out_set |= ICJX_CTRL_WORD_2_NIOL;
    }
  else
    {
      out_set &= ~ICJX_CTRL_WORD_2_NIOL;
    }

  if ((outpins & 0xf0) != 0)
    {
      out_set |= ICJX_CTRL_WORD_2_NIOH;
    }
  else
    {
      out_set &= ~ICJX_CTRL_WORD_2_NIOH;
    }

  ret = icjx_write(priv, regaddr, out_set, ICJX_NOB1);
  nxmutex_unlock(&priv->lock);

  return ret;
}

/****************************************************************************
 * Name: icjx_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
 *   value - The option's value
 *
 * Returned Value:
 *   0 on success, else a negative error code
 *
 ****************************************************************************/

static int icjx_option(FAR struct ioexpander_dev_s *dev, uint8_t pin,
                       int opt, FAR void *value)
{
  /* TODO: Implementation of iC-JX options should be here. This includes
   * setup of filters, ADC etc. The right way to implement
   * this would probably be to introduce config structure to
   * include/nuttx/ioexpanders/icjx.h that the user could use for the
   * nibbles configuration.
   *
   * Currently only interrupts are implemented.
   */

  FAR struct icjx_dev_s *priv = (FAR struct icjx_dev_s *)dev;
  uint8_t data;
  uint8_t reg;
  int ret;

  DEBUGASSERT(priv != NULL && priv->config != NULL);

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

  if (opt == IOEXPANDER_OPTION_INTCFG)
    {
      unsigned int ival = (unsigned int)((uintptr_t)value);
      ret = nxmutex_lock(&priv->lock);
      if (ret < 0)
        {
          return ret;
        }

      switch (ival)
        {
          case IOEXPANDER_VAL_HIGH:
          case IOEXPANDER_VAL_LOW:
          case IOEXPANDER_VAL_RISING:
          case IOEXPANDER_VAL_FALLING:
          case IOEXPANDER_VAL_BOTH:
            priv->irqpins |= 1 << pin;
            break;
          case IOEXPANDER_VAL_DISABLE:
            priv->irqpins &= ~(1 << pin);
            break;
          default:
            nxmutex_unlock(&priv->lock);
            return -EINVAL;
        }

      /* We have to modify ICJX_CHNG_INT_EN_A or ICJX_CHNG_INT_EN_B
       * register.
       */

      if (pin < 8)
        {
          reg = ICJX_CHNG_INT_EN_A;
          data = priv->irqpins & 0xff;
        }
      else
        {
          reg = ICJX_CHNG_INT_EN_B;
          data = (priv->irqpins >> 8) & 0xff;
        }

      ret = icjx_write(priv, reg, data, ICJX_NOB1);
      if (ret < 0)
        {
          gpioerr("Cannot write to %s register\n.",
            reg == ICJX_CHNG_INT_EN_A ?
            "ICJX_CHNG_INT_EN_A" : "ICJX_CHNG_INT_EN_B");
        }

      nxmutex_unlock(&priv->lock);
    }
  else
    {
      return -ENOTTY;
    }

  return ret;
}

/****************************************************************************
 * Name: icjx_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
 *   value - 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 icjx_writepin(FAR struct ioexpander_dev_s *dev, uint8_t pin,
                         bool value)
{
  FAR struct icjx_dev_s *priv = (FAR struct icjx_dev_s *)dev;
  uint8_t outstate;
  uint8_t regaddr;
  int ret;

  if (pin > 16)
    {
      return -ENXIO;
    }

  DEBUGASSERT(priv != NULL && priv->config != NULL);

  gpioinfo("Expander id=%02x, pin=%u\n", priv->config->id, pin);

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

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

  if ((priv->outpins & (1 << pin)) == 0)
    {
      gpioerr("ERROR: pin%u is an input\n", pin);
      nxmutex_unlock(&priv->lock);
      return -EINVAL;
    }

  /* Set/clear a bit in outstate. */

  if (value)
    {
      priv->outstate |= (1 << pin);
    }
  else
    {
      priv->outstate &= ~(1 << pin);
    }

  if (pin < 8)
    {
      regaddr = ICJX_OUTPUT_A;
      outstate = priv->outstate & 0xff;
    }
  else
    {
      regaddr = ICJX_OUTPUT_B;
      outstate = (priv->outstate >> 8) & 0xff;
    }

  ret = icjx_write(priv, regaddr, outstate, ICJX_NOB1);
  nxmutex_unlock(&priv->lock);

  return ret;
}

/****************************************************************************
 * Name: icjx_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
 *   value  - Pointer to a buffer where the pin level is stored. Usually
 *            TRUE if the pin is high, except if OPTION_INVERT has been
 *            set this pin.
 *
 * Returned Value:
 *   0 on success, else a negative error code
 *
 ****************************************************************************/

static int icjx_readpin(FAR struct ioexpander_dev_s *dev, uint8_t pin,
                        FAR bool *value)
{
  FAR struct icjx_dev_s *priv = (FAR struct icjx_dev_s *)dev;
  uint8_t regaddr;
  uint16_t data;
  int ret;

  if (pin > 16)
    {
      return -ENXIO;
    }

  DEBUGASSERT(priv != NULL && priv->config != NULL && value != NULL);

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

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

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

  /* Make sure that this is an output pin */

  if ((priv->outpins & (1 << pin)) != 0)
    {
      *value = ((priv->outstate & (1 << pin)) != 0);
      nxmutex_unlock(&priv->lock);
      return OK;
    }

  regaddr = pin < 8 ? ICJX_INPUT_A : ICJX_INPUT_B;

  ret = icjx_read(priv, regaddr, &data, ICJX_NOB1);
  nxmutex_unlock(&priv->lock);
  if (ret != OK)
    {
      return ret;
    }

  *value = (bool)((data >> (pin & 0xf)) & 1);
  return OK;
}

/****************************************************************************
 * Name: icjx_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
 *   values - The list of pin levels.
 *   count  - Number of pins to be written
 *
 * Returned Value:
 *   0 on success, else a negative error code
 *
 ****************************************************************************/

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

  if (count >= 16)
    {
      return -ENXIO;
    }

  DEBUGASSERT(priv != NULL && priv->config != NULL);

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

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

  for (int i = 0; i < count; i++)
    {
      pin = pins[i];
      value = values[i];

      gpioinfo("Expander id=%02x, pin=%u\n", priv->config->id, pin);

      if ((priv->outpins & (1 << pin)) == 0)
        {
          gpioerr("ERROR: pin%u is an input\n", pin);
          nxmutex_unlock(&priv->lock);
          return -EINVAL;
        }

      /* Set/clear a bit in outstate. */

      if (value)
        {
          priv->outstate |= (1 << pin);
        }
      else
        {
          priv->outstate &= ~(1 << pin);
        }
    }

  ret = icjx_write(priv, ICJX_OUTPUT_A, priv->outstate, ICJX_NOB2);
  nxmutex_unlock(&priv->lock);

  return ret;
}

/****************************************************************************
 * Name: icjx_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
 *   values - Pointer to a buffer where the pin levels are stored.
 *   count  - Number of pins to be read
 *
 * Returned Value:
 *   0 on success, else a negative error code
 *
 ****************************************************************************/

static int icjx_multireadpin(FAR struct ioexpander_dev_s *dev,
                             FAR const uint8_t *pins, FAR bool *values,
                             int count)
{
  FAR struct icjx_dev_s *priv = (FAR struct icjx_dev_s *)dev;
  uint16_t data;
  int pin;
  int ret;

  if (count > 16)
    {
      return -ENXIO;
    }

  DEBUGASSERT(priv != NULL && priv->config != NULL && values != NULL);

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

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

  for (int i = 0; i < count; i++)
    {
      pin = pins[i];

      gpioinfo("Expander id=%02x, pin=%u\n", priv->config->id, pin);

      if ((priv->outpins & (1 << pin)) != 0)
        {
           values[i] = (bool)((priv->outstate & (1 << pin)) != 0);
        }
    }

  ret = icjx_read(priv, ICJX_INPUT_A, &data, ICJX_NOB2);
  nxmutex_unlock(&priv->lock);
  if (ret != OK)
    {
      return ret;
    }

  for (int i = 0; i < count; i++)
    {
      values[i] = (bool)((data >> (pins[i] & 0xf)) & 1);
    }

  return OK;
}
#endif /* CONFIG_IOEXPANDER_MULTIPIN */

/****************************************************************************
 * Name: icjx_attach
 *
 * Description:
 *   Attach and enable 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.
 *   arg      - User-provided callback argument
 *
 * Returned Value:
 *   A non-NULL handle value is returned on success.  This handle may be
 *   used later to detach and disable the pin interrupt.
 *
 ****************************************************************************/

#ifdef CONFIG_IOEXPANDER_INT_ENABLE
static FAR void *icjx_attach(FAR struct ioexpander_dev_s *dev,
                             ioe_pinset_t pinset, ioe_callback_t callback,
                             FAR void *arg)
{
  FAR struct icjx_dev_s *priv = (FAR struct icjx_dev_s *)dev;
  FAR void *handle = NULL;
  int ret;

  /* Get exclusive access */

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

  if (priv->callback.function == NULL)
    {
      /* Yes.. use this entry */

      priv->callback.pinset   = pinset;
      priv->callback.function = callback;
      priv->callback.arg      = arg;
      handle                  = &priv->callback;
    }

  /* Add this callback to the table */

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

/****************************************************************************
 * Name: icjx_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 pca9555_attch()
 *
 * Returned Value:
 *   0 on success, else a negative error code
 *
 ****************************************************************************/

static int icjx_detach(FAR struct ioexpander_dev_s *dev, FAR void *handle)
{
  FAR struct icjx_callback_s *cb =
    (FAR struct icjx_callback_s *)handle;

  cb->pinset = 0;
  cb->function = NULL;
  cb->arg  = NULL;
  return OK;
}

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

#ifdef CONFIG_ICJX_INT_ENABLE
static void icjx_interrupt_worker(void *arg)
{
  FAR struct icjx_dev_s *priv = (FAR struct icjx_dev_s *)arg;
  uint16_t change_of_input;
  uint16_t isr;
  ioe_pinset_t irq_match;
  int ret;

  /* Disable interrupts. All newly incoming interrupts are handled in
   * a while loop if there is something to handle.
   */

  priv->config->enable(priv->config, false);

  /* Read interrupt status register */

  icjx_read(priv, ICJX_INT_STATUS_A, &isr, ICJX_NOB2);
  while (isr != 0)
    {
      if ((isr & ICJX_ISR_A_DCHI) != 0)
        {
          ret = icjx_read(priv, ICJX_CHNG_MSG_A, &change_of_input,
                          ICJX_NOB2);
          if (ret == OK)
            {
              irq_match = change_of_input & priv->callback.pinset;
              if (irq_match != 0)
                {
                  /* Change of input.. perform the callback */

                  priv->callback.function(&priv->dev, irq_match,
                                          priv->callback.arg);
                }
            }
        }

      /* Clear interrupt and check ISR again */

      icjx_write(priv, ICJX_CTRL_WORD_4, ICJX_CTRL_WORD_4_EOI, ICJX_NOB1);
      icjx_read(priv, ICJX_INT_STATUS_A, &isr, ICJX_NOB2);
    }

  /* And enable interrupts again. */

  priv->config->enable(priv->config, true);
}

/****************************************************************************
 * Name: icjx_interrupt
 *
 * Description:
 *   Handle GPIO interrupt events (this function executes in the
 *   context of the interrupt).
 *
 ****************************************************************************/

static int icjx_interrupt(int irq, FAR void *context, FAR void *arg)
{
  FAR struct icjx_dev_s *priv = (FAR struct icjx_dev_s *)arg;

  /* Create HP work to handle the interrupt. We do not want to do
   * this in the interrupt handler because SPI communication speed.
   */

  DEBUGASSERT(work_available(&priv->work));
  DEBUGVERIFY(work_queue(HPWORK, &priv->work, icjx_interrupt_worker,
                         (FAR void *)priv, 0));

  return OK;
}

#endif /* CONFIG_ICJX_INT_ENABLE */
#endif /* CONFIG_IOEXPANDER_INT_ENABLE */

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

/****************************************************************************
 * Name: icjx_initialize
 *
 * Description:
 *   Instantiate and configure the ICJXxx device driver to use the
 *   provided SPI device instance.
 *
 * Input Parameters:
 *   spi     - A SPI driver instance
 *   config  - Persistent board configuration data
 *
 * Returned Value:
 *   an ioexpander_dev_s instance on success, NULL on failure.
 *
 ****************************************************************************/

FAR struct ioexpander_dev_s *icjx_initialize(FAR struct spi_dev_s *spi,
                                      FAR struct icjx_config_s *config)
{
  FAR struct icjx_dev_s *priv;
  uint8_t regval;
  int ret;

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

  priv = kmm_zalloc(sizeof(struct icjx_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_icjx;
#endif

  /* Initialize the device state structure */

  priv->dev.ops  = &g_icjx_ops;
  priv->spi      = spi;
  priv->config   = config;
  priv->outpins  = 0;
  priv->outstate = 0;
  priv->irqpins  = 0;

#ifdef CONFIG_ICJX_INT_ENABLE
  config->attach(config, icjx_interrupt, priv);
  config->enable(config, true);
#endif

  nxmutex_init(&priv->lock);

  /* Set pull up/down based on expander configuration. This is the first
   * time we are accessing the expander therefore we do not have to worry
   * about reading the register content first.
   */

  regval = (config->current_src << 4) | config->current_src;

  ret = icjx_write(priv, ICJX_CTRL_WORD_2_A, regval, ICJX_NOB1);
  if (ret < 0)
    {
      gpioerr("ERROR: Could write to ICJX_CTRL_WORD_2_A: %d!\n", ret);
      goto err;
    }

  ret = icjx_write(priv, ICJX_CTRL_WORD_2_B, regval, ICJX_NOB1);
  if (ret < 0)
    {
      gpioerr("ERROR: Could write to ICJX_CTRL_WORD_2_B: %d!\n", ret);
      goto err;
    }

  /* Bypass filters as those are not yet supported. */

  regval = ICJX_CTRL_WORD_1_BYP0 | ICJX_CTRL_WORD_1_BYP1;
  ret = icjx_write(priv, ICJX_CTRL_WORD_1_A, regval, ICJX_NOB1);
  if (ret < 0)
    {
      gpioerr("ERROR: Could write to ICJX_CTRL_WORD_1_A: %d!\n", ret);
      goto err;
    }

  ret = icjx_write(priv, ICJX_CTRL_WORD_1_B, regval, ICJX_NOB1);
  if (ret < 0)
    {
      gpioerr("ERROR: Could write to ICJX_CTRL_WORD_1_B: %d!\n", ret);
      goto err;
    }

  /* Clear initial interrupts if any */

  icjx_write(priv, ICJX_CTRL_WORD_4, ICJX_CTRL_WORD_4_EOI, ICJX_NOB1);

  return &priv->dev;

err:
  nxmutex_destroy(&priv->lock);
#ifdef CONFIG_ICJX_MULTIPLE
  kmm_free(priv);
#endif
  return NULL;
}

#endif /* CONFIG_IOEXPANDER_ICJX */