// solarsys.cpp
//
// Copyright (C) 2001-2009, the Celestia Development Team
// Original version by Chris Laurel <claurel@gmail.com>
//
// Solar system catalog parser.
//
// 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 <cassert>
#include <cstddef>
#include <cstring>
#include <istream>
#include <limits>
#include <memory>
#include <string>
#include <string_view>
#include <utility>
#include <vector>

#include <Eigen/Geometry>
#include <fmt/printf.h>

#include <celephem/orbit.h>
#include <celephem/rotation.h>
#include <celmath/mathlib.h>
#include <celutil/associativearray.h>
#include <celutil/color.h>
#include <celutil/fsutils.h>
#include <celutil/gettext.h>
#include <celutil/infourl.h>
#include <celutil/logger.h>
#include <celutil/parser.h>
#include <celutil/stringutils.h>
#include <celutil/tokenizer.h>
#include "atmosphere.h"
#include "body.h"
#include "category.h"
#include "frame.h"
#include "frametree.h"
#include "location.h"
#include "meshmanager.h"
#include "parseobject.h"
#include "solarsys.h"
#include "surface.h"
#include "texmanager.h"
#include "timeline.h"
#include "timelinephase.h"
#include "universe.h"

// size_t and strncmp are used by the gperf output code
using std::size_t;
using std::strncmp;
using namespace std::string_view_literals;

using celestia::util::AssociativeArray;
using celestia::util::GetLogger;
using celestia::util::Tokenizer;
using celestia::util::Value;
using celestia::util::ValueArray;
namespace engine = celestia::engine;
namespace ephem = celestia::ephem;
namespace math = celestia::math;
namespace util = celestia::util;

