/*
 * Copyright (c) 2006-2023, RT-Thread Development Team
 *
 * SPDX-License-Identifier: Apache-2.0
 *
 * Change Logs:
 * Date           Author       Notes
 * 2023-11-13     Shell        init ver.
 */

#define DBG_TAG "lwp.tty"
#define DBG_LVL DBG_INFO
#include <rtdbg.h>

#define TTY_CONF_INCLUDE_CCHARS
#include "tty_config.h"
#include "tty_internal.h"
#include "terminal.h"

/* configure option: timeout of tty drain wait */
static int tty_drainwait = 5 * 60;

#define TTY_NAME_PREFIX "tty"
static char *alloc_device_name(const char *name)
{
    char *tty_dev_name;
    long name_buf_len = (sizeof(TTY_NAME_PREFIX) - 1) /* raw prefix */
                        + rt_strlen(name)             /* custom name */
                        + 1;                          /* tailing \0 */

    tty_dev_name = rt_malloc(name_buf_len);
    if (tty_dev_name)
        sprintf(tty_dev_name, "%s%s", TTY_NAME_PREFIX, name);
    return tty_dev_name;
}

/* character device for tty */
#ifdef RT_USING_DEVICE_OPS
const static struct rt_device_ops tty_dev_ops = {
    /* IO directly through device is not allowed */
};
#else
#error Must enable RT_USING_DEVICE_OPS in Kconfig
#endif

static int tty_fops_open(struct dfs_file *file)
{
    int rc;
    lwp_tty_t tp;
    rt_device_t device;
    int devtype = 0; /* unused */

    if (file->vnode && file->vnode->data)
    {
        if (file->vnode->ref_count != 1)
        {
            rc = 0;
        }
        else
        {
            device = (rt_device_t)file->vnode->data;
            tp = rt_container_of(device, struct lwp_tty, parent);
            rc = bsd_ttydev_methods.d_open(tp, file->flags, devtype,
                                           rt_thread_self());
        }
    }
    else
    {
        rc = -EINVAL;
    }

    return rc;
}

static int tty_fops_close(struct dfs_file *file)
{
    int rc;
    lwp_tty_t tp;
    rt_device_t device;
    int fflags = FFLAGS(file->flags);
    int devtype = 0; /* unused */

    if (file->vnode && file->vnode->data)
    {
        if (file->vnode->ref_count != 1)
        {
            rc = 0;
        }
        else
        {
            device = (rt_device_t)file->vnode->data;
            tp = rt_container_of(device, struct lwp_tty, parent);
            rc = bsd_ttydev_methods.d_close(tp, fflags, devtype, rt_thread_self());
        }
    }
    else
    {
        rc = -EINVAL;
    }

    return rc;
}

static int tty_fops_ioctl(struct dfs_file *file, int cmd, void *arg)
{
    int rc;
    lwp_tty_t tp;
    rt_device_t device;

    if (file->vnode && file->vnode->data)
    {
        device = (rt_device_t)file->vnode->data;
        tp = rt_container_of(device, struct lwp_tty, parent);
        rc = lwp_tty_ioctl_adapter(tp, cmd, file->flags, arg, rt_thread_self());
    }
    else
    {
        rc = -EINVAL;
    }

    return rc;
}

static ssize_t tty_fops_read(struct dfs_file *file, void *buf, size_t count,
                             off_t *pos)
{
    ssize_t rc = 0;
    int error;
    struct uio uio;
    struct iovec iov;
    rt_device_t device;
    struct lwp_tty *tp;
    int ioflags;
    int oflags = file->flags;

    if (file->vnode && file->vnode->data)
    {
        device = (rt_device_t)file->vnode->data;
        tp = rt_container_of(device, struct lwp_tty, parent);

        /* setup ioflags */
        ioflags = 0;
        if (oflags & O_NONBLOCK)
            ioflags |= IO_NDELAY;

        /* setup uio parameters */
        iov.iov_base = (void *)buf;
        iov.iov_len = count;
        uio.uio_offset = file->fpos;
        uio.uio_resid = count;
        uio.uio_iov = &iov;
        uio.uio_iovcnt = 1;
        uio.uio_rw = UIO_READ;

        rc = count;
        error = bsd_ttydev_methods.d_read(tp, &uio, ioflags);
        rc -= uio.uio_resid;
        if (error)
        {
            LOG_D("%s: failed to write %d bytes of data. error code %d",
                  __func__, uio.uio_resid, error);
            rc = error;
        }

        /* reset file context */
        file->fpos = uio.uio_offset;
    }

    if (rc)
        LOG_D("%s(len=%d, buf=%c \"%d\")", __func__, rc, *((char *)buf),
              *((char *)buf));
    return rc;
}

