#include <linux/types.h>
#include <linux/delay.h>
#include <linux/kthread.h>
#include <linux/cdev.h>
#include <linux/module.h>

#include "sony_fpga.h"
#include "sony_fpga_local_if_i2c.h"

int module_param_qsfp_poll_spare_time = 400;
module_param(module_param_qsfp_poll_spare_time, int, S_IRUGO | S_IWUSR);

static inline void i2c_reg_write(void __iomem* base_addr, u32 offset, u32 data)
{
    void __iomem* addr;
    addr = (void __iomem*)(base_addr + offset);
    IO_WRITE_32(data, addr);
}

static inline u32 i2c_reg_read(void __iomem* base_addr, u32 offset)
{
    void __iomem* addr;
    addr = (void __iomem*)(base_addr + offset);
    return IO_READ_32(addr);
}

// FIFOをクリアする
static void i2c_clear_fifo(void __iomem* base_addr)
{
    // Module RESET
    i2c_reg_write(base_addr, FPGA_OFFSET_I2C_CMD, FPGA_VAL_I2C_CMD_CORERST);

    // FIFO Clearブロック数
    i2c_reg_write(base_addr, FPGA_OFFSET_I2C_CMD, FPGA_VAL_I2C_CMD_PURGE);
    i2c_reg_write(base_addr, FPGA_OFFSET_I2C_CMD, 0);

    // Core Enable
    i2c_reg_write(base_addr, FPGA_OFFSET_I2C_CMD, FPGA_VAL_I2C_CMD_COREEN);
}

// I2Cステータス取得
static u32 i2c_get_status(void __iomem* base_addr)
{
    return i2c_reg_read(base_addr, FPGA_OFFSET_I2C_STS);
}

// Transactionが終了したことを確認する
static int i2c_waittip(void __iomem* base_addr)
{
    uint i;

    for (i = 0; i < I2C_RETRY_COUNT; ++i) {
        if ((i2c_get_status(base_addr) & FPGA_VAL_I2C_STS_RDWR) == 0) {
            return 0;
        }
        usleep_range(1,2);
    }

    return -1;
}

// TX FIFOがFullでないことを確認する
static int i2c_check_space(void __iomem* base_addr)
{
    uint i;

    for (i = 0; i < I2C_RETRY_COUNT; ++i) {
        if ((i2c_get_status(base_addr) & FPGA_VAL_I2C_STS_TXFULL) == 0) {
            return 0;
        }
        usleep_range(1,2);
    }

    return -1;
}

// RX FIFOにデータが到着するまで待機する
static int i2c_waitrx(void __iomem* base_addr)
{
    uint i;

    for (i = 0; i < I2C_RETRY_COUNT; ++i) {
        if ((i2c_get_status(base_addr) & FPGA_VAL_I2C_STS_RXEXIST) == FPGA_VAL_I2C_STS_RXEXIST) {
            return 0;
        }
        usleep_range(1,2);
    }

    return -1;
}

static int i2c_dword_write_tx(void __iomem* base_addr, u32 data, const char* debug_str)
{
    if (i2c_check_space(base_addr) < 0) {
        I2C_PR_ERR(base_addr, "[%s] Buffer full!", debug_str);
        return -1;
    }

    i2c_reg_write(base_addr, FPGA_OFFSET_I2C_TX, data);

    if (i2c_waittip(base_addr) < 0) {
        I2C_PR_ERR(base_addr, "[%s] Transaction not completed!", debug_str);
        return -1;
    }

    return  0;
}

static int i2c_dword_read_rx(void __iomem* base_addr)
{
    if (i2c_waitrx(base_addr) < 0) {
        return -1;
    }

    return i2c_reg_read(base_addr, FPGA_OFFSET_I2C_RX);
}

static int i2c_check_ack(void __iomem* base_addr)
{
    uint i;

    for (i = 0; i < I2C_RETRY_COUNT; ++i) {
        if ((i2c_get_status(base_addr) & FPGA_VAL_I2C_STS_RXACK) == 0) {
            return 0;
        }
        usleep_range(1,2);
    }

    return -1; // Slave no response
}

static int i2c_check_nack(void __iomem* base_addr)
{
    uint i;

    for (i = 0; i < I2C_RETRY_COUNT; ++i) {
        if ((i2c_get_status(base_addr) & FPGA_VAL_I2C_STS_RXACK) != 0) {
            return 0;
        }
        usleep_range(1,2);
    }

    return -1; // Slave no response
}

static int i2c_select_device(void __iomem* base_addr, const struct i2c_dev_info* dev_info, u32 addr)
{
    // デバイスセレクト
    if (i2c_dword_write_tx(base_addr, (FPGA_VAL_I2C_TX_CMD_START |
                                  FPGA_VAL_I2C_TX_CMD_WRITE |
                                  (dev_info->slave_addr << 1)), "DEV_SEL") < 0) {
        return -1;
    }

    if (i2c_check_ack(base_addr) < 0) {
        I2C_PR_ERR(base_addr, "DEVICE select (w) error! slave_addr=%x addr=%x", dev_info->slave_addr, addr);
        return -1;
    }

    if (dev_info->addr_size > 1) {
        // アドレス上位8バイト指定
        if (i2c_dword_write_tx(base_addr, (FPGA_VAL_I2C_TX_CMD_WRITE |
                                      ((addr >> 8) & 0xff)), "HIGH_ADDR") < 0) {
            return -1;
        }
    }
    // アドレス下位8バイト指定
    if (i2c_dword_write_tx(base_addr, (FPGA_VAL_I2C_TX_CMD_WRITE |
                                  ((addr >> 0) & 0xff)), "LOW_ADDR") < 0) {
        return -1;
    }

    if (i2c_check_ack(base_addr) < 0) {
        I2C_PR_ERR(base_addr, "Slave addr error! %d : %d", dev_info->slave_addr, addr);
        return -1;
    }

    return  0;
}

