// configfile.cpp
//
// Copyright (C) 2001-2009, the Celestia Development 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.

#include "configfile.h"

#include <config.h>
#include <cmath>
#include <fstream>
#include <type_traits>

#include <celengine/texmanager.h>
#include <celutil/associativearray.h>
#include <celutil/fsutils.h>
#include <celutil/logger.h>
#include <celutil/parser.h>
#include <celutil/stringutils.h>
#include <celutil/tokenizer.h>

using namespace std::string_view_literals;

namespace engine = celestia::engine;
namespace util = celestia::util;

using util::AssociativeArray;
using util::GetLogger;

namespace
{

void
applyBoolean(bool& target, const AssociativeArray& hash, std::string_view key)
{
    auto b = hash.getBoolean(key);
    if (b.has_value())
        target = *b;
}

template<typename T, std::enable_if_t<std::is_arithmetic_v<T>, int> = 0>
void
applyNumber(T& target, const AssociativeArray& hash, std::string_view key)
{
    auto number = hash.getNumber<T>(key);
    if (number.has_value())
        target = *number;
}

void
applyString(std::string& target, const AssociativeArray& hash, std::string_view key)
{
    auto str = hash.getString(key);
    if (str != nullptr)
        target = *str;
}

void
applyToneMappingMode(ToneMappingMode& target, const AssociativeArray& hash, std::string_view key)
{
    auto str = hash.getString(key);
    if (str == nullptr)
        return;
    if (compareIgnoringCase(*str, "Off") == 0)
        target = ToneMappingMode::None;
    else if (compareIgnoringCase(*str, "Manual") == 0)
        target = ToneMappingMode::Manual;
    else
        GetLogger()->error("Unknown {} value '{}'.\n", key, *str);
}

void
applyPath(std::filesystem::path& target, const AssociativeArray& hash, std::string_view key)
{
    auto path = hash.getPath(key);
    if (path.has_value())
        target = *path;
}

void
applyTexture(util::TextureHandle& target,
             const AssociativeArray& hash,
             std::string_view key,
             engine::TexturePaths& texturePaths)
{
    auto source = hash.getPath(key);
    if (source.has_value())
        target = texturePaths.getHandle(*source, {}, engine::TextureFlags::WrapTexture);
}

void
applyStringArray(std::vector<std::string>& target, const AssociativeArray& hash, std::string_view key)
{
    auto value = hash.getValue(key);
    if (value == nullptr)
        return;

    if (auto array = value->getArray(); array != nullptr)
    {
        for (const auto& item : *array)
        {
            auto itemStr = item.getString();
            if (itemStr == nullptr)
            {
                GetLogger()->error("Found non-string value in {} array.\n", key);
                break;
            }

            target.emplace_back(*itemStr);
        }
    }
    else
        GetLogger()->error("{} must be an array of strings.\n", key);
}

void
applyPathArray(std::vector<std::filesystem::path>& target, const AssociativeArray& hash, std::string_view key)
{
    auto value = hash.getValue(key);
    if (value == nullptr)
        return;

    if (auto array = value->getArray(); array != nullptr)
    {
        for (const auto& item : *array)
        {
            auto itemStr = item.getString();
            if (itemStr == nullptr)
            {
                GetLogger()->error("Found non-string value in {} array.\n", key);
                break;
            }

            target.emplace_back(celestia::util::PathExp(std::filesystem::u8path(*itemStr)));
        }
    }
    else if (auto str = value->getString(); str != nullptr)
        target.emplace_back(celestia::util::PathExp(std::filesystem::u8path(*str)));
    else
        GetLogger()->error("{} must be a string or an array of strings.\n", key);
}

void
applyPaths(CelestiaConfig::Paths& paths, const AssociativeArray& hash)
{
    applyPath(paths.starDatabaseFile, hash, "StarDatabase"sv);
    applyPath(paths.starNamesFile, hash, "StarNameDatabase"sv);
    applyPathArray(paths.solarSystemFiles, hash, "SolarSystemCatalogs"sv);
    applyPathArray(paths.starCatalogFiles, hash, "StarCatalogs"sv);
    applyPathArray(paths.dsoCatalogFiles, hash, "DeepSkyCatalogs"sv);
    applyPathArray(paths.extrasDirs, hash, "ExtrasDirectories"sv);
    applyPathArray(paths.skipExtras, hash, "SkipExtras"sv);
    applyPath(paths.asterismsFile, hash, "AsterismsFile"sv);
    applyPath(paths.boundariesFile, hash, "BoundariesFile"sv);
    applyPath(paths.favoritesFile, hash, "FavoritesFile"sv);
    applyPath(paths.initScriptFile, hash, "InitScript"sv);
    applyPath(paths.demoScriptFile, hash, "DemoScript"sv);
    applyPath(paths.destinationsFile, hash, "DestinationFile"sv);
    applyPath(paths.HDCrossIndexFile, hash, "HDCrossIndex"sv);
    applyPath(paths.SAOCrossIndexFile, hash, "SAOCrossIndex"sv);
    applyPath(paths.warpMeshFile, hash, "WarpMeshFile"sv);
    applyPath(paths.leapSecondsFile, hash, "LeapSecondsFile"sv);
#ifdef CELX
    applyPath(paths.scriptScreenshotDirectory, hash, "ScriptScreenshotDirectory"sv);
    applyPath(paths.luaHook, hash, "LuaHook"sv);
#endif
}

void
applyFonts(CelestiaConfig::Fonts& fonts, const AssociativeArray& hash)
{
    applyPath(fonts.mainFont, hash, "Font"sv);
    applyPath(fonts.labelFont, hash, "LabelFont"sv);
    applyPath(fonts.titleFont, hash, "TitleFont"sv);
}

void
applyMouse(CelestiaConfig::Mouse& mouse, const AssociativeArray& hash)
{
    applyString(mouse.cursor, hash, "Cursor"sv);
    applyNumber(mouse.rotateAcceleration, hash, "RotateAcceleration"sv);
    applyNumber(mouse.rotationSensitivity, hash, "MouseRotationSensitivity"sv);
    applyBoolean(mouse.reverseWheel, hash, "ReverseMouseWheel"sv);
    applyBoolean(mouse.rayBasedDragging, hash, "RayBasedDragging"sv);
    applyBoolean(mouse.focusZooming, hash, "FocusZooming"sv);
}

void
applyObserver(CelestiaConfig::Observer& observer, const AssociativeArray& hash)
{
    applyBoolean(observer.alignCameraToSurfaceOnLand, hash, "AlignCameraToSurfaceOnLand"sv);
}

void
applyRenderDetails(CelestiaConfig::RenderDetails& renderDetails, const AssociativeArray& hash)
{
    applyNumber(renderDetails.orbitWindowEnd, hash, "OrbitWindowEnd"sv);
    applyNumber(renderDetails.orbitPeriodsShown, hash, "OrbitPeriodsShown"sv);
    applyNumber(renderDetails.linearFadeFraction, hash, "LinearFadeFraction"sv);
    applyNumber(renderDetails.faintestVisible, hash, "FaintestVisibleMagnitude"sv);

    applyNumber(renderDetails.renderAsterismsFadeStartDist, hash, "RenderAsterismsFadeStartDist"sv);
    applyNumber(renderDetails.renderAsterismsFadeEndDist, hash, "RenderAsterismsFadeEndDist"sv);
    applyNumber(renderDetails.renderBoundariesFadeStartDist, hash, "RenderBoundariesFadeStartDist"sv);
    applyNumber(renderDetails.renderBoundariesFadeEndDist, hash, "RenderBoundariesFadeEndDist"sv);
    applyNumber(renderDetails.labelConstellationsFadeStartDist, hash, "LabelConstellationsFadeStartDist"sv);
    applyNumber(renderDetails.labelConstellationsFadeEndDist, hash, "LabelConstellationsFadeEndDist"sv);

    applyNumber(renderDetails.textures.shadowSize, hash, "ShadowTextureSize"sv);
    applyNumber(renderDetails.textures.eclipseSize, hash, "EclipseTextureSize"sv);
    applyNumber(renderDetails.orbitPathSamplePoints, hash, "OrbitPathSamplePoints"sv);
    applyNumber(renderDetails.atmosphere.segmentCount, hash, "AtmosphereSegmentCount"sv);
    renderDetails.atmosphere.segmentCount = std::clamp(renderDetails.atmosphere.segmentCount, 1u, 16u);
    applyNumber(renderDetails.atmosphere.cloudSegmentCount, hash, "CloudSegmentCount"sv);
    renderDetails.atmosphere.cloudSegmentCount = std::clamp(renderDetails.atmosphere.cloudSegmentCount, 1u, 16u);
    applyNumber(renderDetails.atmosphere.extinctionThreshold, hash, "AtmosphereExtinctionThreshold"sv);
    if (renderDetails.atmosphere.extinctionThreshold <= 0.0f ||
        renderDetails.atmosphere.extinctionThreshold >= 1.0f)
    {
        GetLogger()->error("AtmosphereExtinctionThreshold must be between 0 and 1.\n");
        renderDetails.atmosphere.extinctionThreshold = 0.000125f;
    }
    applyBoolean(renderDetails.atmosphere.separateRayleighMieScaleHeights, hash, "SeparateRayleighMieScaleHeights"sv);
    applyNumber(renderDetails.aaSamples, hash, "AntialiasingSamples"sv);
    applyNumber(renderDetails.textures.anisotropy, hash, "TextureAnisotropy"sv);
    renderDetails.textures.anisotropy = std::max(renderDetails.textures.anisotropy, 1);
    applyNumber(renderDetails.SolarSystemMaxDistance, hash, "SolarSystemMaxDistance"sv);
    renderDetails.SolarSystemMaxDistance = std::clamp(renderDetails.SolarSystemMaxDistance, 1.0f, 10.0f);
    applyNumber(renderDetails.ShadowMapSize, hash, "ShadowMapSize"sv);
    applyStringArray(renderDetails.ignoreGLExtensions, hash, "IgnoreGLExtensions"sv);
    applyBoolean(renderDetails.output.sRGB, hash, "SRGBRendering"sv);
    applyNumber(renderDetails.output.toneMappingExposure, hash, "ToneMappingExposure"sv);
    renderDetails.output.toneMappingExposure = std::clamp(renderDetails.output.toneMappingExposure, 1.0e-3f, 1.0e6f);
    applyToneMappingMode(renderDetails.output.toneMapping, hash, "ToneMappingMode"sv);
    applyNumber(renderDetails.stars.pointRadius, hash, "StarPointRadius"sv);
    renderDetails.stars.pointRadius = std::clamp(renderDetails.stars.pointRadius, 1.0f, 10.0f);
    applyNumber(renderDetails.stars.optimization, hash, "StarOptimization"sv);
    renderDetails.stars.optimization = std::clamp(renderDetails.stars.optimization, 0.05f, 1.0f);
    applyNumber(renderDetails.stars.maxIrradiance, hash, "StarMaxIrradiance"sv);
    if (renderDetails.stars.maxIrradiance < 0.0f)
        renderDetails.stars.maxIrradiance = 0.0f;
    else if (renderDetails.stars.maxIrradiance > 0.0f)
        renderDetails.stars.maxIrradiance = std::clamp(renderDetails.stars.maxIrradiance, 1.0f, 1.0e6f);
    applyNumber(renderDetails.stars.dimClipFactor, hash, "StarDimClipFactor"sv);
    renderDetails.stars.dimClipFactor = std::clamp(renderDetails.stars.dimClipFactor, 1.0f, 100.0f);
    applyNumber(renderDetails.stars.exposure, hash, "StarExposure"sv);
    renderDetails.stars.exposure = std::clamp(renderDetails.stars.exposure, 1.0e-3f, 1.0e6f);
}

void
applyStarTextures(StarDetails::StarTextureSet& starTextures,
                  const AssociativeArray& hash,
                  std::string_view key,
                  engine::TexturePaths& texturePaths)
{
    const util::Value* starTexValue = hash.getValue(key);
    if (starTexValue == nullptr)
        return;

    const AssociativeArray* starTexTable = starTexValue->getHash();
    if (starTexTable == nullptr)
    {
        GetLogger()->error("{} must be a property list.\n", key);
        return;
    }

    applyTexture(starTextures.starTex[StellarClass::Spectral_O], *starTexTable, "O"sv, texturePaths);
    applyTexture(starTextures.starTex[StellarClass::Spectral_B], *starTexTable, "B"sv, texturePaths);
    applyTexture(starTextures.starTex[StellarClass::Spectral_A], *starTexTable, "A"sv, texturePaths);
    applyTexture(starTextures.starTex[StellarClass::Spectral_F], *starTexTable, "F"sv, texturePaths);
    applyTexture(starTextures.starTex[StellarClass::Spectral_G], *starTexTable, "G"sv, texturePaths);
    applyTexture(starTextures.starTex[StellarClass::Spectral_K], *starTexTable, "K"sv, texturePaths);
    applyTexture(starTextures.starTex[StellarClass::Spectral_M], *starTexTable, "M"sv, texturePaths);
    applyTexture(starTextures.starTex[StellarClass::Spectral_R], *starTexTable, "R"sv, texturePaths);
    applyTexture(starTextures.starTex[StellarClass::Spectral_S], *starTexTable, "S"sv, texturePaths);
    applyTexture(starTextures.starTex[StellarClass::Spectral_N], *starTexTable, "N"sv, texturePaths);
    applyTexture(starTextures.starTex[StellarClass::Spectral_WC], *starTexTable, "WC"sv, texturePaths);
    applyTexture(starTextures.starTex[StellarClass::Spectral_WN], *starTexTable, "WN"sv, texturePaths);
    applyTexture(starTextures.starTex[StellarClass::Spectral_WO], *starTexTable, "WO"sv, texturePaths);
    applyTexture(starTextures.starTex[StellarClass::Spectral_Unknown], *starTexTable, "Unknown"sv, texturePaths);
    applyTexture(starTextures.starTex[StellarClass::Spectral_L], *starTexTable, "L"sv, texturePaths);
    applyTexture(starTextures.starTex[StellarClass::Spectral_T], *starTexTable, "T"sv, texturePaths);
    applyTexture(starTextures.starTex[StellarClass::Spectral_Y], *starTexTable, "Y"sv, texturePaths);
    applyTexture(starTextures.starTex[StellarClass::Spectral_C], *starTexTable, "C"sv, texturePaths);
    // One texture for all white dwarf types; not sure if this needs to be
    // changed. White dwarfs vary widely in temperature, so texture choice
    // should probably be based on that instead of spectral type.
    applyTexture(starTextures.starTex[StellarClass::Spectral_D], *starTexTable, "WD"sv, texturePaths);
    applyTexture(starTextures.neutronStarTex, *starTexTable, "NeutronStar"sv, texturePaths);
    applyTexture(starTextures.defaultTex, *starTexTable, "Default"sv, texturePaths);
}

} // end unnamed namespace