namespace
{

enum BodyType
{
    ReferencePoint,
    NormalBody,
    SurfaceObject,
    UnknownBodyType,
};

/*!
  Solar system catalog (.ssc) files contain items of three different types:
  bodies, locations, and alternate surfaces.  Bodies planets, moons, asteroids,
  comets, and spacecraft.  Locations are points on the surfaces of bodies which
  may be labelled but aren't rendered.  Alternate surfaces are additional
  surface definitions for bodies.

  An ssc file contains zero or more definitions of this form:

  \code
  [disposition] [item type] "name" "parent name"
  {
     ...object info fields...
  }
  \endcode

  The disposition of the object determines what happens if an item with the
  same parent and same name already exists.  It may be one of the following:
  - Add - Default if none is specified.  Add the item even if one of the
    same name already exists.
  - Replace - Replace an existing item with the new one
  - Modify - Modify the existing item, changing the fields that appear
    in the new definition.

  All dispositions are equivalent to add if no item of the same name
  already exists.

  The item type is one of Body, Location, or AltSurface, defaulting to
  Body when no type is given.

  The name and parent name are both mandatory.
*/

void
sscError(const Tokenizer& tok, const std::string& msg)
{
    GetLogger()->error(_("Error in .ssc file (line {}): {}\n"),
                      tok.getLineNumber(), msg);
}

// Object class properties
constexpr auto CLASSES_UNCLICKABLE           = BodyClassification::Invisible |
                                               BodyClassification::Diffuse;

// lookup table generated by gperf (solarsys.gperf)
#include "solarsys.inc"

BodyClassification GetClassificationId(std::string_view className)
{
    auto ptr = ClassificationMap::getClassification(className.data(), className.size());
    return ptr == nullptr
        ? BodyClassification::Unknown
        : ptr->classification;
}


//! Maximum depth permitted for nested frames.
unsigned int MaxFrameDepth = 50;

bool isFrameCircular(const ReferenceFrame& frame)
{
    return frame.nestingDepth(MaxFrameDepth) > MaxFrameDepth;
}



std::unique_ptr<Location>
CreateLocation(const AssociativeArray* locationData,
               const Body* body)
{
    auto location = std::make_unique<Location>();

    auto longlat = locationData->getSphericalTuple("LongLat").value_or(Eigen::Vector3d::Zero());
    Eigen::Vector3f position = body->geodeticToCartesian(longlat).cast<float>();
    location->setPosition(position);

    auto size = locationData->getLength<float>("Size").value_or(1.0f);
    location->setSize(size);

    auto importance = locationData->getNumber<float>("Importance").value_or(-1.0f);
    location->setImportance(importance);

    if (const std::string* featureTypeName = locationData->getString("Type"); featureTypeName != nullptr)
        location->setFeatureType(Location::parseFeatureType(*featureTypeName));

    if (auto labelColor = locationData->getColor("LabelColor"); labelColor.has_value())
    {
        location->setLabelColor(*labelColor);
        location->setLabelColorOverridden(true);
    }

    return location;
}

template<typename Dst, typename Flag>
inline void SetOrUnset(Dst &dst, Flag flag, bool cond)
{
    if (cond)
        dst |= flag;
    else
        dst &= ~flag;
}

std::optional<std::filesystem::path>
GetFilename(const AssociativeArray& hash,
            std::string_view key,
            const char* errorMessage)
{
    const std::string* value = hash.getString(key);
    if (value == nullptr)
        return std::nullopt;

    auto result = util::U8FileName(*value);
    if (!result.has_value())
        GetLogger()->error(errorMessage);

    return result;
}

void
FillinSurface(const AssociativeArray* surfaceData,
              Surface* surface,
              const std::filesystem::path& path,
              engine::TexturePaths& texturePaths)
{
    if (auto color = surfaceData->getColor("Color"); color.has_value())
        surface->color = *color;
    if (auto specularColor = surfaceData->getColor("SpecularColor"); specularColor.has_value())
        surface->specularColor = *specularColor;
    if (auto specularPower = surfaceData->getNumber<float>("SpecularPower"); specularPower.has_value())
        surface->specularPower = *specularPower;
    if (auto lunarLambert = surfaceData->getNumber<float>("LunarLambert"); lunarLambert.has_value())
        surface->lunarLambert = *lunarLambert;

    auto baseTexture = GetFilename(*surfaceData, "Texture"sv, "Invalid filename in Texture\n");
    auto bumpTexture = GetFilename(*surfaceData, "BumpMap"sv, "Invalid filename in BumpMap\n");
    auto nightTexture = GetFilename(*surfaceData, "NightTexture"sv, "Invalid filename in NightTexture\n");
    auto specularTexture = GetFilename(*surfaceData, "SpecularTexture"sv, "Invalid filename in SpecularTexture\n");
    auto normalTexture = GetFilename(*surfaceData, "NormalMap"sv, "Invalid filename in NormalMap\n");
    auto overlayTexture = GetFilename(*surfaceData, "OverlayTexture"sv, "Invalid filename in OverlayTexture\n");

    constexpr auto baseFlags = engine::TextureFlags::WrapTexture;
    constexpr auto bumpFlags = engine::TextureFlags::WrapTexture | engine::TextureFlags::LinearColorspace;
    constexpr auto nightFlags = engine::TextureFlags::WrapTexture;
    constexpr auto specularFlags = engine::TextureFlags::WrapTexture;

    auto bumpHeight = surfaceData->getNumber<float>("BumpHeight").value_or(2.5f);

    bool blendTexture = surfaceData->getBoolean("BlendTexture").value_or(false);
    bool emissive = surfaceData->getBoolean("Emissive").value_or(false);

    SetOrUnset(surface->appearanceFlags, Surface::BlendTexture, blendTexture);
    SetOrUnset(surface->appearanceFlags, Surface::Emissive, emissive);
    SetOrUnset(surface->appearanceFlags, Surface::ApplyBaseTexture, baseTexture.has_value());
    SetOrUnset(surface->appearanceFlags, Surface::ApplyBumpMap, (bumpTexture.has_value() || normalTexture.has_value()));
    SetOrUnset(surface->appearanceFlags, Surface::ApplyNightMap, nightTexture.has_value());
    SetOrUnset(surface->appearanceFlags, Surface::SeparateSpecularMap, specularTexture.has_value());
    SetOrUnset(surface->appearanceFlags, Surface::ApplyOverlay, overlayTexture.has_value());
    SetOrUnset(surface->appearanceFlags, Surface::SpecularReflection, surface->specularColor != Color(0.0f, 0.0f, 0.0f));

    if (baseTexture.has_value())
        surface->baseTexture = texturePaths.getHandle(*baseTexture, path, baseFlags);
    if (nightTexture.has_value())
        surface->nightTexture = texturePaths.getHandle(*nightTexture, path, nightFlags);
    if (specularTexture.has_value())
        surface->specularTexture = texturePaths.getHandle(*specularTexture, path, specularFlags);

    // If both are present, NormalMap overrides BumpMap
    if (normalTexture.has_value())
        surface->bumpTexture = texturePaths.getHandle(*normalTexture, path, bumpFlags);
    else if (bumpTexture.has_value())
        surface->bumpTexture = texturePaths.getHandle(*bumpTexture, path, bumpFlags, bumpHeight);

    if (overlayTexture.has_value())
        surface->overlayTexture = texturePaths.getHandle(*overlayTexture, path, baseFlags);
}


Selection GetParentObject(PlanetarySystem* system)
{
    Selection parent;
    Body* primary = system->getPrimaryBody();
    if (primary != nullptr)
        parent = Selection(primary);
    else
        parent = Selection(system->getStar());

    return parent;
}


std::unique_ptr<TimelinePhase>
CreateTimelinePhase(Body* body,
                    Universe& universe,
                    const AssociativeArray* phaseData,
                    const std::filesystem::path& path,
                    const ReferenceFrame::SharedConstPtr& defaultOrbitFrame,
                    const ReferenceFrame::SharedConstPtr& defaultBodyFrame,
                    bool isFirstPhase,
                    bool isLastPhase,
                    double previousPhaseEnd)
{
    double beginning = previousPhaseEnd;
    double ending = std::numeric_limits<double>::infinity();

    // Beginning is optional for the first phase of a timeline, and not
    // allowed for the other phases, where beginning is always the ending
    // of the previous phase.
    bool hasBeginning = ParseDate(phaseData, "Beginning", beginning);
    if (!isFirstPhase && hasBeginning)
    {
        GetLogger()->error("Error: Beginning can only be specified for initial phase of timeline.\n");
        return nullptr;
    }

    // Ending is required for all phases except for the final one.
    bool hasEnding = ParseDate(phaseData, "Ending", ending);
    if (!isLastPhase && !hasEnding)
    {
        GetLogger()->error("Error: Ending is required for all timeline phases other than the final one.\n");
        return nullptr;
    }

    // Get the orbit reference frame.
    ReferenceFrame::SharedConstPtr orbitFrame;
    const Value* frameValue = phaseData->getValue("OrbitFrame");
    if (frameValue != nullptr)
    {
        orbitFrame = CreateReferenceFrame(universe, frameValue, defaultOrbitFrame->getCenter(), body);
        if (orbitFrame == nullptr)
        {
            return nullptr;
        }
    }
    else
    {
        // No orbit frame specified; use the default frame.
        orbitFrame = defaultOrbitFrame;
    }

    // Get the body reference frame
    ReferenceFrame::SharedConstPtr bodyFrame;
    const Value* bodyFrameValue = phaseData->getValue("BodyFrame");
    if (bodyFrameValue != nullptr)
    {
        bodyFrame = CreateReferenceFrame(universe, bodyFrameValue, defaultBodyFrame->getCenter(), body);
        if (bodyFrame == nullptr)
        {
            return nullptr;
        }
    }
    else
    {
        // No body frame specified; use the default frame.
        bodyFrame = defaultBodyFrame;
    }

    // Use planet units (AU for semimajor axis) if the center of the orbit
    // reference frame is a star.
    bool usePlanetUnits = orbitFrame->getCenter().star() != nullptr;

    // Get the orbit
    auto orbit = CreateOrbit(orbitFrame->getCenter(), phaseData, path, usePlanetUnits);
    if (!orbit)
    {
        GetLogger()->error("Error: missing orbit in timeline phase.\n");
        return nullptr;
    }

    // Get the rotation model
    // TIMELINE-TODO: default rotation model is UniformRotation with a period
    // equal to the orbital period. Should we do something else?
    auto rotationModel = CreateRotationModel(phaseData, path, orbit->getPeriod());
    if (!rotationModel)
    {
        // TODO: Should distinguish between a missing rotation model (where it's
        // appropriate to use a default one) and a bad rotation model (where
        // we should report an error.)
        rotationModel = ephem::ConstantOrientation::identity();
    }

    auto phase = TimelinePhase::CreateTimelinePhase(universe,
                                                    body,
                                                    beginning, ending,
                                                    orbitFrame,
                                                    orbit,
                                                    bodyFrame,
                                                    rotationModel);

    // Frame ownership transfered to phase; release local references
    return phase;
}


std::unique_ptr<Timeline>
CreateTimelineFromArray(Body* body,
                        Universe& universe,
                        const ValueArray* timelineArray,
                        const std::filesystem::path& path,
                        const ReferenceFrame::SharedConstPtr& defaultOrbitFrame,
                        const ReferenceFrame::SharedConstPtr& defaultBodyFrame)
{
    auto timeline = std::make_unique<Timeline>();
    double previousEnding = -std::numeric_limits<double>::infinity();

    if (timelineArray->empty())
    {
        GetLogger()->error("Error in timeline of '{}': timeline array is empty.\n", body->getName());
        return nullptr;
    }

    const auto finalIter = timelineArray->end() - 1;
    for (auto iter = timelineArray->begin(); iter != timelineArray->end(); iter++)
    {
        const AssociativeArray* phaseData = iter->getHash();
        if (phaseData == nullptr)
        {
            GetLogger()->error("Error in timeline of '{}': phase {} is not a property group.\n", body->getName(), iter - timelineArray->begin() + 1);
            return nullptr;
        }

        bool isFirstPhase = iter == timelineArray->begin();
        bool isLastPhase =  iter == finalIter;

        auto phase = CreateTimelinePhase(body, universe, phaseData,
                                         path,
                                         defaultOrbitFrame,
                                         defaultBodyFrame,
                                         isFirstPhase, isLastPhase, previousEnding);
        if (phase == nullptr)
        {
            GetLogger()->error("Error in timeline of '{}', phase {}.\n",
                               body->getName(),
                               iter - timelineArray->begin() + 1);
            return nullptr;
        }

        previousEnding = phase->endTime();

        timeline->appendPhase(std::move(phase));
    }

    return timeline;
}


bool CreateTimeline(Body* body,
                    PlanetarySystem* system,
                    Universe& universe,
                    const AssociativeArray* planetData,
                    const std::filesystem::path& path,
                    DataDisposition disposition,
                    BodyType bodyType)
{
    FrameTree* parentFrameTree = nullptr;
    Selection parentObject = GetParentObject(system);
    bool orbitsPlanet = false;
    if (parentObject.body())
    {
        parentFrameTree = parentObject.body()->getOrCreateFrameTree();
        //orbitsPlanet = true;
    }
    else if (parentObject.star())
    {
        const SolarSystem* solarSystem = universe.getOrCreateSolarSystem(parentObject.star());
        parentFrameTree = solarSystem->getFrameTree();
    }
    else
    {
        // Bad orbit barycenter specified
        return false;
    }

    ReferenceFrame::SharedConstPtr defaultOrbitFrame;
    ReferenceFrame::SharedConstPtr defaultBodyFrame;
    if (bodyType == SurfaceObject)
    {
        defaultOrbitFrame = std::make_shared<BodyFixedFrame>(parentObject, parentObject);
        defaultBodyFrame = CreateTopocentricFrame(parentObject, parentObject, Selection(body));
    }
    else
    {
        defaultOrbitFrame = parentFrameTree->getDefaultReferenceFrame();
        defaultBodyFrame = parentFrameTree->getDefaultReferenceFrame();
    }

    // If there's an explicit timeline definition, parse that. Otherwise, we'll do
    // things the old way.
    const Value* value = planetData->getValue("Timeline");
    if (value != nullptr)
    {
        const ValueArray* timelineArray = value->getArray();
        if (timelineArray == nullptr)
        {
            GetLogger()->error("Error: Timeline must be an array\n");
            return false;
        }

        std::unique_ptr<Timeline> timeline = CreateTimelineFromArray(body, universe, timelineArray, path,
                                                                     defaultOrbitFrame, defaultBodyFrame);

        if (!timeline)
            return false;

        body->setTimeline(std::move(timeline));
        return true;
    }

    // Information required for the object timeline.
    ReferenceFrame::SharedConstPtr orbitFrame;
    ReferenceFrame::SharedConstPtr bodyFrame;
    std::shared_ptr<const ephem::Orbit> orbit = nullptr;
    std::shared_ptr<const ephem::RotationModel> rotationModel = nullptr;
    double beginning  = -std::numeric_limits<double>::infinity();
    double ending     =  std::numeric_limits<double>::infinity();

    // If any new timeline values are specified, we need to overrideOldTimeline will
    // be set to true.
    bool overrideOldTimeline = false;

    // The interaction of Modify with timelines is slightly complicated. If the timeline
    // is specified by putting the OrbitFrame, Orbit, BodyFrame, or RotationModel directly
    // in the object definition (i.e. not inside a Timeline structure), it will completely
    // replace the previous timeline if it contained more than one phase. Otherwise, the
    // properties of the single phase will be modified individually, for compatibility with
    // Celestia versions 1.5.0 and earlier.
    if (disposition == DataDisposition::Modify)
    {
        const Timeline* timeline = body->getTimeline();
        if (timeline->phaseCount() == 1)
        {
            const auto& phase = timeline->getPhase(0);
            orbitFrame    = phase.orbitFrame();
            bodyFrame     = phase.bodyFrame();
            orbit         = phase.orbit();
            rotationModel = phase.rotationModel();
            beginning     = phase.startTime();
            ending        = phase.endTime();
        }
    }

    // Get the object's orbit reference frame.
    bool newOrbitFrame = false;
    const Value* frameValue = planetData->getValue("OrbitFrame");
    if (frameValue != nullptr)
    {
        auto frame = CreateReferenceFrame(universe, frameValue, parentObject, body);
        if (frame != nullptr)
        {
            orbitFrame = frame;
            newOrbitFrame = true;
            overrideOldTimeline = true;
        }
    }

    // Get the object's body frame.
    bool newBodyFrame = false;
    const Value* bodyFrameValue = planetData->getValue("BodyFrame");
    if (bodyFrameValue != nullptr)
    {
        auto frame = CreateReferenceFrame(universe, bodyFrameValue, parentObject, body);
        if (frame != nullptr)
        {
            bodyFrame = frame;
            newBodyFrame = true;
            overrideOldTimeline = true;
        }
    }

    // If no orbit or body frame was specified, use the default ones
    if (orbitFrame == nullptr)
        orbitFrame = defaultOrbitFrame;
    if (bodyFrame == nullptr)
        bodyFrame = defaultBodyFrame;

    // If the center of the is a star, orbital element units are
    // in AU; otherwise, use kilometers.
    orbitsPlanet = orbitFrame->getCenter().star() == nullptr;

    auto newOrbit = CreateOrbit(orbitFrame->getCenter(), planetData, path, !orbitsPlanet);
    if (newOrbit == nullptr && orbit == nullptr)
    {
        if (body->getTimeline() && disposition == DataDisposition::Modify)
        {
            // The object definition is modifying an existing object with a multiple phase
            // timeline, but no orbit definition was given. This can happen for completely
            // sensible reasons, such a Modify definition that just changes visual properties.
            // Or, the definition may try to change other timeline phase properties such as
            // the orbit frame, but without providing an orbit. In both cases, we'll just
            // leave the original timeline alone.
            return true;
        }
        else
        {
            GetLogger()->error("No valid orbit specified for object '{}'. Skipping.\n", body->getName());
            return false;
        }
    }

    // If a new orbit was given, override any old orbit
    if (newOrbit != nullptr)
    {
        orbit = newOrbit;
        overrideOldTimeline = true;
    }

    // Get the rotation model for this body
    double syncRotationPeriod = orbit->getPeriod();
    auto newRotationModel = CreateRotationModel(planetData, path, syncRotationPeriod);

    // If a new rotation model was given, override the old one
    if (newRotationModel != nullptr)
    {
        rotationModel = newRotationModel;
        overrideOldTimeline = true;
    }

    // If there was no rotation model specified, nor a previous rotation model to
    // override, create the default one.
    if (rotationModel == nullptr)
    {
        // If no rotation model is provided, use a default rotation model--
        // a uniform rotation that's synchronous with the orbit (appropriate
        // for nearly all natural satellites in the solar system.)
        rotationModel = CreateDefaultRotationModel(syncRotationPeriod);
    }

    if (ParseDate(planetData, "Beginning", beginning))
        overrideOldTimeline = true;
    if (ParseDate(planetData, "Ending", ending))
        overrideOldTimeline = true;

    // Something went wrong if the disposition isn't modify and no timeline
    // is to be created.
    assert(disposition == DataDisposition::Modify || overrideOldTimeline);

    if (overrideOldTimeline)
    {
        if (beginning >= ending)
        {
            GetLogger()->error("Beginning time must be before Ending time.\n");
            return false;
        }

        // We finally have an orbit, rotation model, frames, and time range. Create
        // the object timeline.
        auto phase = TimelinePhase::CreateTimelinePhase(universe,
                                                        body,
                                                        beginning, ending,
                                                        orbitFrame,
                                                        orbit,
                                                        bodyFrame,
                                                        rotationModel);

        // We've already checked that beginning < ending; nothing else should go
        // wrong during the creation of a TimelinePhase.
        assert(phase != nullptr);
        if (phase == nullptr)
        {
            GetLogger()->error("Internal error creating TimelinePhase.\n");
            return false;
        }

        auto timeline = std::make_unique<Timeline>();
        timeline->appendPhase(std::move(phase));

        body->setTimeline(std::move(timeline));

        // Check for circular references in frames; this can only be done once the timeline
        // has actually been set.
        // TIMELINE-TODO: This check is not comprehensive; it won't find recursion in
        // multiphase timelines.
        if (newOrbitFrame && isFrameCircular(*body->getOrbitFrame(0.0)))
        {
            GetLogger()->error("Orbit frame for '{}' is nested too deep (probably circular)\n", body->getName());
            return false;
        }

        if (newBodyFrame && isFrameCircular(*body->getBodyFrame(0.0)))
        {
            GetLogger()->error("Body frame for '{}' is nested too deep (probably circular)\n", body->getName());
            return false;
        }
    }

    return true;
}

void
ReadMesh(const AssociativeArray& planetData,
         Body& body,
         const std::filesystem::path& path,
         engine::GeometryPaths& geometryPaths)
{
    auto mesh = planetData.getString("Mesh"sv);
    if (mesh == nullptr)
        return;

    engine::GeometryHandle geometryHandle;
    float geometryScale = 1.0f;
    if (auto geometry = util::U8FileName(*mesh); geometry.has_value())
    {
        auto geometryCenter = planetData.getVector3<float>("MeshCenter"sv).value_or(Eigen::Vector3f::Zero());
        // TODO: Adjust bounding radius if model center isn't
        // (0.0f, 0.0f, 0.0f)

        bool isNormalized = planetData.getBoolean("NormalizeMesh"sv).value_or(true);
        if (auto meshScale = planetData.getLength<float>("MeshScale"sv); meshScale.has_value())
            geometryScale = meshScale.value();

        geometryHandle = geometryPaths.getHandle(*geometry, path, geometryCenter, isNormalized);
    }
    else
    {
        // Some add-ons appear to be using Mesh "" to switch off the geometry
        if (!mesh->empty())
            GetLogger()->error("Invalid filename in Mesh\n");
        geometryHandle = engine::GeometryHandle::Empty;
    }

    body.setGeometry(geometryHandle);
    body.setGeometryScale(geometryScale);
}

void ReadAtmosphere(Body* body,
                    const AssociativeArray* atmosData,
                    const std::filesystem::path& path,
                    DataDisposition disposition,
                    engine::TexturePaths& texturePaths)
{
    auto bodyFeaturesManager = GetBodyFeaturesManager();
    std::unique_ptr<Atmosphere> newAtmosphere = nullptr;
    Atmosphere* atmosphere = nullptr;
    if (disposition == DataDisposition::Modify)
        atmosphere = bodyFeaturesManager->getAtmosphere(body);

    if (atmosphere == nullptr)
    {
        newAtmosphere = std::make_unique<Atmosphere>();
        atmosphere = newAtmosphere.get();
    }

    if (auto height = atmosData->getLength<float>("Height"); height.has_value())
        atmosphere->height = *height;
    if (auto color = atmosData->getColor("Lower"); color.has_value())
        atmosphere->lowerColor = *color;
    if (auto color = atmosData->getColor("Upper"); color.has_value())
        atmosphere->upperColor = *color;
    if (auto color = atmosData->getColor("Sky"); color.has_value())
        atmosphere->skyColor = *color;
    if (auto color = atmosData->getColor("Sunset"); color.has_value())
        atmosphere->sunsetColor = *color;

    if (auto mieCoeff = atmosData->getNumber<float>("Mie"); mieCoeff.has_value())
        atmosphere->mieCoeff = *mieCoeff;
    if (auto mieScaleHeight = atmosData->getLength<float>("MieScaleHeight"))
        atmosphere->mieScaleHeight = *mieScaleHeight;
    if (auto miePhaseAsymmetry = atmosData->getNumber<float>("MieAsymmetry"); miePhaseAsymmetry.has_value())
        atmosphere->miePhaseAsymmetry = *miePhaseAsymmetry;
    if (auto rayleighCoeff = atmosData->getVector3<float>("Rayleigh"); rayleighCoeff.has_value())
        atmosphere->rayleighCoeff = *rayleighCoeff;
    //atmosData->getNumber("RayleighScaleHeight", atmosphere->rayleighScaleHeight);
    if (auto absorptionCoeff = atmosData->getVector3<float>("Absorption"); absorptionCoeff.has_value())
        atmosphere->absorptionCoeff = *absorptionCoeff;

    // Get the cloud map settings
    if (auto cloudHeight = atmosData->getLength<float>("CloudHeight"); cloudHeight.has_value())
        atmosphere->cloudHeight = *cloudHeight;
    if (auto cloudSpeed = atmosData->getNumber<float>("CloudSpeed"); cloudSpeed.has_value())
        atmosphere->cloudSpeed = math::degToRad(*cloudSpeed);

    if (auto cloudTexture = GetFilename(*atmosData, "CloudMap"sv, "Invalid filename in CloudMap\n");
        cloudTexture.has_value())
    {
        constexpr auto cloudFlags = engine::TextureFlags::WrapTexture;
        atmosphere->cloudTexture = texturePaths.getHandle(*cloudTexture, path, cloudFlags);
    }

    if (auto cloudNormalMap = GetFilename(*atmosData, "CloudNormalMap"sv, "Invalid filename in CloudNormalMap\n");
        cloudNormalMap.has_value())
    {
        constexpr auto cloudNormalFlags = engine::TextureFlags::WrapTexture | engine::TextureFlags::LinearColorspace;
        atmosphere->cloudNormalMap = texturePaths.getHandle(*cloudNormalMap, path, cloudNormalFlags);
    }

    if (auto cloudShadowDepth = atmosData->getNumber<float>("CloudShadowDepth"); cloudShadowDepth.has_value())
    {
        cloudShadowDepth = std::clamp(*cloudShadowDepth, 0.0f, 1.0f);
        atmosphere->cloudShadowDepth = *cloudShadowDepth;
    }

    if (newAtmosphere != nullptr)
        bodyFeaturesManager->setAtmosphere(body, std::move(newAtmosphere));
}


void ReadRings(Body* body,
               const AssociativeArray* ringsData,
               const std::filesystem::path& path,
               DataDisposition disposition,
               engine::TexturePaths& texturePaths)
{
    auto inner = ringsData->getLength<float>("Inner");
    auto outer = ringsData->getLength<float>("Outer");

    std::unique_ptr<RingSystem> newRings = nullptr;
    RingSystem* rings = nullptr;
    auto bodyFeaturesManager = GetBodyFeaturesManager();
    if (disposition == DataDisposition::Modify)
        rings = bodyFeaturesManager->getRings(body);

    if (rings == nullptr)
    {
        if (!inner.has_value() || !outer.has_value())
        {
            GetLogger()->error(_("Ring system needs inner and outer radii.\n"));
            return;
        }

        newRings = std::make_unique<RingSystem>(*inner, *outer);
        rings = newRings.get();
    }
    else
    {
        if (inner.has_value())
            rings->innerRadius = *inner;
        if (outer.has_value())
            rings->outerRadius = *outer;
    }

    if (auto color = ringsData->getColor("Color"); color.has_value())
        rings->color = *color;

    if (auto textureName = GetFilename(*ringsData, "Texture"sv, "Invalid filename in rings Texture\n");
        textureName.has_value())
    {
        rings->texture = texturePaths.getHandle(*textureName, path);
    }

    if (newRings != nullptr)
        bodyFeaturesManager->setRings(body, std::move(newRings));
}


// Create a body (planet, moon, spacecraft, etc.) using the values from a
// property list. The usePlanetsUnits flags specifies whether period and
// semi-major axis are in years and AU rather than days and kilometers.
Body* CreateBody(const std::string& name,
                 PlanetarySystem* system,
                 Universe& universe,
                 Body* existingBody,
                 const AssociativeArray* planetData,
                 const std::filesystem::path& path,
                 DataDisposition disposition,
                 BodyType bodyType,
                 engine::GeometryPaths& geometryPaths,
                 engine::TexturePaths& texturePaths)
{
    Body* body = nullptr;

    if (disposition == DataDisposition::Modify || disposition == DataDisposition::Replace)
        body = existingBody;

    if (body == nullptr)
    {
        body = system->addBody(name);
        // If the body doesn't exist, always treat the disposition as 'Add'
        disposition = DataDisposition::Add;

        // Set the default classification for new objects based on the body type.
        // This may be overridden by the Class property.
        if (bodyType == SurfaceObject)
        {
            body->setClassification(BodyClassification::SurfaceFeature);
        }
    }

    if (!CreateTimeline(body, system, universe, planetData, path, disposition, bodyType))
    {
        // No valid timeline given; give up.
        if (body != existingBody)
            system->removeBody(body);
        return nullptr;
    }

    // Three values control the shape and size of an ellipsoidal object:
    // semiAxes, radius, and oblateness. It is an error if neither the
    // radius nor semiaxes are set. If both are set, the radius is
    // multipled by each of the specified semiaxis to give the shape of
    // the body ellipsoid. Oblateness is ignored if semiaxes are provided;
    // otherwise, the ellipsoid has semiaxes: ( radius, radius, 1-radius ).
    // These rather complex rules exist to maintain backward compatibility.
    //
    // If the body also has a mesh, it is always scaled in x, y, and z by
    // the maximum semiaxis, never anisotropically.

    auto radius = static_cast<double>(body->getRadius());
    bool radiusSpecified = false;
    if (auto rad = planetData->getLength<double>("Radius"); rad.has_value())
    {
        radius = *rad;
        body->setSemiAxes(Eigen::Vector3f::Constant((float) radius));
        radiusSpecified = true;
    }

    bool semiAxesSpecified = false;
    auto semiAxes = planetData->getVector3<double>("SemiAxes");

    if (semiAxes.has_value())
    {
        if ((*semiAxes).x() <= 0.0 || (*semiAxes).y() <= 0.0 || (*semiAxes).z() <= 0.0)
        {
            GetLogger()->error(_("Invalid SemiAxes value for object {}: [{}, {}, {}]\n"),
                               name,
                               (*semiAxes).x(),
                               (*semiAxes).y(),
                               (*semiAxes).z());
            semiAxes.reset();
        }
    }

    if (radiusSpecified && semiAxes.has_value())
    {
        // If the radius has been specified, treat SemiAxes as dimensionless
        // (ignore units) and multiply the SemiAxes by the Radius.
        *semiAxes *= radius;
    }

    if (semiAxes.has_value())
    {
        // Swap y and z to match internal coordinate system
        semiAxes->tail<2>().reverseInPlace();
        body->setSemiAxes(semiAxes->cast<float>());
        semiAxesSpecified = true;
    }

    if (!semiAxesSpecified)
    {
        auto oblateness = planetData->getNumber<float>("Oblateness");
        if (oblateness.has_value())
        {
            if (*oblateness >= 0.0f && *oblateness < 1.0f)
            {
                body->setSemiAxes(body->getRadius() * Eigen::Vector3f(1.0f, 1.0f - *oblateness, 1.0f));
            }
            else
            {
                GetLogger()->error(_("Invalid Oblateness value for object {}: {}\n"), name, *oblateness);
            }
        }
    }

    BodyClassification classification = body->getClassification();
    if (const std::string* classificationName = planetData->getString("Class"); classificationName != nullptr)
        classification = GetClassificationId(*classificationName);

    if (classification == BodyClassification::Unknown)
    {
        // Try to guess the type
        if (system->getPrimaryBody() != nullptr)
            classification = radius > 0.1 ? BodyClassification::Moon : BodyClassification::Spacecraft;
        else
            classification = radius < 1000.0 ? BodyClassification::Asteroid : BodyClassification::Planet;
    }
    body->setClassification(classification);

    if (classification == BodyClassification::Invisible)
        body->setVisible(false);

    // Set default properties for the object based on its classification
    if (util::is_set(classification, CLASSES_UNCLICKABLE))
        body->setClickable(false);

    // TODO: should be own class
    if (const auto *infoURLValue = planetData->getString("InfoURL"); infoURLValue != nullptr)
    {
        if (std::string infoURL = util::BuildInfoURL(*infoURLValue, path); !infoURL.empty())
            body->setInfoURL(std::move(infoURL));
        else
            GetLogger()->error(_("Invalid InfoURL used in {} definition.\n"), name);
    }

    if (auto albedo = planetData->getNumber<float>("Albedo"); albedo.has_value())
    {
        // TODO: make this warn
        GetLogger()->verbose("Deprecated parameter Albedo used in {} definition.\nUse GeomAlbedo & BondAlbedo instead.\n", name);
        body->setGeomAlbedo(*albedo);
    }

    if (auto albedo = planetData->getNumber<float>("GeomAlbedo"); albedo.has_value())
    {
        if (*albedo > 0.0)
        {
            body->setGeomAlbedo(*albedo);
            // Set the BondAlbedo and Reflectivity values if it is <1, otherwise as 1.
            if (*albedo > 1.0f)
                albedo = 1.0f;
            body->setBondAlbedo(*albedo);
            body->setReflectivity(*albedo);
        }
        else
        {
            GetLogger()->error(_("Incorrect GeomAlbedo value: {}\n"), *albedo);
        }
    }

    if (auto reflectivity = planetData->getNumber<float>("Reflectivity"); reflectivity.has_value())
    {
        if (*reflectivity >= 0.0f && *reflectivity <= 1.0f)
            body->setReflectivity(*reflectivity);
        else
            GetLogger()->error(_("Incorrect Reflectivity value: {}\n"), *reflectivity);
    }

    if (auto albedo = planetData->getNumber<float>("BondAlbedo"); albedo.has_value())
    {
        if (*albedo >= 0.0f && *albedo <= 1.0f)
            body->setBondAlbedo(*albedo);
        else
            GetLogger()->error(_("Incorrect BondAlbedo value: {}\n"), *albedo);
    }

    if (auto temperature = planetData->getNumber<float>("Temperature"); temperature.has_value() && *temperature > 0.0f)
        body->setTemperature(*temperature);
    if (auto emissivity = planetData->getNumber<float>("Emissivity"); emissivity.has_value())
        body->setEmissivity(*emissivity);
    if (auto internalHeatFlux = planetData->getNumber<float>("InternalHeatFlux"); internalHeatFlux.has_value())
        body->setInternalHeatFlux(*internalHeatFlux);
    if (auto mass = planetData->getMass<float>("Mass", 1.0, 1.0); mass.has_value())
        body->setMass(*mass);
    if (auto density = planetData->getNumber<float>("Density"); density.has_value())
       body->setDensity(*density);

    if (auto orientation = planetData->getRotation("Orientation"); orientation.has_value())
        body->setGeometryOrientation(*orientation);

    Surface surface;
    if (disposition == DataDisposition::Modify)
        surface = body->getSurface();
    else
        surface.color = Color(1.0f, 1.0f, 1.0f);

    FillinSurface(planetData, &surface, path, texturePaths);
    body->setSurface(surface);

    ReadMesh(*planetData, *body, path, geometryPaths);

    // Read the atmosphere
    if (const Value* atmosDataValue = planetData->getValue("Atmosphere"); atmosDataValue != nullptr)
    {
        if (const AssociativeArray* atmosData = atmosDataValue->getHash(); atmosData == nullptr)
            GetLogger()->error(_("Atmosphere must be an associative array.\n"));
        else
            ReadAtmosphere(body, atmosData, path, disposition, texturePaths);
    }

    // Read the ring system
    if (const Value* ringsDataValue = planetData->getValue("Rings"); ringsDataValue != nullptr)
    {
        if (const AssociativeArray* ringsData = ringsDataValue->getHash(); ringsData == nullptr)
            GetLogger()->error(_("Rings must be an associative array.\n"));
        else
            ReadRings(body, ringsData, path, disposition, texturePaths);
    }

    auto bodyFeaturesManager = GetBodyFeaturesManager();

    // Read comet tail color
    if (auto cometTailColor = planetData->getColor("TailColor"); cometTailColor.has_value())
        bodyFeaturesManager->setCometTailColor(body, *cometTailColor);

    if (auto clickable = planetData->getBoolean("Clickable"); clickable.has_value())
        body->setClickable(*clickable);

    if (auto visible = planetData->getBoolean("Visible"); visible.has_value())
        body->setVisible(*visible);

    if (auto orbitColor = planetData->getColor("OrbitColor"); orbitColor.has_value())
    {
        bodyFeaturesManager->setOrbitColor(body, *orbitColor);
        bodyFeaturesManager->setOrbitColorOverridden(body, true);
    }

    return body;
}


// Create a barycenter object using the values from a hash
Body* CreateReferencePoint(const std::string& name,
                           PlanetarySystem* system,
                           Universe& universe,
                           Body* existingBody,
                           const AssociativeArray* refPointData,
                           const std::filesystem::path& path,
                           DataDisposition disposition)
{
    Body* body = nullptr;
    if (disposition == DataDisposition::Modify || disposition == DataDisposition::Replace)
    {
        body = existingBody;
    }

    if (body == nullptr)
    {
        body = system->addBody(name);
        // If the point doesn't exist, always treat the disposition as 'Add'
        disposition = DataDisposition::Add;
    }

    body->setSemiAxes(Eigen::Vector3f::Ones());
    body->setClassification(BodyClassification::Invisible);
    body->setVisible(false);
    body->setClickable(false);

    if (!CreateTimeline(body, system, universe, refPointData, path, disposition, ReferencePoint))
    {
        // No valid timeline given; give up.
        if (body != existingBody)
            system->removeBody(body);
        return nullptr;
    }

    // Reference points can be marked visible; no geometry is shown, but the label and orbit
    // will be.
    if (auto visible = refPointData->getBoolean("Visible"); visible.has_value())
    {
        body->setVisible(*visible);
    }

    if (auto clickable = refPointData->getBoolean("Clickable"); clickable.has_value())
    {
        body->setClickable(*clickable);
    }

    if (auto orbitColor = refPointData->getColor("OrbitColor"); orbitColor.has_value())
    {
        GetBodyFeaturesManager()->setOrbitColor(body, *orbitColor);
        GetBodyFeaturesManager()->setOrbitColorOverridden(body, true);
    }

    return body;
}
} // end unnamed namespace

