/* ScummVM - Graphic Adventure Engine
 *
 * ScummVM is the legal property of its developers, whose names
 * are too numerous to list here. Please refer to the COPYRIGHT
 * file distributed with this source distribution.
 *
 * 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 3 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, see <http://www.gnu.org/licenses/>.
 *
 */

//
// Game configuration
//
#include "ags/engine/ac/game_setup.h"
#include "ags/shared/ac/game_setup_struct.h"
#include "ags/engine/ac/global_translation.h"
#include "ags/engine/ac/path_helper.h"
#include "ags/shared/ac/sprite_cache.h"
#include "ags/engine/ac/system.h"
#include "ags/shared/core/platform.h"
#include "ags/engine/debugging/debugger.h"
#include "ags/engine/debugging/debug_log.h"
#include "ags/engine/device/mouse_w32.h"
#include "ags/engine/main/config.h"
#include "ags/engine/media/audio/audio_system.h"
#include "ags/engine/platform/base/ags_platform_driver.h"
#include "ags/shared/util/directory.h"
#include "ags/shared/util/ini_util.h"
#include "ags/shared/util/text_stream_reader.h"
#include "ags/shared/util/path.h"
#include "ags/shared/util/string_utils.h"
#include "ags/metaengine.h"
#include "common/config-manager.h"
#include "common/language.h"

