/*
   forward-fd940.c - driver for SoftLab-NSK FD940 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 "fd940_reg.h"

#include <linux/stat.h>

#define FD940_ALL_INTERRUPTS \
	((1 << 0) | (1 << 3) | (1 << 6) | (1 << 9) | (1 << 12) | (1 << 14) | (1 << 16) | (1 << 18))

#define FD940_ALL_INTERRUPTS_DATA_MASK \
	((3 << 1) | (3 << 4) | (3 << 7) | (3 << 10) | (1 << 13) | (1 << 15) | (1 << 17) | (1 << 19))

static int fd940_init(struct forward_dev *dev)
{
	return 0;
}
static void fd940_fini(struct forward_dev *dev)
{
}

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

	FD940_SoftID id = FD940_SoftID_R(dev->csr);
	FD940_StaticVersion v = FD940_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 fd940_enable_interrupts(struct forward_dev *dev, const forward_irq_t *interrupts)
{
	u32 irq = ioread32(&dev->csr[FD940_IRQEnable_A]);
	irq |= interrupts->irq[0] & FD940_ALL_INTERRUPTS;
	iowrite32(irq, &dev->csr[FD940_IRQEnable_A]);
}

static void fd940_disable_interrupts(struct forward_dev *dev, const forward_irq_t *interrupts)
{
	u32 irq = ioread32(&dev->csr[FD940_IRQEnable_A]);
	irq &= ~(interrupts->irq[0] & FD940_ALL_INTERRUPTS);
	iowrite32(irq, &dev->csr[FD940_IRQEnable_A]);
	iowrite32(interrupts->irq[0] & FD940_ALL_INTERRUPTS, &dev->csr[FD940_IRQFlags_A]);
}

static bool fd940_read_interrupts(struct forward_dev *dev, forward_irq_t *interrupts,
				  forward_irq_t *data, forward_irq_t *data_mask)
{
	u32 irq = ioread32(&dev->csr[FD940_IRQFlags_A]);
	iowrite32((irq & FD940_ALL_INTERRUPTS), &dev->csr[FD940_IRQFlags_A]);
	interrupts->irq[0] = irq & FD940_ALL_INTERRUPTS;
	data->irq[0] = irq & FD940_ALL_INTERRUPTS_DATA_MASK;
	data_mask->irq[0] = FD940_ALL_INTERRUPTS_DATA_MASK;

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

static void fd940_enable_vdma(struct forward_dev *dev, u32 map_addr)
{
	FD940_VDMADescriptor desc = { .address = map_addr };
	FD940_VDMA vdma = { .enable = 1 };
	FD940_VDMADescriptor_W(dev->csr, desc);
	FD940_VDMA_W(dev->csr, vdma);
}

static void fd940_disable_vdma(struct forward_dev *dev)
{
	FD940_VDMA vdma = { .enable = 0 };
	FD940_VDMA_W(dev->csr, vdma);
}

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

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

static enum forward_io_state fd940_io_state(struct forward_dev *dev, int io)
{
	FD940_VideoInCS cs = FD940_VideoInCS_R(dev->csr, io);
	return cs.enable ? FORWARD_IO_RX : FORWARD_IO_DISABLED;
}

static int fd940_set_io_state(struct forward_dev *dev, int io, enum forward_io_state state)
{
	FD940_VideoInCS cs;

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

	cs = FD940_VideoInCS_R(dev->csr, io);
	cs.enable = (state == FORWARD_IO_DISABLED) ? 0 : 1;
	FD940_VideoInCS_W(dev->csr, io, cs);

	forward_vdma_init_region(dev->vdma, io * 0x10000000UL, 0x10000000UL, true);

	return 0;
}

static void fd940_toggle_streaming(struct forward_dev *dev, int number, bool enabled)
{
	FD940_VideoInCS cs = FD940_VideoInCS_R(dev->csr, number);
	cs.capture = enabled ? 1 : 0;
	FD940_VideoInCS_W(dev->csr, number, cs);
}

static ssize_t fd940_attr_show_adc(struct device *dev, struct device_attribute *attr, char *buf);
static ssize_t fd940_attr_show_version(struct device *dev, struct device_attribute *attr,
				       char *buf);

static DEVICE_ATTR(temperature, S_IRUGO, fd940_attr_show_adc, NULL);
static DEVICE_ATTR(vcore, S_IRUGO, fd940_attr_show_adc, NULL);
static DEVICE_ATTR(vaux, S_IRUGO, fd940_attr_show_adc, NULL);
static DEVICE_ATTR(version, S_IRUGO, fd940_attr_show_version, NULL);

static ssize_t fd940_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) {
		FD940_TemperatureMonitor temp = FD940_TemperatureMonitor_R(fdev->csr);
		int value = temp.temperature * 503975 / 1024 - 273150;
		dec = value / 1000;
		frac = abs(value % 1000);
	} else if (attr == &dev_attr_vcore) {
		FD940_VCoreMonitor v = FD940_VCoreMonitor_R(fdev->csr);
		int value = v.voltage * 3 * 1000 / 1024;
		dec = value / 1000;
		frac = value % 1000;
	} else if (attr == &dev_attr_vaux) {
		FD940_VAuxMonitor v = FD940_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 fd940_attr_show_version(struct device *dev, struct device_attribute *attr, char *buf)
{
	struct forward_dev *fdev = dev_get_drvdata(dev);
	FD940_StaticVersion version = FD940_StaticVersion_R(fdev->csr);
	int hw_major = 0, hw_minor = 0;

	if (version.hardware == 1) {
		hw_major = 1;
		hw_minor = 0;
	} else if (version.hardware == 2) {
		hw_major = 1;
		hw_minor = 1;
	} else if (version.hardware == 3) {
		hw_major = 2;
		hw_minor = 0;
	}

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

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

static struct attribute_group fd940_attr_group = {
	.attrs = fd940_dev_attrs,
};

struct forward_dev_config fd940_cfg = {
	.vdma_size_mb = 1024,
	.sdma_size = 0,
	.sdma_rings = 0,
	.io_count = 4,

	.attributes = &fd940_attr_group,

	.private = NULL,

	.init = fd940_init,
	.fini = fd940_fini,

	.read_id = fd940_read_id,

	.enable_interrupts = fd940_enable_interrupts,
	.disable_interrupts = fd940_disable_interrupts,
	.read_interrupts = fd940_read_interrupts,

	.enable_vdma = fd940_enable_vdma,
	.disable_vdma = fd940_disable_vdma,

	.io_type = fd940_io_type,
	.io_number = fd940_io_number,
	.io_state = fd940_io_state,
	.set_io_state = fd940_set_io_state,

	.toggle_streaming = fd940_toggle_streaming,
};
