using static SpaceGame.Api.Stations.Simulation.InfrastructureSimulationService;

namespace SpaceGame.Api.Universe.Simulation;

internal sealed class OrbitalStateUpdater
{
    private readonly OrbitalSimulationOptions _orbitalSimulation;

    internal OrbitalStateUpdater(OrbitalSimulationOptions orbitalSimulation)
    {
        _orbitalSimulation = orbitalSimulation;
    }

    private static Vector3 ComputePlanetPosition(PlanetDefinition planet, float timeSeconds)
    {
        var eccentricity = Math.Clamp(planet.OrbitEccentricity, 0f, 0.85f);
        var meanAnomaly = DegreesToRadians(planet.OrbitPhaseAtEpoch) + (timeSeconds * planet.OrbitSpeed);
        var eccentricAnomaly = meanAnomaly
            + (eccentricity * MathF.Sin(meanAnomaly))
            + (0.5f * eccentricity * eccentricity * MathF.Sin(2f * meanAnomaly));
        var semiMajorAxis = SimulationUnits.AuToKilometers(planet.OrbitRadius);
        var semiMinorAxis = semiMajorAxis * MathF.Sqrt(MathF.Max(1f - (eccentricity * eccentricity), 0.05f));
        var local = new Vector3(
            semiMajorAxis * (MathF.Cos(eccentricAnomaly) - eccentricity),
            0f,
            semiMinorAxis * MathF.Sin(eccentricAnomaly));

        local = RotateAroundY(local, DegreesToRadians(planet.OrbitArgumentOfPeriapsis));
        local = RotateAroundX(local, DegreesToRadians(planet.OrbitInclination));
        local = RotateAroundY(local, DegreesToRadians(planet.OrbitLongitudeOfAscendingNode));
        return local;
    }

    private static Vector3 ComputeMoonOffset(MoonDefinition moon, float timeSeconds)
    {
        var angle = DegreesToRadians(moon.OrbitPhaseAtEpoch) + (timeSeconds * moon.OrbitSpeed);
        var local = new Vector3(
            MathF.Cos(angle) * moon.OrbitRadius,
            0f,
            MathF.Sin(angle) * moon.OrbitRadius);
        local = RotateAroundX(local, DegreesToRadians(moon.OrbitInclination));
        local = RotateAroundY(local, DegreesToRadians(moon.OrbitLongitudeOfAscendingNode));
        return local;
    }

    private static float ComputeResourceNodeOrbitSpeed(ResourceNodeRuntime node)
    {
        var baseSpeed = 0.24f;
        return baseSpeed / MathF.Sqrt(MathF.Max(node.OrbitRadius / 180000f, 0.45f));
    }

    private static Vector3 ComputeResourceNodeOffset(ResourceNodeRuntime node, float timeSeconds)
    {
        var angle = node.OrbitPhase + (timeSeconds * ComputeResourceNodeOrbitSpeed(node));
        var orbit = new Vector3(
            MathF.Cos(angle) * node.OrbitRadius,
            0f,
            MathF.Sin(angle) * node.OrbitRadius);
        return RotateAroundX(orbit, node.OrbitInclination);
    }

    private static IEnumerable<LagrangePointPlacement> EnumeratePlanetLagrangePoints(
        Vector3 planetPosition,
        PlanetDefinition planet)
    {
        var radial = NormalizeOrFallback(planetPosition, new Vector3(1f, 0f, 0f));
        var tangential = new Vector3(-radial.Z, 0f, radial.X);
        var orbitRadiusKm = MathF.Sqrt(planetPosition.X * planetPosition.X + planetPosition.Z * planetPosition.Z);
        var offset = ComputePlanetLocalLagrangeOffset(orbitRadiusKm, planet);
        var triangularAngle = MathF.PI / 3f;

        yield return new LagrangePointPlacement("L1", Add(planetPosition, Scale(radial, -offset)));
        yield return new LagrangePointPlacement("L2", Add(planetPosition, Scale(radial, offset)));
        yield return new LagrangePointPlacement("L3", Scale(radial, -orbitRadiusKm));
        yield return new LagrangePointPlacement(
            "L4",
            Add(
                Scale(radial, orbitRadiusKm * MathF.Cos(triangularAngle)),
                Scale(tangential, orbitRadiusKm * MathF.Sin(triangularAngle))));
        yield return new LagrangePointPlacement(
            "L5",
            Add(
                Scale(radial, orbitRadiusKm * MathF.Cos(triangularAngle)),
                Scale(tangential, -orbitRadiusKm * MathF.Sin(triangularAngle))));
    }

