/*
 * Copyright (c) 2018-2020 CTCaer
 * Copyright (c) 2019 Atmosphère-NX
 *
 * This program is free software; you can redistribute it and/or modify it
 * under the terms and conditions of the GNU General Public License,
 * version 2, as published by the Free Software Foundation.
 *
 * This program is distributed in the hope 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, see <http://www.gnu.org/licenses/>.
 */

#include <string.h>
#include <stdlib.h>

#include "hos.h"
#include "../config.h"
#include <display/di.h>
#include <gfx_utils.h>
#include <libs/fatfs/ff.h>
#include <mem/heap.h>
#include <soc/fuse.h>
#include "../storage/emummc.h"
#include "../storage/nx_emmc.h"
#include <storage/nx_sd.h>
#include <storage/sdmmc.h>
#include <utils/btn.h>
#include <utils/util.h>
#include <utils/types.h>

extern hekate_config h_cfg;

enum emuMMC_Type
{
	emuMMC_None = 0,
	emuMMC_Partition,
	emuMMC_File,
	emuMMC_MAX
};

/* "EFS0" */
#define EMUMMC_MAGIC 0x30534645
#define EMUMMC_FILE_PATH_MAX 0x80

typedef struct
{
	u32 magic;
	u32 type;
	u32 id;
	u32 fs_ver;
} emummc_base_config_t;

typedef struct
{
	u64 start_sector;
} emummc_partition_config_t;

typedef struct
{
	char path[EMUMMC_FILE_PATH_MAX];
} emummc_file_config_t;

typedef struct
{
	emummc_base_config_t base_cfg;
	union
	{
		emummc_partition_config_t partition_cfg;
		emummc_file_config_t file_cfg;
	};
	char nintendo_path[EMUMMC_FILE_PATH_MAX];
} exo_emummc_config_t;

typedef struct _exo_cfg_t
{
	u32 magic;
	u32 fwno;
	u32 flags[2];
	u16 display_id;
	u8  uart_port;
	u8  uart_invert;
	u32 uart_baudrate;
	u32 rsvd1[2];
	exo_emummc_config_t emummc_cfg;
} exo_cfg_t;

typedef struct _atm_meta_t
{
	u32 magic;
	u32 fwno;
} wb_cfg_t;

// Atmosphère reboot-to-fatal-error.
typedef struct _atm_fatal_error_ctx
{
	u32 magic;
	u32 error_desc;
	u64 title_id;
	union
	{
		u64 gprs[32];
		struct
		{
			u64 _gprs[29];
			u64 fp;
			u64 lr;
			u64 sp;
		};
	};
	u64 pc;
	u64 module_base;
	u32 pstate;
	u32 afsr0;
	u32 afsr1;
	u32 esr;
	u64 far;
	u64 report_identifier; // Normally just system tick.
	u64 stack_trace_size;
	u64 stack_dump_size;
	u64 stack_trace[0x20];
	u8  stack_dump[0x100];
	u8  tls[0x100];
} atm_fatal_error_ctx;

#define ATM_FATAL_ERR_CTX_ADDR 0x4003E000
#define  ATM_FATAL_MAGIC       0x30454641 // AFE0

#define ATM_EXO_FATAL_ADDR     0x80020000
#define  ATM_EXO_FATAL_SIZE    0x20000

#define ATM_WB_HEADER_OFF      0x244
#define  ATM_WB_MAGIC          0x30544257 // WBT0

// Exosphère mailbox defines.
#define EXO_CFG_ADDR             0x8000F000
#define  EXO_MAGIC_VAL           0x304F5845
#define  EXO_FLAG_DBG_PRIV        BIT(1)
#define  EXO_FLAG_DBG_USER        BIT(2)
#define  EXO_FLAG_NO_USER_EXC     BIT(3)
#define  EXO_FLAG_USER_PMU        BIT(4)
#define  EXO_FLAG_CAL0_BLANKING   BIT(5)
#define  EXO_FLAG_CAL0_WRITES_SYS BIT(6)
#define  EXO_FLAG_ENABLE_USB3     BIT(7)

#define EXO_FW_VER(mj, mn, rv) (((mj) << 24) | ((mn) << 16) | ((rv) << 8))

