/*
 * Copyright (C) 2020, Samsung Electronics Co., Ltd.
 *
 * TUI LL common functions for all drivers
 */

#include <atomic.h>
#include <errno.h>
#include <macros.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/uio.h>

#include <tee_ta_destructor.h>
#include "tee_tui_low_api.h"
#include "tuill_client_drv.h"
#include "tuill_drv.h"
#include "tuill_log.h"

int clear_HAL(void);
static void clear_HAL_common(void);
static void tuill_drv_destructor(int sig_id);

struct tuill_client_drv_context drv_ctx;

static pthread_mutex_t drv_mutex = PTHREAD_RECURSIVE_MUTEX_INITIALIZER_NP;
static volatile int ref_cnt = 0;

static void _mutex_unlock_(pthread_mutex_t **mut)
{
    ALWAYS_ZERO(pthread_mutex_unlock(*mut));
}

static pthread_mutex_t *_mutex_lock_(pthread_mutex_t *mut)
{
    ALWAYS_ZERO(pthread_mutex_lock(mut));
    return mut;
}

#define TUILL_MUTEX_LOCK(x) __attribute__((cleanup(_mutex_unlock_))) \
     _unused_ pthread_mutex_t *lmut = _mutex_lock_(x)


int32_t register_driver(void)
{
    int32_t ret = 0;
    TUILL_CALL_TRACE();
    TUILL_MUTEX_LOCK(&drv_mutex);
    if (ref_cnt == 0) {
        TEE_Result res = TEES_RegisterDriverDestructor(tuill_drv_destructor);
        if (res) {
            syslog(LOG_ERR, "TEES_DriverDestructor_t() failed, res = %#x\n", res);
            return -TUILLE_GENERIC;
        }

        ret = TEES_InitDriver(drv_ctx.drv_name,
                              &drv_ctx.file_ops,
                              drv_ctx.drv_id,
                              &drv_ctx.driver_info);
        if (ret) {
            syslog(LOG_ERR, "TEES_InitDriver() failed, ret = %d\n", ret);
            return -TUILLE_GENERIC;
        }

        ret = init_tuill_driver();
        if (ret) {
            ALWAYS_ZERO(TEES_FiniDriver(drv_ctx.driver_info));
            return ret;
        }
    }

    if (ret == 0) {
        ref_cnt++;
    }
    syslog(LOG_DEBUG, "ref_cnt = %d\n", ref_cnt);
    return 0;
}

int32_t unregister_driver(void)
{
    TUILL_CALL_TRACE();
    TUILL_MUTEX_LOCK(&drv_mutex);
    if (ref_cnt == 0) {
        return 0;
    }
    if (ref_cnt == 1) {
        uninit_all();
        syslog(LOG_DEBUG, "driver_info=%p\n", drv_ctx.driver_info);
        if (drv_ctx.driver_info) {
            ALWAYS_ZERO(TEES_FiniDriver(drv_ctx.driver_info));
        }
    }
    ref_cnt--;
    syslog(LOG_DEBUG, "ref_cnt = %d\n", ref_cnt);
    return 0;
}

#ifdef BUILD_TYPE_debug
static void reboot_client(struct tuilldrv_setsock_data *ioctl_data)
{
    (void)ioctl_data;
    TUILL_CALL_TRACE();
    syslog(LOG_DEBUG, "info=%p\n", ioctl_data);
    uninit_tuill_driver();
    syslog(LOG_DEBUG, "Before panic: reboot\n");
    TEE_Panic(0);
}
#endif /* BUILD_TYPE_debug */

static int32_t drv_input(void *user_data, struct tuill_buffer *buff)
{
    TUILL_CALL_TRACE();
    struct tuill_internal_command rsp = {};
    struct tuill_internal_command *cmd = (struct tuill_internal_command *)buff->data;
    if (cmd->ret_code & INJECT_ERR_FLAG) {
        syslog(LOG_DEBUG, "error injection\n");
        rsp.cmd = cmd->cmd | RESPONSE_FLAG;
        rsp.ret_code = -TUILLE_GENERIC;
        rsp.task_state = cmd->task_state;
        rsp.task_id = cmd->task_id;
        return tuild_send_cmd(&rsp);
    }

    if (cmd->ret_code & MAKE_TIMEOUT_FLAG) {
        syslog(LOG_DEBUG, "timeout injection\n");
        return 0;
    }

    if (drv_ctx.drv_cb->income) {
        drv_ctx.drv_cb->income(user_data, buff);
    }

    syslog(LOG_DEBUG, "data_len=%d\n", buff->data_len);
    return 0;
}

static int32_t drv_hangup(void *user_data)
{
    TUILL_CALL_TRACE();
    if (drv_ctx.drv_cb->hangup) {
        drv_ctx.drv_cb->hangup(user_data);
    }

    drv_ctx.drv_state_machine = DRV_STATE_DISCONNECTED;
    clear_HAL_common();
    return 0;
}

void drv_send_state(void)
{
    struct tuill_internal_command cmd = {};
    cmd.cmd = TUILL_ICMD_SET_DRV_STATE;
    cmd.set_drv_info_cmd.drv_tui_mode = drv_ctx.tuilldrv.tuill_state & TEE_PERIPHERAL_FLAG_LOCKED;
    tuill_drv_send_to_server(drv_ctx.connect_ctx, &cmd);
}

