/*
 * This file is part of OpenTTD.
 * OpenTTD 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.
 * OpenTTD 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 OpenTTD. If not, see <http://www.gnu.org/licenses/>.
 */

/** @file survey.cpp Functions to survey the current game / system, for crashlog and network-survey. */

#include "stdafx.h"

#include "survey.h"

#include "settings_table.h"
#include "network/network.h"
#include "rev.h"
#include "settings_type.h"
#include "timer/timer_game_tick.h"
#include "timer/timer_game_calendar.h"
#include "timer/timer_game_economy.h"
#include "3rdparty/fmt/ranges.h"

#include "currency.h"
#include "fontcache.h"
#include "language.h"

#include "ai/ai_info.hpp"
#include "game/game.hpp"
#include "game/game_info.hpp"

#include "music/music_driver.hpp"
#include "sound/sound_driver.hpp"
#include "video/video_driver.hpp"

#include "base_media_base.h"
#include "base_media_graphics.h"
#include "base_media_music.h"
#include "base_media_sounds.h"
#include "blitter/factory.hpp"

#include "social_integration.h"

#ifdef WITH_ALLEGRO
#	include <allegro.h>
#endif /* WITH_ALLEGRO */
#ifdef WITH_FONTCONFIG
#	include <fontconfig/fontconfig.h>
#endif /* WITH_FONTCONFIG */
#ifdef WITH_PNG
	/* pngconf.h, included by png.h doesn't like something in the
	 * freetype headers. As such it's not alphabetically sorted. */
#	include <png.h>
#endif /* WITH_PNG */
#ifdef WITH_FREETYPE
#	include <ft2build.h>
#	include FT_FREETYPE_H
#endif /* WITH_FREETYPE */
#ifdef WITH_HARFBUZZ
#	include <hb.h>
#endif /* WITH_HARFBUZZ */
#ifdef WITH_ICU_I18N
#	include <unicode/uversion.h>
#endif /* WITH_ICU_I18N */
#ifdef WITH_LIBLZMA
#	include <lzma.h>
#endif
#ifdef WITH_LZO
#include <lzo/lzo1x.h>
#endif
#ifdef WITH_SDL2
#include <SDL.h>
#endif /* WITH_SDL2 */
#ifdef WITH_ZLIB
# include <zlib.h>
#endif
#ifdef WITH_CURL
# include <curl/curl.h>
#endif

#include "safeguards.h"

NLOHMANN_JSON_SERIALIZE_ENUM(GRFStatus, {
	{GRFStatus::GCS_UNKNOWN, "unknown"},
	{GRFStatus::GCS_DISABLED, "disabled"},
	{GRFStatus::GCS_NOT_FOUND, "not found"},
	{GRFStatus::GCS_INITIALISED, "initialised"},
	{GRFStatus::GCS_ACTIVATED, "activated"},
})

NLOHMANN_JSON_SERIALIZE_ENUM(SocialIntegrationPlugin::State, {
	{SocialIntegrationPlugin::State::RUNNING, "running"},
	{SocialIntegrationPlugin::State::FAILED, "failed"},
	{SocialIntegrationPlugin::State::PLATFORM_NOT_RUNNING, "platform_not_running"},
	{SocialIntegrationPlugin::State::UNLOADED, "unloaded"},
	{SocialIntegrationPlugin::State::DUPLICATE, "duplicate"},
	{SocialIntegrationPlugin::State::UNSUPPORTED_API, "unsupported_api"},
	{SocialIntegrationPlugin::State::INVALID_SIGNATURE, "invalid_signature"},
})


/** Lookup table to convert a VehicleType to a string. */
static const std::string _vehicle_type_to_string[] = {
	"train",
	"roadveh",
	"ship",
	"aircraft",
};

/**
 * List of all the generic setting tables.
 *
 * There are a few tables that are special and not processed like the rest:
 * - _currency_settings
 * - _misc_settings
 * - _company_settings
 * - _win32_settings
 * As such, they are not part of this list.
 */
static auto &GenericSettingTables()
{
	static const SettingTable _generic_setting_tables[] = {
		_difficulty_settings,
		_economy_settings,
		_game_settings,
		_gui_settings,
		_linkgraph_settings,
		_locale_settings,
		_multimedia_settings,
		_network_settings,
		_news_display_settings,
		_pathfinding_settings,
		_script_settings,
		_world_settings,
	};
	return _generic_setting_tables;
}

/**
 * Convert a settings table to JSON.
 *
 * @param survey The JSON object.
 * @param table The settings table to convert.
 * @param object The object to get the settings from.
 * @param skip_if_default If true, skip any settings that are on their default value.
 */
