/*
 * Copyright (c) 2015-2020, NVIDIA CORPORATION.  All rights reserved. All
 * information contained herein is proprietary and confidential to NVIDIA
 * Corporation.  Any use, reproduction, or disclosure without the written
 * permission of NVIDIA Corporation is prohibited.
 */

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

#include "log_utils.h"
#include "devblk_cdi.h"
#include "cdi_max96705.h"
#include "os_common.h"

#define REGISTER_ADDRESS_BYTES  1
#define REG_WRITE_BUFFER        32

#define GET_BLOCK_LENGTH(x) x[0]
#define GET_BLOCK_DATA(x)   &x[1]
#define SET_NEXT_BLOCK(x)   x += (x[0] + 1)

unsigned char max96705_enable_reverse_channel[] = {
    2, 0x04, 0x43,  // Enable config link, wait 5ms
};

unsigned char max96705_enable_serial_link[] = {
    2, 0x04, 0x83,   // Enable serial link, disable config link, wait 5ms
};

unsigned char max96705_defaults[] = {
    2, 0x08, 0x01,
    2, 0x97, 0x5F, //enable bit only on 96705
};

unsigned char max96705_program_cutoff_freq[] = {
    2, 0x08, 0x03,
    2, 0x97, 0x3F,
};

unsigned char max96705_config_input_mode[] = {
    2, 0x07, 0xC4,  // PCLKIN setting DBL=1, HIBW=1, BWS=0, ES=0, HVEN=1
};

unsigned char max96705_set_translator_a[] = {
    2, 0x09, 0x00,
    2, 0x0a, 0x00,
};

unsigned char max96705_set_translator_b[] = {
    2, 0x0b, 0x00,
    2, 0x0c, 0x00,
};

unsigned char max96705_set_xbar[] = {
    2, 0x20, 0x04,
    2, 0x21, 0x03,
    2, 0x22, 0x02,
    2, 0x23, 0x01,
    2, 0x24, 0x00,
    2, 0x25, 0x40,
    2, 0x26, 0x40,
    2, 0x27, 0x0E,
    2, 0x28, 0x2F,
    2, 0x29, 0x0E,
    2, 0x2A, 0x40,
    2, 0x2B, 0x40,
    2, 0x2C, 0x40,
    2, 0x2D, 0x40,
    2, 0x2E, 0x40,
    2, 0x2F, 0x40,
    2, 0x30, 0x17,
    2, 0x31, 0x16,
    2, 0x32, 0x15,
    2, 0x33, 0x14,
    2, 0x34, 0x13,
    2, 0x35, 0x12,
    2, 0x36, 0x11,
    2, 0x37, 0x10,
    2, 0x38, 0x07,
    2, 0x39, 0x06,
    2, 0x3A, 0x05,
    2, 0x3B, 0x40,
    2, 0x3C, 0x40,
    2, 0x3D, 0x40,
    2, 0x3E, 0x40,
    2, 0x3F, 0x0E,
    2, 0x40, 0x2F,
    2, 0x41, 0x0E,
};

unsigned char max96705_auto_config_link[] = {
    2, 0x67, 0xE4,
};

unsigned char max96705_double_input_mode[] = {
    2, 0x07, 0x80,  // PCLKIN setting DBL=1
};

unsigned char max96705_disable_reverse_channel_HIM_mode[] = {
    2, 0x4D, 0x40,
};

unsigned char max96705_enable_reverse_channel_HIM_mode[] = {
    2, 0x4D, 0xC0,
};

unsigned char max96705_i2c_remote_master_timeout[] = {
    2, 0x99, 0x0F, // set remote master timeout to never (Bug 1802338, 200419005)
};

static NvMediaStatus
DriverCreate(
    DevBlkCDIDevice *handle,
    void *clientContext)
{
    if(!handle)
        return NVMEDIA_STATUS_BAD_PARAMETER;

    handle->deviceDriverHandle = NULL;

    return NVMEDIA_STATUS_OK;
}

static NvMediaStatus
DriverDestroy(
    DevBlkCDIDevice *handle)
{
    if(!handle)
        return NVMEDIA_STATUS_BAD_PARAMETER;

    return NVMEDIA_STATUS_OK;
}

