/*
   forward-fd722.c - driver for SoftLab-NSK FD722 video board

   Copyright (C) 2017 - 2024 SoftLab-NSK <forward@softlab.tv>

   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 "forward-irq.h"

#include "forward-si532x.h"
#include "forward-si534x.h"
#include "fd722_reg.h"

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

#define FD722_ALL_INTERRUPTS ((1 << 4) | (1 << 6) | (1 << 8) | (1 << 10) | (1 << 16) | (1 << 20))

#define FD722_ALL_INTERRUPTS_DATA_MASK \
	((1 << 5) | (1 << 7) | (1 << 9) | (1 << 11) | (7 << 17) | (7 << 21))

#define FD722_I2C_TIMEOUT_MS 100
#define FD722_I2C_CYCLE_US 1000
#define FD722_PLL_ADDRESS 0x68

#define FD722_IS_M2(dev) (dev->pci_dev->device == 0x0023)

static bool fd722_i2c_xfer(struct forward_dev *dev, u8 bus, bool read, uint8_t addr,
			   uint8_t subaddr, uint8_t *data)
{
	FD722_I2C i2c;
	unsigned long timeout;

	i2c.cs = 1;
	i2c.rw = read ? 1 : 0;
	i2c.address = addr;
	i2c.subAddress = subaddr;
	i2c.data = *data;

	FD722_I2C_W(dev->csr, i2c);

	timeout = jiffies + (HZ * FD722_I2C_TIMEOUT_MS) / 1000;

	do {
		usleep_range(FD722_I2C_CYCLE_US, FD722_I2C_CYCLE_US * 2);
		i2c = FD722_I2C_R(dev->csr);
	} while (time_before(jiffies, timeout) && i2c.cs);

	if (read && !i2c.cs && !i2c.nack)
		*data = i2c.data;

	return !i2c.cs && !i2c.nack;
}

static bool fd722_spi_xfer(struct forward_dev *dev, u8 bus, u8 *tx, int tx_bits, u8 *rx,
			   int rx_bits)
{
	int bits = max(tx_bits, rx_bits);
	int word_count = (bits - 1) / 16 + 1;
	int last_word_bits = ((bits - 1) % 16) + 1;
	bool last8 = last_word_bits <= 8;
	int i;
	FD722_SPI ctl;

	ctl.bus = bus;
	ctl.start = 0;
	ctl.cs = 1;
	FD722_SPI_W(dev->csr, ctl);

	for (i = 0; i < word_count; i++) {
		if (tx && ((i * 16) < tx_bits)) {
			uint8_t cur_tx_bits = ((tx_bits - i * 16 - 1) % 16) + 1;
			if (cur_tx_bits <= 8) {
				ctl.data = tx[i * 2] << (16 - cur_tx_bits);
			} else {
				ctl.data = (tx[i * 2] << 8) | (tx[i * 2 + 1] << (16 - cur_tx_bits));
			}
		} else {
			ctl.data = 0xFFFF;
		}

		ctl.bus = bus;
		ctl.cs = 1;
		ctl.start = 1;
		ctl.bit8 = ((i == word_count - 1) && last8) ? 0 : 1;
		FD722_SPI_W(dev->csr, ctl);

		do {
			ctl = FD722_SPI_R(dev->csr);
		} while (ctl.start);

		if (rx && ((i * 16) < rx_bits)) {
			uint8_t cur_rx_bits = ((rx_bits - i * 16 - 1) % 16) + 1;
			uint16_t data = ctl.data;
			if (cur_rx_bits <= 8) {
				rx[i * 2] = data & 0xFF;
			} else {
				rx[i * 2] = (data >> 8) & 0xFF;
				rx[i * 2 + 1] = data & 0xFF;
			}
		}
	}

	ctl.bus = bus;
	ctl.start = 0;
	ctl.cs = 0;

	FD722_SPI_W(dev->csr, ctl);

	return true;
}

static int fd722_init(struct forward_dev *dev)
{
	FD722_DynamicVersion v = FD722_DynamicVersion_R(dev->csr);

	if ((v.minor >= 6) || FD722_IS_M2(dev)) {
		dev->spi_xfer = &fd722_spi_xfer;
		forward_si534x_init(dev, &dev->pll, 1, 3);
	} else {
		dev->i2c_xfer = &fd722_i2c_xfer;
		forward_si532x_init(dev, &dev->pll, 0, FD722_PLL_ADDRESS, 2);
	}

	return 0;
}

static void fd722_fini(struct forward_dev *dev)
{
	if ((((dev->fw_version >> 16) & 0xF) >= 6) || FD722_IS_M2(dev))
		forward_si534x_fini(dev, &dev->pll);
	else
		forward_si532x_fini(dev, &dev->pll);
}

static void fd722_reboot(struct forward_dev *dev)
{
	FD722_GlobalCSR g = FD722_GlobalCSR_R(dev->csr);
	pci_save_state(dev->pci_dev);

	g.reboot = 1;
	FD722_GlobalCSR_W(dev->csr, g);
	// Board dies in ~2-5s

	msleep(5000);

	pci_restore_state(dev->pci_dev);
	pci_set_master(dev->pci_dev);
}

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

	FD722_SoftID id = FD722_SoftID_R(dev->csr);
	FD722_DynamicVersion v = FD722_DynamicVersion_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 = (v.major << 24) | (v.minor << 16) | v.revision;
}

static void fd722_enable_interrupts(struct forward_dev *dev, const forward_irq_t *interrupts)
{
	u32 irq = ioread32(&dev->csr[FD722_IRQEnable_A]);
	irq |= interrupts->irq[0] & FD722_ALL_INTERRUPTS;
	iowrite32(irq, &dev->csr[FD722_IRQEnable_A]);
}

static void fd722_disable_interrupts(struct forward_dev *dev, const forward_irq_t *interrupts)
{
	u32 irq = ioread32(&dev->csr[FD722_IRQEnable_A]);
	irq &= ~(interrupts->irq[0] & FD722_ALL_INTERRUPTS);
	iowrite32(irq, &dev->csr[FD722_IRQEnable_A]);
	iowrite32(interrupts->irq[0] & FD722_ALL_INTERRUPTS, &dev->csr[FD722_IRQFlags_A]);
}

static bool fd722_read_interrupts(struct forward_dev *dev, forward_irq_t *interrupts,
				  forward_irq_t *data, forward_irq_t *data_mask)
{
	u32 irq = ioread32(&dev->csr[FD722_IRQFlags_A]);
	iowrite32((irq & FD722_ALL_INTERRUPTS), &dev->csr[FD722_IRQFlags_A]);

	interrupts->irq[0] = irq & FD722_ALL_INTERRUPTS;
	data->irq[0] = irq & FD722_ALL_INTERRUPTS_DATA_MASK;
	data_mask->irq[0] = FD722_ALL_INTERRUPTS_DATA_MASK;

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

static void fd722_enable_vdma(struct forward_dev *dev, u32 map_addr)
{
	FD722_VDMADescriptor desc = { .address = map_addr };
	FD722_VDMA vdma = { .enable = 1 };
	FD722_VDMADescriptor_W(dev->csr, desc);
	FD722_VDMA_W(dev->csr, vdma);
}

static void fd722_disable_vdma(struct forward_dev *dev)
{
	FD722_VDMA vdma = { .enable = 0 };
	FD722_VDMA_W(dev->csr, vdma);
}

static enum forward_io_type fd722_io_type(struct forward_dev *dev, int io)
{
	return (io >= 2) ? FORWARD_IO_OUTPUT : FORWARD_IO_INPUT;
}

static enum forward_io_type fd722m2_io_type(struct forward_dev *dev, int io)
{
	switch (io) {
	case 0:
		return FORWARD_IO_INPUT;
	case 1:
		return FORWARD_IO_BIDIR;
	case 2:
		return FORWARD_IO_OUTPUT;
	default:
		return FORWARD_IO_INPUT;
	}
}

static enum forward_io_state fd722_io_state(struct forward_dev *dev, int io)
{
	if (io < 2)
		return FORWARD_IO_RX;
	else {
		FD722_VideoOutCS cs = FD722_VideoOutCS_R(dev->csr, (io % 2));
		return cs.reset ? FORWARD_IO_DISABLED : FORWARD_IO_TX;
	}
}

static enum forward_io_state fd722m2_io_state(struct forward_dev *dev, int io)
{
	if (io == 0)
		return FORWARD_IO_RX;
	else if (io == 2) {
		FD722_VideoOutCS cs = FD722_VideoOutCS_R(dev->csr, 1);
		return cs.reset ? FORWARD_IO_DISABLED : FORWARD_IO_TX;
	} else {
		FD722_M2VideoIODir dir = FD722_M2VideoIODir_R(dev->csr);
		FD722_VideoOutCS cs = FD722_VideoOutCS_R(dev->csr, 0);
		return dir.dir ? (cs.reset ? FORWARD_IO_DISABLED : FORWARD_IO_TX) : FORWARD_IO_RX;
	}
}

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

static int fd722m2_io_number(struct forward_dev *dev, int io)
{
	switch (io) {
	case 0:
		return 1;
	case 1:
		return (fd722m2_io_state(dev, io) == FORWARD_IO_RX) ? 2 : 1;
	case 2:
		return 2;
	default:
		return 0;
	}
}

static int fd722_set_io_state(struct forward_dev *dev, int io, enum forward_io_state state)
{
	if (io < 2) {
		if ((state != FORWARD_IO_DISABLED) && (state != FORWARD_IO_RX))
			return -EOPNOTSUPP;

		forward_vdma_init_region(dev->vdma, io * 0x02000000UL + 0x00000000, 0x02000000UL,
					 true);
		forward_vdma_init_region(dev->vdma, io * 0x00800000UL + 0x04000000, 0x00800000UL,
					 true);
	} else {
		FD722_VideoOutCS cs;

		if ((state != FORWARD_IO_DISABLED) && (state != FORWARD_IO_TX))
			return -EOPNOTSUPP;

		cs = FD722_VideoOutCS_R(dev->csr, (io % 2));
		cs.reset = state == FORWARD_IO_DISABLED ? 1 : 0;
		FD722_VideoOutCS_W(dev->csr, (io % 2), cs);

		forward_vdma_init_region(dev->vdma, (io % 2) * 0x01000000UL + 0x05000000,
					 0x01000000UL, false);
		forward_vdma_init_region(dev->vdma, (io % 2) * 0x00100000UL + 0x07000000,
					 0x00100000UL, false);
	}

	return 0;
}

static int fd722m2_set_io_state(struct forward_dev *dev, int io, enum forward_io_state state)
{
	if (io == 0) {
		if ((state != FORWARD_IO_DISABLED) && (state != FORWARD_IO_RX))
			return -EOPNOTSUPP;

		forward_vdma_init_region(dev->vdma, io * 0x02000000UL + 0x00000000, 0x02000000UL,
					 true);
		forward_vdma_init_region(dev->vdma, io * 0x00800000UL + 0x04000000, 0x00800000UL,
					 true);
	} else if (io == 1) {
		FD722_M2VideoIODir dir;
		FD722_VideoOutCS cs;
		bool output;

		if ((state != FORWARD_IO_DISABLED) && (state != FORWARD_IO_RX) &&
		    (state != FORWARD_IO_TX))
			return -EOPNOTSUPP;

		output = (state == FORWARD_IO_TX) || (state == FORWARD_IO_DISABLED);

		dir = FD722_M2VideoIODir_R(dev->csr);
		dir.dir = output ? 1 : 0;
		FD722_M2VideoIODir_W(dev->csr, dir);

		if (output) {
			cs = FD722_VideoOutCS_R(dev->csr, 0);
			cs.reset = state == FORWARD_IO_DISABLED ? 1 : 0;
			FD722_VideoOutCS_W(dev->csr, 0, cs);
		}

		if (output) {
			forward_vdma_init_region(dev->vdma, 0 * 0x01000000UL + 0x05000000,
						 0x01000000UL, false);
			forward_vdma_init_region(dev->vdma, 0 * 0x00100000UL + 0x07000000,
						 0x00100000UL, false);
		} else {
			forward_vdma_init_region(dev->vdma, 1 * 0x02000000UL + 0x00000000,
						 0x02000000UL, true);
			forward_vdma_init_region(dev->vdma, 1 * 0x00800000UL + 0x04000000,
						 0x00800000UL, true);
		}
	} else {
		FD722_VideoOutCS cs;

		if ((state != FORWARD_IO_DISABLED) && (state != FORWARD_IO_TX))
			return -EOPNOTSUPP;

		cs = FD722_VideoOutCS_R(dev->csr, 1);
		cs.reset = state == FORWARD_IO_DISABLED ? 1 : 0;
		FD722_VideoOutCS_W(dev->csr, 1, cs);

		forward_vdma_init_region(dev->vdma, 1 * 0x01000000UL + 0x05000000, 0x01000000UL,
					 false);
		forward_vdma_init_region(dev->vdma, 1 * 0x00100000UL + 0x07000000, 0x00100000UL,
					 false);
	}

	return 0;
}

static void fd722_toggle_streaming(struct forward_dev *dev, int io, bool enabled)
{
	if (io < 2) {
		FD722_VideoInCS cs = FD722_VideoInCS_R(dev->csr, io);
		cs.capture = enabled ? 1 : 0;
		FD722_VideoInCS_W(dev->csr, io, cs);
	} else {
		FD722_VideoOutCS cs = FD722_VideoOutCS_R(dev->csr, (io % 2));
		cs.playback = enabled ? 1 : 0;
		FD722_VideoOutCS_W(dev->csr, (io % 2), cs);
	}
}

static void fd722m2_toggle_streaming(struct forward_dev *dev, int io, bool enabled)
{
	if (io == 0) {
		FD722_VideoInCS cs = FD722_VideoInCS_R(dev->csr, 0);
		cs.capture = enabled ? 1 : 0;
		FD722_VideoInCS_W(dev->csr, 0, cs);
	} else if (io == 1) {
		FD722_M2VideoIODir dir = FD722_M2VideoIODir_R(dev->csr);
		if (dir.dir) {
			FD722_VideoOutCS cs = FD722_VideoOutCS_R(dev->csr, 0);
			cs.playback = enabled ? 1 : 0;
			FD722_VideoOutCS_W(dev->csr, 0, cs);
		} else {
			FD722_VideoInCS cs = FD722_VideoInCS_R(dev->csr, 1);
			cs.capture = enabled ? 1 : 0;
			FD722_VideoInCS_W(dev->csr, 1, cs);
		}
	} else {
		FD722_VideoOutCS cs = FD722_VideoOutCS_R(dev->csr, 1);
		cs.playback = enabled ? 1 : 0;
		FD722_VideoOutCS_W(dev->csr, 1, cs);
	}
}

static ssize_t fd722_attr_show_adc(struct device *dev, struct device_attribute *attr, char *buf);
static ssize_t fd722_attr_show_version(struct device *dev, struct device_attribute *attr,
				       char *buf);
static ssize_t fd722_attr_show_dma(struct device *dev, struct device_attribute *attr, char *buf);

static DEVICE_ATTR(temperature, S_IRUGO, fd722_attr_show_adc, NULL);
static DEVICE_ATTR(vcore, S_IRUGO, fd722_attr_show_adc, NULL);
static DEVICE_ATTR(vaux, S_IRUGO, fd722_attr_show_adc, NULL);
static DEVICE_ATTR(version, S_IRUGO, fd722_attr_show_version, NULL);
static DEVICE_ATTR(dma_lat_max, S_IRUGO, fd722_attr_show_dma, NULL);
static DEVICE_ATTR(dma_lat_avg, S_IRUGO, fd722_attr_show_dma, NULL);
static DEVICE_ATTR(dma_perf_read, S_IRUGO, fd722_attr_show_dma, NULL);
static DEVICE_ATTR(dma_perf_write, S_IRUGO, fd722_attr_show_dma, NULL);

static ssize_t fd722_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) {
		FD722_TemperatureMonitor temp = FD722_TemperatureMonitor_R(fdev->csr);
		int value = temp.temperature * 503975 / 1024 - 273150;
		dec = value / 1000;
		frac = abs(value % 1000);
	} else if (attr == &dev_attr_vcore) {
		FD722_VCoreMonitor v = FD722_VCoreMonitor_R(fdev->csr);
		int value = v.voltage * 3 * 1000 / 1024;
		dec = value / 1000;
		frac = value % 1000;
	} else if (attr == &dev_attr_vaux) {
		FD722_VAuxMonitor v = FD722_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 fd722_attr_show_dma(struct device *dev, struct device_attribute *attr, char *buf)
{
	struct forward_dev *fdev = dev_get_drvdata(dev);
	long value = 0;

	if (attr == &dev_attr_dma_perf_read) {
		FD722_PCIEPerfomance perf = FD722_PCIEPerfomance_R(fdev->csr);
		value = (long)perf.rxCounter * 16 * 125000000 / 65535;
	} else if (attr == &dev_attr_dma_perf_write) {
		FD722_PCIEPerfomance perf = FD722_PCIEPerfomance_R(fdev->csr);
		value = (long)perf.txCounter * 16 * 125000000 / 65535;
	} else if (attr == &dev_attr_dma_lat_max) {
		FD722_PCIERxMaxLatency lat = FD722_PCIERxMaxLatency_R(fdev->csr);
		value = (long)lat.maxLatency * 16;
	} else if (attr == &dev_attr_dma_lat_avg) {
		FD722_PCIERxMaxLatency lat = FD722_PCIERxMaxLatency_R(fdev->csr);
		FD722_PCIERxTotalLatency tlat = FD722_PCIERxTotalLatency_R(fdev->csr);
		if (lat.numRequests)
			value = (long)tlat.latency * 16 / ((long)lat.numRequests * 4);
	}
	return scnprintf(buf, PAGE_SIZE, "%ld\n", value);
}

static ssize_t fd722_attr_show_version(struct device *dev, struct device_attribute *attr, char *buf)
{
	struct forward_dev *fdev = dev_get_drvdata(dev);
	FD722_StaticVersion version = FD722_StaticVersion_R(fdev->csr);
	int hw_major = 0, hw_minor = 0;

	if (version.minor == 3)
		hw_major = 3;
	else if (version.minor == 4)
		hw_major = 4;
	else if (version.minor == 5) {
		hw_major = 4;
		hw_minor = 1;
	} else if (version.minor == 6)
		hw_major = 5;
	else if (version.minor == 7)
		hw_major = 5;

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

static struct attribute *fd722_dev_attrs[] = {
	&dev_attr_temperature.attr, &dev_attr_vcore.attr,	  &dev_attr_vaux.attr,
	&dev_attr_version.attr,	    &dev_attr_dma_perf_read.attr, &dev_attr_dma_perf_write.attr,
	&dev_attr_dma_lat_max.attr, &dev_attr_dma_lat_avg.attr,	  NULL,
};

static struct attribute_group fd722_attr_group = {
	.attrs = fd722_dev_attrs,
};

struct forward_dev_config fd722_cfg = {
	.vdma_size_mb = 128,
	.sdma_size = 0,
	.sdma_rings = 0,
	.io_count = 4,

	.attributes = &fd722_attr_group,

	.private = NULL,

	.init = fd722_init,
	.fini = fd722_fini,
	.reboot = fd722_reboot,

	.read_id = fd722_read_id,

	.enable_interrupts = fd722_enable_interrupts,
	.disable_interrupts = fd722_disable_interrupts,
	.read_interrupts = fd722_read_interrupts,

	.enable_vdma = fd722_enable_vdma,
	.disable_vdma = fd722_disable_vdma,

	.io_type = fd722_io_type,
	.io_number = fd722_io_number,
	.io_state = fd722_io_state,
	.set_io_state = fd722_set_io_state,

	.toggle_streaming = fd722_toggle_streaming,
};

struct forward_dev_config fd722m2_cfg = {
	.vdma_size_mb = 128,
	.sdma_size = 0,
	.sdma_rings = 0,
	.io_count = 3,

	.attributes = &fd722_attr_group,

	.private = NULL,

	.init = fd722_init,
	.fini = fd722_fini,
	.reboot = fd722_reboot,

	.read_id = fd722_read_id,

	.enable_interrupts = fd722_enable_interrupts,
	.disable_interrupts = fd722_disable_interrupts,
	.read_interrupts = fd722_read_interrupts,

	.enable_vdma = fd722_enable_vdma,
	.disable_vdma = fd722_disable_vdma,

	.io_type = fd722m2_io_type,
	.io_number = fd722m2_io_number,
	.io_state = fd722m2_io_state,
	.set_io_state = fd722m2_set_io_state,

	.toggle_streaming = fd722m2_toggle_streaming,
};
