/*
   forward-fd922.c - driver for SoftLab-NSK FD922 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-mcap.h"
#include "forward-si534x.h"

#include "fd922_reg.h"

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

#define FD922_ALL_INTERRUPTS ((1 << 0) | (1 << 3) | (1 << 6) | (1 << 9) | (1 << 12) | (1 << 14))

#define FD922_ALL_INTERRUPTS_DATA_MASK \
	((3 << 1) | (3 << 4) | (1 << 7) | (1 << 10) | (1 << 13) | (1 << 15))

#define FD922_IS_V4(dev) (dev->pci_dev->device == 0x0025)

static bool fd922_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;

	FD922_SPIControl ctl;
	FD922_SPIData data;
	int i;

	ctl.cs = 1;
	ctl.bus = bus;
	ctl.bits = bits;
	ctl.read = 1;
	ctl.write = 1;
	FD922_SPIControl_W(dev->csr, ctl);

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

		data.data = 0;
		if (tx) {
			for (j = 0; j < 4; j++) {
				if (i * 4 + j < byte_count)
					data.data |= tx[i * 4 + j] << ((3 - j) * 8);
			}
		} else
			data.data = 0xFFFFFFFF;

		FD922_SPIData_W(dev->csr, data);
		data = FD922_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;
			}
		}
	}
	return true;
}

static int fd922_init(struct forward_dev *dev)
{
	dev->spi_xfer = &fd922_spi_xfer;

	forward_si534x_init(dev, &dev->pll, 4, 3);

	return 0;
}
static void fd922_fini(struct forward_dev *dev)
{
	forward_si534x_fini(dev, &dev->pll);
}

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

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

	FD922_SoftID id = FD922_SoftID_R(dev->csr);
	FD922_StaticVersion v = FD922_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 = (v.hardware << 24) | (v.major << 16) | v.revision;
}

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

static void fd922_disable_interrupts(struct forward_dev *dev, const forward_irq_t *interrupts)
{
	u32 irq = ioread32(&dev->csr[FD922_IRQEnable_A]);
	irq &= ~(interrupts->irq[0] & FD922_ALL_INTERRUPTS);
	iowrite32(irq, &dev->csr[FD922_IRQEnable_A]);
	iowrite32(interrupts->irq[0] & FD922_ALL_INTERRUPTS, &dev->csr[FD922_IRQFlags_A]);
}

static bool fd922_read_interrupts(struct forward_dev *dev, forward_irq_t *interrupts,
				  forward_irq_t *data, forward_irq_t *data_mask)
{
	u32 irq = ioread32(&dev->csr[FD922_IRQFlags_A]);
	iowrite32((irq & FD922_ALL_INTERRUPTS), &dev->csr[FD922_IRQFlags_A]);
	interrupts->irq[0] = irq & FD922_ALL_INTERRUPTS;
	data->irq[0] = irq & FD922_ALL_INTERRUPTS_DATA_MASK;
	data_mask->irq[0] = FD922_ALL_INTERRUPTS_DATA_MASK;

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

static void fd922_enable_vdma(struct forward_dev *dev, u32 map_addr)
{
	FD922_VDMADescriptor desc = { .address = map_addr };
	FD922_VDMA vdma = { .enable = 1 };
	FD922_VDMADescriptor_W(dev->csr, desc);
	FD922_VDMA_W(dev->csr, vdma);
}

static void fd922_disable_vdma(struct forward_dev *dev)
{
	FD922_VDMA vdma = { .enable = 0 };
	FD922_VDMA_W(dev->csr, vdma);
}

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

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

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

static int fd922_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 * 0x20000000UL, 0x20000000UL, true);
	} else {
		FD922_VideoOutCS cs;

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

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

		forward_vdma_init_region(dev->vdma, (io % 2) * 0x20000000UL + 0x40000000UL,
					 0x20000000UL, false);
	}

	return 0;
}

static void fd922_toggle_streaming(struct forward_dev *dev, int io, bool enabled)
{
	if (io < 2) {
		FD922_VideoInCS cs = FD922_VideoInCS_R(dev->csr, io);
		cs.capture = enabled ? 1 : 0;
		FD922_VideoInCS_W(dev->csr, io, cs);
	} else {
		FD922_VideoOutCS cs = FD922_VideoOutCS_R(dev->csr, (io % 2));
		cs.playback = enabled ? 1 : 0;
		FD922_VideoOutCS_W(dev->csr, (io % 2), cs);
	}
}

static ssize_t fd922_attr_show_adc(struct device *dev, struct device_attribute *attr, char *buf);
static ssize_t fd922_attr_show_version(struct device *dev, struct device_attribute *attr,
				       char *buf);

static DEVICE_ATTR(temperature, S_IRUGO, fd922_attr_show_adc, NULL);
static DEVICE_ATTR(vcore, S_IRUGO, fd922_attr_show_adc, NULL);
static DEVICE_ATTR(vaux, S_IRUGO, fd922_attr_show_adc, NULL);
static DEVICE_ATTR(version, S_IRUGO, fd922_attr_show_version, NULL);

static ssize_t fd922_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) {
		FD922_TemperatureMonitor temp = FD922_TemperatureMonitor_R(fdev->csr);
		int value;
		if (FD922_IS_V4(fdev))
			value = temp.temperature * 509314 / 1024 - 280231;
		else
			value = temp.temperature * 501374 / 1024 - 273678;

		dec = value / 1000;
		frac = abs(value % 1000);
	} else if (attr == &dev_attr_vcore) {
		FD922_VCoreMonitor v = FD922_VCoreMonitor_R(fdev->csr);
		int value = v.voltage * 3 * 1000 / 1024;
		dec = value / 1000;
		frac = value % 1000;
	} else if (attr == &dev_attr_vaux) {
		FD922_VAuxMonitor v = FD922_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 fd922_attr_show_version(struct device *dev, struct device_attribute *attr, char *buf)
{
	struct forward_dev *fdev = dev_get_drvdata(dev);
	FD922_StaticVersion version = FD922_StaticVersion_R(fdev->csr);
	int hw_major = 0, hw_minor = 0;

	if (version.hardware == 3)
		hw_major = 3;
	else if (version.hardware == 4)
		hw_major = 4;

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

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

static struct attribute_group fd922_attr_group = {
	.attrs = fd922_dev_attrs,
};

struct forward_dev_config fd922_cfg = {
	.vdma_size_mb = 2048,
	.sdma_size = 0,
	.sdma_rings = 0,
	.io_count = 4,

	.attributes = &fd922_attr_group,

	.private = NULL,

	.init = fd922_init,
	.fini = fd922_fini,
	.reboot = fd922_reboot,

	.read_id = fd922_read_id,

	.enable_interrupts = fd922_enable_interrupts,
	.disable_interrupts = fd922_disable_interrupts,
	.read_interrupts = fd922_read_interrupts,

	.enable_vdma = fd922_enable_vdma,
	.disable_vdma = fd922_disable_vdma,

	.io_type = fd922_io_type,
	.io_number = fd922_io_number,
	.io_state = fd922_io_state,
	.set_io_state = fd922_set_io_state,

	.toggle_streaming = fd922_toggle_streaming,
};