static void SurveySettingsTable(nlohmann::json &survey, const SettingTable &table, void *object, bool skip_if_default)
{
	for (auto &desc : table) {
		const SettingDesc *sd = GetSettingDesc(desc);
		/* Skip any old settings we no longer save/load. */
		if (!SlIsObjectCurrentlyValid(sd->save.version_from, sd->save.version_to)) continue;

		const auto &name = sd->GetName();
		if (skip_if_default && sd->IsDefaultValue(object)) continue;
		survey[name] = sd->FormatValue(object);
	}
}

/**
 * Convert settings to JSON.
 *
 * @param survey The JSON object.
 */
void SurveySettings(nlohmann::json &survey, bool skip_if_default)
{
	SurveySettingsTable(survey, _misc_settings, nullptr, skip_if_default);
#if defined(_WIN32) && !defined(DEDICATED)
	SurveySettingsTable(survey, _win32_settings, nullptr, skip_if_default);
#endif
	for (auto &table : GenericSettingTables()) {
		SurveySettingsTable(survey, table, &_settings_game, skip_if_default);
	}
	SurveySettingsTable(survey, _currency_settings, &GetCustomCurrency(), skip_if_default);
	SurveySettingsTable(survey, _company_settings, &_settings_client.company, skip_if_default);
}

/**
 * Convert compiler information to JSON.
 *
 * @param survey The JSON object.
 */
void SurveyCompiler(nlohmann::json &survey)
{
#if defined(_MSC_VER)
	survey["name"] = "MSVC";
	survey["version"] = _MSC_VER;
#elif defined(__ICC) && defined(__GNUC__)
	survey["name"] = "ICC";
	survey["version"] = __ICC;
#	if defined(__GNUC__)
		survey["extra"] = fmt::format("GCC {}.{}.{} mode", __GNUC__, __GNUC_MINOR__, __GNUC_PATCHLEVEL__);
#	endif
#elif defined(__GNUC__)
	survey["name"] = "GCC";
	survey["version"] = fmt::format("{}.{}.{}", __GNUC__, __GNUC_MINOR__, __GNUC_PATCHLEVEL__);
#else
	survey["name"] = "unknown";
#endif

#if defined(__VERSION__)
	survey["extra"] = __VERSION__;
#endif
}

/**
 * Convert generic OpenTTD information to JSON.
 *
 * @param survey The JSON object.
 */
void SurveyOpenTTD(nlohmann::json &survey)
{
	survey["version"]["revision"] = std::string(_openttd_revision);
	survey["version"]["modified"] = _openttd_revision_modified;
	survey["version"]["tagged"] = _openttd_revision_tagged;
	survey["version"]["hash"] = std::string(_openttd_revision_hash);
	survey["version"]["newgrf"] = fmt::format("{:X}", _openttd_newgrf_version);
	survey["version"]["content"] = std::string(_openttd_content_version);
	survey["build_date"] = std::string(_openttd_build_date);
	survey["bits"] =
#ifdef POINTER_IS_64BIT
			64
#else
			32
#endif
		;
	if constexpr (std::endian::native == std::endian::little) survey["endian"] = "little";
	if constexpr (std::endian::native == std::endian::big) survey["endian"] = "big";
	survey["dedicated_build"] =
#ifdef DEDICATED
			"yes"
#else
			"no"
#endif
		;
}

/**
 * Convert game session information to JSON.
 *
 * @param survey The JSON object.
 */
void SurveyGameSession(nlohmann::json &survey)
{
	survey["id"] = _game_session_stats.savegame_id;
	survey["seconds"] = std::chrono::duration_cast<std::chrono::seconds>(std::chrono::steady_clock::now() - _game_session_stats.start_time).count();
	if (_game_session_stats.savegame_size.has_value()) {
		survey["savegame_size"] = _game_session_stats.savegame_size.value();
	}
}

/**
 * Convert generic game information to JSON.
 *
 * @param survey The JSON object.
 */