void config_exosphere(launch_ctxt_t *ctxt, u32 warmboot_base, bool exo_new)
{
	u32 exo_fw_no = 0;
	u32 exo_flags = 0;
	bool usb3_force = false;
	bool user_debug = false;
	bool cal0_blanking = false;
	bool cal0_allow_writes_sys = false;

	memset((exo_cfg_t *)EXO_CFG_ADDR, 0, sizeof(exo_cfg_t));

	volatile exo_cfg_t *exo_cfg = (exo_cfg_t *)EXO_CFG_ADDR;

	// Old exosphere target versioning. Use fuses for a simpler encoding.
	if (ctxt->pkg1_id->fuses <= 3 || ctxt->pkg1_id->fuses >= 10) // 1.0.0 - 3.0.0, 8.1.0+.
		exo_fw_no = ctxt->pkg1_id->fuses;
	else
		exo_fw_no = ctxt->pkg1_id->fuses - 1;                    // 3.0.1 - 7.0.1, 8.0.0 - 8.0.1.

	if (!memcmp(ctxt->pkg1_id->id, "20190314172056", 8))         // 8.0.0 - 8.0.1.
		exo_fw_no++;

	if (!memcmp(ctxt->pkg1_id->id, "20210129111626", 8))         // 12.0.0.
		exo_fw_no++;

	// Feed old exosphere target versioning to new.
	if (exo_new)
	{
		switch (exo_fw_no)
		{
		case 1 ... 4:
		case 6:
			exo_fw_no = EXO_FW_VER(exo_fw_no, 0, 0);
			break;
		case 5:
			if (!ctxt->exo_ctx.fs_is_510)
				exo_fw_no = EXO_FW_VER(5, 0, 0);
			else
				exo_fw_no = EXO_FW_VER(5, 1, 0);
			break;
		case 7:
			exo_fw_no = EXO_FW_VER(6, 2, 0);
			break;
		case 8 ... 9:
			exo_fw_no = EXO_FW_VER(exo_fw_no - 1, 0, 0);
			break;
		case 10:
			exo_fw_no = EXO_FW_VER(8, 1, 0);
			break;
		case 11:
			exo_fw_no = EXO_FW_VER(9, 0, 0);
			break;
		case 12:
			exo_fw_no = EXO_FW_VER(9, 1, 0);
			break;
		case 13 ... 15:
			exo_fw_no = EXO_FW_VER(exo_fw_no - 3, 0, 0);
			break;
		}
	}

	// Parse exosphere.ini.
	if (!ctxt->stock)
	{
		LIST_INIT(ini_sections);
		if (ini_parse(&ini_sections, "exosphere.ini", false))
		{
			LIST_FOREACH_ENTRY(ini_sec_t, ini_sec, &ini_sections, link)
			{
				// Only parse exosphere section.
				if (!(ini_sec->type == INI_CHOICE) || strcmp(ini_sec->name, "exosphere"))
					continue;

				LIST_FOREACH_ENTRY(ini_kv_t, kv, &ini_sec->kvs, link)
				{
					if (!strcmp("debugmode_user", kv->key))
						user_debug = atoi(kv->val);
					else if (!strcmp("log_port", kv->key))
						exo_cfg->uart_port = atoi(kv->val);
					else if (!strcmp("log_inverted", kv->key))
						exo_cfg->uart_invert = atoi(kv->val);
					else if (!strcmp("log_baud_rate", kv->key))
						exo_cfg->uart_baudrate = atoi(kv->val);
					else if (emu_cfg.enabled && !h_cfg.emummc_force_disable)
					{
						if (!strcmp("blank_prodinfo_emummc", kv->key))
							cal0_blanking = atoi(kv->val);
					}
					else
					{
						if (!strcmp("blank_prodinfo_sysmmc", kv->key))
							cal0_blanking = atoi(kv->val);
						else if (!strcmp("allow_writing_to_cal_sysmmc", kv->key))
							cal0_allow_writes_sys = atoi(kv->val);
					}
				}
				break;
			}
		}

		// Parse usb mtim settings. Avoid parsing if it's overridden.
		if (ctxt->fss0_main_path && !ctxt->exo_ctx.usb3_force)
		{
			char set_path[256];
			strcpy(set_path, ctxt->fss0_main_path);
			strcat(set_path, "config/system_settings.ini");
			LIST_INIT(sys_settings);
			if (ini_parse(&ini_sections, set_path, false))
			{
				LIST_FOREACH_ENTRY(ini_sec_t, ini_sec, &ini_sections, link)
				{
					// Only parse usb section.
					if (!(ini_sec->type == INI_CHOICE) || strcmp(ini_sec->name, "usb"))
						continue;

					LIST_FOREACH_ENTRY(ini_kv_t, kv, &ini_sec->kvs, link)
					{
						if (!strcmp("usb30_force_enabled", kv->key))
						{
							usb3_force = !strcmp("u8!0x1", kv->val);
							break; // Only parse usb30_force_enabled key.
						}
					}
					break;
				}
			}
		}
	}

	// To avoid problems, make private debug mode always on if not semi-stock.
	if (!ctxt->stock || (emu_cfg.enabled && !h_cfg.emummc_force_disable))
		exo_flags |= EXO_FLAG_DBG_PRIV;

	// Enable user debug.
	if (user_debug)
		exo_flags |= EXO_FLAG_DBG_USER;

	// Disable proper failure handling.
	if (ctxt->exo_ctx.no_user_exceptions)
		exo_flags |= EXO_FLAG_NO_USER_EXC;

	// Enable user access to PMU.
	if (ctxt->exo_ctx.user_pmu)
		exo_flags |= EXO_FLAG_USER_PMU;

	// Enable USB 3.0. Check if system_settings ini value is overridden. If not, check if enabled in ini.
	if ((ctxt->exo_ctx.usb3_force && *ctxt->exo_ctx.usb3_force)
			|| (!ctxt->exo_ctx.usb3_force && usb3_force))
		exo_flags |= EXO_FLAG_ENABLE_USB3;

	// Enable prodinfo blanking. Check if exo ini value is overridden. If not, check if enabled in exo ini.
	if ((ctxt->exo_ctx.cal0_blank && *ctxt->exo_ctx.cal0_blank)
			|| (!ctxt->exo_ctx.cal0_blank && cal0_blanking))
		exo_flags |= EXO_FLAG_CAL0_BLANKING;

	// Allow prodinfo writes. Check if exo ini value is overridden. If not, check if enabled in exo ini.
	if ((ctxt->exo_ctx.cal0_allow_writes_sys && *ctxt->exo_ctx.cal0_allow_writes_sys)
			|| (!ctxt->exo_ctx.cal0_allow_writes_sys && cal0_allow_writes_sys))
		exo_flags |= EXO_FLAG_CAL0_WRITES_SYS;

	// Set mailbox values.
	exo_cfg->magic = EXO_MAGIC_VAL;
	exo_cfg->fwno = exo_fw_no;
	exo_cfg->flags[0] = exo_flags;

	// If warmboot is lp0fw, add in RSA modulus.
	volatile wb_cfg_t *wb_cfg = (wb_cfg_t *)(warmboot_base + ATM_WB_HEADER_OFF);

	if (wb_cfg->magic == ATM_WB_MAGIC)
	{
		wb_cfg->fwno = exo_fw_no;

		// Set warmboot binary rsa modulus.
		u8 *rsa_mod = (u8 *)malloc(512);

		sdmmc_storage_set_mmc_partition(&emmc_storage, EMMC_BOOT0);

		u32 sector;
		u8  mod0, mod1;

		// Get the correct RSA modulus byte masks.
		nx_emmc_get_autorcm_masks(&mod0, &mod1);

		// Iterate BCTs.
		for (u32 i = 0; i < 4; i++)
		{
			sector = 1 + (32 * i); // 0x4000 bct + 0x200 offset.
			sdmmc_storage_read(&emmc_storage, sector, 1, rsa_mod);

			// Check if 2nd byte of modulus is correct.
			if (rsa_mod[0x11] != mod1)
				continue;

			// Patch AutoRCM out.
			rsa_mod[0x10] = mod0;

			break;
		}

		memcpy((void *)(warmboot_base + 0x10), rsa_mod + 0x10, 0x100);
	}

	if (emu_cfg.enabled && !h_cfg.emummc_force_disable)
	{
		exo_cfg->emummc_cfg.base_cfg.magic = EMUMMC_MAGIC;
		exo_cfg->emummc_cfg.base_cfg.type = emu_cfg.sector ? emuMMC_Partition : emuMMC_File;
		exo_cfg->emummc_cfg.base_cfg.fs_ver = emu_cfg.fs_ver;
		exo_cfg->emummc_cfg.base_cfg.id = emu_cfg.id;

		if (emu_cfg.sector)
			exo_cfg->emummc_cfg.partition_cfg.start_sector = emu_cfg.sector;
		else
			strcpy((char *)exo_cfg->emummc_cfg.file_cfg.path, emu_cfg.path);

		if (emu_cfg.nintendo_path && !ctxt->stock)
			strcpy((char *)exo_cfg->emummc_cfg.nintendo_path, emu_cfg.nintendo_path);
		else if (ctxt->stock)
			strcpy((char *)exo_cfg->emummc_cfg.nintendo_path, "Nintendo");
		else
			exo_cfg->emummc_cfg.nintendo_path[0] = 0;
	}

	// Copy over exosphere fatal for Mariko.
	if (h_cfg.t210b01)
	{
		memset((void *)ATM_EXO_FATAL_ADDR, 0, ATM_EXO_FATAL_SIZE);
		if (ctxt->exofatal)
			memcpy((void *)ATM_EXO_FATAL_ADDR, ctxt->exofatal, ctxt->exofatal_size);

		// Set display id.
		exo_cfg->display_id = display_get_decoded_panel_id();
	}

#ifdef DEBUG_UART_PORT
	// Ovverride logging parameters if set in compile time.
	if (!ctxt->stock)
	{
		exo_cfg->uart_port = DEBUG_UART_PORT;
		exo_cfg->uart_invert = DEBUG_UART_INVERT;
		exo_cfg->uart_baudrate = DEBUG_UART_BAUDRATE;
	}
#endif
}

