/*
 * This file is part of the coreboot project.
 *
 * Copyright 2018       Facebook, Inc.
 * Copyright 2003-2017  Cavium Inc.  <support@cavium.com>
 *
 * 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; version 2 of the License.
 *
 * 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.
 *
 * Derived from Cavium's BSD-3 Clause OCTEONTX-SDK-6.2.0.
 */

#include <bootmode.h>
#include <console/console.h>
#include <device/device.h>
#include <soc/addressmap.h>
#include <soc/clock.h>
#include <soc/sdram.h>
#include <soc/timer.h>
#include <soc/uart.h>
#include <stddef.h>
#include <stdlib.h>
#include <string.h>
#include <symbols.h>
#include <libbdk-boot/bdk-boot.h>
#include <soc/ecam0.h>
#include <console/uart.h>
#include <libbdk-hal/bdk-pcie.h>
#include <device/pci.h>
#include <libbdk-hal/bdk-qlm.h>
#include <libbdk-hal/bdk-config.h>
#include <libbdk-arch/bdk-csrs-bgx.h>
#include <bootmem.h>
#include <soc/bl31_plat_params.h>
#include <cbfs.h>
#include <cbmem.h>
#include <fit.h>

static const char *QLM_BGX_MODE_MAP[BDK_QLM_MODE_LAST] = {
	[BDK_QLM_MODE_SGMII_4X1]    = "sgmii",
	[BDK_QLM_MODE_SGMII_2X1]    = "sgmii",
	[BDK_QLM_MODE_SGMII_1X1]    = "sgmii",
	[BDK_QLM_MODE_XAUI_1X4]     = "xaui",
	[BDK_QLM_MODE_RXAUI_2X2]    = "rxaui",
	[BDK_QLM_MODE_RXAUI_1X2]    = "rxaui",
	[BDK_QLM_MODE_XFI_4X1]      = "xfi",
	[BDK_QLM_MODE_XFI_2X1]      = "xfi",
	[BDK_QLM_MODE_XFI_1X1]      = "xfi",
	[BDK_QLM_MODE_XLAUI_1X4]    = "xlaui",
	[BDK_QLM_MODE_10G_KR_4X1]   = "xfi-10g-kr",
	[BDK_QLM_MODE_10G_KR_2X1]   = "xfi-10g-kr",
	[BDK_QLM_MODE_10G_KR_1X1]   = "xfi-10g-kr",
	[BDK_QLM_MODE_40G_KR4_1X4]  = "xlaui-40g-kr",
	[BDK_QLM_MODE_QSGMII_4X1]   = "qsgmii",
};

static void dt_platform_fixup_phy(struct device_tree_node *node, char *path,
				  int64_t phy_address, bdk_qlm_modes_t qlm_mode)
{
	const char *data = NULL;
	size_t size = 0;
	dt_find_bin_prop(node, "qlm-mode", (const void **)&data, &size);

	if (!data || strncmp(data, path, 6) != 0)
		return; /* No key prefix match. */
	printk(BIOS_INFO, "%s: Node %s = %s\n", __func__, node->name, data);

	if (strlen(path) == strlen(data) && strcmp(data, path) == 0) {
		/* Keep node, remove "qlm-mode" property */
		dt_delete_prop(node, "qlm-mode");
		printk(BIOS_INFO, "%s: Removing qlm-mode on "
		       "node %s\n", __func__, node->name);
		/* Linux only access the Phy via MDIO.
		Remove 'phy-handle' if this option is not available */
		switch (qlm_mode) {
		case BDK_QLM_MODE_SGMII_4X1:
		case BDK_QLM_MODE_SGMII_2X1:
		case BDK_QLM_MODE_SGMII_1X1:
		case BDK_QLM_MODE_QSGMII_4X1:
			if ((phy_address & BDK_IF_PHY_TYPE_MASK) !=
			    BDK_IF_PHY_MDIO) {
				dt_delete_prop(node, "phy-handle");
				printk(BIOS_INFO, "%s: Removing phy-handle on "
				       "node %s\n", __func__, node->name);
			}
			break;
		default:
			break;
		}
	} else {
		printk(BIOS_INFO, "%s: Removing node %s\n", __func__,
		       node->name);
		/* No match, remove node */
		list_remove(&node->list_node);
	}
}