NvMediaStatus
MAX96705CheckPresence(
    DevBlkCDIDevice *handle)
{
    NvMediaStatus status = NVMEDIA_STATUS_OK;
    unsigned char reg = 0x1e;
    unsigned char readBuff = 0;

    if(!handle)
        return NVMEDIA_STATUS_BAD_PARAMETER;


    status = DevBlkCDIDeviceRead(handle, 0,
                REGISTER_ADDRESS_BYTES,
                &reg,
                1,
                &readBuff);

    if(status != NVMEDIA_STATUS_OK)
        return status;

    if (CHECK_96705ID(readBuff) == 0) {
        return NVMEDIA_STATUS_ERROR;
    }

    reg = 0x1f;

    status = DevBlkCDIDeviceRead(handle, 0,
                REGISTER_ADDRESS_BYTES,
                &reg,
                1,
                &readBuff);
    if(status != NVMEDIA_STATUS_OK) {
        return status;
    }

    LOG_MSG("MAX96705: Revision %u detected!\n", readBuff & 0xF);
    return NVMEDIA_STATUS_OK;
}

static NvMediaStatus
SetPreEmp(
    DevBlkCDIDevice *handle,
    unsigned char preemp)
{
    NvMediaStatus status = NVMEDIA_STATUS_OK;
    unsigned char reg[2] = {0x06, 0xA0};

    if (!handle) {
        return NVMEDIA_STATUS_BAD_PARAMETER;
    }

    reg[1] |= (preemp & 0xF);

    status = DevBlkCDIDeviceWrite(handle, 0,
                                  sizeof(reg),
                                  reg);

    LOG_MSG("MAX96705: Pre-emphasis set to 0x%02x\n", reg[1]);

    if (status != NVMEDIA_STATUS_OK) {
        return status;
    }

    nvsleep(5000); /* Delay to wait since I2C unavailable while GMSL locks from programming guide */

    return NVMEDIA_STATUS_OK;
}

static NvMediaStatus
WriteArray(
    DevBlkCDIDevice *handle,
    uint32_t arrayByteLength,
    const uint8_t *arrayData)
{
    NvMediaStatus status;

    if((!handle) || (!arrayData))
        return NVMEDIA_STATUS_BAD_PARAMETER;


    while(arrayByteLength) {
        if(arrayData[0] < 10) {
            status = DevBlkCDIDeviceWrite(
                        handle, 0,
                        GET_BLOCK_LENGTH(arrayData),
                        GET_BLOCK_DATA(arrayData));

            if(status != NVMEDIA_STATUS_OK) {
                LOG_DBG("%s, max96705 : error: wri2c   0x80    0x%.2X    0x%.2X\n",
                    __func__, (uint32_t)arrayData[1], (uint32_t)arrayData[2]);
                break;
            }

            arrayByteLength -= GET_BLOCK_LENGTH(arrayData) + 1;
            SET_NEXT_BLOCK(arrayData);
        } else {
            nvsleep(arrayData[0] * 10);
            arrayData++;
            arrayByteLength--;
        }
    }

    return status;
}

static NvMediaStatus
SetRegenVsync(
    DevBlkCDIDevice *handle,
    unsigned int vsync_high,
    unsigned int vsync_low,
    unsigned int vsync_delay,
    unsigned char vsync_trig,
    unsigned int pclk)
{
    NvMediaStatus status = NVMEDIA_STATUS_OK;
    unsigned char max96705_regen_vsync_delay[] = {
        2, 0x44, 0x00,
        2, 0x45, 0x9C,
        2, 0x46, 0x80
    };
    unsigned char max96705_regen_vsync_high[] = {
        2, 0x47, 0x00,
        2, 0x48, 0xb0,
        2, 0x49, 0x00
    };
    unsigned char max96705_regen_vsync_low[] = {
        2, 0x4A, 0x00,
        2, 0x4B, 0xb0,
        2, 0x4C, 0x00,
    };
    unsigned char max96705_regen_vsync_trig[] = {
        2, 0x43, 0x21
    };
#if 0 // TODO : need to check with MAXIM
    unsigned char max96705_vsync_align[] = {
        2, 0x67, 0xc4,
    };
#endif

    if (handle == NULL) {
        return NVMEDIA_STATUS_BAD_PARAMETER;
    }

    if (vsync_delay != 0) {
        max96705_regen_vsync_delay[2] = vsync_delay * (pclk / 1000000) / 256 / 256;
        max96705_regen_vsync_delay[5] = ((vsync_delay * (pclk / 1000000)) % (256 * 256)) / 256;
        max96705_regen_vsync_delay[8] = ((vsync_delay * (pclk / 1000000)) % 256) / 256;

        status = WriteArray(handle,
                            sizeof(max96705_regen_vsync_delay),
                            max96705_regen_vsync_delay);
        if (status != NVMEDIA_STATUS_OK) {
            return status;
        }
    }

    if (vsync_high != 0) {
        max96705_regen_vsync_high[2] = vsync_high * (pclk / 1000000) / 256 / 256;
        max96705_regen_vsync_high[5] = ((vsync_high * (pclk / 1000000)) % (256 * 256)) / 256;
        max96705_regen_vsync_high[8] = ((vsync_high * (pclk / 1000000)) % 256) / 256;

        status = WriteArray(handle,
                            sizeof(max96705_regen_vsync_high),
                            max96705_regen_vsync_high);
        if (status != NVMEDIA_STATUS_OK) {
            return status;
        }
    }

    if (vsync_low != 0) {
        max96705_regen_vsync_low[2] = vsync_low * (pclk / 1000000) / 256 / 256;
        max96705_regen_vsync_low[5] = ((vsync_low * (pclk / 1000000)) % (256 * 256)) / 256;
        max96705_regen_vsync_low[8] = ((vsync_low * (pclk / 1000000)) % 256) / 256;

        status = WriteArray(handle,
                            sizeof(max96705_regen_vsync_low),
                            max96705_regen_vsync_low);
        if (status != NVMEDIA_STATUS_OK) {
            return status;
        }
    }

    if ((vsync_trig == 1) || (vsync_trig == 2)) {
        if (vsync_trig == 2) {
           max96705_regen_vsync_trig[2] |= (1 << 2);
        }

        status = WriteArray(handle,
                            sizeof(max96705_regen_vsync_trig),
                            max96705_regen_vsync_trig);
        if (status != NVMEDIA_STATUS_OK) {
            return status;
        }
    }

    return NVMEDIA_STATUS_OK;
}