    private static float ComputePlanetLocalLagrangeOffset(float orbitRadiusKm, PlanetDefinition planet)
    {
        var planetMassProxy = EstimatePlanetMassRatio(planet);
        var hillLikeOffset = orbitRadiusKm * MathF.Cbrt(MathF.Max(planetMassProxy / 3f, 1e-9f));
        var minimumOffset = MathF.Max(planet.Size * 4f, 25000f);
        return MathF.Max(minimumOffset, hillLikeOffset);
    }

    private static float EstimatePlanetMassRatio(PlanetDefinition planet)
    {
        var earthRadiusRatio = MathF.Max(planet.Size / 6371f, 0.05f);
        var densityFactor = planet.PlanetType switch
        {
            "gas-giant" => 0.24f,
            "ice-giant" => 0.18f,
            "oceanic" => 0.95f,
            "ice" => 0.7f,
            _ => 1f,
        };

        var earthMasses = MathF.Pow(earthRadiusRatio, 3f) * densityFactor;
        return earthMasses / 332_946f;
    }

    private static Vector3 NormalizeOrFallback(Vector3 value, Vector3 fallback)
    {
        var length = MathF.Sqrt(value.LengthSquared());
        if (length <= 0.0001f)
        {
            return fallback;
        }

        return value.Divide(length);
    }

    private static Vector3 RotateAroundX(Vector3 value, float angle)
    {
        var cos = MathF.Cos(angle);
        var sin = MathF.Sin(angle);
        return new Vector3(
            value.X,
            (value.Y * cos) - (value.Z * sin),
            (value.Y * sin) + (value.Z * cos));
    }

    private static Vector3 RotateAroundY(Vector3 value, float angle)
    {
        var cos = MathF.Cos(angle);
        var sin = MathF.Sin(angle);
        return new Vector3(
            (value.X * cos) + (value.Z * sin),
            value.Y,
            (-value.X * sin) + (value.Z * cos));
    }

    private static Vector3 Add(Vector3 left, Vector3 right) => new(left.X + right.X, left.Y + right.Y, left.Z + right.Z);

    private static Vector3 Scale(Vector3 value, float scalar) => new(value.X * scalar, value.Y * scalar, value.Z * scalar);

    private static float DegreesToRadians(float degrees) => degrees * (MathF.PI / 180f);

    private static float HashUnit(string input)
    {
        unchecked
        {
            var hash = 2166136261u;
            foreach (var character in input)
            {
                hash ^= character;
                hash *= 16777619u;
            }

            return (hash & 0x00FFFFFF) / (float)0x01000000;
        }
    }

