/*
   forward-fd2110.c - driver for SoftLab-NSK FD2110 video board

   Copyright (C) 2017 - 2023 Konstantin Oblaukhov <oblaukhov@sl.iae.nsk.su>

   This program is free software; you can redistribute it and/or
   modify it under the terms of the GNU General Public License
   as published by the Free Software Foundation; either version 2
   of the License, or (at your option) any later version.

   This program is distributed in the hope that it will be useful,
   but WITHOUT ANY WARRANTY; without even the implied warranty of
   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
   GNU General Public License for more details.

   You should have received a copy of the GNU General Public License
   along with this program; if not, write to the Free Software
   Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
*/

#include "forward.h"
#include "forward-vdma.h"

#include "fd2110_reg.h"

#include <linux/stat.h>
#include <linux/firmware.h>
#include <linux/delay.h>

#define FD2110_ALL_INTERRUPTS ((1 << 12) | (1 << 14) | (1 << 16) | (1 << 18))

#define FD2110_ALL_INTERRUPTS_DATA_MASK ((1 << 13) | (1 << 15) | (1 << 17) | (1 << 19))

#define MCAP_EXT_CAP_HEADER 0x00
#define MCAP_VEND_SPEC_HEADER 0x04
#define MCAP_FPGA_JTAG_ID 0x08
#define MCAP_FPGA_BIT_VERSION 0x0C
#define MCAP_STATUS 0x10
#define MCAP_CONTROL 0x14
#define MCAP_DATA 0x18
#define MCAP_READ_DATA_0 0x1C
#define MCAP_READ_DATA_1 0x20
#define MCAP_READ_DATA_2 0x24
#define MCAP_READ_DATA_3 0x28

#define MCAP_CTRL_ENABLE (1 << 0)
#define MCAP_CTRL_READ_ENABLE (1 << 1)
#define MCAP_CTRL_RESET (1 << 4)
#define MCAP_CTRL_MOD_RESET (1 << 5)
#define MCAP_CTRL_REQUEST (1 << 8)
#define MCAP_CTRL_DESIGN_SWITCH (1 << 12)
#define MCAP_CTRL_WR_DATA_EN (1 << 16)

#define MCAP_STS_ERROR (1 << 0)
#define MCAP_STS_EOS (1 << 1)
#define MCAP_STS_READ_CMP (1 << 4)
#define MCAP_STS_READ_COUNT (7 << 5)
#define MCAP_STS_FIFO_OVERFLOW (1 << 8)
#define MCAP_STS_FIFO_OCCUPANCY (15 << 12)
#define MCAP_STS_RELEASE (1 << 24)

struct fd2110_dev {
	struct forward_dev *dev;
	u16 mcap_addr;
	u32 mcap_fw_version;
};

inline int mcap_read_reg(struct fd2110_dev *fd2110, u8 reg, u32 *value)
{
	return pci_read_config_dword(fd2110->dev->pci_dev, fd2110->mcap_addr + reg, value);
}

inline int mcap_write_reg(struct fd2110_dev *fd2110, u8 reg, u32 value)
{
	return pci_write_config_dword(fd2110->dev->pci_dev, fd2110->mcap_addr + reg, value);
}

inline int mcap_write_reg_mask(struct fd2110_dev *fd2110, u8 reg, u32 value, u32 mask)
{
	u32 v;
	int status;

	status = mcap_read_reg(fd2110, reg, &v);
	if (status)
		return status;

	v = (v & (~mask)) | (value & mask);

	status = mcap_write_reg(fd2110, reg, v);
	return status;
}

static int mcap_request(struct fd2110_dev *fd2110)
{
	unsigned long timeout;
	u32 status;
	int result = 0;

	result = mcap_write_reg_mask(fd2110, MCAP_CONTROL, MCAP_CTRL_ENABLE | MCAP_CTRL_REQUEST,
				     MCAP_CTRL_ENABLE | MCAP_CTRL_REQUEST);

	if (result)
		return result;

	timeout = jiffies + HZ;

	do {
		msleep(1);
		result = mcap_read_reg(fd2110, MCAP_STATUS, &status);
	} while (!result && (status & MCAP_STS_RELEASE) && time_before(jiffies, timeout));

	if (result)
		return result;

	if (!time_before(jiffies, timeout)) {
		result = -ETIMEDOUT;
		return result;
	}

	return 0;
}

static int mcap_reset(struct fd2110_dev *fd2110)
{
	int status = 0;

	status = mcap_write_reg_mask(fd2110, MCAP_CONTROL, MCAP_CTRL_RESET | MCAP_CTRL_MOD_RESET,
				     MCAP_CTRL_RESET | MCAP_CTRL_MOD_RESET);
	if (status)
		return status;

	status =
		mcap_write_reg_mask(fd2110, MCAP_CONTROL, 0, MCAP_CTRL_RESET | MCAP_CTRL_MOD_RESET);
	return status;
}