NvMediaStatus
MAX96705SetDefaults(
    DevBlkCDIDevice *handle)
{
    NvMediaStatus status;

    status = WriteArray(
                handle,
                sizeof(max96705_defaults),
                max96705_defaults);
    return status;
}

NvMediaStatus
MAX96705SetDeviceConfig(
        DevBlkCDIDevice *handle,
        uint32_t enumeratedDeviceConfig)
{
    NvMediaStatus status;

    switch(enumeratedDeviceConfig) {
        case CDI_CONFIG_MAX96705_DEFAULT:
            status = MAX96705SetDefaults(
                handle);
           break;
        case CDI_CONFIG_MAX96705_ENABLE_SERIAL_LINK:
            status = WriteArray(
                handle,
                sizeof(max96705_enable_serial_link),
                max96705_enable_serial_link);
            break;
        case CDI_CONFIG_MAX96705_PCLKIN:
            status = WriteArray(
                handle,
                sizeof(max96705_config_input_mode),
                max96705_config_input_mode);
            nvsleep(10000);  /* wait 10ms */
            break;
        case CDI_CONFIG_MAX96705_ENABLE_REVERSE_CHANNEL:
            status = WriteArray(
                handle,
                sizeof(max96705_enable_reverse_channel),
                max96705_enable_reverse_channel);
            nvsleep(5000);  /* wait 5ms */
            break;
        case CDI_CONFIG_MAX96705_SET_AUTO_CONFIG_LINK:
            status = WriteArray(
                handle,
                sizeof(max96705_auto_config_link),
                max96705_auto_config_link);
            break;
        case CDI_CONFIG_MAX96705_DOUBLE_INPUT_MODE:
            status = WriteArray(
                handle,
                sizeof(max96705_double_input_mode),
                max96705_double_input_mode);
            break;
        case CDI_CONFIG_MAX96705_DISABLE_HIM_MODE:
            status = WriteArray(
                handle,
                sizeof(max96705_disable_reverse_channel_HIM_mode),
                max96705_disable_reverse_channel_HIM_mode);
            nvsleep(10000);  /* wait 10ms */
            break;
        case CDI_CONFIG_MAX96705_ENABLE_HIM_MODE:
            status = WriteArray(
                handle,
                sizeof(max96705_enable_reverse_channel_HIM_mode),
                max96705_enable_reverse_channel_HIM_mode);
            nvsleep(10000);  /* wait 10ms */
            break;
        case CDI_CONFIG_MAX96705_SET_XBAR:
            status = WriteArray(
                handle,
                sizeof(max96705_set_xbar),
                max96705_set_xbar);
            break;
         case CDI_CONFIG_MAX96705_SET_MAX_REMOTE_I2C_MASTER_TIMEOUT:
            status = WriteArray(
                handle,
                sizeof(max96705_i2c_remote_master_timeout),
                max96705_i2c_remote_master_timeout);
            break;
        case CDI_CONFIG_MAX96705_PROGRAM_CUTOFF_FREQ:
            status = WriteArray(
                handle,
                sizeof(max96705_program_cutoff_freq),
                max96705_program_cutoff_freq);
           break;
       default:
            status =  NVMEDIA_STATUS_NOT_SUPPORTED;
            break;
    }

    return status;
}