static void dt_iterate_phy(struct device_tree_node *parent,
				  const char *name,
				  char *path,
				  int64_t phy_address,
				  bdk_qlm_modes_t qlm_mode)
{
	struct device_tree_property *prop;

	/* Check if parent itself has the required property value. */
	list_for_each(prop, parent->properties, list_node) {
		if (!strcmp(name, prop->prop.name)) {
			dt_platform_fixup_phy(parent, path, phy_address,
					      qlm_mode);
		}
	}

	struct device_tree_node *child;
	list_for_each(child, parent->children, list_node) {
		dt_iterate_phy(child, name, path, phy_address, qlm_mode);
	}
}

static void dt_platform_fixup_mac(struct device_tree_node *node)
{
	const char *name = "local-mac-address";
	const u64 *localmac = NULL;
	size_t size = 0;

	dt_find_bin_prop(node, name, (const void **)&localmac, &size);

	if (!localmac)
		return;
	static size_t used_mac;

	/* Extract our MAC address info so we can assign them */
	size_t next_free_mac_address =
		bdk_config_get_int(BDK_CONFIG_MAC_ADDRESS);
	size_t num_free_mac_addresses =
		bdk_config_get_int(BDK_CONFIG_MAC_ADDRESS_NUM);
	size_t num_free_override =
		bdk_config_get_int(BDK_CONFIG_MAC_ADDRESS_NUM_OVERRIDE);
	if (num_free_override != -1)
		num_free_mac_addresses = num_free_override;

	if (size == 6) {
		if (*localmac)
			return;
		if (used_mac < num_free_mac_addresses) {
			u64 genmac = next_free_mac_address + used_mac;
			dt_add_bin_prop(node, name, &genmac, 6);
			used_mac++;
			return;
		}
	}

	printk(BIOS_INFO, "%s: Removing node %s\n", __func__, node->name);
	list_remove(&node->list_node);
}

static void dt_iterate_mac(struct device_tree_node *parent)
{
	struct device_tree_property *prop;
	const char *name = "local-mac-address";

	/* Check if parent itself has the required property value. */
	list_for_each(prop, parent->properties, list_node) {
		if (!strcmp(name, prop->prop.name))
			dt_platform_fixup_mac(parent);
	}

	struct device_tree_node *child;
	list_for_each(child, parent->children, list_node) {
		dt_iterate_mac(child);
	}
}

/* Do additional device_tree modifications. */
static int dt_platform_fixup(struct device_tree_fixup *fixup,
			      struct device_tree *tree)
{
	struct device_tree_node *dt_node;
	size_t i;

	/* Set the sclk clock rate. */
	dt_node = dt_find_node_by_path(tree, "/soc@0/sclk", NULL, NULL, 0);
	if (dt_node) {
		const u32 freq = thunderx_get_io_clock();
		printk(BIOS_INFO, "%s: Set SCLK to %u Hz\n", __func__, freq);
		dt_add_u32_prop(dt_node, "clock-frequency", freq);
	} else
		printk(BIOS_ERR, "%s: Node not found. OS might miss-behave !\n",
		       __func__);

	/* Set refclkuaa clock rate. */
	dt_node = dt_find_node_by_path(tree, "/soc@0/refclkuaa", NULL,
				       NULL, 0);
	if (dt_node) {
		const u32 freq = uart_platform_refclk();
		printk(BIOS_INFO, "%s: Set REFCLKUAA to %u Hz\n", __func__,
		       freq);
		dt_add_u32_prop(dt_node, "clock-frequency", freq);
	} else
		printk(BIOS_ERR, "%s: Node not found. OS might miss-behave !\n",
		       __func__);