static int mcap_release(struct fd2110_dev *fd2110)
{
	return mcap_write_reg_mask(fd2110, MCAP_CONTROL, 0, MCAP_CTRL_ENABLE | MCAP_CTRL_REQUEST);
}

static int mcap_design_switch(struct fd2110_dev *fd2110, bool on)
{
	return mcap_write_reg_mask(fd2110, MCAP_CONTROL, on ? MCAP_CTRL_DESIGN_SWITCH : 0,
				   MCAP_CTRL_DESIGN_SWITCH);
}

static int mcap_write_bitstream(struct fd2110_dev *fd2110, const struct firmware *fw)
{
	int result = 0;
	size_t i;
	unsigned long timeout;
	u32 status;

	result = mcap_write_reg_mask(fd2110, MCAP_CONTROL, MCAP_CTRL_WR_DATA_EN,
				     MCAP_CTRL_WR_DATA_EN | MCAP_CTRL_READ_ENABLE);
	if (result)
		return result;

	for (i = 0; i < fw->size / 4; i++) {
		u32 data = cpu_to_be32(((u32 *)fw->data)[i]);
		result = mcap_write_reg(fd2110, MCAP_DATA, data);
		if (result)
			return result;
	}

	timeout = jiffies + HZ;

	do {
		msleep(1);
		result = mcap_read_reg(fd2110, MCAP_STATUS, &status);
	} while (!result && !(status & MCAP_STS_EOS) && time_before(jiffies, timeout));

	if (!time_before(jiffies, timeout)) {
		result = mcap_write_reg_mask(fd2110, MCAP_CONTROL, 0,
					     MCAP_CTRL_WR_DATA_EN | MCAP_CTRL_READ_ENABLE);
		return -ETIMEDOUT;
	}

	return result;
}

static int fd2110_flash_firmware(struct fd2110_dev *fd2110, const struct firmware *fw)
{
	int result = 0;

	result = mcap_request(fd2110);
	if (result) {
		dev_warn(fd2110->dev->parent_dev, "MCAP request failed: %d\n", result);
		goto end;
	}

	result = mcap_reset(fd2110);
	if (result) {
		dev_warn(fd2110->dev->parent_dev, "MCAP reset failed: %d\n", result);
		goto end;
	}

	result = mcap_design_switch(fd2110, false);
	if (result) {
		dev_warn(fd2110->dev->parent_dev, "MCAP design siwtch off failed: %d\n", result);
		goto end;
	}

	result = mcap_write_bitstream(fd2110, fw);
	if (result) {
		dev_warn(fd2110->dev->parent_dev, "MCAP bitstream write failed: %d\n", result);
		goto end;
	}

	result = mcap_design_switch(fd2110, true);
	if (result) {
		dev_warn(fd2110->dev->parent_dev, "MCAP design siwtch on failed: %d\n", result);
		goto end;
	}

end:
	mcap_release(fd2110);

	return result;
}

static int fd2110_init(struct forward_dev *dev)
{
	u32 mcap_fw_version;
	const struct firmware *fw = NULL;
	char fw_name[64];
	int result = 0;
	struct fd2110_dev *fd2110 =
		devm_kzalloc(dev->parent_dev, sizeof(struct fd2110_dev), GFP_KERNEL);

	if (!fd2110)
		return -ENOMEM;

	fd2110->dev = dev;
	dev->cfg.private = fd2110;

	fd2110->mcap_addr = pci_find_ext_capability(dev->pci_dev, PCI_EXT_CAP_ID_VNDR);
	if (!fd2110->mcap_addr) {
		dev_warn(dev->parent_dev, "MCAP not found, assume firmware already loaded\n");
		goto flash_end;
	}

	mcap_read_reg(fd2110, MCAP_FPGA_BIT_VERSION, &mcap_fw_version);
	dev_info(dev->parent_dev, "MCAP firmware version: 0x%08x\n", mcap_fw_version);

	snprintf(fw_name, sizeof(fw_name), "forward/FD2110/v%d_%d/fd2110_%d.bin",
		 (mcap_fw_version >> 26) & 0x7, (mcap_fw_version >> 24) & 0x3,
		 (mcap_fw_version >> 0) & 0xFFFF);

	dev_info(dev->parent_dev, "Requesting firmware: %s\n", fw_name);
	result = request_firmware(&fw, fw_name, dev->parent_dev);

	if (result)
		goto flash_end;

	result = fd2110_flash_firmware(fd2110, fw);

flash_end:
	if (fw)
		release_firmware(fw);

	return result;
}