// 指定アドレスから指定サイズ分データを書き込む
static int i2c_tx_data(void __iomem* base_addr, const struct i2c_dev_info* dev_info, u32 addr, const u8* data, size_t length)
{
    size_t size;
    uint i;

    // ページ境界に合わせた書込みサイズを計算
    size = I2C_PAGE_SIZE - (addr % I2C_PAGE_SIZE);
    if (size > length) {
        size = length;
    }

    if (size == 0) {
        return 0;
    }

    // デバイスセレクト & アドレス指定
    if (i2c_select_device(base_addr, dev_info, addr) < 0) {
        return -1;
    }

    for (i = 0; i < size; ++i) {
        u32 cmd = FPGA_VAL_I2C_TX_CMD_WRITE | (*data);

        if (i == (size - 1)) {
            cmd |= FPGA_VAL_I2C_TX_CMD_STOP;
        }

        if (i2c_dword_write_tx(base_addr, cmd, "WR_DATA") < 0) {
            return -1;
        }

        if (i2c_check_ack(base_addr) < 0) {
            I2C_PR_ERR(base_addr, "Data write error!, i=%d", i);
            return -1;
        }

        ++data;
    }

    return size;
}

// 指定アドレスから指定サイズ分データを読み込む
static int i2c_rx_data(void __iomem* base_addr, const struct i2c_dev_info* dev_info, u32 addr, u8* data, int length)
{
    size_t size;
    uint i;

    // ページ境界に合わせた書込みサイズを計算
    size = I2C_PAGE_SIZE - (addr % I2C_PAGE_SIZE);
    if (size > length) {
        size = length;
    }

    if (size == 0) {
        return 0;
    }

    // デバイスセレクト & アドレス指定
    if (i2c_select_device(base_addr, dev_info, addr) < 0) {
        return -1;
    }

    // 読み込み用にデバイスセレクト
    if (i2c_dword_write_tx(base_addr, (FPGA_VAL_I2C_TX_CMD_START |
                                  FPGA_VAL_I2C_TX_CMD_WRITE |
                                  ((dev_info->slave_addr << 1) | 0x1)), "RD_SEL") < 0) {
        return -1;
    }

    if (i2c_check_ack(base_addr) < 0) {
        I2C_PR_ERR(base_addr, "DEVICE select (r) error!");
        return -1;
    }

    for (i = 0; i < size ; ++i) {
        u32 cmd = FPGA_VAL_I2C_TX_CMD_READ;

        if (i == (size - 1)) {
            cmd |= FPGA_VAL_I2C_TX_CMD_STOP;
        }

        if (i2c_dword_write_tx(base_addr, cmd, "RD_CMD") < 0) {
            return -1;
        }

        if (i < (size - 1)) {
            if (i2c_check_ack(base_addr) < 0) {
                I2C_PR_ERR(base_addr, "Data read error!, i=%d", i);
                return -1;
            }
        }
        else {
            if (i2c_check_nack(base_addr) < 0) {
                I2C_PR_ERR(base_addr, "Data read error!, i=%d", i);
                return -1;
            }
        }
    }

    for (i = 0; i < size; ++i) {
        const int rdata = i2c_dword_read_rx(base_addr);
        if (rdata < 0) {
            I2C_PR_ERR(base_addr, "RX fifo read error!, i=%d", i);
            return -1;
        }
        *data = (u8)(rdata & 0xff);
        pr_debug("rdata[%d] %02x", i, *data);
        ++data;
    }

    return size;
}

//----------

// I2Cコントローラーを初期化する
void i2c_driver_init(void __iomem* base_addr)
{
    // Set Prescaler
    i2c_reg_write(base_addr, FPGA_OFFSET_I2C_PRSCL, ((I2C_SYS_FREQ / (5*100*1000)) - 1));

    i2c_clear_fifo(base_addr);
}

int i2c_tx(void __iomem* base_addr, const struct i2c_dev_info* dev_info, u32 addr, const u8* data, u32 length)
{
    int status = 0;

    if (length == 0) {
        return 0;
    }

    i2c_clear_fifo(base_addr);

    pr_debug("i2ctx %llx slave:%08x addr:%x data:%x(%d)\n", (u64 __force)base_addr, dev_info->slave_addr, addr, *data, length);
    while (length > 0) {
        const int write_size = i2c_tx_data(base_addr, dev_info, addr, data, length);
        if (write_size < 0) {
            status = write_size;
            break;
        }

        addr += write_size;
        data += write_size;
        length -= write_size;
    }

    return status;
}

int i2c_rx(void __iomem* base_addr, const struct i2c_dev_info* dev_info, u32 addr, u8* data, u32 length)
{
    int status = 0;

    if (length == 0) {
        return 0;
    }

    i2c_clear_fifo(base_addr);

    while (length > 0) {
        const int read_length = i2c_rx_data(base_addr, dev_info, addr, data, length);
        if (read_length < 0) {
            status = read_length;
            break;
        }

        pr_debug("i2c_rx %llx slave:%08x addr:%x data:%x\n", (u64 __force)base_addr, dev_info->slave_addr, addr, *data);
        addr += read_length;
        data += read_length;
        length -= read_length;
    }

    return status;
}
