#include <core/access.h>
#include <core/driver.h>
#include <atomic.h>
#include <cache.h>
#include <errno.h>
#include <tees_driver.h>
#include <fcntl.h>
#include <inttypes.h>
#include <stdarg.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/ioctl.h>
#include <unistd.h>
#include <driver/mem/phys.h>
#include <tee_internal_api.h>
#include <tee_ta_destructor.h>
#include "board.h"
#include "bsp_common.h"
#include "dbg.h"
#include "disp_core.h"
#include "tuiHal.h"
#include "display_driver_TA.h"
#include "secmap.h"

struct disp_dev {
    struct atomic_t opened;
};

static struct usr_drv_info *driver_info = NULL;
static char driver_name[] = "tui_display";
static volatile struct disp_dev dev;

static int check_buffer_param(tuiDisplayBuffer_t *fb)
{
    uint32_t frame_buffer_pa   = fb->physicalFb;
    uint32_t frame_buffer_size = fb->physicalFbLen;
    uint32_t work_buffer_pa    = fb->physicalWb;
    uint32_t work_buffer_size  = fb->physicalWbLen;
    uint32_t next_pos;
    uint32_t fb_size;
    uint32_t wb_size;

    dbgPrintf("width             = %d\n", fb->width);
    dbgPrintf("height            = %d\n", fb->height);
    dbgPrintf("frame_buffer_pa   = 0x%08x\n", frame_buffer_pa);
    dbgPrintf("frame_buffer_size = 0x%08x\n", frame_buffer_size);
    dbgPrintf("work_buffer_pa    = 0x%08x\n", work_buffer_pa);
    dbgPrintf("work_buffer_size  = 0x%08x\n", work_buffer_size);

    if (fb->width > 4096 || fb->height > 4096) {
        errPrintf("too large width %d or height %d\n",
                  fb->width, fb->height);
        return -1;
    }

    fb_size = fb->width * fb->height * (BOARD_BITSPERPIXEL >> 3);
    fb_size = STUI_ALIGN_UP(fb_size, FB_ALIGN);
    wb_size = fb->width * fb->height * (2 * (BOARD_BITSPERPIXEL >> 3) + 1);
    wb_size = STUI_ALIGN_UP(wb_size, FB_ALIGN);

    /* Check frame buffer address and size */
    if (!frame_buffer_pa || frame_buffer_size != fb_size) {
        errPrintf("null frame buffer address or incorrect size 0x%08x\n",
                  fb_size);
        return -1;
    }

    if (work_buffer_pa != 0) {
        if (__builtin_add_overflow(frame_buffer_pa,
                                   frame_buffer_size,
                                   &next_pos)) {
            errPrintf("integer overflow\n");
            return -1;
        }

        /* Check WB begins exactly after the frame-buffer */
        if (work_buffer_pa != next_pos) {
            errPrintf("WB is not right after FB\n");
            return -1;
        }

        /* Check WB size */
        if (work_buffer_size > wb_size) {
            errPrintf("WB size is larger than expectation\n");
            return -1;
        }
    } else {
        /* No working buffer */
        fb->physicalWbLen = 0;
    }

    return 0;
}

static int drv_open(struct drv_info *info, const char *drv_path, ...)
{
    dbgPrintf("DISP >> %s\n", __func__);
    int ret = 0;
    (void)info;
    (void)drv_path;

    if (atomic_inc(dev.opened) >= 1) {
        atomic_dec(dev.opened);
        errPrintf("Display already opened\n");
        return -EBUSY;
    }

    ret = drv_video_init();
    if (ret) {
        atomic_dec(dev.opened);
        errPrintf("drv_video_init() FAILED\n\n");
        return -EIO;
    }
    infoPrintf("DISP << %s\n", __func__);
    return ret;
}

static int drv_close(struct drv_info *info)
{
    dbgPrintf("DISP >> %s\n", __func__);
    int ret = 0;
    TEE_Result res;

    (void)info;

    if (atomic_dec(dev.opened) > 0) {
        /* Uninitialize display controller */
        res = tuiHalDisplayUninitialize();
        if (res) {
            errPrintf("tuiHalDisplayUninitialize() failed, res = %d\n", res);
        }
        sec_release();
    } else {
        atomic_inc(dev.opened);
    }
    infoPrintf("DISP << %s\n", __func__);
    return ret;
}

