/****************************************************************************
 * drivers/note/noterpmsg_driver.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/note/note_driver.h>
#include <nuttx/rpmsg/rpmsg.h>
#include <nuttx/sched_note.h>
#include <nuttx/wqueue.h>
#include <nuttx/spinlock.h>

#include "note_driver.h"

/****************************************************************************
 * Pre-processor definitions
 ****************************************************************************/

#define NOTE_RPMSG_WORK_DELAY MSEC2TICK(CONFIG_DRIVERS_NOTERPMSG_WORK_DELAY)

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

struct noterpmsg_driver_s
{
  struct note_driver_s  driver;
  FAR uint8_t          *buffer;
  volatile size_t       head;
  volatile size_t       tail;
  struct work_s         work;
  struct rpmsg_endpoint ept;
  spinlock_t            lock;
};

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

static void noterpmsg_add(FAR struct note_driver_s *driver,
                          FAR const void *note, size_t notelen,
                          bool noswitches);
static void noterpmsg_vprintf(FAR struct note_driver_s *drv, uint8_t tag,
                              uint8_t level, uintptr_t ip, uint64_t type,
                              FAR const char *fmt, va_list va);

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

static uint8_t g_noterpmsg_buffer[CONFIG_DRIVERS_NOTERPMSG_BUFSIZE];

static const struct note_driver_ops_s g_noterpmsg_ops =
{
  .add = noterpmsg_add,
  .vprintf = noterpmsg_vprintf,
};

struct noterpmsg_driver_s g_noterpmsg_driver =
{
  {
#ifdef CONFIG_SCHED_INSTRUMENTATION_FILTER
    "rpmsg",
    {
      {
        CONFIG_DRIVERS_NOTERPMSG_FILTER_DEFAULT_MODE,
#  ifdef CONFIG_SMP
        CONFIG_DRIVERS_NOTERPMSG_CPUSET
#  endif
      },

#  ifdef CONFIG_SCHED_INSTRUMENTATION_DUMP
      {
        CONFIG_DRIVERS_NOTE_DEFAULT_LEVEL,
      },
#  endif
    },
#endif
    &g_noterpmsg_ops
  },
  g_noterpmsg_buffer
};

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

static inline size_t noterpmsg_next(FAR struct noterpmsg_driver_s *drv,
                                    size_t pos, size_t offset)
{
  pos += offset;
  if (pos >= CONFIG_DRIVERS_NOTERPMSG_BUFSIZE)
    {
      pos -= CONFIG_DRIVERS_NOTERPMSG_BUFSIZE;
    }

  return pos;
}

static inline size_t noterpmsg_length(FAR struct noterpmsg_driver_s *drv)
{
  size_t head = drv->head;
  size_t tail = drv->tail;

  if (tail > head)
    {
      head += CONFIG_DRIVERS_NOTERPMSG_BUFSIZE;
    }

  return head - tail;
}

static inline void noterpmsg_remove(FAR struct noterpmsg_driver_s *drv)
{
  size_t tail = drv->tail;
  uint8_t notelen = drv->buffer[tail];

  DEBUGASSERT(notelen <= noterpmsg_length(drv));
  drv->tail = noterpmsg_next(drv, tail, NOTE_ALIGN(notelen));
}

static bool noterpmsg_transfer(FAR struct noterpmsg_driver_s *drv,
                               bool wait)
{
  for (; ; )
    {
      FAR uint8_t *buffer;
      irqstate_t flags;
      uint32_t space;
      size_t len;

      if (!is_rpmsg_ept_ready(&drv->ept))
        {
          return false;
        }

      buffer = rpmsg_get_tx_payload_buffer(&drv->ept, &space, wait);
      if (buffer == NULL)
        {
          return false;
        }

      flags = spin_lock_irqsave_notrace(&drv->lock);
      len = noterpmsg_length(drv);
      if (len == 0)
        {
          spin_unlock_irqrestore_notrace(&drv->lock, flags);
          rpmsg_release_tx_buffer(&drv->ept, buffer);
          return true;
        }

      if (space < len)
        {
          /* Find the len of large entire note data */

          size_t pos = drv->tail;
          uint8_t notelen = drv->buffer[pos];

          len = 0;
          while (len + notelen <= space)
            {
              pos = noterpmsg_next(drv, pos, NOTE_ALIGN(notelen));
              len += NOTE_ALIGN(notelen);
              notelen = drv->buffer[pos];
            }
        }

      space = CONFIG_DRIVERS_NOTERPMSG_BUFSIZE - drv->tail;
      space = space < len ? space : len;

      memcpy(buffer, drv->buffer + drv->tail, space);
      memcpy(buffer + space, drv->buffer, len - space);

      if (rpmsg_send_nocopy(&drv->ept, buffer, len) < 0)
        {
          rpmsg_release_tx_buffer(&drv->ept, buffer);
        }

      drv->tail = noterpmsg_next(drv, drv->tail, NOTE_ALIGN(len));
      spin_unlock_irqrestore_notrace(&drv->lock, flags);
    }
}

static void noterpmsg_work(FAR void *priv)
{
  FAR struct noterpmsg_driver_s *drv = priv;

  if (!noterpmsg_transfer(drv, false))
    {
      work_queue(HPWORK, &drv->work, noterpmsg_work, drv,
                 NOTE_RPMSG_WORK_DELAY);
    }
}