NvMediaStatus
MAX96705ReadRegister(
    DevBlkCDIDevice *handle,
    uint32_t deviceIndex,
    uint32_t registerNum,
    uint32_t dataLength,
    unsigned char *dataBuff)
{
    unsigned char registerData[REGISTER_ADDRESS_BYTES];
    NvMediaStatus status;

    if(!handle || !dataBuff)
        return NVMEDIA_STATUS_BAD_PARAMETER;


    registerData[0] = registerNum & 0xFF;
    status = DevBlkCDIDeviceRead(handle,
        deviceIndex,
        REGISTER_ADDRESS_BYTES,
        registerData,
        dataLength,
        dataBuff);

    return status;
}

NvMediaStatus
MAX96705WriteRegister(
    DevBlkCDIDevice *handle,
    uint32_t deviceIndex,
    uint32_t registerNum,
    uint32_t dataLength,
    unsigned char *dataBuff)
{
    unsigned char data[REGISTER_ADDRESS_BYTES + REG_WRITE_BUFFER];
    NvMediaStatus status;

    if(!handle || !dataBuff)
        return NVMEDIA_STATUS_BAD_PARAMETER;


    data[0] = registerNum & 0xFF;
    memcpy(&data[1], dataBuff, (REG_WRITE_BUFFER < dataLength ? REG_WRITE_BUFFER: dataLength));

    status = DevBlkCDIDeviceWrite(
                handle,
                deviceIndex,
                dataLength + REGISTER_ADDRESS_BYTES,
                data);

    return status;
}

static NvMediaStatus
SetTranslator(
        DevBlkCDIDevice *handle,
        uint32_t parameterType,
        WriteReadParametersParamMAX96705 *param)
{
    NvMediaStatus status;
    unsigned char *max96705_set_translator;

    if(parameterType == CDI_WRITE_PARAM_CMD_MAX96705_SET_TRANSLATOR_A) {
            max96705_set_translator = &max96705_set_translator_a[0];
    } else {
            max96705_set_translator = &max96705_set_translator_b[0];
    }
    max96705_set_translator[2] = param->Translator.source << 1;
    max96705_set_translator[5] = param->Translator.destination << 1;

    status = WriteArray(
        handle,
        sizeof(max96705_set_translator_a),
        max96705_set_translator);

    return status;
}

static NvMediaStatus
SetDeviceAddress(
        DevBlkCDIDevice *handle,
        unsigned char address)
{
    NvMediaStatus status;
    unsigned char data[2];

    if(!handle)
        return NVMEDIA_STATUS_BAD_PARAMETER;


    data[0] = 0x00;

    if(address > 0x80)
        return NVMEDIA_STATUS_BAD_PARAMETER;

    data[1] = address << 1;

    status = DevBlkCDIDeviceWrite(handle, 0,
        2,
        data);

    return status;
}

static NvMediaStatus
GetDeviceAddress(
        DevBlkCDIDevice *handle,
        unsigned char *address)
{
    unsigned char data[2];
    NvMediaStatus status;

    if(!handle)
        return NVMEDIA_STATUS_BAD_PARAMETER;


    data[0] = 0x00;

    status = DevBlkCDIDeviceRead(handle, 0,
        1,
        &data[0],
        1,
        &data[1]);

    if (status == NVMEDIA_STATUS_OK)
        *address = data[1];

    return status;
}

static NvMediaStatus
SetInputMode(
        DevBlkCDIDevice *handle,
        ConfigureInputModeMAX96705 *inputmode)
{
    NvMediaStatus status;

    if(!handle)
        return NVMEDIA_STATUS_BAD_PARAMETER;


    max96705_config_input_mode[2] =  (unsigned char)inputmode->byte;

    status = DevBlkCDIDeviceWrite(handle, 0,
        max96705_config_input_mode[0],
        &max96705_config_input_mode[1]);

    return status;
}