	/* Remove unused UART entries */
	for (i = 0; i < 4; i++) {
		char path[32];
		const uint64_t addr = UAAx_PF_BAR0(i);
		/* Remove the node */
		snprintf(path, sizeof(path), "/soc@0/serial@%llx", addr);
		dt_node = dt_find_node_by_path(tree, path, NULL, NULL, 0);
		if (!dt_node || uart_is_enabled(i)) {
			printk(BIOS_INFO, "%s: ignoring %s\n", __func__, path);
			continue;
		}
		printk(BIOS_INFO, "%s: Removing node %s\n", __func__, path);
		list_remove(&dt_node->list_node);
	}

	/* Remove unused PEM entries */
	for (i = 0; i < 8; i++) {
		char path[32];
		u32 phandle = 0;
		const uint64_t addr = PEM_PEMX_PF_BAR0(i);
		/* Remove the node */
		snprintf(path, sizeof(path), "/soc@0/pci@%llx", addr);
		dt_node = dt_find_node_by_path(tree, path, NULL, NULL, 0);
		if (!dt_node || bdk_pcie_is_running(0, i)) {
			printk(BIOS_INFO, "%s: ignoring %s\n", __func__, path);
			continue;
		}
		/* Store the phandle */
		phandle = dt_node->phandle;
		printk(BIOS_INFO, "%s: Removing node %s\n", __func__, path);
		list_remove(&dt_node->list_node);

		/* Remove phandle to non existing nodes */
		snprintf(path, sizeof(path), "/soc@0/smmu0@%llx", SMMU_PF_BAR0);
		dt_node = dt_find_node_by_path(tree, path, NULL, NULL, 0);
		if (!dt_node) {
			printk(BIOS_ERR, "%s: SMMU entry not found\n",
			       __func__);
			continue;
		}
		const u32 *data = NULL;
		size_t size = 0;
		dt_find_bin_prop(dt_node, "mmu-masters", (const void **)&data,
				 &size);
		if (!size) {
			printk(BIOS_ERR, "%s: mmu-masters entry not found\n",
			       __func__);
			continue;
		}

		u32 *data_cleaned = malloc(size);
		if (!data_cleaned)
			continue;

		size_t n = 0;
		/* Remove phandle from mmu-masters list */
		for (size_t j = 0; j < size / (sizeof(u32) * 2); j++)
			if (be32_to_cpu(data[j * 2]) != phandle) {
				data_cleaned[n * 2] = data[j * 2];
				data_cleaned[n * 2 + 1] = data[j * 2 + 1];
				n++;
			}

		dt_add_bin_prop(dt_node, "mmu-masters", data_cleaned,
				n * sizeof(u32) * 2);

		free(data_cleaned);
	}

	/* Remove QLM mode entries */
	size_t bgx_index, bgx_iface;
	for (bgx_iface = 0; bgx_iface < 4; bgx_iface++) {
		for (bgx_index = 0; bgx_index < 4; bgx_index++) {
			char path[32];
			int qlm = bdk_qlm_get_qlm_num(0, BDK_IF_BGX,
						      bgx_iface, bgx_index);
			bdk_qlm_modes_t qlm_mode = (qlm == -1) ?
				BDK_QLM_MODE_DISABLED :
				bdk_qlm_get_mode(0, qlm);

			/* BGXX_CMRX_RX_DMAC_CTL is used to mark ports as
			 * disabled that would otherwise be enabled */
			if (qlm_mode != BDK_QLM_MODE_DISABLED) {
				BDK_CSR_INIT(rx_dmac_ctl, 0,
					BDK_BGXX_CMRX_RX_DMAC_CTL(bgx_iface,
								  bgx_index));
				if (rx_dmac_ctl.u == 0)
					qlm_mode = BDK_QLM_MODE_DISABLED;
			}

			if (qlm_mode == BDK_QLM_MODE_DISABLED)
				snprintf(path, sizeof(path), "0x0%x%x,disabled",
					 bgx_iface, bgx_index);
			else
				snprintf(path, sizeof(path), "0x0%x%x,%s",
					 bgx_iface, bgx_index,
					 QLM_BGX_MODE_MAP[qlm_mode]);

			int64_t phy_address =
			    bdk_config_get_int(BDK_CONFIG_PHY_ADDRESS, 0,
					       bgx_iface, bgx_index);

			dt_iterate_phy(tree->root, "qlm-mode", path,
				       phy_address, qlm_mode);
		}
	}