static const char *get_error_desc(u32 error_desc)
{
	switch (error_desc)
	{
	case 0x100:
		return "IABRT"; // Instruction Abort.
	case 0x101:
		return "DABRT"; // Data Abort.
	case 0x102:
		return "IUA";   // Instruction Unaligned Access.
	case 0x103:
		return "DUA";   // Data Unaligned Access.
	case 0x104:
		return "UDF";   // Undefined Instruction.
	case 0x106:
		return "SYS";   // System Error.
	case 0x301:
		return "SVC";   // Bad arguments or unimplemented SVC.
	case 0xF00:
		return "KRNL";  // Kernel panic.
	case 0xFFD:
		return "SO";    // Stack Overflow.
	case 0xFFE:
		return "std::abort";
	default:
		return "UNK";
	}
}

void secmon_exo_check_panic()
{
	volatile atm_fatal_error_ctx *rpt = (atm_fatal_error_ctx *)ATM_FATAL_ERR_CTX_ADDR;

	// Mask magic to maintain compatibility with any AFE version, thanks to additive struct members.
	if ((rpt->magic & 0xF0FFFFFF) != ATM_FATAL_MAGIC)
		return;

	gfx_clear_grey(0x1B);
	gfx_con_setpos(0, 0);

	WPRINTF("Panic occurred while running Atmosphere.\n\n");
	WPRINTFARGS("Title ID: %08X%08X", (u32)((u64)rpt->title_id >> 32), (u32)rpt->title_id);
	WPRINTFARGS("Error:    %s (0x%x)\n", get_error_desc(rpt->error_desc), rpt->error_desc);

	// Save context to the SD card.
	char filepath[0x40];
	f_mkdir("atmosphere/fatal_errors");
	strcpy(filepath, "/atmosphere/fatal_errors/report_");
	itoa((u32)((u64)rpt->report_identifier >> 32), filepath + strlen(filepath), 16);
	itoa((u32)(rpt->report_identifier), filepath + strlen(filepath), 16);
	strcat(filepath, ".bin");

	if (!sd_save_to_file((void *)rpt, sizeof(atm_fatal_error_ctx), filepath))
	{
		gfx_con.fntsz = 8;
		WPRINTFARGS("Report saved to %s\n", filepath);
		gfx_con.fntsz = 16;
	}

	// Change magic to invalid, to prevent double-display of error/bootlooping.
	rpt->magic = 0;

	gfx_printf("\n\nPress POWER to continue.\n");

	display_backlight_brightness(100, 1000);
	msleep(1000);

	while (!(btn_wait() & BTN_POWER))
		;

	display_backlight_brightness(0, 1000);
	gfx_con_setpos(0, 0);
}