NvMediaStatus
MAX96705WriteParameters(
        DevBlkCDIDevice *handle,
        uint32_t parameterType,
        uint32_t parameterSize,
        void *parameter)
{
    NvMediaStatus status;
    WriteReadParametersParamMAX96705 *param;

    if((!parameter) || (!handle))
        return NVMEDIA_STATUS_BAD_PARAMETER;

    param = (WriteReadParametersParamMAX96705 *)parameter;

    switch(parameterType) {
        case CDI_WRITE_PARAM_CMD_MAX96705_SET_TRANSLATOR_A:
        case CDI_WRITE_PARAM_CMD_MAX96705_SET_TRANSLATOR_B:
            if(parameterSize != sizeof(param->Translator))
                return NVMEDIA_STATUS_BAD_PARAMETER;
            status = SetTranslator(
                handle,
                parameterType,
                param);
           break;
        case CDI_WRITE_PARAM_CMD_MAX96705_SET_DEVICE_ADDRESS:
            if(parameterSize != sizeof(param->DeviceAddress))
                return NVMEDIA_STATUS_BAD_PARAMETER;
            status = SetDeviceAddress(
                handle,
                param->DeviceAddress.address);
           break;
        case CDI_WRITE_PARAM_CMD_MAX96705_CONFIG_INPUT_MODE:
            if(parameterSize != sizeof(param->inputmode))
                return NVMEDIA_STATUS_BAD_PARAMETER;
            status = SetInputMode(
                handle,
                param->inputmode);
           break;
        case CDI_WRITE_PARAM_CMD_MAX96705_SET_PREEMP:
            if(parameterSize != sizeof(param->preemp))
                return NVMEDIA_STATUS_BAD_PARAMETER;
            status = SetPreEmp(
                handle,
                param->preemp);
            break;
        case CDI_WRITE_PARAM_CMD_MAX96705_REGEN_VSYNC:
            if (parameterSize != sizeof(param->vsyncRegen)) {
                return NVMEDIA_STATUS_BAD_PARAMETER;
            }
            status = SetRegenVsync(handle,
                                   param->vsyncRegen.vsync_high,
                                   param->vsyncRegen.vsync_low,
                                   param->vsyncRegen.vsync_delay,
                                   param->vsyncRegen.vsync_trig,
                                   param->vsyncRegen.pclk);
            break;
        default:
            status = NVMEDIA_STATUS_NOT_SUPPORTED;
            break;
    }

    return status;
}

NvMediaStatus
MAX96705ReadParameters(
        DevBlkCDIDevice *handle,
        uint32_t parameterType,
        uint32_t parameterSize,
        void *parameter)
{
    NvMediaStatus status;
    WriteReadParametersParamMAX96705 *param;

    if(!parameter)
        return NVMEDIA_STATUS_BAD_PARAMETER;
    if(!handle)
        return NVMEDIA_STATUS_BAD_PARAMETER;

    param = (WriteReadParametersParamMAX96705 *)parameter;

    switch(parameterType) {
        case CDI_READ_PARAM_CMD_MAX96705_GET_DEVICE_ADDRESS:
            if(parameterSize != sizeof(param->DeviceAddress))
                return NVMEDIA_STATUS_BAD_PARAMETER;
            status = GetDeviceAddress(
                handle,
                (unsigned char*)param);
            break;
        default:
            status = NVMEDIA_STATUS_NOT_SUPPORTED;
            break;
    }

    return status;
}

NvMediaStatus
MAX96705DumpRegisters(
    DevBlkCDIDevice *handle)
{
    NvMediaStatus status = NVMEDIA_STATUS_OK;
    uint8_t data[3] = {3, 0x00, 0};
    uint32_t i = 0u, addr = 0u;

    if((handle == NULL)) {
        return NVMEDIA_STATUS_BAD_PARAMETER;
    }


    for (i = 0; i <= MAX96705_MAX_REG_ADDRESS; i++) {
        data[1] = i;

        status = DevBlkCDIDeviceRead(handle,0,
                REGISTER_ADDRESS_BYTES,
                &data[1],
                1,
                &data[2]);
        if (status != NVMEDIA_STATUS_OK)
            return status;

        if (i == 0)
            addr = data[2];

        printf(" Max96705(0x%x) : 0x%02x - 0x%02x\n", addr, data[1], data[2]);
    }

    return status;
}

static DevBlkCDIDeviceDriver deviceDriver = {
    .deviceName = "Maxim 96705 Serializer",
    .regLength = 1,
    .dataLength = 1,
    .DriverCreate = DriverCreate,
    .DriverDestroy = DriverDestroy,
};

DevBlkCDIDeviceDriver *
GetMAX96705Driver(void)
{
    return &deviceDriver;
}