    internal void Update(SimulationWorld world)
    {
        var worldTimeSeconds = (float)world.OrbitalTimeSeconds;
        var celestialsById = world.Celestials.ToDictionary(c => c.Id, StringComparer.Ordinal);

        foreach (var system in world.Systems)
        {
            for (var starIndex = 0; starIndex < system.Definition.Stars.Count; starIndex += 1)
            {
                var star = system.Definition.Stars[starIndex];
                var starNodeId = $"node-{system.Definition.Id}-star-{starIndex + 1}";
                if (!celestialsById.TryGetValue(starNodeId, out var starNode))
                {
                    continue;
                }

                if (star.OrbitRadius <= 0f)
                {
                    starNode.Position = Vector3.Zero;
                }
                else
                {
                    var angle = DegreesToRadians(star.OrbitPhaseAtEpoch) + (worldTimeSeconds * star.OrbitSpeed);
                    starNode.Position = new Vector3(MathF.Cos(angle) * star.OrbitRadius, 0f, MathF.Sin(angle) * star.OrbitRadius);
                }
            }

            for (var planetIndex = 0; planetIndex < system.Definition.Planets.Count; planetIndex += 1)
            {
                var planet = system.Definition.Planets[planetIndex];
                var planetNodeId = $"node-{system.Definition.Id}-planet-{planetIndex + 1}";
                if (!celestialsById.TryGetValue(planetNodeId, out var planetNode))
                {
                    continue;
                }

                var planetPosition = ComputePlanetPosition(planet, worldTimeSeconds);
                planetNode.Position = planetPosition;

                foreach (var lagrange in EnumeratePlanetLagrangePoints(planetPosition, planet))
                {
                    var lagrangeId = $"node-{system.Definition.Id}-planet-{planetIndex + 1}-{lagrange.Designation.ToLowerInvariant()}";
                    if (celestialsById.TryGetValue(lagrangeId, out var lagrangeNode))
                    {
                        lagrangeNode.Position = lagrange.Position;
                    }
                }

                for (var moonIndex = 0; moonIndex < planet.Moons.Count; moonIndex += 1)
                {
                    var moonId = $"node-{system.Definition.Id}-planet-{planetIndex + 1}-moon-{moonIndex + 1}";
                    if (!celestialsById.TryGetValue(moonId, out var moonNode))
                    {
                        continue;
                    }

                    moonNode.Position = Add(planetPosition, ComputeMoonOffset(planet.Moons[moonIndex], worldTimeSeconds));
                }
            }
        }

        foreach (var station in world.Stations)
        {
            if (station.CelestialId is null || !celestialsById.TryGetValue(station.CelestialId, out var anchorCelestial))
            {
                continue;
            }

            station.Position = anchorCelestial.Position;
        }

        foreach (var node in world.Nodes)
        {
            if (node.CelestialId is null || !celestialsById.TryGetValue(node.CelestialId, out var anchorCelestial))
            {
                continue;
            }

            node.Position = Add(anchorCelestial.Position, ComputeResourceNodeOffset(node, worldTimeSeconds));
        }

        foreach (var ship in world.Ships.Where(ship => ship.DockedStationId is not null))
        {
            var station = world.Stations.FirstOrDefault(candidate => candidate.Id == ship.DockedStationId);
            if (station is null)
            {
                continue;
            }

            var dockedPosition = GetShipDockedPosition(ship, station);
            ship.Position = dockedPosition;
            ship.TargetPosition = dockedPosition;
        }
    }

    internal void SyncSpatialState(SimulationWorld world)
    {
        foreach (var ship in world.Ships)
        {
            ship.SpatialState.CurrentSystemId = ship.SystemId;
            ship.SpatialState.LocalPosition = ship.Position;
            ship.SpatialState.SystemPosition = ship.Position;
            if (ship.SpatialState.Transit is not null)
            {
                ship.SpatialState.CurrentCelestialId = null;
                continue;
            }

            ship.SpatialState.SpaceLayer = SpaceLayerKind.LocalSpace;
            ship.SpatialState.MovementRegime = MovementRegimeKind.LocalFlight;
            var nearestCelestial = world.Celestials
                .Where(candidate => candidate.SystemId == ship.SystemId)
                .OrderBy(candidate => candidate.Position.DistanceTo(ship.Position))
                .FirstOrDefault();
            ship.SpatialState.CurrentCelestialId = nearestCelestial?.Id;

            if (ship.DockedStationId is null)
            {
                continue;
            }

            var station = world.Stations.FirstOrDefault(candidate => candidate.Id == ship.DockedStationId);
            if (station?.CelestialId is not null)
            {
                ship.SpatialState.CurrentCelestialId = station.CelestialId;
            }
        }
    }

    private readonly record struct LagrangePointPlacement(string Designation, Vector3 Position);
}