	/* Set local MAC address */
	dt_iterate_mac(tree->root);

	return 0;
}

extern u8 _sff8104[];
extern u8 _esff8104[];

void bootmem_platform_add_ranges(void)
{
	bootmem_add_range((uintptr_t)_sff8104,
			  ((uintptr_t)_esff8104 - (uintptr_t)_sff8104),
			  BM_MEM_RESERVED);

	/* Scratchpad for ATF SATA quirks */
	bootmem_add_range((sdram_size_mb() - 1) * MiB, 1 * MiB,
			  BM_MEM_RESERVED);
}

static void soc_read_resources(struct device *dev)
{
	// HACK: Don't advertise bootblock romstage CAR region, it's broken...
	ram_resource(dev, 0, 2 * KiB, sdram_size_mb() * KiB - 2 * KiB);
}

static void soc_init_atf(void)
{
	static struct bl31_fdt_param fdt_param = {
		.h = { .type = PARAM_FDT, },
	};

	size_t size = 0;

	void *ptr = cbfs_boot_map_with_leak("sff8104-linux.dtb",
					    CBFS_TYPE_RAW, &size);
	if (ptr)
		memcpy(_sff8104, ptr, size);
	/* Point to devicetree in secure memory */
	fdt_param.fdt_ptr = (uintptr_t)_sff8104;

	cn81xx_register_bl31_param(&fdt_param.h);

	static struct bl31_u64_param cbtable_param = {
		.h = { .type = PARAM_COREBOOT_TABLE, },
	};
	/* Point to coreboot tables */
	cbtable_param.value = (uint64_t)cbmem_find(CBMEM_ID_CBTABLE);
	if (cbtable_param.value)
		cn81xx_register_bl31_param(&cbtable_param.h);
}

static void soc_init(struct device *dev)
{
	/* Init ECAM, MDIO, PEM, PHY, QLM ... */
	bdk_boot();

	if (CONFIG(PAYLOAD_FIT_SUPPORT)) {
		struct device_tree_fixup *dt_fixup;

		dt_fixup = malloc(sizeof(*dt_fixup));
		if (dt_fixup) {
			dt_fixup->fixup = dt_platform_fixup;
			list_insert_after(&dt_fixup->list_node,
					  &device_tree_fixups);
		}
	}

	if (CONFIG(ARM64_USE_ARM_TRUSTED_FIRMWARE))
		soc_init_atf();
}

static void soc_final(struct device *dev)
{
	watchdog_disable(0);
}

static struct device_operations soc_ops = {
	.read_resources   = soc_read_resources,
	.set_resources    = DEVICE_NOOP,
	.enable_resources = DEVICE_NOOP,
	.init             = soc_init,
	.final            = soc_final,
	.scan_bus         = NULL,
};

static void enable_soc_dev(struct device *dev)
{
	if (dev->path.type == DEVICE_PATH_DOMAIN &&
		dev->path.domain.domain == 0) {
		dev->ops = &pci_domain_ops_ecam0;
	} else if (dev->path.type == DEVICE_PATH_CPU_CLUSTER) {
		dev->ops = &soc_ops;
	}
}

struct chip_operations soc_cavium_cn81xx_ops = {
	CHIP_NAME("SOC Cavium CN81XX")
	.enable_dev = enable_soc_dev,
};