static void fd2110_fini(struct forward_dev *dev)
{
	struct fd2110_dev *fd2110 = dev->cfg.private;

	if (fd2110)
		devm_kfree(dev->parent_dev, fd2110);
}

static void fd2110_read_id(struct forward_dev *dev)
{
	int cap_addr = pci_find_ext_capability(dev->pci_dev, PCI_EXT_CAP_ID_DSN);

	FD2110_SoftID id = FD2110_SoftID_R(dev->csr);
	FD2110_StaticVersion v = FD2110_StaticVersion_R(dev->csr);

	dev->soft_id = (s32)id.id;

	if (dev->soft_id < 0)
		dev_warn(dev->parent_dev, "Invalid device serial: %lld\n", dev->soft_id);

	if (cap_addr) {
		u32 hi, lo;
		pci_read_config_dword(dev->pci_dev, cap_addr + 4, &lo);
		pci_read_config_dword(dev->pci_dev, cap_addr + 8, &hi);
		dev->hard_id = ((u64)hi << 32) | (u64)lo;
	} else {
		dev_warn(dev->parent_dev, "Cannot get device hardware serial\n");
		dev->hard_id = (u64)dev->soft_id;
	}
	dev->fw_version = *((uint32_t *)&v);
}

static void fd2110_enable_interrupts(struct forward_dev *dev, u32 interrupts)
{
	u32 irq = ioread32(&dev->csr[FD2110_IRQEnable_A]);
	irq |= interrupts & FD2110_ALL_INTERRUPTS;
	iowrite32(irq, &dev->csr[FD2110_IRQEnable_A]);
}

static void fd2110_disable_interrupts(struct forward_dev *dev, u32 interrupts)
{
	u32 irq = ioread32(&dev->csr[FD2110_IRQEnable_A]);
	irq &= ~(interrupts & FD2110_ALL_INTERRUPTS);
	iowrite32(irq, &dev->csr[FD2110_IRQEnable_A]);
	iowrite32(interrupts & FD2110_ALL_INTERRUPTS, &dev->csr[FD2110_IRQFlags_A]);
}

static bool fd2110_read_interrupts(struct forward_dev *dev, u32 *interrupts, u32 *data,
				   u32 *data_mask)
{
	u32 irq = ioread32(&dev->csr[FD2110_IRQFlags_A]);
	iowrite32((irq & FD2110_ALL_INTERRUPTS), &dev->csr[FD2110_IRQFlags_A]);
	*interrupts = irq & FD2110_ALL_INTERRUPTS;
	*data = irq & FD2110_ALL_INTERRUPTS_DATA_MASK;
	*data_mask = FD2110_ALL_INTERRUPTS_DATA_MASK;

	return (irq & FD2110_ALL_INTERRUPTS) ? true : false;
}

static void fd2110_enable_vdma(struct forward_dev *dev, u32 map_addr)
{
	/*FD2110_VDMADescriptor desc = { .address = map_addr };
	FD2110_VDMA vdma = { .enable = 1 };
	FD2110_VDMADescriptor_W(dev->csr, desc);
    FD2110_VDMA_W(dev->csr, vdma);*/
}

static void fd2110_disable_vdma(struct forward_dev *dev)
{
	//FD2110_VDMA vdma = { .enable = 0 };
	//FD2110_VDMA_W(dev->csr, vdma);
}

static enum forward_io_type fd2110_io_type(struct forward_dev *dev, int io)
{
	return FORWARD_IO_BIDIR;
}

static int fd2110_io_number(struct forward_dev *dev, int io)
{
	return io + 1;
}

static enum forward_io_state fd2110_io_state(struct forward_dev *dev, int io)
{
	FD2110_VideoConfig r = FD2110_VideoConfig_R(dev->csr);
	uint32_t cfg = *((uint32_t *)&r);

	if (cfg & (1 << io)) {
		FD2110_VideoOutCS cs = FD2110_VideoOutCS_R(dev->csr, io);
		return cs.reset ? FORWARD_IO_DISABLED : FORWARD_IO_TX;
	} else {
		return FORWARD_IO_RX;
	}
}

static int fd2110_set_io_state(struct forward_dev *dev, int io, enum forward_io_state state)
{
	FD2110_VideoConfig r = FD2110_VideoConfig_R(dev->csr);
	uint32_t cfg = *((uint32_t *)&r);

	if (state == FORWARD_IO_TXRX)
		return -ENOTSUPP;

	if (state == FORWARD_IO_TX)
		cfg |= (1 << io);
	else
		cfg &= ~(1 << io);

	r = *((FD2110_VideoConfig *)&cfg);
	FD2110_VideoConfig_W(dev->csr, r);

	if (cfg & (1 << io)) {
		FD2110_VideoOutCS cs = FD2110_VideoOutCS_R(dev->csr, io);
		cs.reset = state == FORWARD_IO_DISABLED ? 1 : 0;
		FD2110_VideoOutCS_W(dev->csr, io, cs);
	}

	//forward_vdma_init_region(dev->vdma, io * 0x08000000UL, 0x08000000UL,
	//             (cfg & (1 << io)) ? false : true);

	return 0;
}

