/*
 * COPYRIGHT (C) 2011-2021, Real-Thread Information Technology Ltd
 *
 * SPDX-License-Identifier: Apache-2.0
 *
 * Change Logs:
 * Date           Author       Notes
 * 2013-11-04     Grissiom     add comment
 */

#include <rthw.h>
#include <rtthread.h>
#include <rtdevice.h>

#include "vbus.h"

static void _rx_indicate(void *ctx)
{
    rt_device_t dev = ctx;

    if (dev->rx_indicate)
        dev->rx_indicate(dev, 0);
}

static void _tx_complete(void *ctx)
{
    rt_device_t dev = ctx;

    if (dev->tx_complete)
        dev->tx_complete(dev, 0);
}

static rt_err_t _open(rt_device_t dev, rt_uint16_t oflag)
{
    int chnr;
    struct rt_vbus_dev *vdev = dev->user_data;

    if (vdev->chnr)
        return RT_EOK;

    /* FIXME: request the same name for twice will crash */
    chnr = rt_vbus_request_chn(&vdev->req, RT_WAITING_FOREVER);
    if (chnr < 0)
        return chnr;

    vdev->chnr = chnr;
    rt_vbus_register_listener(chnr, RT_VBUS_EVENT_ID_RX, _rx_indicate, dev);
    rt_vbus_register_listener(chnr, RT_VBUS_EVENT_ID_TX, _tx_complete, dev);

    return RT_EOK;
}

static rt_err_t _close(rt_device_t dev)
{
    struct rt_vbus_dev *vdev = dev->user_data;

    RT_ASSERT(vdev->chnr != 0);

    rt_vbus_close_chn(vdev->chnr);
    vdev->chnr = 0;

    return RT_EOK;
}

static rt_size_t _read(rt_device_t dev, rt_off_t pos, void *buffer, rt_size_t size)
{
    rt_size_t outsz = 0;
    struct rt_vbus_dev *vdev = dev->user_data;

    RT_ASSERT(vdev->chnr != 0);

    if (vdev->act == RT_NULL)
    {
        vdev->act = rt_vbus_data_pop(vdev->chnr);
        vdev->pos = 0;
    }

    while (1)
    {
        rt_err_t err;

        while (vdev->act)
        {
            rt_size_t cpysz;

            if (size - outsz > vdev->act->size - vdev->pos)
                cpysz = vdev->act->size - vdev->pos;
            else
                cpysz = size - outsz;

            rt_memcpy((char*)buffer + outsz, ((char*)(vdev->act+1)) + vdev->pos, cpysz);
            vdev->pos += cpysz;

            outsz += cpysz;
            if (outsz == size)
            {
                return outsz;
            }
            else if (outsz > size)
                RT_ASSERT(0);

            /* free old and get new */
            rt_free(vdev->act);
            vdev->act = rt_vbus_data_pop(vdev->chnr);
            vdev->pos = 0;
        }

        /* TODO: We don't want to touch the rx_indicate here. But this lead to
         * some duplication. Maybe we should find a better way to handle this.
         */
        if (rt_interrupt_get_nest() == 0)
        {
            err = rt_vbus_listen_on(vdev->chnr, RT_WAITING_FOREVER);
        }
        else
        {
            err = rt_vbus_listen_on(vdev->chnr, 0);
        }
        if (err != RT_EOK)
        {
            rt_set_errno(err);
            return outsz;
        }
        vdev->act = rt_vbus_data_pop(vdev->chnr);
        vdev->pos = 0;
    }
}

static rt_size_t _write(rt_device_t dev, rt_off_t pos, const void *buffer, rt_size_t size)
{
    rt_err_t err;
    struct rt_vbus_dev *vdev = dev->user_data;

    RT_ASSERT(vdev->chnr != 0);

    if (rt_interrupt_get_nest() == 0)
    {
        /* Thread context. */
        err = rt_vbus_post(vdev->chnr, vdev->req.prio,
                           buffer, size, RT_WAITING_FOREVER);
    }
    else
    {
        /* Interrupt context. */
        err = rt_vbus_post(vdev->chnr, vdev->req.prio,
                           buffer, size, 0);
    }

    if (err)
    {
        rt_set_errno(err);
        return 0;
    }

    return size;
}

rt_err_t  _control(rt_device_t dev, int cmd, void *args)
{
    RT_ASSERT(dev);

    switch (cmd) {
    case VBUS_IOC_LISCFG: {
        struct rt_vbus_dev *vdev = dev->user_data;
        struct rt_vbus_dev_liscfg *liscfg = args;

        RT_ASSERT(vdev->chnr != 0);
        if (!liscfg)
            return -RT_ERROR;

        rt_vbus_register_listener(vdev->chnr, liscfg->event,
                                  liscfg->listener, liscfg->ctx);
        return RT_EOK;
    }
        break;
#ifdef RT_VBUS_USING_FLOW_CONTROL
    case VBUS_IOCRECV_WM: {
        struct rt_vbus_dev *vdev = dev->user_data;
        struct rt_vbus_wm_cfg *cfg;

        RT_ASSERT(vdev->chnr != 0);

        if (!args)
            return -RT_ERROR;

        cfg = (struct rt_vbus_wm_cfg*)args;
        if (cfg->low > cfg->high)
            return -RT_ERROR;

        rt_vbus_set_recv_wm(vdev->chnr, cfg->low, cfg->high);
        return RT_EOK;
    }
        break;
    case VBUS_IOCPOST_WM: {
        struct rt_vbus_dev *vdev = dev->user_data;
        struct rt_vbus_wm_cfg *cfg;

        RT_ASSERT(vdev->chnr != 0);

        if (!args)
            return -RT_ERROR;

        cfg = (struct rt_vbus_wm_cfg*)args;
        if (cfg->low > cfg->high)
            return -RT_ERROR;

        rt_vbus_set_post_wm(vdev->chnr, cfg->low, cfg->high);
        return RT_EOK;
    }
        break;
#endif
    default:
        break;
    };

    return -RT_ENOSYS;
}

rt_uint8_t rt_vbus_get_chnnr(rt_device_t dev)
{
    struct rt_vbus_dev *vdev;

    RT_ASSERT(dev);

    vdev = dev->user_data;

    return vdev->chnr;
}

void rt_vbus_chnx_register_disconn(rt_device_t dev,
                                   rt_vbus_event_listener indi,
                                   void *ctx)
{
    struct rt_vbus_dev *vdev = dev->user_data;

    RT_ASSERT(vdev->chnr != 0);

    if (vdev)
        rt_vbus_register_listener(vdev->chnr, RT_VBUS_EVENT_ID_DISCONN,
                                  indi, ctx);
}

#define ARRAY_SIZE(a) (sizeof(a)/sizeof(a[0]))

extern struct rt_vbus_dev rt_vbus_chn_devx[];
static struct rt_device _devx[32];

rt_err_t rt_vbus_chnx_init(void)
{
    int i;
    struct rt_vbus_dev *p;

    for (i = 0,                   p = rt_vbus_chn_devx;
         i < ARRAY_SIZE(_devx) && p->req.name;
         i++,                     p++)
    {
        _devx[i].type      = RT_Device_Class_Char;
        _devx[i].open      = _open;
        _devx[i].close     = _close;
        _devx[i].read      = _read;
        _devx[i].write     = _write;
        _devx[i].control   = _control;
        _devx[i].user_data = p;
        rt_device_register(&_devx[i], p->req.name, RT_DEVICE_FLAG_RDWR);
    }

    return RT_EOK;
}