static int32_t drv_handshake(int32_t server_fd, void *user_data)
{
    (void)server_fd;
    (void)user_data;
    TUILL_CALL_TRACE();
    if (drv_ctx.drv_state_machine == DRV_STATE_DISCONNECTED) {
        drv_send_state();
        drv_ctx.drv_state_machine = DRV_STATE_CONNECTED;
    }

    return 0;
}

int32_t tuild_send_cmd(struct tuill_internal_command *cmd)
{
    return tuill_drv_send_to_server(drv_ctx.connect_ctx, cmd);
}

static int tuill_do_client_ioctls(struct drv_info *info, int ioctl_cmd, struct ioctl_arg *arg)
{
    (void)arg;
    (void)info;
    TUILL_CALL_TRACE();

    switch (ioctl_cmd) {
#ifdef BUILD_TYPE_debug
    case TUILLDRV_IOCTL_REBOOT: {
        syslog(LOG_DEBUG, "Processing TUILLDRV_IOCTL_REBOOT...\n");
        struct tuilldrv_setsock_data *virt_ioctl_data =
            (struct tuilldrv_setsock_data *)arg->input[0].iov_base;
        reboot_client(virt_ioctl_data);
        break;
    }
#endif /* BUILD_TYPE_debug */
    default:
        syslog(LOG_DEBUG, "Unknown ioctl=%d\n", ioctl_cmd);
        return -TUILLE_BAD_PARAMETERS;
    }

    return 0;
}

int tuilld_open(struct drv_info *info, const char *drv_path, ...)
{
    (void)drv_path;
    (void)info;
    TUILL_CALL_TRACE();

    return 0;
}

int tuilld_close(struct drv_info *info)
{
    (void)info;
    TUILL_CALL_TRACE();
    return 0;
}

int tuilld_ioctl(struct drv_info *info, int ioctl_cmd, struct ioctl_arg *arg)
{
    TUILL_CALL_TRACE();
    TUILL_MUTEX_LOCK(&drv_mutex);

    int ret = tuill_do_client_ioctls(info, ioctl_cmd, arg);
    if (ret == 0) {
        goto exit;
    }

    if (drv_ctx.drv_ioctl) {
        ret = drv_ctx.drv_ioctl(info, ioctl_cmd, arg);
        if (ret == 0) {
            goto exit;
        }
    }

    /* TODO: process other ioctls here */
exit:
    return ret;
}

ssize_t tuilld_read(struct drv_info *filp, void *buf, size_t len)
{
    TUILL_CALL_TRACE();
    TUILL_MUTEX_LOCK(&drv_mutex);

    int ret = 0;

    if (drv_ctx.drv_read) {
        ret = drv_ctx.drv_read(filp, buf, len);
    }

    return ret;
}

ssize_t tuilld_write(struct drv_info *filp, void *buf, size_t len)
{
    TUILL_CALL_TRACE();
    TUILL_MUTEX_LOCK(&drv_mutex);

    int ret = 0;

    if (drv_ctx.drv_write) {
        ret = drv_ctx.drv_write(filp, buf, len);
    }

    return ret;
}

/* TODO: drop this function when future of UDF's fsync will be decided */
int tuilld_fsync(struct drv_info *info)
{
    (void)info;
    TUILL_CALL_TRACE();
    return 0;
}

int init_tuill_driver(void)
{
    TUILL_CALL_TRACE();
    struct socket_callbacks cb;
    memset(&cb, 0, sizeof(struct socket_callbacks));
    drv_ctx.tuilldrv.tuill_state = TEE_PERIPHERAL_FLAG_TEE_CONTROLLABLE
                                 + TEE_PERIPHERAL_FLAG_EVENT_SOURCE;
    cb.income    = drv_input;
    cb.hangup    = drv_hangup;
    cb.handshake = drv_handshake;
    return tuill_init_client_drv(&drv_ctx.connect_ctx, &cb);
}

void uninit_tuill_driver(void)
{
    TUILL_CALL_TRACE();
    if (drv_ctx.connect_ctx) {
        tuill_uninit_client_drv(drv_ctx.connect_ctx);
        memset(&drv_ctx, 0, sizeof(struct tuill_client_drv_context));
    }
}

static void tuill_drv_destructor(int sig_id)
{
    (void)sig_id;
    TUILL_CALL_TRACE();
    TUILL_MUTEX_LOCK(&drv_mutex);
    uninit_all();
}

static void clear_HAL_common(void)
{
    int ret = 0;
    TUILL_CALL_TRACE();
    if (drv_ctx.tuilldrv.tuill_state & TEE_PERIPHERAL_FLAG_LOCKED) {
        ret = clear_HAL();
        if (ret == 0 && drv_ctx.drv_state_machine == DRV_STATE_CONNECTED) {
            syslog(LOG_DEBUG, "sending TUILL_ICMD_DRIVER_CLOSED\n");
            struct tuill_internal_command cmd = {};
            cmd.cmd = TUILL_ICMD_DRIVER_CLOSED;
            tuild_send_cmd(&cmd);
        }
    }
}

void uninit_all(void)
{
    TUILL_CALL_TRACE();
    static bool uninit_done = false;
    if (!uninit_done) {
        clear_HAL_common();
        tuilld_close(NULL); /* close driver if opened */
        uninit_tuill_driver(); /* close socket & thread */
        uninit_done = true;
    }
}
