/****************************************************************************
 * drivers/serial/ptmx.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 <sys/types.h>
#include <stdbool.h>
#include <unistd.h>
#include <fcntl.h>
#include <stdio.h>
#include <string.h>
#include <poll.h>
#include <assert.h>
#include <errno.h>

#include <nuttx/fs/fs.h>
#include <nuttx/mutex.h>
#include <nuttx/serial/pty.h>

#include "pty.h"

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

#ifndef CONFIG_PTY_MAXPTY
#  define CONFIG_PTY_MAXPTY 32
#endif

#if CONFIG_PTY_MAXPTY > 256
#  define CONFIG_PTY_MAXPTY 256
#endif

#define PTY_MAX   ((CONFIG_PTY_MAXPTY + 31) & ~31)
#define INDEX_MAX (PTY_MAX >> 5)

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

/* PTMX device state */

struct ptmx_dev_s
{
  mutex_t px_lock;                  /* Supports mutual exclusion */
  uint8_t px_next;                  /* Next minor number to allocate */
  uint32_t px_alloctab[INDEX_MAX];  /* Set of allocated PTYs */
};

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

static int     ptmx_open(FAR struct file *filep);
static ssize_t ptmx_read(FAR struct file *filep, FAR char *buffer,
                 size_t buflen);
static ssize_t ptmx_write(FAR struct file *filep, FAR const char *buffer,
                 size_t buflen);

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

static const struct file_operations g_ptmx_fops =
{
  ptmx_open,     /* open */
  NULL,          /* close */
  ptmx_read,     /* read */
  ptmx_write,    /* write */
};

static struct ptmx_dev_s g_ptmx =
{
  NXMUTEX_INITIALIZER,
};

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

/****************************************************************************
 * Name: ptmx_minor_allocate
 *
 * Description:
 *   Allocate a new unique PTY minor number.
 *
 * Assumptions:
 *   Caller hold the px_lock
 *
 ****************************************************************************/

static int ptmx_minor_allocate(void)
{
  uint8_t startaddr = g_ptmx.px_next;
  uint8_t minor;
  int index;
  int bitno;

  /* Loop until we find a valid device address */

  for (; ; )
    {
      /* Try the next device address */

      minor = g_ptmx.px_next;
      if (g_ptmx.px_next >= PTY_MAX)
        {
          g_ptmx.px_next = 0;
        }
      else
        {
          g_ptmx.px_next++;
        }

      /* Is this address already allocated? */

      index = minor >> 5;
      bitno = minor & 31;
      if ((g_ptmx.px_alloctab[index] & (1 << bitno)) == 0)
        {
          /* No... allocate it now */

          g_ptmx.px_alloctab[index] |= (1 << bitno);
          return (int)minor;
        }

      /* This address has already been allocated.  The following logic will
       * prevent (unexpected) infinite loops.
       */

      if (startaddr == minor)
        {
          /* We are back where we started... the are no free device address */

          return -ENOMEM;
        }
    }
}

/****************************************************************************
 * Name: ptmx_open
 ****************************************************************************/

static int ptmx_open(FAR struct file *filep)
{
  struct file temp;
  char devname[32];
  int minor;
  int ret;

  /* Get exclusive access */

  ret = nxmutex_lock(&g_ptmx.px_lock);
  if (ret < 0)
    {
      return ret;
    }

  /* Allocate a PTY minor */

  minor = ptmx_minor_allocate();
  nxmutex_unlock(&g_ptmx.px_lock);
  if (minor < 0)
    {
      return minor;
    }

  /* Create the master slave pair.  This should create:
   *
   *   Slave device:  /dev/pts/N
   *   Master device: /dev/ptyN
   *
   * Where N=minor
   */

  ret = pty_register2(minor, true);
  if (ret < 0)
    {
      ptmx_minor_free(minor);
      return ret;
    }

  /* Open the master device:  /dev/ptyN, where N=minor */

  snprintf(devname, sizeof(devname), "/dev/pty%d", minor);
  memcpy(&temp, filep, sizeof(temp));
  ret = file_open(filep, devname, O_RDWR | O_CLOEXEC);
  DEBUGASSERT(ret >= 0);  /* file_open() should never fail */

  /* Close the multiplexor device: /dev/ptmx */

  ret = file_close(&temp);
  DEBUGASSERT(ret >= 0);  /* file_close() should never fail */

  /* Now unlink the master.  This will remove it from the VFS namespace,
   * the driver will still persist because of the open count on the
   * driver.
   */

  ret = unregister_driver(devname);
  DEBUGASSERT(ret >= 0 || ret == -EBUSY);  /* unregister_driver() should never fail */

  return OK;
}

/****************************************************************************
 * Name: ptmx_read
 ****************************************************************************/

static ssize_t ptmx_read(FAR struct file *filep,
                         FAR char *buffer, size_t len)
{
  return 0; /* Return EOF */
}

/****************************************************************************
 * Name: ptmx_write
 ****************************************************************************/

static ssize_t ptmx_write(FAR struct file *filep,
                          FAR const char *buffer, size_t len)
{
  return len; /* Say that everything was written */
}

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

/****************************************************************************
 * Name: ptmx_register
 *
 * Input Parameters:
 *   None
 *
 * Description:
 *   Register the master pseudo-terminal multiplexor device at /dev/ptmx
 *
 * Returned Value:
 *   Zero (OK) is returned on success; a negated errno value is returned on
 *   any failure.
 *
 ****************************************************************************/

int ptmx_register(void)
{
  /* Register the PTMX driver */

  return register_driver("/dev/ptmx", &g_ptmx_fops, 0666, NULL);
}

/****************************************************************************
 * Name: ptmx_minor_free
 *
 * Description:
 *   De-allocate a PTY minor number.
 *
 * Assumptions:
 *   Caller hold the px_lock
 *
 ****************************************************************************/

void ptmx_minor_free(uint8_t minor)
{
  int index;
  int bitno;

  nxmutex_lock(&g_ptmx.px_lock);

  /* Free the address by clearing the associated bit in the px_alloctab[]; */

  index = minor >> 5;
  bitno = minor & 31;

  DEBUGASSERT((g_ptmx.px_alloctab[index] & (1 << bitno)) != 0);
  g_ptmx.px_alloctab[index] &= ~(1 << bitno);

  /* Reset the next pointer if the one just released has a lower value */

  if (minor < g_ptmx.px_next)
    {
      g_ptmx.px_next = minor;
    }

  nxmutex_unlock(&g_ptmx.px_lock);
}