bool
ReadCelestiaConfig(const std::filesystem::path& filename,
                   CelestiaConfig& config,
                   engine::TexturePaths& texturePaths)
{
    std::ifstream configFile(filename);
    if (!configFile.good())
    {
        GetLogger()->error("Error opening config file '{}'.\n", filename);
        return false;
    }

    util::Tokenizer tokenizer(configFile);
    util::Parser parser(&tokenizer);

    tokenizer.nextToken();
    if (auto tokenValue = tokenizer.getNameValue(); tokenValue != "Configuration")
    {
        GetLogger()->error("{}:{} 'Configuration' expected.\n", filename,
                           tokenizer.getLineNumber());
        return false;
    }

    util::Value configParamsValue = parser.readValue();
    const AssociativeArray* configParams = configParamsValue.getHash();
    if (configParams == nullptr)
    {
        GetLogger()->error("{}: Bad configuration file.\n", filename);
        return false;
    }

    applyPaths(config.paths, *configParams);
    applyFonts(config.fonts, *configParams);
    applyMouse(config.mouse, *configParams);
    applyObserver(config.observer, *configParams);
    applyRenderDetails(config.renderDetails, *configParams);
    applyStarTextures(config.starTextures, *configParams, "StarTextures"sv, texturePaths);

    applyString(config.projectionMode, *configParams, "ProjectionMode"sv);
    applyString(config.viewportEffect, *configParams, "ViewportEffect"sv);
    applyString(config.x264EncoderOptions, *configParams, "X264EncoderOptions"sv);
    applyString(config.ffvhEncoderOptions, *configParams, "FFVHEncoderOptions"sv);
    applyString(config.measurementSystem, *configParams, "MeasurementSystem"sv);
    applyString(config.temperatureScale, *configParams, "TemperatureScale"sv);
    applyString(config.layoutDirection, *configParams, "LayoutDirection"sv);
    applyString(config.scriptSystemAccessPolicy, *configParams, "ScriptSystemAccessPolicy"sv);

    applyNumber(config.consoleLogRows, *configParams, "LogSize"sv);

#ifdef CELX
    // Move the value into the config object to retain ownership of the hash
    config.configParams = std::move(configParamsValue);
#endif

    return true;
}
