#include <atomic.h>
#include <core/access.h>
#include <core/driver.h>
#include <tees_driver.h>
#include <errno.h>
#include <fcntl.h>
#include <limits.h>
#include <macros.h>
#include <pthread.h>
#include <stdarg.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/ioctl.h>
#include <tee_internal_api.h>
#include <tee_ta_destructor.h>
#include <unistd.h>

#include "board.h"
#include "bsp_common.h"
#include "dbg.h"
#include "touch_driver_TA.h"
#include "touch_gpio.h"
#include "touch_queue.h"
#include "tuiHal.h"

#ifdef USE_REE_CANCEL_LISTENER
#include "ree_cancel_listener.h"
#endif

/* Uncomment to simulate touches by random numbers */
/* #define SIMULATE_TOUCH_COUNT        1000 */

struct touch_dev {
    struct atomic_t opened;
    int cancelled;
    int started;
    uint32_t time_out;
};

struct read_params_struct {
    struct drv_info info;
    char *data;
    unsigned int data_len;
};

#define TUI_REE_EXTERNAL_EVENT       42
#define THREAD_STOPPED               0
#define THREAD_STARTED               1
#define THREAD_TERMINATED            2

static struct usr_drv_info *driver_info = NULL;
static char driver_name[] = "tui_touch";
static volatile struct touch_dev dev;
static pthread_t thread;
static int thread_status = THREAD_STOPPED;
static pthread_mutex_t mt = PTHREAD_MUTEX_INITIALIZER;
static pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
static bool sync_requested = false;

static void cancel_tui_session()
{
    if (atomic_read(dev.opened) > 0) {
        dbgPrintf("TOUCH canceling");
        dev.cancelled = 1;
#ifdef USE_TOUCH_INTERRUPT
        gpio_complete_touch_irq();
#endif
    }
}

#ifdef SIMULATE_TOUCH_COUNT
static int get_rand(int min, int max)
{
    long long int val = 0;

    TEE_GenerateRandom(&val, 4);
    val *= (max - min + 1);
    val >>= 32;
    val += min;
    if (val > max) {
        val %= max;
    }
    return (int)val;
}

static int read_stub(void *data, int size)
{
    touch_info_t *touch;
    static int touch_x;
    static int touch_y;
    static int touch_state;
    static int touch_count;

    TEE_Wait(3);
    if (size != sizeof(touch_info_t)) {
        return 0;
    }
    touch = (touch_info_t *)data;
    if (!touch_state) {
        ++touch_count;
        touch_x = get_rand((touch_count < SIMULATE_TOUCH_COUNT) ? 10 :
                           (BOARD_SCREENWIDTH / 2),
                           BOARD_SCREENWIDTH - 10);
        touch_y = get_rand(10, (touch_count < SIMULATE_TOUCH_COUNT) ?
                           ((BOARD_SCREENHEIGHT * 2) / 3) :
                           BOARD_SCREENHEIGHT - 10);
    }
    touch_state = 1 - touch_state;
    touch->x = touch_x;
    touch->y = touch_y;
    touch->ev_type = touch_state;
    touch->n_finger = 0;
    return 1;
}
#endif /* SIMULATE_TOUCH_COUNT */

static int read_touch_value(unsigned long ioctl_data)
{
    int ret = 0;
    /* If touch queue is empty, try to process to not lose any pending irqs */
    int i = touch_event_count();
    if (i == 0) {
        TEE_Result retHal = tuiHalTouchProcess();
        if (retHal != TEE_SUCCESS) {
            errPrintf("tuiHalTouchProcess() failed\n");
            ret = -EIO;
            goto finish_function;
        }
    }
#ifndef SIMULATE_TOUCH_COUNT
    i = touch_event_count();
    if (i != 0)
#endif
    {
        mt_touch_info_t *ti = (mt_touch_info_t *)ioctl_data;
        ret = get_touch_event(ti, sizeof(mt_touch_info_t));
#ifdef SIMULATE_TOUCH_COUNT
        if (ret == 0) {
            ret = read_stub((char *)ti, sizeof(touch_info_t));
        }
#endif
        if (ret > 0) { /* We have touch data */
            infoPrintf("got %d touch event out of %d events\n", ret, i);
        } else if (ret < 0) {
            errPrintf("get_touch_event failed ret=%d\n", ret);
            ret = -EINVAL;
        }
    }
finish_function:
    return ret;
}