static void noterpmsg_add(FAR struct note_driver_s *driver,
                          FAR const void *note, size_t notelen,
                          bool noswitches)
{
  FAR struct noterpmsg_driver_s *drv =
    (FAR struct noterpmsg_driver_s *)driver;
  bool sent = false;
  irqstate_t flags;
  size_t space;

  flags = spin_lock_irqsave_notrace(&drv->lock);

  space = CONFIG_DRIVERS_NOTERPMSG_BUFSIZE - noterpmsg_length(drv);
  if (space < NOTE_ALIGN(notelen))
    {
      if (!up_interrupt_context() && !sched_idletask())
        {
          spin_unlock_irqrestore_notrace(&drv->lock, flags);
          sent = noterpmsg_transfer(drv, true);
          flags = spin_lock_irqsave_notrace(&drv->lock);
        }

      if (!sent)
        {
          /* Overwrite */

          do
            {
              noterpmsg_remove(drv);
              space = CONFIG_DRIVERS_NOTERPMSG_BUFSIZE -
                      noterpmsg_length(drv);
            }
          while (space < NOTE_ALIGN(notelen));
        }
    }

  space = CONFIG_DRIVERS_NOTERPMSG_BUFSIZE - drv->head;
  space = space < notelen ? space : notelen;

  memcpy(drv->buffer + drv->head, note, space);
  memcpy(drv->buffer, note + space, notelen - space);

  drv->head = noterpmsg_next(drv, drv->head, NOTE_ALIGN(notelen));
  spin_unlock_irqrestore_notrace(&drv->lock, flags);

  if (!noswitches && OSINIT_OS_READY() && work_available(&drv->work))
    {
      work_queue(HPWORK, &drv->work, noterpmsg_work, drv,
                 NOTE_RPMSG_WORK_DELAY);
    }
}

static void noterpmsg_vprintf(FAR struct note_driver_s *drv, uint8_t tag,
                              uint8_t level, uintptr_t ip, uint64_t type,
                              FAR const char *fmt, va_list va)
{
  FAR struct tcb_s *tcb = running_task();
  FAR struct note_printf_s *note;
  uint8_t data[NOTE_BUFFER_SIZE];
  size_t length;
  size_t fmtlen;
  size_t next;

  note = (FAR struct note_printf_s *)data;
  fmtlen = strlen(fmt) + 1;
  memcpy(note->npt_data, fmt, fmtlen);

  length = sizeof(data) - SIZEOF_NOTE_PRINTF(0) - fmtlen;
  next = lib_bsprintf(note->npt_data + fmtlen, length, fmt, va);
  DEBUGASSERT(next < sizeof(data) - sizeof(*note));

  length = SIZEOF_NOTE_PRINTF(next + fmtlen);
  note_common(tcb, &note->npt_cmn, length, NOTE_DUMP_PRINTF);
  note->npt_ip = ip;
  note->npt_tag = tag;
  note->npt_fmt = NULL;
  note->npt_type = type;
  note->npt_level = level;

  /* Add the note to circular buffer */

  noterpmsg_add(drv, note, length, false);
}

static int noterpmsg_ept_cb(FAR struct rpmsg_endpoint *ept,
                            FAR void *data, size_t len, uint32_t src,
                            FAR void *priv)
{
  return 0;
}

static void noterpmsg_device_created(FAR struct rpmsg_device *rdev,
                                     FAR void *priv)
{
  FAR struct noterpmsg_driver_s *drv = priv;
  int ret;

  if (strcmp(CONFIG_DRIVERS_NOTERPMSG_SERVER_NAME,
             rpmsg_get_cpuname(rdev)) == 0)
    {
      drv->ept.priv = drv;
      spin_lock_init(&drv->lock);

      ret = rpmsg_create_ept(&drv->ept, rdev, NOTERPMSG_EPT_NAME,
                             RPMSG_ADDR_ANY, RPMSG_ADDR_ANY,
                             noterpmsg_ept_cb, NULL);
      if (ret >= 0)
        {
          work_queue(HPWORK, &drv->work, noterpmsg_work, drv, 0);
        }
    }
}

static void noterpmsg_device_destroy(FAR struct rpmsg_device *rdev,
                                     FAR void *priv)
{
  FAR struct noterpmsg_driver_s *drv = priv;

  if (strcmp(CONFIG_DRIVERS_NOTERPMSG_SERVER_NAME,
             rpmsg_get_cpuname(rdev)) == 0)
    {
      rpmsg_destroy_ept(&drv->ept);
    }
}

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

/****************************************************************************
 * Name: noterpmsg_init
 *
 * Description:
 *   Register a rmpsg channel to note.
 *
 * Input Parameters:
 *   None
 *
 * Returned Value:
 *   Zero on success. A negated errno value is returned on a failure.
 *
 ****************************************************************************/

int noterpmsg_init(void)
{
  return rpmsg_register_callback(&g_noterpmsg_driver,
                                 noterpmsg_device_created,
                                 noterpmsg_device_destroy,
                                 NULL,
                                 NULL);
}