/*
   forward-fd788.c - driver for SoftLab-NSK FD788 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-mcap.h"

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

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

#define FD788_ALL_INTERRUPTS                                                             \
	((1 << 0) | (1 << 3) | (1 << 6) | (1 << 9) | (1 << 12) | (1 << 15) | (1 << 18) | \
	 (1 << 21) | (1 << 24) | (1 << 26) | (1 << 28) | (1 << 30))

#define FD788_ALL_INTERRUPTS_DATA_MASK                                                    \
	((3 << 1) | (3 << 4) | (3 << 7) | (3 << 10) | (3 << 13) | (3 << 16) | (3 << 19) | \
	 (3 << 22) | (1 << 25) | (1 << 27) | (1 << 29) | (1 << 31))

#define FD788_NUM_IO (8)

#define FD788_PLL_ADDRESS 0x68
#define FD788_I2C_TIMEOUT_MS 100
#define FD788_I2C_CYCLE_US 1000

#define FD788_IS_V3(dev) (dev->pci_dev->device == 0x0026)

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

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

	FD788_I2C_W(dev->csr, i2c);

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

	do {
		usleep_range(FD788_I2C_CYCLE_US, FD788_I2C_CYCLE_US * 2);
		i2c = FD788_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 fd788_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) / 32 + 1;
	int byte_count = (bits - 1) / 8 + 1;
	int last_word_bits = bits % 32;

	FD788_SPIControl ctl;
	FD788_SPIData data;
	int i;

	ctl.bus = bus + 1;
	ctl.rxBits = rx_bits;
	ctl.txBits = tx_bits;
	FD788_SPIControl_W(dev->csr, ctl);

	for (i = 0; i < word_count; i++) {
		int j;

		if ((i * 8) < tx_bits) {
			u32 value = 0;
			if (tx) {
				for (j = 0; j < 4; j++) {
					if (i * 4 + j < byte_count)
						value |= tx[i * 4 + j] << ((3 - j) * 8);
				}
			} else
				value = 0xFFFFFFFF;
			data.data = value;
			FD788_SPIData_W(dev->csr, data);
		}

		if ((i * 8) < rx_bits) {
			data = FD788_SPIData_R(dev->csr);
			if (i == (word_count - 1))
				data.data <<= (32 - last_word_bits) % 32;

			if (rx) {
				for (j = 0; j < 4; j++) {
					if (i * 4 + j < byte_count)
						rx[i * 4 + j] = (data.data >> ((3 - j) * 8)) & 0xFF;
				}
			}
		}
	}

	ctl.bus = 0;
	ctl.rxBits = 0;
	ctl.txBits = 0;
	FD788_SPIControl_W(dev->csr, ctl);

	return true;
}

static int fd788_init(struct forward_dev *dev)
{
	FD788_DynamicVersion v = FD788_DynamicVersion_R(dev->csr);

	if (v.hardware >= 2) {
		dev->spi_xfer = &fd788_spi_xfer;
		forward_si534x_init(dev, &dev->pll, 1, 3);
	} else {
		dev->i2c_xfer = &fd788_i2c_xfer;
		forward_si532x_init(dev, &dev->pll, 0, FD788_PLL_ADDRESS, 2);
	}

	return 0;
}

static void fd788_fini(struct forward_dev *dev)
{
	if (((dev->fw_version >> 24) & 0xF) >= 2)
		forward_si534x_fini(dev, &dev->pll);
	else
		forward_si532x_fini(dev, &dev->pll);
}

static void fd788_reboot(struct forward_dev *dev)
{
	if (FD788_IS_V3(dev)) {
		int result = forward_mcap_reboot(dev, 0x01000000, false);
		if (result)
			dev_warn(dev->parent_dev, "MCAP reboot failed: %d\n", result);
	} else {
		FD788_GlobalCSR g = FD788_GlobalCSR_R(dev->csr);
		pci_clear_master(dev->pci_dev);
		pci_save_state(dev->pci_dev);

		g.reboot = 1;
		FD788_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 fd788_read_id(struct forward_dev *dev)
{
	int cap_addr = pci_find_ext_capability(dev->pci_dev, PCI_EXT_CAP_ID_DSN);

	FD788_SoftID id = FD788_SoftID_R(dev->csr);
	FD788_DynamicVersion v = FD788_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.hardware << 24) | (v.minor << 16) | v.revision;
}

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

static void fd788_disable_interrupts(struct forward_dev *dev, const forward_irq_t *interrupts)
{
	u32 irq = ioread32(&dev->csr[FD788_IRQEnable_A]);
	irq &= ~(interrupts->irq[0] & FD788_ALL_INTERRUPTS);
	iowrite32(irq, &dev->csr[FD788_IRQEnable_A]);
	iowrite32(interrupts->irq[0] & FD788_ALL_INTERRUPTS, &dev->csr[FD788_IRQFlags_A]);
}

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

	interrupts->irq[0] = irq & FD788_ALL_INTERRUPTS;
	data->irq[0] = irq & FD788_ALL_INTERRUPTS_DATA_MASK;
	data_mask->irq[0] = FD788_ALL_INTERRUPTS_DATA_MASK;

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

static void fd788_enable_vdma(struct forward_dev *dev, u32 map_addr)
{
	FD788_VDMADescriptor desc = { .address = map_addr };
	FD788_VDMA vdma = { .enable = 1 };
	FD788_VDMADescriptor_W(dev->csr, desc);
	FD788_VDMA_W(dev->csr, vdma);
}

static void fd788_disable_vdma(struct forward_dev *dev)
{
	FD788_VDMA vdma = { .enable = 0 };
	FD788_VDMA_W(dev->csr, vdma);
}

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

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

static enum forward_io_state fd788_io_state(struct forward_dev *dev, int io)
{
	FD788_VideoConfig r = FD788_VideoConfig_R(dev->csr);
	uint32_t cfg = *((uint32_t *)&r);

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

static int fd788_set_io_state(struct forward_dev *dev, int io, enum forward_io_state state)
{
	FD788_VideoConfig r = FD788_VideoConfig_R(dev->csr);
	uint32_t cfg = *((uint32_t *)&r);

	if (state == FORWARD_IO_TXRX)
		return -EOPNOTSUPP;

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

	r = *((FD788_VideoConfig *)&cfg);
	FD788_VideoConfig_W(dev->csr, r);

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

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

	return 0;
}

static void fd788_toggle_streaming(struct forward_dev *dev, int io, bool enabled)
{
	FD788_VideoConfig r = FD788_VideoConfig_R(dev->csr);
	uint32_t cfg = *((uint32_t *)&r);

	if (cfg & (1 << io)) {
		FD788_VideoInCS cs = FD788_VideoInCS_R(dev->csr, io);
		cs.capture = enabled ? 1 : 0;
		FD788_VideoInCS_W(dev->csr, io, cs);
	} else {
		FD788_VideoOutCS cs = FD788_VideoOutCS_R(dev->csr, io);
		cs.playback = enabled ? 1 : 0;
		FD788_VideoOutCS_W(dev->csr, io, cs);
	}
}

static ssize_t fd788_attr_show_adc(struct device *dev, struct device_attribute *attr, char *buf);
static ssize_t fd788_attr_show_version(struct device *dev, struct device_attribute *attr,
				       char *buf);
static ssize_t fd788_attr_show_dma(struct device *dev, struct device_attribute *attr, char *buf);

static DEVICE_ATTR(temperature, S_IRUGO, fd788_attr_show_adc, NULL);
static DEVICE_ATTR(vcore, S_IRUGO, fd788_attr_show_adc, NULL);
static DEVICE_ATTR(vaux, S_IRUGO, fd788_attr_show_adc, NULL);
static DEVICE_ATTR(version, S_IRUGO, fd788_attr_show_version, NULL);
static DEVICE_ATTR(dma_lat_max, S_IRUGO, fd788_attr_show_dma, NULL);
static DEVICE_ATTR(dma_lat_avg, S_IRUGO, fd788_attr_show_dma, NULL);
static DEVICE_ATTR(dma_perf_read, S_IRUGO, fd788_attr_show_dma, NULL);
static DEVICE_ATTR(dma_perf_write, S_IRUGO, fd788_attr_show_dma, NULL);

static ssize_t fd788_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) {
		FD788_TemperatureMonitor temp = FD788_TemperatureMonitor_R(fdev->csr);
		int value;
		if (FD788_IS_V3(fdev))
			value = temp.temperature * 509314 / 1024 - 280231;
		else
			value = temp.temperature * 503975 / 1024 - 273150;
		dec = value / 1000;
		frac = abs(value % 1000);
	} else if (attr == &dev_attr_vcore) {
		FD788_VCoreMonitor v = FD788_VCoreMonitor_R(fdev->csr);
		int value = v.voltage * 3 * 1000 / 1024;
		dec = value / 1000;
		frac = value % 1000;
	} else if (attr == &dev_attr_vaux) {
		FD788_VAuxMonitor v = FD788_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 fd788_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) {
		FD788_PCIEPerfomance perf = FD788_PCIEPerfomance_R(fdev->csr);
		if (FD788_IS_V3(fdev))
			value = (long)perf.rxCounter * 16 * 250000000 / 65535;
		else
			value = (long)perf.rxCounter * 16 * 125000000 / 65535;
	} else if (attr == &dev_attr_dma_perf_write) {
		FD788_PCIEPerfomance perf = FD788_PCIEPerfomance_R(fdev->csr);
		if (FD788_IS_V3(fdev))
			value = (long)perf.txCounter * 16 * 250000000 / 65535;
		else
			value = (long)perf.txCounter * 16 * 250000000 / 65535;
	} else if (attr == &dev_attr_dma_lat_max) {
		FD788_PCIERxMaxLatency lat = FD788_PCIERxMaxLatency_R(fdev->csr);
		value = (long)lat.maxLatency * 16;
	} else if (attr == &dev_attr_dma_lat_avg) {
		FD788_PCIERxMaxLatency lat = FD788_PCIERxMaxLatency_R(fdev->csr);
		FD788_PCIERxTotalLatency tlat = FD788_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 fd788_attr_show_version(struct device *dev, struct device_attribute *attr, char *buf)
{
	struct forward_dev *fdev = dev_get_drvdata(dev);
	FD788_StaticVersion version = FD788_StaticVersion_R(fdev->csr);
	int hw_major = 0, hw_minor = 0;

	if (version.major == 0)
		hw_major = 1;
	else if (version.major == 2)
		hw_major = 2;
	else if (version.major == 3)
		hw_major = 3;

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

static struct attribute *fd788_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 fd788_attr_group = {
	.attrs = fd788_dev_attrs,
};

struct forward_dev_config fd788_cfg = {
	.vdma_size_mb = 1024,
	.sdma_size = 0,
	.sdma_rings = 0,
	.io_count = 8,

	.attributes = &fd788_attr_group,

	.private = NULL,

	.init = fd788_init,
	.fini = fd788_fini,
	.reboot = fd788_reboot,

	.read_id = fd788_read_id,

	.enable_interrupts = fd788_enable_interrupts,
	.disable_interrupts = fd788_disable_interrupts,
	.read_interrupts = fd788_read_interrupts,

	.enable_vdma = fd788_enable_vdma,
	.disable_vdma = fd788_disable_vdma,

	.io_type = fd788_io_type,
	.io_number = fd788_io_number,
	.io_state = fd788_io_state,
	.set_io_state = fd788_set_io_state,

	.toggle_streaming = fd788_toggle_streaming,
};
