/*
 *  Copyright (C) 2020-2023  The DOSBox Staging Team
 *  Copyright (C) 2002-2021  The DOSBox Team
 *
 *  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 "autoexec.h"

#include "checks.h"
#include "control.h"
#include "dosbox.h"
#include "fs_utils.h"
#include "keyboard.h"
#include "setup.h"
#include "shell.h"
#include "string_utils.h"

#include <algorithm>
#include <iostream>
#include <sstream>

CHECK_NARROWING();

// Uncomment line below to print the generated AUTOEXEC.BAT in the log output
// #define DEBUG_AUTOEXEC

// ***************************************************************************
// Constants
// ***************************************************************************

static const std::string AutoexecFileName = "AUTOEXEC.BAT";
static const std::string CmdBoot          = "@Z:\\BOOT.COM ";
static const std::string CmdConfig        = "@Z:\\CONFIG.COM ";
static const std::string CmdMount         = "@Z:\\MOUNT.COM ";
static const std::string CmdImgMount      = "@Z:\\IMGMOUNT.COM ";
static const std::string CmdEchoOff       = "@ECHO OFF";
static const std::string CmdSet           = "@SET ";
static const std::string CmdSetPath       = "@SET PATH=";
static const std::string CmdCall          = "CALL ";
static const std::string CmdDriveC        = "@C:";
static const std::string ToNul            = " >NUL";
static const std::string Quote            = "\"";

constexpr char char_lf = 0x0a; // line feed
constexpr char char_cr = 0x0d; // carriage return

// ***************************************************************************
// AUTOEXEC.BAT data - both source and binary
// ***************************************************************************

// Generated AUTOEXEC.BAT, un UTF-8 format
static std::string autoexec_bat_utf8 = {};
// Whether AUTOEXEC.BAT is already registered on the Z: drive
static bool is_vfile_registered = false;
// Code page used to generate Z:\AUTOEXEC.BAT from the internal UTF-8 version
static uint16_t vfile_code_page = 0;

// Data to be used to generate AUTOEXEC.BAT

// If true, put ECHO OFF before the content of [autoexec] section
static bool autoexec_has_echo_off = false;
// Environment variables to be set in AUTOEXEC.BAT
static std::map<std::string, std::string> autoexec_variables = {};

enum class Placement {
	// Autogenerated commands placed BEFORE content of [autoexec]
	InitialAutogeneratedCommands,
	// User commands executed just before secure mode gets enabled
	CommandsBeforeSecureMode,
	// Command to enable secure mode
	SecureModeCommand,
	// Content of [autoexec] section from the configuration file(s)
	AutoexecSection,
	// Final commands to boot guest OS or start given program or BAT file
	CommandsAfterAutoexecSection,
};

// Lines to be placed in the generated AUTOEXEC.BAT, by section (placement)
static std::map<Placement, std::list<std::string>> autoexec_lines;

// ***************************************************************************
// AUTOEXEC.BAT generation code
// ***************************************************************************

std::string create_autoexec_bat_utf8()
{
	std::string out;

	// Helper lamdbas

	auto push_new_line = [&] { // DOS line ending is CR+LF
		out.push_back(char_cr);
		out.push_back(char_lf);
	};

	auto push_string = [&](const std::string& line) {
		if (line.empty()) {
			push_new_line();
			return;
		}

		for (const auto& character : line) {
			out.push_back(character);
		}
		push_new_line();
	};

	auto push_header = [&](const std::string& comment) {
		if (!out.empty()) {
			push_new_line();
		}
		push_string(comment);
		push_new_line();
	};

	// Generate AUTOEXEC.BAT, in UTF-8 format

	// If currently printed lines are generated from configuration files and
	// command line options
	bool is_pushing_generated = false;
	// If currently printed lines are from '[autoexec]' section(s)
	bool is_pushing_autoexec_section = false;

	// We want unprocessed UTF8 form of messages here (thus 'MSG_GetRaw'
	// calls, not 'MSG_Get'), they will be converted to DOS code page later,
	// together with the '[autoexec]' section content
	static const std::string comment_start    = ":: ";
	static const std::string header_generated = comment_start +
		MSG_GetRaw("AUTOEXEC_BAT_GENERATED");
	static const std::string header_autoexec_section = comment_start +
		MSG_GetRaw("AUTOEXEC_BAT_CONFIG_SECTION");

	// Put 'ECHO OFF' and 'SET variable=value' if needed

	if (autoexec_has_echo_off || !autoexec_variables.empty()) {
		push_string(header_generated);
		is_pushing_generated = true;

		if (autoexec_has_echo_off) {
			push_new_line();
			push_string(CmdEchoOff);
		}

		if (!autoexec_variables.empty()) {
			push_new_line();
			for (const auto& [name, value] : autoexec_variables) {
				push_string(CmdSet + name + "=" + value);
			}
		}
	}

	if (is_pushing_generated) {
		push_new_line();
	}

	// Put remaining AUTOEXEC.BAT content

	for (const auto& [placement, list_lines] : autoexec_lines) {
		if (list_lines.empty()) {
			continue;
		}

		switch (placement) {
		case Placement::InitialAutogeneratedCommands:
		case Placement::CommandsBeforeSecureMode:
		case Placement::SecureModeCommand:
		case Placement::CommandsAfterAutoexecSection:
			if (!is_pushing_generated) {
				push_header(header_generated);
				is_pushing_generated        = true;
				is_pushing_autoexec_section = false;
			} else if (placement == Placement::CommandsBeforeSecureMode ||
			           placement == Placement::SecureModeCommand) {
				push_new_line();
			}
			break;
		case Placement::AutoexecSection:
			if (!is_pushing_autoexec_section) {
				push_header(header_autoexec_section);
				is_pushing_generated        = false;
				is_pushing_autoexec_section = true;
			}
			break;
		default: assert(false);
		}

		// Push the content of the current AUTOEXEC.BAT section
		for (const auto& autoexec_line : list_lines) {
			push_string(autoexec_line);
		}
	}

#ifdef DEBUG_AUTOEXEC
	LOG_INFO("AUTOEXEC: New file content\n\n%s", out.c_str());
#endif // DEBUG_AUTOEXEC

	return out;
}

static void create_autoexec_bat_dos(const std::string& input_utf8,
                                    const uint16_t code_page)
{
	// Convert UTF-8 AUTOEXEC.BAT to DOS code page
	std::string autoexec_bat_dos = {};
	utf8_to_dos(input_utf8, autoexec_bat_dos, UnicodeFallback::Box, code_page);

	// Convert the result to a binary format
	auto autoexec_bat_bin = std::vector<uint8_t>(autoexec_bat_dos.begin(),
	                                             autoexec_bat_dos.end());

	// Register/refresh Z:\AUTOEXEC.BAT file
	if (is_vfile_registered) {
		VFILE_Update(AutoexecFileName.c_str(), std::move(autoexec_bat_bin));
	} else {
		VFILE_Register(AutoexecFileName.c_str(),
		               std::move(autoexec_bat_bin));
		is_vfile_registered = true;
	}

	// Store current code page for caching purposes
	vfile_code_page = code_page;
}

// ***************************************************************************
// AUTOEXEC class declaration and implementation
// ***************************************************************************

class AutoExecModule final : public Module_base {
public:
	AutoExecModule(Section* configuration);

private:
	void ProcessConfigFile(const Section_line& section,
	                       const std::string& source_name);

	void AddLine(const Placement placement, const std::string& line);

	// Mount drives from standard location in host filesystem
	void AutoMountDrive(const std::string& dir_letter);
	// Mount drive C from a directory
	void AutoMountDriveC(const std::string& directory,
	                     const Placement placement = Placement::InitialAutogeneratedCommands);
	// Mount drive D as CD-ROM from images
	void AutoMountDriveD(const std::string& cdrom_images,
	                     const Placement placement = Placement::InitialAutogeneratedCommands);

	// Mount specified directory as drive C, after [autoexec] is executed,
	// just before executing the final command
	void ReMountDirAsDriveC(const std::string& directory);

	void AddMessages();
};

AutoExecModule::AutoExecModule(Section* configuration)
        : Module_base(configuration)
{
	AddMessages();

	// Get the [dosbox] conf section
	const auto sec = static_cast<Section_prop*>(control->GetSection("dosbox"));
	assert(sec);

	// Auto-mount drives (except for DOSBox's Z:) prior to [autoexec]
	if (sec->Get_bool("automount")) {
		for (char letter = 'a'; letter < 'z'; ++letter) {
			AutoMountDrive({letter});
		}
	}

	// Check -securemode switch to disable mount/imgmount/boot after
	// running autoexec.bat
	const auto cmdline = control->cmdline; // short-lived copy
	const auto arguments = &control->arguments;
	const bool has_option_securemode = arguments->securemode;

	// Are autoexec sections permitted?
	const bool has_option_no_autoexec = arguments->noautoexec;

	// Should autoexec sections be joined or overwritten?
	const std::string section_pref = sec->Get_string("autoexec_section");
	const bool should_join_autoexecs = (section_pref == "join");

	// Check to see for extra command line options to be added
	// (before the command specified on commandline)
	std::string argument = {};

	bool exit_call_exists = false;
	while (cmdline->FindString("-c", argument, true)) {
#if defined(WIN32)
		// Replace single with double quotes so that mount commands
		// can contain spaces
		for (auto& character : argument) {
			if (character == '\'') {
				character = '\"';
			}
		}
#endif // Linux users can simply use \" in their shell

		// If the user's added an exit call, simply store that
		// fact but don't insert it because otherwise it can
		// precede follow on [autoexec] calls.
		if (argument == "exit" || argument == "\"exit\"") {
			exit_call_exists = true;
			continue;
		}
		AddLine(Placement::CommandsBeforeSecureMode, argument);
	}

	// Check for the -exit switch, which indicates they want to quit
	const bool exit_arg_exists = arguments->exit;

	// Check if instant-launch is active
	const bool using_instant_launch_with_executable =
	        control->GetStartupVerbosity() == Verbosity::InstantLaunch &&
	        cmdline->HasExecutableName();

	// Should we add an 'exit' call to the end of autoexec.bat?
	const bool should_add_exit = exit_call_exists || exit_arg_exists ||
	                             using_instant_launch_with_executable;

	bool has_boot_image     = false;
	bool has_dir_or_command = false;

	std::string drive_c_directory = {};
	std::string cdrom_images      = {};

	unsigned int index = 1;
	while (cmdline->FindCommand(index++, argument)) {
		if (starts_with(argument, "-")) {
			LOG_WARNING("CONFIG: Illegal command line switch '%s'",
			            argument.c_str());
			continue;
		}

		// Check if argument is a file/directory
		std_fs::path path = argument;
		bool is_directory = std_fs::is_directory(path);
		if (!is_directory) {
			path = std_fs::current_path() / path;
			is_directory = std_fs::is_directory(path);
		}

		if (is_directory) {
			drive_c_directory = Quote + argument + Quote;
			has_dir_or_command = true;
			continue;
		}

		path = argument;

		// Retrieve file extension
		auto extension_ucase = path.extension().string();
		if (!extension_ucase.empty() && extension_ucase[0] == '.') {
			extension_ucase = extension_ucase.substr(1);
		}
		upcase(extension_ucase);

		// Check if argument is a batch file
		if (extension_ucase == "BAT") {
			ReMountDirAsDriveC(path.parent_path().string());
			// BATch files are called else exit will not work
			AddLine(Placement::CommandsAfterAutoexecSection,
			        CmdCall + path.filename().string());
			has_dir_or_command = true;
			break;
		}

		// Check if argument is a boot image file
		if (extension_ucase == "IMG" || extension_ucase == "IMA") {
			AddLine(Placement::CommandsAfterAutoexecSection,
			        CmdBoot + Quote + argument + Quote);
			has_boot_image     = true;
			has_dir_or_command = true;
			break;
		}

		// Check if argument is a CD image
		if (extension_ucase == "ISO" || extension_ucase == "CUE") {
			if (!cdrom_images.empty()) {
				cdrom_images += " ";
			}
			cdrom_images += Quote + argument + Quote;
			continue;
		}

		// Consider argument as executable
		ReMountDirAsDriveC(path.parent_path().string());
		AddLine(Placement::CommandsAfterAutoexecSection,
		        path.filename().string());
		has_dir_or_command = true;
		break;
	}

	// Mount drives

	if (!drive_c_directory.empty()) {
		AutoMountDriveC(drive_c_directory);
	}

	if (!cdrom_images.empty()) {
		AutoMountDriveD(cdrom_images);
	}

	// Fetch [autoexec] sections

	if (!has_option_no_autoexec) {
		if (should_join_autoexecs) {
			ProcessConfigFile(*static_cast<const Section_line*>(configuration),
			                  "one or more joined sections");
		} else if (!has_dir_or_command) {
			ProcessConfigFile(control->GetOverwrittenAutoexecSection(),
			                  control->GetOverwrittenAutoexecConf());
		} else {
			LOG_MSG("AUTOEXEC: Using commands provided on the command line");
		}
	}

	// Enable secure boot if needed

	if (has_option_securemode) {
		if (has_boot_image) {
			// Secure mode does not allow booting - so skip it
			LOG_WARNING("AUTOEXEC: Secure mode skipped as it does not allow booting");
		} else {
			AddLine(Placement::SecureModeCommand,
			        CmdConfig + "-securemode");
			// Safety measure to prevent malicious user from
			// interrupting AUTOEXEC.BAT execution until secure mode
			// is enabled
			KEYBOARD_WaitForSecureMode();
		}
	}

	// Add exit command if needed

	if (should_add_exit) {
		AddLine(Placement::CommandsAfterAutoexecSection, "@EXIT");
	}

	// Register the AUTOEXEC.BAT file if not already done
	AUTOEXEC_RegisterFile();
}

void AutoExecModule::ProcessConfigFile(const Section_line& section,
                                       const std::string& source_name)
{
	if (section.data.empty()) {
		return;
	}

	LOG_MSG("AUTOEXEC: Using autoexec from %s", source_name.c_str());

	auto check_echo_off = [](const std::string& line) {
		if (line.empty()) {
			return false;
		}

		std::string tmp = (line[0] == '@') ? line.substr(1) : line;
		if (tmp.length() < 8) {
			return false;
		}

		lowcase(tmp);
		if (tmp.substr(0, 4) != "echo" || !ends_with(tmp, "off")) {
			return false;
		}

		tmp = tmp.substr(4, tmp.length() - 7);
		for (const auto character : tmp) {
			if (!isspace(character)) {
				return false;
			}
		}

		return true;
	};

	std::istringstream input;
	input.str(section.data);

	std::string line = {};

	bool is_first_line = true;
	while (std::getline(input, line)) {
		trim(line);

		// If the first line is 'echo off' command, skip it and replace
		// with auto-generated one

		if (is_first_line) {
			is_first_line = false;
			if (check_echo_off(line)) {
				autoexec_has_echo_off = true;
				continue;
			}
		}

		AddLine(Placement::AutoexecSection, line);
	}
}

void AutoExecModule::AddLine(const Placement placement, const std::string& line)
{
	autoexec_lines[placement].push_back(line);
}

// Takes in a drive letter (eg: 'c') and attempts to mount the 'drives/c',
// extends system path if needed
void AutoExecModule::AutoMountDrive(const std::string& dir_letter)
{
	// Does drives/[x] exist?
	const auto drive_path = GetResourcePath("drives", dir_letter);
	if (!path_exists(drive_path)) {
		return;
	}

	// Try parsing the [x].conf file
	const auto conf_path  = drive_path.string() + ".conf";
	const auto [drive_letter, mount_args, path_val, is_verbose] =
	        parse_drive_conf(dir_letter, conf_path);

	// Install mount as an autoexec command
	AddLine(Placement::InitialAutogeneratedCommands,
	        CmdMount + drive_letter + " " + Quote +
	                simplify_path(drive_path).string() + Quote +
	                mount_args + (is_verbose ? "" : ToNul));

	// Install PATH as an autoexec command
	if (!path_val.empty()) {
		AddLine(Placement::InitialAutogeneratedCommands,
		        CmdSetPath + path_val);
	}
}

void AutoExecModule::AutoMountDriveC(const std::string& directory,
                                     const Placement placement)
{
	AddLine(placement, CmdMount + "-u C" + ToNul);
	if (directory.empty()) {
		AddLine(placement, CmdMount + "C ." + ToNul);
	} else {
		AddLine(placement, CmdMount + "C " + Quote + directory + Quote + ToNul);
	}
	AddLine(placement, CmdDriveC);
}

void AutoExecModule::AutoMountDriveD(const std::string& cdrom_images,
                                     const Placement placement)
{
	AddLine(placement, CmdMount + "-u D" + ToNul);
	AddLine(placement, CmdImgMount + "D " + cdrom_images + " -t iso" + ToNul);
}

void AutoExecModule::ReMountDirAsDriveC(const std::string& directory)
{
	AutoMountDriveC(directory, Placement::CommandsAfterAutoexecSection);
}

void AutoExecModule::AddMessages()
{
	MSG_Add("AUTOEXEC_BAT_GENERATED",
	        "generated from configuration and command line");
	MSG_Add("AUTOEXEC_BAT_CONFIG_SECTION", "from [autoexec] section");
}

void AUTOEXEC_NotifyNewCodePage()
{
	// No need to do anything during the shutdown or if Z:\AUTOEXEC.BAT file
	// does not exist yet
	if (shutdown_requested || !is_vfile_registered) {
		return;
	}

	// No need to do anything if the code page used by UTF-8 engine is still
	// the same as when Z:\AUTOEXEC.BAT was generated/refreshed
	const auto code_page = get_utf8_code_page();
	if (code_page == vfile_code_page) {
		return;
	}

	// Recreate the AUTOEXEC.BAT file as visible on DOS side
	create_autoexec_bat_dos(autoexec_bat_utf8, code_page);
}

void AUTOEXEC_SetVariable(const std::string& name, const std::string& value)
{
#if C_DEBUG
	if (!std::all_of(name.cbegin(), name.cend(), is_printable_ascii)) {
		E_Exit("AUTOEXEC: Variable name is not a printable ASCII");
	}
	if (!std::all_of(value.cbegin(), value.cend(), is_printable_ascii)) {
		E_Exit("AUTOEXEC: Variable value is not a printable ASCII");
	}
#endif

	auto name_upcase = name;
	upcase(name_upcase);

	// If shell is already running, refresh variable content
	if (first_shell) {
		first_shell->SetEnv(name_upcase.c_str(), value.c_str());
	}

	// Update our internal list of variables to set in AUTOEXEC.BAT
	if (value.empty()) {
		autoexec_variables.erase(name_upcase);
	} else {
		autoexec_variables[name_upcase] = value;
	}
}

void AUTOEXEC_RegisterFile()
{
	autoexec_bat_utf8 = create_autoexec_bat_utf8();
	create_autoexec_bat_dos(autoexec_bat_utf8, get_utf8_code_page());
}

static std::unique_ptr<AutoExecModule> autoexec_module{};

void AUTOEXEC_Init(Section* sec)
{
	autoexec_module = std::make_unique<AutoExecModule>(sec);
}