void SurveyConfiguration(nlohmann::json &survey)
{
	survey["network"] = _networking ? (_network_server ? "server" : "client") : "no";
	if (_current_language != nullptr) {
		survey["language"]["filename"] = FS2OTTD(_current_language->file.filename().native());
		survey["language"]["name"] = _current_language->name;
		survey["language"]["isocode"] = _current_language->isocode;
	}
	if (BlitterFactory::GetCurrentBlitter() != nullptr) {
		survey["blitter"] = BlitterFactory::GetCurrentBlitter()->GetName();
	}
	if (MusicDriver::GetInstance() != nullptr) {
		survey["music_driver"] = MusicDriver::GetInstance()->GetName();
	}
	if (SoundDriver::GetInstance() != nullptr) {
		survey["sound_driver"] = SoundDriver::GetInstance()->GetName();
	}
	if (VideoDriver::GetInstance() != nullptr) {
		survey["video_driver"] = VideoDriver::GetInstance()->GetName();
		survey["video_info"] = VideoDriver::GetInstance()->GetInfoString();
	}
	if (BaseGraphics::GetUsedSet() != nullptr) {
		survey["graphics_set"] = fmt::format("{}.{}", BaseGraphics::GetUsedSet()->name, fmt::join(BaseGraphics::GetUsedSet()->version, "."));
		const GRFConfig *extra_cfg = BaseGraphics::GetUsedSet()->GetExtraConfig();
		if (extra_cfg != nullptr && !extra_cfg->param.empty()) {
			survey["graphics_set_parameters"] = std::span<const uint32_t>(extra_cfg->param);
		} else {
			survey["graphics_set_parameters"] = std::span<const uint32_t>();
		}
	}
	if (BaseMusic::GetUsedSet() != nullptr) {
		survey["music_set"] = fmt::format("{}.{}", BaseMusic::GetUsedSet()->name, fmt::join(BaseMusic::GetUsedSet()->version, "."));
	}
	if (BaseSounds::GetUsedSet() != nullptr) {
		survey["sound_set"] = fmt::format("{}.{}", BaseSounds::GetUsedSet()->name, fmt::join(BaseSounds::GetUsedSet()->version, "."));
	}
}

/**
 * Convert font information to JSON.
 *
 * @param survey The JSON object.
 */
void SurveyFont(nlohmann::json &survey)
{
	survey["small"] = FontCache::Get(FS_SMALL)->GetFontName();
	survey["medium"] = FontCache::Get(FS_NORMAL)->GetFontName();
	survey["large"] = FontCache::Get(FS_LARGE)->GetFontName();
	survey["mono"] = FontCache::Get(FS_MONO)->GetFontName();
}

/**
 * Convert company information to JSON.
 *
 * @param survey The JSON object.
 */
void SurveyCompanies(nlohmann::json &survey)
{
	for (const Company *c : Company::Iterate()) {
		auto &company = survey[fmt::format("{}", c->index.base())];
		if (c->ai_info == nullptr) {
			company["type"] = "human";
		} else {
			company["type"] = "ai";
			company["script"] = fmt::format("{}.{}", c->ai_info->GetName(), c->ai_info->GetVersion());
		}

		for (VehicleType type = VEH_BEGIN; type < VEH_COMPANY_END; type++) {
			uint amount = c->group_all[type].num_vehicle;
			company["vehicles"][_vehicle_type_to_string[type]] = amount;
		}

		company["infrastructure"]["road"] = c->infrastructure.GetRoadTotal();
		company["infrastructure"]["tram"] = c->infrastructure.GetTramTotal();
		company["infrastructure"]["rail"] = c->infrastructure.GetRailTotal();
		company["infrastructure"]["signal"] = c->infrastructure.signal;
		company["infrastructure"]["water"] = c->infrastructure.water;
		company["infrastructure"]["station"] = c->infrastructure.station;
		company["infrastructure"]["airport"] = c->infrastructure.airport;
	}
}

/**
 * Convert timer information to JSON.
 *
 * @param survey The JSON object.
 */
void SurveyTimers(nlohmann::json &survey)
{
	survey["ticks"] = TimerGameTick::counter;

	TimerGameEconomy::YearMonthDay economy_ymd = TimerGameEconomy::ConvertDateToYMD(TimerGameEconomy::date);
	survey["economy"] = fmt::format("{:04}-{:02}-{:02} ({})", economy_ymd.year, economy_ymd.month + 1, economy_ymd.day, TimerGameEconomy::date_fract);

	TimerGameCalendar::YearMonthDay ymd = TimerGameCalendar::ConvertDateToYMD(TimerGameCalendar::date);
	survey["calendar"] = fmt::format("{:04}-{:02}-{:02} ({})", ymd.year, ymd.month + 1, ymd.day, TimerGameCalendar::date_fract);
}

/**
 * Convert GRF information to JSON.
 *
 * @param survey The JSON object.
 */