static void stop_read_thread(void)
{
    pthread_mutex_lock(&mt);
    if (thread_status == THREAD_STARTED) {
        thread_status = THREAD_TERMINATED;
        sync_requested = true;
        ALWAYS_ZERO(pthread_cond_signal(&cond));
        ALWAYS_ZERO(pthread_mutex_unlock(&mt));
        pthread_join(thread, NULL);
        dbgPrintf("joined old thread\n");
        thread_status = THREAD_STOPPED;
    } else {
        ALWAYS_ZERO(pthread_mutex_unlock(&mt));
    }
}

static int drv_open(struct drv_info *info, const char *drv_path, ...)
{
    TEE_Result retHal = TEE_SUCCESS;

    (void)info;
    (void)drv_path;
    dbgPrintf("TOUCH >> %s\n", __func__);

    if (atomic_inc(dev.opened) >= 1) {
        atomic_dec(dev.opened);
        errPrintf("Touch already opened\n");
        return -EBUSY;
    }

#ifdef USE_REE_CANCEL_LISTENER
    if (start_ree_cancel_listener(cancel_tui_session) == false) {
        atomic_dec(dev.opened);
        errPrintf("start_ree_cancel_listener failed.\n");
        return -EIO;
    }
#endif
    retHal = tuiHalBoardInit();
    if (retHal != TEE_SUCCESS) {
        atomic_dec(dev.opened);
        errPrintf("tuiHalBoardInitn() FAILED\n");
        return -EIO;
    }

    dev.cancelled = 0;
    dev.started = 0;
    dev.time_out  = TOUCH_WAIT_TIMEOUT_MS;
    infoPrintf("TOUCH << %s\n", __func__);
    return retHal;
}

static int drv_close(struct drv_info *info)
{
    (void)info;
    dbgPrintf("TOUCH >> %s\n", __func__);

    if (atomic_dec(dev.opened) > 0) {

        dev.cancelled = 1;
#ifdef USE_TOUCH_INTERRUPT
        gpio_complete_touch_irq();
#endif

        tuiHalTouchClose();
        stop_read_thread();
#ifdef USE_REE_CANCEL_LISTENER
        stop_ree_cancel_listener();
#endif
        dev.cancelled = 0;
        dev.started = 0;
    } else {
        atomic_inc(dev.opened);
    }
    infoPrintf("TOUCH << %s\n", __func__);
    return 0;
}

static int drv_ioctl(struct drv_info *info, int ioctl_cmd, struct ioctl_arg *ioctl_data)
{
    ioctl_range_t range_cmd;
    int ret = 0;
    TEE_Result tee_ret = TEE_ERROR_GENERIC;
    (void)info;

    if (atomic_read(dev.opened) != 1) {
        return -EPERM;
    }
    if (dev.cancelled) {
        infoPrintf("Session is cancelled\n");
        return -ECANCELED;
    }
    switch (ioctl_cmd) {
    case TOUCH_IOCTL_SET_INFO:
        dbgPrintf("TOUCH_IOCTL_SET_INFO execution...\n");
        memcpy((void *)&range_cmd, ioctl_data->input[0].iov_base, ioctl_data->input[0].iov_len);
        tee_ret = tuiHalTouchOpen(range_cmd.width, range_cmd.height, range_cmd.touch_type);
        if (tee_ret != TEE_SUCCESS) {
            errPrintf("tuiHalTouchOpen() FAILED: tee_ret=%#x\n", tee_ret);
            return (tee_ret == TEE_ERROR_OUT_OF_MEMORY) ? -ENOMEM : -1;
        }
        dev.started = 1;
        dbgPrintf("TOUCH_IOCTL_SET_INFO finish...\n");
        break;
    case TOUCH_IOCTL_CANCEL_READ:
        dbgPrintf("TOUCH_IOCTL_CANCEL_READ execution...\n");
        dev.cancelled = 1;
        break;
    case TOUCH_IOCTL_SET_TIMEOUT:
        dbgPrintf("TOUCH_IOCTL_SET_TIMEOUT execution...\n");
        if (*(unsigned long *)ioctl_data->input[0].iov_base > UINT_MAX) {
            dbgPrintf("wrong time delay value = %lu\n", *(long *)ioctl_data->input[0].iov_base);
            return -EINVAL;
        }
        dbgPrintf("time delay value = %lu\n", *(long *)ioctl_data->input[0].iov_base);
        dev.time_out  = *(uint32_t *)ioctl_data->input[0].iov_base ;
        break;
    case TOUCH_IOCTL_GET_DATA:
        ret = read_touch_value(*(unsigned long *)ioctl_data->input[0].iov_base);
        break;
    default:
        errPrintf("Unknown IOCTL code\n");
        ret = -EINVAL;
    }
    return ret;
}