bool LoadSolarSystemObjects(std::istream& in,
                            Universe& universe,
                            const std::filesystem::path& directory,
                            engine::GeometryPaths& geometryPaths,
                            engine::TexturePaths& texturePaths)
{
    Tokenizer tokenizer(in);
    util::Parser parser(&tokenizer);

#ifdef ENABLE_NLS
    std::string s = directory.string();
    const char* d = s.c_str();
    bindtextdomain(d, d); // domain name is the same as resource path
#endif

    while (tokenizer.nextToken() != util::TokenType::End)
    {
        // Read the disposition; if none is specified, the default is Add.
        DataDisposition disposition = DataDisposition::Add;
        if (auto tokenValue = tokenizer.getNameValue(); tokenValue.has_value())
        {
            if (*tokenValue == "Add")
            {
                disposition = DataDisposition::Add;
                tokenizer.nextToken();
            }
            else if (*tokenValue == "Replace")
            {
                disposition = DataDisposition::Replace;
                tokenizer.nextToken();
            }
            else if (*tokenValue == "Modify")
            {
                disposition = DataDisposition::Modify;
                tokenizer.nextToken();
            }
        }

        // Read the item type; if none is specified the default is Body
        std::string itemType("Body");
        if (auto tokenValue = tokenizer.getNameValue(); tokenValue.has_value())
        {
            itemType = *tokenValue;
            tokenizer.nextToken();
        }

        // The name list is a string with zero more names. Multiple names are
        // delimited by colons.
        std::string nameList;
        if (auto tokenValue = tokenizer.getStringValue(); tokenValue.has_value())
        {
            nameList = *tokenValue;
        }
        else
        {
            sscError(tokenizer, "object name expected");
            return false;
        }

        tokenizer.nextToken();
        std::string parentName;
        if (auto tokenValue = tokenizer.getStringValue(); tokenValue.has_value())
        {
            parentName = *tokenValue;
        }
        else
        {
            sscError(tokenizer, "bad parent object name");
            return false;
        }

        const Value objectDataValue = parser.readValue();
        const AssociativeArray* objectData = objectDataValue.getHash();
        if (objectData == nullptr)
        {
            sscError(tokenizer, "{ expected");
            return false;
        }

        Selection parent = universe.findPath(parentName, {});
        PlanetarySystem* parentSystem = nullptr;

        std::vector<std::string> names;
        // Iterate through the string for names delimited
        // by ':', and insert them into the name list.
        if (nameList.empty())
        {
            names.push_back("");
        }
        else
        {
            std::string::size_type startPos   = 0;
            while (startPos != std::string::npos)
            {
                std::string::size_type next   = nameList.find(':', startPos);
                std::string::size_type length = std::string::npos;
                if (next != std::string::npos)
                {
                    length = next - startPos;
                    ++next;
                }
                names.push_back(nameList.substr(startPos, length));
                startPos   = next;
            }
        }
        std::string primaryName = names.front();

        BodyType bodyType = UnknownBodyType;
        if (itemType == "Body")
            bodyType = NormalBody;
        else if (itemType == "ReferencePoint")
            bodyType = ReferencePoint;
        else if (itemType == "SurfaceObject")
            bodyType = SurfaceObject;

        if (bodyType != UnknownBodyType)
        {
            //bool orbitsPlanet = false;
            if (parent.star() != nullptr)
            {
                const SolarSystem* solarSystem = universe.getOrCreateSolarSystem(parent.star());
                parentSystem = solarSystem->getPlanets();
            }
            else if (parent.body() != nullptr)
            {
                // Parent is a planet or moon
                parentSystem = parent.body()->getOrCreateSatellites();
            }
            else
            {
                sscError(tokenizer, fmt::sprintf(_("parent body '%s' of '%s' not found.\n"), parentName, primaryName));
            }

            if (parentSystem != nullptr)
            {
                Body* existingBody = parentSystem->find(primaryName);
                if (existingBody)
                {
                    if (disposition == DataDisposition::Add)
                        sscError(tokenizer, fmt::sprintf(_("warning duplicate definition of %s %s\n"), parentName, primaryName));
                    else if (disposition == DataDisposition::Replace)
                        existingBody->setDefaultProperties();
                }

                Body* body;
                if (bodyType == ReferencePoint)
                    body = CreateReferencePoint(primaryName, parentSystem, universe, existingBody, objectData, directory, disposition);
                else
                    body = CreateBody(primaryName, parentSystem, universe, existingBody, objectData, directory, disposition, bodyType, geometryPaths, texturePaths);

                if (body != nullptr)
                {
                    UserCategory::loadCategories(body, *objectData, disposition, directory.string());
                    if (disposition == DataDisposition::Add)
                        for (const auto& name : names)
                            body->addAlias(name);
                }
            }
        }
        else if (itemType == "AltSurface")
        {
            auto surface = std::make_unique<Surface>();
            surface->color = Color(1.0f, 1.0f, 1.0f);
            FillinSurface(objectData, surface.get(), directory, texturePaths);
            if (parent.body() != nullptr)
                GetBodyFeaturesManager()->addAlternateSurface(parent.body(), primaryName, std::move(surface));
            else
                sscError(tokenizer, _("bad alternate surface"));
        }
        else if (itemType == "Location")
        {
            if (parent.body() != nullptr)
            {
                std::unique_ptr<Location> location = CreateLocation(objectData, parent.body());
                if (location != nullptr)
                {
                    UserCategory::loadCategories(location.get(), *objectData, disposition, directory.string());
                    location->setName(primaryName);
                    GetBodyFeaturesManager()->addLocation(parent.body(), std::move(location));
                }
                else
                {
                    sscError(tokenizer, _("bad location"));
                }
            }
            else
            {
                sscError(tokenizer, fmt::sprintf(_("parent body '%s' of '%s' not found.\n"), parentName, primaryName));
            }
        }
    }

    // TODO: Return some notification if there's an error parsing the file
    return true;
}


SolarSystem::SolarSystem(Star* _star) :
    star(_star)
{
    planets = std::make_unique<PlanetarySystem>(star);
    frameTree = std::make_unique<FrameTree>(star);
}

SolarSystem::~SolarSystem() = default;


Star* SolarSystem::getStar() const
{
    return star;
}

Eigen::Vector3f SolarSystem::getCenter() const
{
    // TODO: This is a very simple method at the moment, but it will get
    // more complex when planets around multistar systems are supported
    // where the planets may orbit the center of mass of two stars.
    return star->getPosition();
}

PlanetarySystem* SolarSystem::getPlanets() const
{
    return planets.get();
}

FrameTree* SolarSystem::getFrameTree() const
{
    return frameTree.get();
}