void SurveyGrfs(nlohmann::json &survey)
{
	for (const auto &c : _grfconfig) {
		auto grfid = fmt::format("{:08x}", std::byteswap(c->ident.grfid));
		auto &grf = survey[std::move(grfid)];

		grf["md5sum"] = FormatArrayAsHex(c->ident.md5sum);
		grf["status"] = c->status;

		if ((c->palette & GRFP_GRF_MASK) == GRFP_GRF_UNSET) grf["palette"] = "unset";
		if ((c->palette & GRFP_GRF_MASK) == GRFP_GRF_DOS) grf["palette"] = "dos";
		if ((c->palette & GRFP_GRF_MASK) == GRFP_GRF_WINDOWS) grf["palette"] = "windows";
		if ((c->palette & GRFP_GRF_MASK) == GRFP_GRF_ANY) grf["palette"] = "any";

		if ((c->palette & GRFP_BLT_MASK) == GRFP_BLT_UNSET) grf["blitter"] = "unset";
		if ((c->palette & GRFP_BLT_MASK) == GRFP_BLT_32BPP) grf["blitter"] = "32bpp";

		grf["is_static"] = c->flags.Test(GRFConfigFlag::Static);
		grf["parameters"] = std::span<const uint32_t>(c->param);
	}
}

/**
 * Convert game-script information to JSON.
 *
 * @param survey The JSON object.
 */
void SurveyGameScript(nlohmann::json &survey)
{
	if (Game::GetInfo() == nullptr) return;

	survey = fmt::format("{}.{}", Game::GetInfo()->GetName(), Game::GetInfo()->GetVersion());
}

/**
 * Convert compiled libraries information to JSON.
 *
 * @param survey The JSON object.
 */
void SurveyLibraries(nlohmann::json &survey)
{
#ifdef WITH_ALLEGRO
	survey["allegro"] = std::string(allegro_id);
#endif /* WITH_ALLEGRO */

#ifdef WITH_FONTCONFIG
	int version = FcGetVersion();
	survey["fontconfig"] = fmt::format("{}.{}.{}", version / 10000, (version / 100) % 100, version % 100);
#endif /* WITH_FONTCONFIG */

#ifdef WITH_FREETYPE
	FT_Library library;
	int major, minor, patch;
	FT_Init_FreeType(&library);
	FT_Library_Version(library, &major, &minor, &patch);
	FT_Done_FreeType(library);
	survey["freetype"] = fmt::format("{}.{}.{}", major, minor, patch);
#endif /* WITH_FREETYPE */

#if defined(WITH_HARFBUZZ)
	survey["harfbuzz"] = hb_version_string();
#endif /* WITH_HARFBUZZ */

#if defined(WITH_ICU_I18N)
	/* 4 times 0-255, separated by dots (.) and a trailing '\0' */
	char buf[4 * 3 + 3 + 1];
	UVersionInfo ver;
	u_getVersion(ver);
	u_versionToString(ver, buf);
	survey["icu_i18n"] = buf;
#endif /* WITH_ICU_I18N */

#ifdef WITH_LIBLZMA
	survey["lzma"] = lzma_version_string();
#endif

#ifdef WITH_LZO
	survey["lzo"] = lzo_version_string();
#endif

#ifdef WITH_PNG
	survey["png"] = png_get_libpng_ver(nullptr);
#endif /* WITH_PNG */

#ifdef WITH_SDL2
	SDL_version sdl2_v;
	SDL_GetVersion(&sdl2_v);
	survey["sdl2"] = fmt::format("{}.{}.{}", sdl2_v.major, sdl2_v.minor, sdl2_v.patch);
#endif

#ifdef WITH_ZLIB
	survey["zlib"] = zlibVersion();
#endif

#ifdef WITH_CURL
	auto *curl_v = curl_version_info(CURLVERSION_NOW);
	survey["curl"] = curl_v->version;
	survey["curl_ssl"] = curl_v->ssl_version == nullptr ? "none" : curl_v->ssl_version;
#endif
}

/**
 * Convert plugin information to JSON.
 *
 * @param survey The JSON object.
 */
void SurveyPlugins(nlohmann::json &survey)
{
	auto _plugins = SocialIntegration::GetPlugins();

	for (auto &plugin : _plugins) {
		auto &platform = survey[plugin->social_platform];
		platform.push_back({
			{"name", plugin->name},
			{"version", plugin->version},
			{"basepath", plugin->basepath},
			{"state", plugin->state},
		});
	}
}

/**
 * Change the bytes of memory into a textual version rounded up to the biggest unit.
 *
 * For example, 16751108096 would become 16 GiB.
 *
 * @param memory The bytes of memory.
 * @return std::string A textual representation.
 */
std::string SurveyMemoryToText(uint64_t memory)
{
	memory = memory / 1024; // KiB
	memory = CeilDiv(memory, 1024); // MiB

	/* Anything above 512 MiB we represent in GiB. */
	if (memory > 512) {
		return fmt::format("{} GiB", CeilDiv(memory, 1024));
	}

	/* Anything above 64 MiB we represent in a multiplier of 128 MiB. */
	if (memory > 64) {
		return fmt::format("{} MiB", Ceil(memory, 128));
	}

	/* Anything else in a multiplier of 4 MiB. */
	return fmt::format("{} MiB", Ceil(memory, 4));
}