static int drv_ioctl(struct drv_info *info, int ioctl_cmd, struct ioctl_arg *ioctl_data)
{
    dbgPrintf("DISP >> %s\n", __func__);
    (void) info;
    int ret = 0;
    ioctl_fbset_t fbset_cmd;
    ioctl_getinfo_t getinfo_cmd;
    ioctl_update_t update_cmd;
    TEE_Result hal_res;

    if (atomic_read(dev.opened) != 1) {
        return -EPERM;
    }
    switch (ioctl_cmd) {
    case DISP_IOCTL_SET_FB:
        dbgPrintf("DISP_IOCTL_SET_FB execution...\n");
        memcpy((void *)&fbset_cmd, ioctl_data->input[0].iov_base, ioctl_data->input[0].iov_len);
        if (check_buffer_param(&fbset_cmd.fb)) {
            return -E_TUI_HAL_BAD_PARAMETERS;
        }
        hal_res = tuiHalDisplayInitialize(&fbset_cmd.fb);
        if (hal_res == E_TUI_HAL_BAD_PARAMETERS) {
            ret = -EINVAL;
        } else if (hal_res != 0) {
            ret = -EPERM;
        }
        if (ioctl_data->output[0].iov_base) {
            memcpy(ioctl_data->output[0].iov_base, (void *)&fbset_cmd, ioctl_data->output[0].iov_len);
        }
        infoPrintf("DISP_IOCTL_SET_FB finish...\n");
        break;

    case DISP_IOCTL_RELEASE_FB:
        dbgPrintf("DISP_IOCTL_RELEASE_FB execution...\n");
        hal_res = tuiHalDisplayUninitialize();
        if (hal_res) {
            errPrintf("tuiHalDisplayUninitialize() failed, res = %d\n", hal_res);
        }
        dbgPrintf("DISP_IOCTL_RELEASE_FB finish...\n");
        break;

    case DISP_IOCTL_GET_INFO:
        dbgPrintf("DISP_IOCTL_GET_INFO execution...\n");
        ret = drv_video_getinfo(&getinfo_cmd.info);
        if (!ret) {
            getinfo_cmd.cmd_size = sizeof(getinfo_cmd);
            if (ioctl_data->output[0].iov_base) {
                memcpy(ioctl_data->output[0].iov_base, &getinfo_cmd, ioctl_data->output[0].iov_len);
            }
        }
        dbgPrintf("DISP_IOCTL_GET_INFO finish...\n");
        break;

    case DISP_IOCTL_UPDATE:
        dbgPrintf("DISP_IOCTL_UPDATE execution...\n");
        memcpy((void *)&update_cmd, (void *)ioctl_data->input[0].iov_base, ioctl_data->input[0].iov_len);
        /* Only one frame buffer allowed */
        if (update_cmd.fb_index != 0) {
            return -EINVAL;
        }
#ifndef USE_NON_CACHED_FB
        if (TEES_DcacheFlush(drv_video_get_vaddr(), (size_t)drv_video_get_fbsize()) != 0) {
            ret = -EPERM;
        } else if (drv_video_trigger() != 0) {
            ret = -EPERM;
        }
#else
        if (drv_video_trigger() != 0) {
            ret = -EPERM;
        }
#endif
        infoPrintf("DISP_IOCTL_UPDATE finish...\n");
        break;

    default:
        return -EINVAL;
    }
    dbgPrintf("DISP << %s\n", __func__);
    return ret;
}

static void *drv_mmap(struct drv_info *filp, void *addr, size_t data_len,
                      int prot, int flags, off_t offset)
{
    dbgPrintf("DISP >> %s\n", __func__);
    (void)filp;
    (void)addr;
    (void)prot;
    (void)flags;
    unsigned int fb_size;
    unsigned int wb_size;
    void *ret_val = 0;

    if (atomic_read(dev.opened) != 1) {
        return (void *)-EPERM;
    }

    fb_size = drv_video_get_fbsize();
    wb_size = tuiHalWBGetSize();

    /*
     * display driver TA mmap-share either whole frame buffer
     * or whole working buffer depending on offset
     */
    if (offset == 0 && data_len == fb_size) {
        ret_val = drv_video_get_vaddr();
    } else if (offset == fb_size && data_len == wb_size) {
        ret_val = tuiHalWBGetVaddr();
    } else {
        errPrintf("Invalid argument offset=%" PRId64 ", data_len=%zd, fb_size=%d, wb_size=%d\n",
                  offset, data_len, fb_size, wb_size);
        ret_val = (void *)-EINVAL;
        goto mmap_err;
    }

    if (!ret_val) {
        ret_val = (void *)-EPERM;
    }

mmap_err:
    dbgPrintf("DISP << %s\n", __func__);
    return ret_val;
}

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("DISP >> %s\n", __func__);
    uninit();
    dbgPrintf("DISP << %s\n", __func__);
}

TEE_Result TA_CreateEntryPoint(void)
{
    int ret;
    TEE_Result res;
    static struct fops file_ops = {
        .open  = drv_open,
        .close = drv_close,
        .ioctl_iov = drv_ioctl,
        .mmap  = drv_mmap
    };

    dbgPrintf("DISP >> %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_DISPLAY, &driver_info);
    if (ret) {
        errPrintf("TEES_InitDriver() failed, ret = %d\n", ret);
        return TEE_ERROR_GENERIC;
    }

    return TEE_SUCCESS;
}

void TA_DestroyEntryPoint(void)
{
    uninit();

    infoPrintf("DISP << %s\n", __func__);
}

TEE_Result TA_OpenSessionEntryPoint(uint32_t paramTypes,
                                    TEE_Param params[4],
                                    void **sessionContext)
{
    (void)paramTypes;
    (void)params;
    (void)sessionContext;

    infoPrintf("DISP << %s\n", __func__);

    return TEE_SUCCESS;
}

void TA_CloseSessionEntryPoint(void *sessionContext)
{
    (void)sessionContext;

    infoPrintf("DISP << %s\n", __func__);
}

TEE_Result TA_InvokeCommandEntryPoint(void *sessionContext,
        uint32_t commandID, uint32_t paramTypes, TEE_Param params[4])
{
    (void)sessionContext;
    (void)commandID;
    (void)paramTypes;
    (void)params;
    return TEE_SUCCESS;
}