namespace AGS3 {

using namespace AGS::Shared;
using namespace AGS::Engine;

// Filename of the default config file, the one found in the game installation
const char *DefaultConfigFileName = "acsetup.cfg";

WindowSetup parse_window_mode(const String &option, bool as_windowed, WindowSetup def_value) {
	// "full_window" option means pseudo fullscreen ("borderless fullscreen window")
	if (!as_windowed && (option.CompareNoCase("full_window") == 0))
		return WindowSetup(kWnd_FullDesktop);
	// Check supported options for explicit resolution or scale factor,
	// in which case we'll use either a resizing window or a REAL fullscreen mode
	const WindowMode exp_wmode = as_windowed ? kWnd_Windowed : kWnd_Fullscreen;
	// Note that for "desktop" we return "default" for windowed, this will result
	// in referring to the  desktop size but resizing in accordance to the scaling style
	if (option.CompareNoCase("desktop") == 0)
		return as_windowed ? WindowSetup(exp_wmode) : WindowSetup(get_desktop_size(), exp_wmode);
	// "Native" means using game resolution as a window size
	if (option.CompareNoCase("native") == 0)
		return WindowSetup(_GP(game).GetGameRes(), exp_wmode);
	// Try parse an explicit resolution type or game scale factor --
	size_t at = option.FindChar('x');
	if (at == 0) { // try parse as a scale (xN)
		int scale = StrUtil::StringToInt(option.Mid(1));
		if (scale > 0) return WindowSetup(scale, exp_wmode);
	} else if (at != String::NoIndex) {
		// else try parse as a "width x height"
		Size sz = Size(StrUtil::StringToInt(option.Mid(0, at)),
			StrUtil::StringToInt(option.Mid(at + 1)));
		if (!sz.IsNull()) return WindowSetup(sz, exp_wmode);
	}
	// In case of "default" option, or any format mistake, return the default
	return def_value;
}

// Legacy screen size definition
enum ScreenSizeDefinition {
	kScreenDef_Undefined = -1,
	kScreenDef_Explicit,        // define by width & height
	kScreenDef_ByGameScaling,   // define by game scale factor
	kScreenDef_MaxDisplay,      // set to maximal supported (desktop/device screen size)
	kNumScreenDef
};

static ScreenSizeDefinition parse_legacy_screendef(const String &option) {
	const char *screen_sz_def_options[kNumScreenDef] = { "explicit", "scaling", "max" };
	for (int i = 0; i < kNumScreenDef; ++i) {
		if (option.CompareNoCase(screen_sz_def_options[i]) == 0) {
			return (ScreenSizeDefinition)i;
		}
	}
	return kScreenDef_Undefined;
}

FrameScaleDef parse_scaling_option(const String &option, FrameScaleDef def_value) {
	if (option.CompareNoCase("round") == 0 || option.CompareNoCase("max_round") == 0)
		return kFrame_Round;
	if (option.CompareNoCase("stretch") == 0)
		return kFrame_Stretch;
	if (option.CompareNoCase("proportional") == 0)
		return kFrame_Proportional;
	return def_value;
}

static FrameScaleDef parse_legacy_scaling_option(const String &option, int &scale) {
	FrameScaleDef frame = parse_scaling_option(option, kFrame_Undefined);
	if (frame == kFrame_Undefined) {
		scale = StrUtil::StringToInt(option);
		return scale > 0 ? kFrame_Round : kFrame_Undefined;
	}
	return frame;
}

// Parses legacy filter ID and converts it into current scaling options
bool parse_legacy_frame_config(const String &scaling_option, String &filter_id,
	FrameScaleDef &frame, int &scale_factor) {
	struct {
		String LegacyName;
		String CurrentName;
		int    Scaling;
	} legacy_filters[6] = { {"none", "none", -1}, {"max", "StdScale", 0}, {"StdScale", "StdScale", -1},
						   {"AAx", "Linear", -1}, {"Hq2x", "Hqx", 2}, {"Hq3x", "Hqx", 3} };

	for (int i = 0; i < 6; i++) {
		if (scaling_option.CompareLeftNoCase(legacy_filters[i].LegacyName) == 0) {
			filter_id = legacy_filters[i].CurrentName;
			frame = kFrame_Round;
			scale_factor = legacy_filters[i].Scaling >= 0 ? legacy_filters[i].Scaling :
				scaling_option.Mid(legacy_filters[i].LegacyName.GetLength()).ToInt();
			return true;
		}
	}
	return false;
}

String make_scaling_option(FrameScaleDef scale_def) {
	switch (scale_def) {
	case kFrame_Stretch:
		return "stretch";
	case kFrame_Proportional:
		return "proportional";
	default:
		return "round";
	}
}

uint32_t convert_scaling_to_fp(int scale_factor) {
	if (scale_factor >= 0)
		return scale_factor <<= kShift;
	else
		return kUnit / abs(scale_factor);
}

int convert_fp_to_scaling(uint32_t scaling) {
	if (scaling == 0)
		return 0;
	return scaling >= kUnit ? (scaling >> kShift) : -kUnit / (int32_t)scaling;
}

String find_default_cfg_file() {
	return Path::ConcatPaths(_GP(usetup).startup_dir, DefaultConfigFileName);
}

String find_user_global_cfg_file() {
	return Path::ConcatPaths(GetGlobalUserConfigDir().FullDir, DefaultConfigFileName);
}

String find_user_cfg_file() {
	return Path::ConcatPaths(GetGameUserConfigDir().FullDir, DefaultConfigFileName);
}

void config_defaults() {
#if AGS_PLATFORM_OS_WINDOWS
	_GP(usetup).Screen.DriverID = "D3D9";
#else
	_GP(usetup).Screen.DriverID = "OGL";
#endif
	// Defaults for the window style are max resizing window and "fullscreen desktop"
	_GP(usetup).Screen.FsSetup = WindowSetup(kWnd_FullDesktop);
	_GP(usetup).Screen.WinSetup = WindowSetup(kWnd_Windowed);
}

static void read_legacy_graphics_config(const ConfigTree &cfg) {
	// Pre-3.* game resolution setup
	int default_res = CfgReadInt(cfg, "misc", "defaultres", kGameResolution_Default);
	int screen_res = CfgReadInt(cfg, "misc", "screenres", 0);
	if (screen_res > 0 &&
		(default_res >= kGameResolution_Default && default_res <= kGameResolution_320x240)) {
		_GP(usetup).override_upscale = true; // run low-res game in high-res mode
	}

	_GP(usetup).Screen.Windowed = CfgReadBoolInt(cfg, "misc", "windowed");
	_GP(usetup).Screen.DriverID = CfgReadString(cfg, "misc", "gfxdriver", _GP(usetup).Screen.DriverID);

	// Window setup: style and size definition, game frame style
	{
		String legacy_filter = CfgReadString(cfg, "misc", "gfxfilter");
		if (!legacy_filter.IsEmpty()) {
			// Legacy scaling config is applied only to windowed setting
			int scale_factor = 0;
			parse_legacy_frame_config(legacy_filter, _GP(usetup).Screen.Filter.ID, _GP(usetup).Screen.WinGameFrame,
				scale_factor);
			if (scale_factor > 0)
				_GP(usetup).Screen.WinSetup = WindowSetup(scale_factor);

			// AGS 3.2.1 and 3.3.0 aspect ratio preferences for fullscreen
			if (!_GP(usetup).Screen.Windowed) {
				bool allow_borders =
					(CfgReadBoolInt(cfg, "misc", "sideborders") || CfgReadBoolInt(cfg, "misc", "forceletterbox") ||
					CfgReadBoolInt(cfg, "misc", "prefer_sideborders") || CfgReadBoolInt(cfg, "misc", "prefer_letterbox"));
				_GP(usetup).Screen.FsGameFrame = allow_borders ? kFrame_Proportional : kFrame_Stretch;
			}
		}

		// AGS 3.4.0 - 3.4.1-rc uniform scaling option
		String uniform_frame_scale = CfgReadString(cfg, "graphics", "game_scale");
		if (!uniform_frame_scale.IsEmpty()) {
			int src_scale = 1;
			FrameScaleDef frame = parse_legacy_scaling_option(uniform_frame_scale, src_scale);
			_GP(usetup).Screen.FsGameFrame = frame;
			_GP(usetup).Screen.WinGameFrame = frame;
		}

		// AGS 3.5.* gfx mode with screen definition
		const bool is_windowed = CfgReadBoolInt(cfg, "graphics", "windowed");
		WindowSetup &ws = is_windowed ? _GP(usetup).Screen.WinSetup : _GP(usetup).Screen.FsSetup;
		const WindowMode wm = is_windowed ? kWnd_Windowed : kWnd_Fullscreen;
		ScreenSizeDefinition scr_def = parse_legacy_screendef(CfgReadString(cfg, "graphics", "screen_def"));
		switch (scr_def) {
		case kScreenDef_Explicit:
		{
			Size sz(
				CfgReadInt(cfg, "graphics", "screen_width"),
				CfgReadInt(cfg, "graphics", "screen_height"));
			ws = WindowSetup(sz, wm);
		}
		break;
		case kScreenDef_ByGameScaling:
		{
			int src_scale = 0;
			is_windowed ?
				parse_legacy_scaling_option(CfgReadString(cfg, "graphics", "game_scale_win"), src_scale) :
				parse_legacy_scaling_option(CfgReadString(cfg, "graphics", "game_scale_fs"), src_scale);
			ws = WindowSetup(src_scale, wm);
		}
		break;
		case kScreenDef_MaxDisplay:
			ws = is_windowed ? WindowSetup() : WindowSetup(kWnd_FullDesktop);
			break;
		default:
			break;
		}
	}

	_GP(usetup).Screen.Params.RefreshRate = CfgReadInt(cfg, "misc", "refresh");
	_GP(usetup).enable_antialiasing = CfgReadBoolInt(cfg, "misc", "antialias");
}

static void read_legacy_config(const ConfigTree &cfg) {
	read_legacy_graphics_config(cfg);

	_GP(usetup).SpriteCacheSize = CfgReadInt(cfg, "misc", "cachemax", _GP(usetup).SpriteCacheSize);
}

void override_config_ext(ConfigTree &cfg) {
	_G(platform)->ReadConfiguration(cfg);
}

void apply_config(const ConfigTree &cfg) {
	// Legacy settings have to be translated into new options;
	// they must be read first, to let newer options override them, if ones are present
	read_legacy_config(cfg);

	{
		// Audio options
		_GP(usetup).audio_enabled = CfgReadBoolInt(cfg, "sound", "enabled", _GP(usetup).audio_enabled);
		_GP(usetup).audio_driver = CfgReadString(cfg, "sound", "driver");
		// This option is backwards (usevox is 0 if no_speech_pack)
		_GP(usetup).no_speech_pack = !CfgReadBoolInt(cfg, "sound", "usespeech", true);

		// Graphics mode and options
		_GP(usetup).Screen.DriverID = CfgReadString(cfg, "graphics", "driver", _GP(usetup).Screen.DriverID);
		_GP(usetup).Screen.Windowed = CfgReadBoolInt(cfg, "graphics", "windowed", _GP(usetup).Screen.Windowed);
		_GP(usetup).Screen.FsSetup =
			parse_window_mode(CfgReadString(cfg, "graphics", "fullscreen", "default"), false, _GP(usetup).Screen.FsSetup);
		_GP(usetup).Screen.WinSetup =
			parse_window_mode(CfgReadString(cfg, "graphics", "window", "default"), true, _GP(usetup).Screen.WinSetup);

		_GP(usetup).Screen.Filter.ID = CfgReadString(cfg, "graphics", "filter", "StdScale");
		_GP(usetup).Screen.FsGameFrame =
			parse_scaling_option(CfgReadString(cfg, "graphics", "game_scale_fs", "proportional"), _GP(usetup).Screen.FsGameFrame);
		_GP(usetup).Screen.WinGameFrame =
			parse_scaling_option(CfgReadString(cfg, "graphics", "game_scale_win", "round"), _GP(usetup).Screen.WinGameFrame);

		_GP(usetup).Screen.Params.RefreshRate = CfgReadInt(cfg, "graphics", "refresh");

		// Use ScummVM options to set the vsync flag, if available
		if (ConfMan.hasKey("vsync"))
			_GP(usetup).Screen.Params.VSync = ConfMan.getBool("vsync");
		else
			_GP(usetup).Screen.Params.VSync = CfgReadBoolInt(cfg, "graphics", "vsync");

		_GP(usetup).RenderAtScreenRes = CfgReadBoolInt(cfg, "graphics", "render_at_screenres");
		_GP(usetup).enable_antialiasing = CfgReadBoolInt(cfg, "graphics", "antialias");
		_GP(usetup).software_render_driver = CfgReadString(cfg, "graphics", "software_driver");

#ifdef TODO
		_GP(usetup).rotation = (ScreenRotation)CfgReadInt(cfg, "graphics", "rotation", _GP(usetup).rotation);
		String rotation_str = CfgReadString(cfg, "graphics", "rotation", "unlocked");
		_GP(usetup).rotation = StrUtil::ParseEnum<ScreenRotation>(
			rotation_str, CstrArr<kNumScreenRotationOptions>{ "unlocked", "portrait", "landscape" },
			_GP(usetup).rotation);
#endif

		// Custom paths
		_GP(usetup).load_latest_save = CfgReadBoolInt(cfg, "misc", "load_latest_save", _GP(usetup).load_latest_save);
		_GP(usetup).user_data_dir = CfgReadString(cfg, "misc", "user_data_dir");
		_GP(usetup).shared_data_dir = CfgReadString(cfg, "misc", "shared_data_dir");
		_GP(usetup).show_fps = CfgReadBoolInt(cfg, "misc", "show_fps");

		// Translation / localization
		Common::String translation;

		if (!ConfMan.get("language").empty() && ConfMan.isKeyTemporary("language")) {
			// Map the language defined in the command-line "language" option to its description
			Common::Language lang = Common::parseLanguage(ConfMan.get("language"));

			if (lang != Common::Language::UNK_LANG) {
				Common::String translationCode = Common::getLanguageCode(lang);
				translationCode.toLowercase();
				translation = Common::getLanguageDescription(lang);
				translation.toLowercase();

				// Check if the game actually has such a translation, and set it if it does
				// The name of translation files can be anything, but in general they are one of:
				// - English name of the language, for example French.tra or Spanish.tra (covered)
				// - Translated name of the language, for example polsky.tra or francais.tra (not covered)
				// - The language code, for example FR.tra or DE.tra (covered)
				// - And these can be combined with a prefix or suffix, for example Nelly_Polish.tra, english2.tra (covered)
				Common::StringArray traFileNames = AGSMetaEngine::getGameTranslations(ConfMan.getActiveDomainName());
				for (Common::StringArray::iterator iter = traFileNames.begin(); iter != traFileNames.end(); ++iter) {
					Common::String traFileName = *iter;
					traFileName.toLowercase();
					if (traFileName.contains(translation) || traFileName.equals(translationCode)) {
						_GP(usetup).translation = *iter;
						break;
					}
				}
			}
		} else if (ConfMan.getActiveDomain()->tryGetVal("translation", translation) && !translation.empty())
			_GP(usetup).translation = translation;
		else
			_GP(usetup).translation = CfgReadString(cfg, "language", "translation");

		// Resource caches and options
		_GP(usetup).clear_cache_on_room_change = CfgReadBoolInt(cfg, "misc", "clear_cache_on_room_change", _GP(usetup).clear_cache_on_room_change);
		_GP(usetup).SpriteCacheSize = CfgReadInt(cfg, "graphics", "sprite_cache_size", _GP(usetup).SpriteCacheSize);
		_GP(usetup).TextureCacheSize = CfgReadInt(cfg, "graphics", "texture_cache_size", _GP(usetup).TextureCacheSize);

		// Mouse options
		_GP(usetup).mouse_auto_lock = CfgReadBoolInt(cfg, "mouse", "auto_lock");
		_GP(usetup).mouse_speed = CfgReadFloat(cfg, "mouse", "speed", 1.f);
		if (_GP(usetup).mouse_speed <= 0.f)
			_GP(usetup).mouse_speed = 1.f;
		const char *mouse_ctrl_options[kNumMouseCtrlOptions] = { "never", "fullscreen", "always" };
		String mouse_str = CfgReadString(cfg, "mouse", "control_when", "fullscreen");
		for (int i = 0; i < kNumMouseCtrlOptions; ++i) {
			if (mouse_str.CompareNoCase(mouse_ctrl_options[i]) == 0) {
				_GP(usetup).mouse_ctrl_when = (MouseControlWhen)i;
				break;
			}
		}
		_GP(usetup).mouse_ctrl_enabled = CfgReadBoolInt(cfg, "mouse", "control_enabled", _GP(usetup).mouse_ctrl_enabled);
		const char *mouse_speed_options[kNumMouseSpeedDefs] = { "absolute", "current_display" };
		mouse_str = CfgReadString(cfg, "mouse", "speed_def", "current_display");
		for (int i = 0; i < kNumMouseSpeedDefs; ++i) {
			if (mouse_str.CompareNoCase(mouse_speed_options[i]) == 0) {
				_GP(usetup).mouse_speed_def = (MouseSpeedDef)i;
				break;
			}
		}

		// Various system options
		_GP(usetup).multitasking = CfgReadInt(cfg, "misc", "background", 0) != 0;

		// User's overrides and hacks
		_GP(usetup).override_multitasking = CfgReadInt(cfg, "override", "multitasking", -1);
		_GP(usetup).override_script_os = -1;
		// Looks for the existence of the Linux executable
		if (File::IsFile(Path::ConcatPaths(_GP(usetup).startup_dir, "ags64"))) {
			_GP(usetup).override_script_os = eOS_Linux;
		}
		String override_os = CfgReadString(cfg, "override", "os");
		if (override_os.CompareNoCase("dos") == 0) {
			_GP(usetup).override_script_os = eOS_DOS;
		} else if (override_os.CompareNoCase("win") == 0) {
			_GP(usetup).override_script_os = eOS_Win;
		} else if (override_os.CompareNoCase("linux") == 0) {
			_GP(usetup).override_script_os = eOS_Linux;
		} else if (override_os.CompareNoCase("mac") == 0) {
			_GP(usetup).override_script_os = eOS_Mac;
		}
		_GP(usetup).override_upscale = CfgReadBoolInt(cfg, "override", "upscale", _GP(usetup).override_upscale);
		_GP(usetup).legacysave_assume_dataver = static_cast<GameDataVersion>(CfgReadInt(cfg, "override", "legacysave_assume_dataver", kGameVersion_Undefined));
		_GP(usetup).legacysave_let_gui_diff = CfgReadBoolInt(cfg, "override", "legacysave_let_gui_diff");
		_GP(usetup).key_save_game = CfgReadInt(cfg, "override", "save_game_key", 0);
		_GP(usetup).key_restore_game = CfgReadInt(cfg, "override", "restore_game_key", 0);
	}

	// Apply logging configuration
	apply_debug_config(cfg);
}

void post_config() {
	if (_GP(usetup).Screen.DriverID.IsEmpty() || _GP(usetup).Screen.DriverID.CompareNoCase("DX5") == 0)
		_GP(usetup).Screen.DriverID = "Software";

	// FIXME: this correction is needed at the moment because graphics driver
	// implementation requires some filter to be created anyway
	_GP(usetup).Screen.Filter.UserRequest = _GP(usetup).Screen.Filter.ID;
	if (_GP(usetup).Screen.Filter.ID.IsEmpty() || _GP(usetup).Screen.Filter.ID.CompareNoCase("none") == 0) {
		_GP(usetup).Screen.Filter.ID = "StdScale";
	}
}

void save_config_file() {
	// Translation / localization
	if (!_GP(usetup).translation.IsEmpty()) {
		ConfMan.getActiveDomain()->setVal("translation", _GP(usetup).translation.GetCStr());
		ConfMan.flushToDisk();
	} else if (ConfMan.getActiveDomain()->contains("translation")) {
		ConfMan.getActiveDomain()->erase("translation");
		ConfMan.flushToDisk();
	}

	// ScummVM doesn't write out other configuration changes
}

} // namespace AGS3