static ssize_t tty_fops_write(struct dfs_file *file, const void *buf,
                              size_t count, off_t *pos)
{
    ssize_t rc = 0;
    int error;
    struct uio uio;
    struct iovec iov;
    rt_device_t device;
    struct lwp_tty *tp;
    int ioflags;
    int oflags = file->flags;

    if (file->vnode && file->vnode->data)
    {
        device = (rt_device_t)file->vnode->data;
        tp = rt_container_of(device, struct lwp_tty, parent);

        /* setup ioflags */
        ioflags = 0;
        if (oflags & O_NONBLOCK)
            ioflags |= IO_NDELAY;

        /* setup uio parameters */
        iov.iov_base = (void *)buf;
        iov.iov_len = count;
        uio.uio_offset = file->fpos;
        uio.uio_resid = count;
        uio.uio_iov = &iov;
        uio.uio_iovcnt = 1;
        uio.uio_rw = UIO_WRITE;

        rc = count;
        error = bsd_ttydev_methods.d_write(tp, &uio, ioflags);
        if (error)
        {
            rc = error;
            LOG_D("%s: failed to write %d bytes of data. error code %d",
                  __func__, uio.uio_resid, error);
        }
        else
        {
            rc -= uio.uio_resid;
        }

        /* reset file context */
        file->fpos = uio.uio_offset;
    }
    return rc;
}

static int tty_fops_flush(struct dfs_file *file)
{
    return -EINVAL;
}

static off_t tty_fops_lseek(struct dfs_file *file, off_t offset, int wherece)
{
    return -EINVAL;
}

static int tty_fops_truncate(struct dfs_file *file, off_t offset)
{
    /**
     * regarding to POSIX.1, TRUNC is not supported for tty device.
     * return 0 always to make filesystem happy
     */
    return 0;
}

static int tty_fops_poll(struct dfs_file *file, struct rt_pollreq *req)
{
    int rc;
    rt_device_t device;
    struct lwp_tty *tp;

    if (file->vnode && file->vnode->data)
    {
        device = (rt_device_t)file->vnode->data;
        tp = rt_container_of(device, struct lwp_tty, parent);

        rc = bsd_ttydev_methods.d_poll(tp, req, rt_thread_self());
    }
    else
    {
        rc = -1;
    }

    return rc;
}

static int tty_fops_mmap(struct dfs_file *file, struct lwp_avl_struct *mmap)
{
    return -EINVAL;
}

static int tty_fops_lock(struct dfs_file *file, struct file_lock *flock)
{
    return -EINVAL;
}

static int tty_fops_flock(struct dfs_file *file, int operation, struct file_lock *flock)
{
    return -EINVAL;
}

static struct dfs_file_ops tty_file_ops = {
    .open = tty_fops_open,
    .close = tty_fops_close,
    .ioctl = tty_fops_ioctl,
    .read = tty_fops_read,
    .write = tty_fops_write,
    .flush = tty_fops_flush,
    .lseek = tty_fops_lseek,
    .truncate = tty_fops_truncate,
    .poll = tty_fops_poll,
    .mmap = tty_fops_mmap,
    .lock = tty_fops_lock,
    .flock = tty_fops_flock,
};

rt_inline void device_setup(lwp_tty_t terminal)
{
    terminal->parent.type = RT_Device_Class_Char;
#ifdef RT_USING_DEVICE_OPS
    terminal->parent.ops = &tty_dev_ops;
#else
#error Must enable RT_USING_DEVICE_OPS in Kconfig
#endif
}

/* register TTY device */
rt_err_t lwp_tty_register(lwp_tty_t terminal, const char *name)
{
    rt_err_t rc = -RT_ENOMEM;
    const char *tty_name;
    char *alloc_name;

    if (terminal->t_devsw->tsw_flags & TF_NOPREFIX)
    {
        alloc_name = RT_NULL;
        tty_name = name;
    }
    else
    {
        alloc_name = alloc_device_name(name);
        tty_name = alloc_name;
    }

    if (tty_name)
    {
        device_setup(terminal);
        rc = rt_device_register(&terminal->parent, tty_name, 0);
        if (rc == RT_EOK)
        {
            terminal->parent.fops = &tty_file_ops;

            LOG_D("%s() /dev/%s device registered", __func__, tty_name);
        }

        rt_free(alloc_name);
    }
    return rc;
}