static void fd2110_toggle_streaming(struct forward_dev *dev, int number, bool enabled)
{
	//FD2110_VideoInCS cs = FD2110_VideoInCS_R(dev->csr, number);
	//cs.capture = enabled ? 1 : 0;
	//FD2110_VideoInCS_W(dev->csr, number, cs);
}

static ssize_t fd2110_attr_show_adc(struct device *dev, struct device_attribute *attr, char *buf);
static ssize_t fd2110_attr_show_version(struct device *dev, struct device_attribute *attr,
					char *buf);
static ssize_t fd2110_attr_show_hw_version(struct device *dev, struct device_attribute *attr,
					   char *buf);

static DEVICE_ATTR(temperature, S_IRUGO, fd2110_attr_show_adc, NULL);
static DEVICE_ATTR(vcore, S_IRUGO, fd2110_attr_show_adc, NULL);
static DEVICE_ATTR(vaux, S_IRUGO, fd2110_attr_show_adc, NULL);
static DEVICE_ATTR(version, S_IRUGO, fd2110_attr_show_version, NULL);
static DEVICE_ATTR(hw_version, S_IRUGO, fd2110_attr_show_hw_version, NULL);

static ssize_t fd2110_attr_show_adc(struct device *dev, struct device_attribute *attr, char *buf)
{
	struct forward_dev *fdev = dev_get_drvdata(dev);
	int dec = 0, frac = 0;

	if (attr == &dev_attr_temperature) {
		FD2110_TemperatureMonitor temp = FD2110_TemperatureMonitor_R(fdev->csr);
		int value = temp.temperature * 503975 / 1024 - 273150;
		dec = value / 1000;
		frac = abs(value % 1000);
	} else if (attr == &dev_attr_vcore) {
		FD2110_VCoreMonitor v = FD2110_VCoreMonitor_R(fdev->csr);
		int value = v.voltage * 3 * 1000 / 1024;
		dec = value / 1000;
		frac = value % 1000;
	} else if (attr == &dev_attr_vaux) {
		FD2110_VAuxMonitor v = FD2110_VAuxMonitor_R(fdev->csr);
		int value = v.voltage * 3 * 1000 / 1024;
		dec = value / 1000;
		frac = value % 1000;
	}
	return scnprintf(buf, PAGE_SIZE, "%d.%03d\n", dec, frac);
}

static ssize_t fd2110_attr_show_version(struct device *dev, struct device_attribute *attr,
					char *buf)
{
	struct forward_dev *fdev = dev_get_drvdata(dev);
	FD2110_StaticVersion version = FD2110_StaticVersion_R(fdev->csr);

	return scnprintf(buf, PAGE_SIZE, "%dr%d\n", version.major, version.revision);
}

static ssize_t fd2110_attr_show_hw_version(struct device *dev, struct device_attribute *attr,
					   char *buf)
{
	struct forward_dev *fdev = dev_get_drvdata(dev);
	FD2110_StaticVersion version = FD2110_StaticVersion_R(fdev->csr);

	return scnprintf(buf, PAGE_SIZE, "%d.%d\n", version.hwRevision, version.hwSubRevision);
}

static struct attribute *fd2110_dev_attrs[] = {
	&dev_attr_temperature.attr, &dev_attr_vcore.attr,      &dev_attr_vaux.attr,
	&dev_attr_version.attr,	    &dev_attr_hw_version.attr, NULL,
};

static struct attribute_group fd2110_attr_group = {
	.attrs = fd2110_dev_attrs,
};

struct forward_dev_config fd2110_cfg = {
	.vdma_size_mb = 1024,
	.io_count = 4,

	.attributes = &fd2110_attr_group,

	.private = NULL,

	.init = fd2110_init,
	.fini = fd2110_fini,

	.read_id = fd2110_read_id,

	.enable_interrupts = fd2110_enable_interrupts,
	.disable_interrupts = fd2110_disable_interrupts,
	.read_interrupts = fd2110_read_interrupts,

	.enable_vdma = fd2110_enable_vdma,
	.disable_vdma = fd2110_disable_vdma,

	.io_type = fd2110_io_type,
	.io_number = fd2110_io_number,
	.io_state = fd2110_io_state,
	.set_io_state = fd2110_set_io_state,

	.toggle_streaming = fd2110_toggle_streaming,
};
