/* SPDX-License-Identifier: GPL-2.0-only */

#include <acpi/acpi.h>
#include <acpi/acpigen.h>
#include <baseboard/variants.h>
#include <boardid.h>
#include <delay.h>
#include <device/pci.h>
#include <gpio.h>
#include <timer.h>
#include <types.h>

#define GPU_1V2_PWR_EN		GPP_D0
#define GPU_1V2_PG		GPP_D1
#define GPU_1V8_PWR_EN		GPP_E11
#define GPU_1V8_PG		GPP_E20
#define GPU_3V3_PWR_EN		GPP_E1
#define GPU_3V3_PG		GPP_E2
#define NVVDD_PWR_EN		GPP_E0
#define NVVDD_PG		GPP_E8
#define PEXVDD_PWR_EN		GPP_E10
#define PEXVDD_PG		GPP_E17
#define FBVDD_PWR_EN		GPP_A19
#define FBVDD_PG		GPP_E4
#define GPU_PERST_L		GPP_B3
#define GPU_ALLRAILS_PG		GPP_E5

#define DEFAULT_PG_TIMEOUT_US	20000

#define VGAR_BYTE_OFFSET	5

/* Maximum size of PCI config space to save. */
#define GPU_CONFIG_SAVE_SPACE_BYTES	0x100

static bool gpu_powered_on;

struct power_rail_sequence {
	const char *name;

	/* This is the GPIO (output) connected to the VR's enable pin. */
	gpio_t pwr_en_gpio;
	bool pwr_en_active_low;

	/* This is the GPIO (input) connected to the VR's power-good pin. */
	gpio_t pg_gpio;

	/* Delay after sequencing this rail. */
	unsigned int delay_ms;
};

/* In GCOFF exit order (i.e., power-on order) */
static struct power_rail_sequence gpu_on_seq[] = {
	{ "GPU 1.2V",		GPU_1V2_PWR_EN,	false, GPU_1V2_PG, },
	{ "GPU 1.8V",		GPU_1V8_PWR_EN,	false, GPU_1V8_PG, },
	{ "GPU 3.3V",		GPU_3V3_PWR_EN,	false, GPU_3V3_PG, },
	{ "NVVDD+MSVDD",	NVVDD_PWR_EN,	false, NVVDD_PG, },
	{ "PEXVDD",		PEXVDD_PWR_EN,	false, PEXVDD_PG, },
	{ "FBVDD",		FBVDD_PWR_EN,	false,  FBVDD_PG, },
};

/* In GCOFF entry order (i.e., power-off order) */
static struct power_rail_sequence gpu_off_seq[] = {
	{ "FBVDD",		FBVDD_PWR_EN,	false,  FBVDD_PG,	0,},
	{ "PEXVDD",		PEXVDD_PWR_EN,	false, PEXVDD_PG,	2,},
	{ "NVVDD+MSVDD",	NVVDD_PWR_EN,	false, NVVDD_PG,	2,},
	{ "GPU 3.3V",		GPU_3V3_PWR_EN,	false, GPU_3V3_PG,	4,},
	{ "GPU 1.8V",		GPU_1V8_PWR_EN,	false, GPU_1V8_PG,	0,},
	{ "GPU 1.2V",		GPU_1V2_PWR_EN,	false, GPU_1V2_PG,	0,},
};

enum rail_state {
	RAIL_OFF = 0,
	RAIL_ON = 1,
};

/* Assert the VR's enable pin, and wait until the VR's power-good is asserted. */
static bool sequence_rail(const struct power_rail_sequence *seq, enum rail_state state)
{
	enum rail_state pwr_en_state = state;
	bool result;

	if (seq->pwr_en_active_low)
		pwr_en_state = !pwr_en_state;

	gpio_output(seq->pwr_en_gpio, pwr_en_state);
	result = wait_us(DEFAULT_PG_TIMEOUT_US, gpio_get(seq->pg_gpio) == state) > 0;
	if (seq->delay_ms)
		mdelay(seq->delay_ms);

	return result;
}

static void dgpu_power_sequence_off(void)
{
	/* Assert reset and clear power-good */
	gpio_output(GPU_PERST_L, 0);

	/* Inform the GPU that the power is no longer good. */
	gpio_output(GPU_ALLRAILS_PG, 0);

	for (size_t i = 0; i < ARRAY_SIZE(gpu_off_seq); i++) {
		if (!sequence_rail(&gpu_off_seq[i], RAIL_OFF)) {
			printk(BIOS_ERR, "Failed to disable %s rail, continuing!\n",
			       gpu_off_seq[i].name);
		}
	}
}

static void dgpu_power_sequence_on(void)
{
	/* Assert PERST# */
	gpio_output(GPU_PERST_L, 0);

	for (size_t i = 0; i < ARRAY_SIZE(gpu_on_seq); i++) {
		if (!sequence_rail(&gpu_on_seq[i], RAIL_ON)) {
			printk(BIOS_ERR, "Failed to enable %s rail, sequencing back down!\n",
			       gpu_on_seq[i].name);

			/* If an error occurred, then perform the power-off sequence and
			   return early to avoid setting GPU_ALLRAILS_PG and PERST_L. */
			dgpu_power_sequence_off();
			return;
		}
	}

	/* Set power-good and release PERST# */
	gpio_output(GPU_ALLRAILS_PG, 1);
	mdelay(1);
	gpio_output(GPU_PERST_L, 1);

	printk(BIOS_INFO, "Sequenced GPU successfully\n");
	mdelay(1);

	gpu_powered_on = true;
}

void variant_init(void)
{
	if (acpi_is_wakeup_s3())
		return;

	dgpu_power_sequence_on();
}

/*
 * Pass the specific GPIO names
 */
void variant_fill_ssdt(const struct device *dev)
{
	const int nvvdd_pg_gpio = NVVDD_PG;
	const int gpu_1v8_en_gpio = GPU_1V8_PWR_EN;
	acpigen_write_scope("\\_SB.PCI0.PEG0.PEGP");
	acpigen_write_method("_INI", 0);
	acpigen_write_store_int_to_namestr(nvvdd_pg_gpio, "NVPG");
	acpigen_write_store_int_to_namestr(gpu_1v8_en_gpio, "GPEN");
	acpigen_write_method_end();
	acpigen_write_scope_end();
}

void variant_finalize(void)
{
	/*
	 * Currently the `pch_pirq_init()` function in lpc_lib.c will program
	 * PIRQ IRQs for all PCI devices discovered during enumeration. This may
	 * not be correct for all devices, and causes strange behavior with the
	 * Nvidia dGPU; it will start out with IRQ 11 and then after a
	 * suspend/resume cycle, it will get programmed back to 16, so the Linux
	 * kernel must be doing some IRQ sanitization at some point.  To fix
	 * this anomaly, explicitly program the IRQ to 16 (which we know is what
	 * IRQ it will eventually take).
	*/
	const struct device *dgpu = DEV_PTR(dgpu);
	pci_write_config8(dgpu, PCI_INTERRUPT_LINE, 16);
}