static void tty_init_termios(lwp_tty_t tp)
{
    struct termios *t = &tp->t_termios_init_in;

    t->c_cflag = TTYDEF_CFLAG;
    t->c_iflag = TTYDEF_IFLAG;
    t->c_lflag = TTYDEF_LFLAG;
    t->c_oflag = TTYDEF_OFLAG;
    t->__c_ispeed = TTYDEF_SPEED;
    t->__c_ospeed = TTYDEF_SPEED;

    memcpy(&t->c_cc, tty_ctrl_charset,
           sizeof(tty_ctrl_charset) / sizeof(tty_ctrl_charset[0]));

#ifdef USING_BSD_INIT_LOCK_DEVICE
    tp->t_termios_init_out = *t;
#endif /* USING_BSD_INIT_LOCK_DEVICE */
}

lwp_tty_t lwp_tty_create_ext(lwp_ttydevsw_t handle, void *softc,
                             rt_mutex_t custom_mtx)
{
    lwp_tty_t tp;

    tp = rt_calloc(1, sizeof(struct lwp_tty)
    #ifdef USING_BSD_SIGINFO
            + LWP_TTY_PRBUF_SIZE
    #endif
            );

    if (!tp)
        return tp;

    bsd_devsw_init(handle);

#ifdef USING_BSD_SIGINFO
    tp->t_prbufsz = LWP_TTY_PRBUF_SIZE;
#endif
    tp->t_devsw = handle;
    tp->t_devswsoftc = softc;
    tp->t_flags = handle->tsw_flags;
    tp->t_drainwait = tty_drainwait;

    tty_init_termios(tp);

    cv_init(&tp->t_inwait, "ttyin");
    cv_init(&tp->t_outwait, "ttyout");
    cv_init(&tp->t_outserwait, "ttyosr");
    cv_init(&tp->t_bgwait, "ttybg");
    cv_init(&tp->t_dcdwait, "ttydcd");

    rt_wqueue_init(&tp->t_inpoll);
    rt_wqueue_init(&tp->t_outpoll);

    /* Allow drivers to use a custom mutex to lock the TTY. */
    if (custom_mtx != NULL)
    {
        tp->t_mtx = custom_mtx;
    }
    else
    {
        tp->t_mtx = &tp->t_mtxobj;
        rt_mutex_init(&tp->t_mtxobj, "ttydev", RT_IPC_FLAG_PRIO);
    }

#ifdef USING_BSD_POLL
    knlist_init_mtx(&tp->t_inpoll.si_note, tp->t_mtx);
    knlist_init_mtx(&tp->t_outpoll.si_note, tp->t_mtx);
#endif

    return tp;
}

lwp_tty_t lwp_tty_create(lwp_ttydevsw_t handle, void *softc)
{
    return lwp_tty_create_ext(handle, softc, NULL);
}

void lwp_tty_delete(lwp_tty_t tp)
{
    /*
     * ttyydev_leave() usually frees the i/o queues earlier, but it is
     * not always called between queue allocation and here.  The queues
     * may be allocated by ioctls on a pty control device without the
     * corresponding pty slave device ever being open, or after it is
     * closed.
     */
    ttyinq_free(&tp->t_inq);
    ttyoutq_free(&tp->t_outq);
    rt_wqueue_wakeup_all(&tp->t_inpoll, (void *)POLLHUP);
    rt_wqueue_wakeup_all(&tp->t_outpoll, (void *)POLLHUP);

#ifdef USING_BSD_POLL
    knlist_destroy(&tp->t_inpoll.si_note);
    knlist_destroy(&tp->t_outpoll.si_note);
#endif

    cv_destroy(&tp->t_inwait);
    cv_destroy(&tp->t_outwait);
    cv_destroy(&tp->t_bgwait);
    cv_destroy(&tp->t_dcdwait);
    cv_destroy(&tp->t_outserwait);

    if (tp->t_mtx == &tp->t_mtxobj)
        rt_mutex_detach(&tp->t_mtxobj);
    ttydevsw_free(tp);
    rt_device_unregister(&tp->parent);
    rt_free(tp);
}

/*
 * Report on state of foreground process group.
 */
void tty_info(struct lwp_tty *tp)
{
    /* TODO */
    return;
}