static ssize_t drv_read(struct drv_info *info, void *data, size_t data_len)
{
    int ret = 0;
    (void)info;

    if (atomic_read(dev.opened) != 1 || dev.started != 1) {
        errPrintf("Touch is not opened\n");
        return -EPERM;
    }
    if (dev.cancelled) {
        infoPrintf("Session is cancelled\n");
        return -ECANCELED;
    }
    if (!data || data_len < sizeof(mt_touch_info_t)) {
        errPrintf("Not enought buffer\n");
        return -EINVAL;
    }
    if (data_len > sizeof(mt_touch_info_t)) {
        data_len = sizeof(mt_touch_info_t);
    }

    ret = read_touch_value((uintptr_t)data);

    return ret;
}

static void uninit(void)
{
    static bool uninit_done = false;
    int ret;

    if (!uninit_done) {
        drv_close(NULL);
        ret = TEES_FiniDriver(driver_info);
        if (ret) {
            errPrintf("TEES_FiniDriver() failed, ret = %d\n", ret);
        }
        uninit_done = true;
    }
}

static void destructor_func(int sig_id)
{
    (void)sig_id;
    dbgPrintf("TOUCH >> %s\n", __func__);
    uninit();
    dbgPrintf("TOUCH << %s\n", __func__);
}

TEE_Result TA_CreateEntryPoint(void)
{
    int ret;
    TEE_Result res;
    static struct fops file_ops = {
        .open  = drv_open,
        .close = drv_close,
        .read  = drv_read,
        .ioctl_iov = drv_ioctl,
    };

    dbgPrintf("TOUCH >> %s\n", __func__);
    res = TEES_RegisterDriverDestructor(destructor_func);
    if (res != TEE_SUCCESS) {
        errPrintf("TEES_DriverDestructor_t() failed, res = %#x\n", res);
        return TEE_ERROR_GENERIC;
    }
    ret = TEES_InitDriver(driver_name, &file_ops, ACC_PERM_I2C, &driver_info);
    if (ret) {
        errPrintf("TEES_InitDriver() failed, ret = %d\n", ret);
        return TEE_ERROR_GENERIC;
    }

    dbgPrintf("TOUCH << %s\n", __func__);
    return TEE_SUCCESS;
}

void TA_DestroyEntryPoint(void)
{
    uninit();

    infoPrintf("TOUCH << %s\n", __func__);
}

TEE_Result TA_OpenSessionEntryPoint(uint32_t paramTypes,
                                    TEE_Param params[4], void **sessionContext)
{
    (void)paramTypes;
    (void)params;
    (void)sessionContext;

    infoPrintf("TOUCH << %s\n", __func__);

    return TEE_SUCCESS;
}

void TA_CloseSessionEntryPoint(void *sessionContext)
{
    (void)sessionContext;

    infoPrintf("TOUCH << %s\n", __func__);
}

TEE_Result TA_InvokeCommandEntryPoint(void *sessionContext,
                                      uint32_t commandID,
                                      uint32_t paramTypes,
                                      TEE_Param params[4])
{
    (void)sessionContext;
    (void)paramTypes;
    (void)params;

    switch (commandID) {
    case TUI_REE_EXTERNAL_EVENT:
        infoPrintf("TOUCH %s TUI_REE_EXTERNAL_EVENT\n", __func__);
        cancel_tui_session();
        break;
    default:
        break;
    }
    return TEE_SUCCESS;
}
