#include "SpaceWarp.h"

#include <algorithm>
#include <array>
#include <cmath>

namespace {
constexpr float MIN_ASPECT = 0.001f;
}

SpaceWarp::SpaceWarp() {
    std::random_device rd;
    rng.seed(rd());
    setFlightMode(SpaceWarpFlightMode::Forward);
}

void SpaceWarp::init(int w, int h, int starCount) {
    resize(w, h);
    stars.resize(std::max(8, starCount));
    for (auto& star : stars) {
        respawn(star, true);
    }
    comets.clear();
    cometSpawnTimer = randomRange(settings.cometSpawnIntervalMin, settings.cometSpawnIntervalMax);
}

void SpaceWarp::resize(int w, int h) {
    width = std::max(1, w);
    height = std::max(1, h);
    centerX = width * 0.5f;
    centerY = height * 0.5f;
    warpFactor = std::max(width, height) * settings.warpFactorScale;
}

void SpaceWarp::setSettings(const SpaceWarpSettings& newSettings) {
    settings = newSettings;
    warpFactor = std::max(width, height) * settings.warpFactorScale;
    cometSpawnTimer = std::clamp(cometSpawnTimer, settings.cometSpawnIntervalMin, settings.cometSpawnIntervalMax);
}

void SpaceWarp::setFlightMode(SpaceWarpFlightMode mode) {
    flightMode = mode;
    autoPilotEnabled = false;
    switch (mode) {
        case SpaceWarpFlightMode::Forward:
            motion = {1.0f, 0.0f, 0.0f};
            break;
        case SpaceWarpFlightMode::BankLeft:
            motion = {1.05f, -0.85f, 0.0f};
            break;
        case SpaceWarpFlightMode::BankRight:
            motion = {1.05f, 0.85f, 0.0f};
            break;
        case SpaceWarpFlightMode::Reverse:
            motion = {-0.6f, 0.0f, 0.0f};
            break;
        case SpaceWarpFlightMode::Custom:
        default:
            break;
    }
}

void SpaceWarp::setFlightMotion(const SpaceWarpFlightMotion& newMotion) {
    motion = newMotion;
    flightMode = SpaceWarpFlightMode::Custom;
    autoPilotEnabled = false;
}

void SpaceWarp::setAutoPilotEnabled(bool enabled) {
    autoPilotEnabled = enabled;
    if (enabled) {
        flightMode = SpaceWarpFlightMode::Custom;
        motionTarget = motion;
        autoTimer = 0.0f;
        scheduleNewAutoTarget();
    }
}

void SpaceWarp::scheduleNewAutoTarget() {
    // Autopilot behavior:
    // - 90% of the time: gentle forward flight with small lateral/vertical drift
    // - 10% of the time: short lateral "bank" burst (stronger lateral speed) for a while
    float choice = randomRange(0.0f, 1.0f);
    if (choice < 0.90f) {
        // Normal forward flight
        motionTarget.forwardScale = randomRange(0.95f, 1.12f);
        motionTarget.lateralSpeed = randomRange(-0.18f, 0.18f);
        motionTarget.verticalSpeed = randomRange(-0.12f, 0.12f);
        // Longer interval between aggressive maneuvers
        autoTimer = randomRange(autoMinInterval, autoMaxInterval);
    } else {
        // Occasional lateral bank burst
        motionTarget.forwardScale = randomRange(0.90f, 1.10f);
        // Pick left or right burst
        float dir = (randomRange(0.0f, 1.0f) < 0.5f) ? -1.0f : 1.0f;
        motionTarget.lateralSpeed = dir * randomRange(0.70f, 1.35f);
        // Allow modest vertical bias during a bank
        motionTarget.verticalSpeed = randomRange(-0.35f, 0.35f);
        // Shorter duration for the burst so it feels like a brief maneuver
        autoTimer = randomRange(1.0f, 3.0f);
    }
}

void SpaceWarp::spawnComet() {
    WarpComet comet;
    float aspect = static_cast<float>(width) / static_cast<float>(std::max(1, height));
    float normalizedAspect = std::max(aspect, MIN_ASPECT);
    float xRange = settings.baseSpawnRange * 1.2f * (aspect >= 1.0f ? aspect : 1.0f);
    float yRange = settings.baseSpawnRange * 1.2f * (aspect >= 1.0f ? 1.0f : (1.0f / normalizedAspect));
    // Avoid spawning comets exactly on (or extremely near) the view axis,
    // which can project to a nearly static bright dot.
    const float axisMinFrac = 0.06f;
    bool axisOk = false;
    for (int attempt = 0; attempt < 10 && !axisOk; ++attempt) {
        comet.x = randomRange(-xRange, xRange);
        comet.y = randomRange(-yRange, yRange);
        float nx = comet.x / std::max(xRange, 0.0001f);
        float ny = comet.y / std::max(yRange, 0.0001f);
        axisOk = (nx * nx + ny * ny) >= (axisMinFrac * axisMinFrac);
    }
    if (!axisOk) {
        float ang = randomRange(0.0f, 6.28318530718f);
        comet.x = std::cos(ang) * xRange * axisMinFrac;
        comet.y = std::sin(ang) * yRange * axisMinFrac;
    }
    comet.z = randomRange(minDepth + 4.0f, maxDepth);
    float baseSpeed = randomRange(settings.minSpeed, settings.maxSpeed);
    float multiplier = randomRange(settings.cometSpeedMultiplierMin, settings.cometSpeedMultiplierMax);
    comet.speed = baseSpeed * multiplier;
    comet.size = randomRange(settings.cometMinSize, settings.cometMaxSize);
    comet.trailLength = randomRange(settings.cometMinTrail, settings.cometMaxTrail);
    comet.life = randomRange(1.8f, 3.4f);
    comet.maxLife = comet.life;
    float shade = randomRange(0.85f, 1.0f);
    Uint8 c = static_cast<Uint8>(std::clamp(220.0f + shade * 35.0f, 0.0f, 255.0f));
    comet.color = SDL_Color{c, Uint8(std::min(255.0f, c * 0.95f)), 255, 255};
    // Initialize screen positions based on projection so the comet is not stuck at center
    float sx = 0.0f, sy = 0.0f;
    if (projectPoint(comet.x, comet.y, comet.z, sx, sy)) {
        comet.screenX = sx;
        comet.screenY = sy;
        // Place prev slightly behind the head so the first frame shows motion/trail
        float jitter = std::max(4.0f, comet.trailLength * 0.08f);
        float ang = randomRange(0.0f, 6.28318530718f);
        comet.prevScreenX = comet.screenX - std::cos(ang) * jitter;
        comet.prevScreenY = comet.screenY - std::sin(ang) * jitter;
    } else {
        comet.prevScreenX = centerX;
        comet.prevScreenY = centerY;
        comet.screenX = centerX;
        comet.screenY = centerY;
    }
    comets.push_back(comet);
}

float SpaceWarp::randomRange(float min, float max) {
    std::uniform_real_distribution<float> dist(min, max);
    return dist(rng);
}

static int randomIntInclusive(std::mt19937& rng, int min, int max) {
    std::uniform_int_distribution<int> dist(min, max);
    return dist(rng);
}

void SpaceWarp::respawn(WarpStar& star, bool randomDepth) {
    float aspect = static_cast<float>(width) / static_cast<float>(std::max(1, height));
    float normalizedAspect = std::max(aspect, MIN_ASPECT);
    float xRange = settings.baseSpawnRange * (aspect >= 1.0f ? aspect : 1.0f);
    float yRange = settings.baseSpawnRange * (aspect >= 1.0f ? 1.0f : (1.0f / normalizedAspect));
    // Avoid axis-aligned stars (x≈0,y≈0) which can project to a static, bright center dot.
    const float axisMinFrac = 0.06f;
    bool axisOk = false;
    for (int attempt = 0; attempt < 10 && !axisOk; ++attempt) {
        star.x = randomRange(-xRange, xRange);
        star.y = randomRange(-yRange, yRange);
        float nx = star.x / std::max(xRange, 0.0001f);
        float ny = star.y / std::max(yRange, 0.0001f);
        axisOk = (nx * nx + ny * ny) >= (axisMinFrac * axisMinFrac);
    }
    if (!axisOk) {
        float ang = randomRange(0.0f, 6.28318530718f);
        star.x = std::cos(ang) * xRange * axisMinFrac;
        star.y = std::sin(ang) * yRange * axisMinFrac;
    }

    // Keep z slightly above minDepth so projection never starts from the exact singular plane.
    star.z = randomDepth ? randomRange(minDepth + 0.25f, maxDepth) : maxDepth;
    star.speed = randomRange(settings.minSpeed, settings.maxSpeed);
    star.shade = randomRange(settings.minShade, settings.maxShade);
    static constexpr Uint8 GRAY_SHADES[] = {160, 180, 200, 220, 240};
    int idx = randomIntInclusive(rng, 0, int(std::size(GRAY_SHADES)) - 1);
    star.baseShade = GRAY_SHADES[idx];
    // Compute initial projected screen position so newly spawned stars aren't frozen at center
    float sx = 0.0f, sy = 0.0f;
    if (projectPoint(star.x, star.y, star.z, sx, sy)) {
        star.screenX = sx;
        star.screenY = sy;
        // give a small previous offset so trails and motion are visible immediately
        float jitter = std::max(1.0f, settings.maxTrailLength * 0.06f);
        float ang = randomRange(0.0f, 6.28318530718f);
        star.prevScreenX = star.screenX - std::cos(ang) * jitter;
        star.prevScreenY = star.screenY - std::sin(ang) * jitter;
    } else {
        star.prevScreenX = centerX;
        star.prevScreenY = centerY;
        star.screenX = centerX;
        star.screenY = centerY;
    }
}

bool SpaceWarp::project(const WarpStar& star, float& outX, float& outY) const {
    return projectPoint(star.x, star.y, star.z, outX, outY);
}

bool SpaceWarp::projectPoint(float x, float y, float z, float& outX, float& outY) const {
    if (z <= minDepth) {
        return false;
    }
    float perspective = warpFactor / (z + 0.001f);
    outX = centerX + x * perspective;
    outY = centerY + y * perspective;
    const float margin = settings.spawnMargin;
    return outX >= -margin && outX <= width + margin && outY >= -margin && outY <= height + margin;
}

void SpaceWarp::update(float deltaSeconds) {
    if (stars.empty()) {
        return;
    }

    if (settings.cometSpawnIntervalMax > 0.0f) {
        cometSpawnTimer -= deltaSeconds;
        if (cometSpawnTimer <= 0.0f) {
            spawnComet();
            cometSpawnTimer = randomRange(settings.cometSpawnIntervalMin, settings.cometSpawnIntervalMax);
        }
    }

    if (autoPilotEnabled) {
        autoTimer -= deltaSeconds;
        if (autoTimer <= 0.0f) {
            scheduleNewAutoTarget();
        }
        auto follow = std::clamp(deltaSeconds * 0.45f, 0.0f, 1.0f);
        motion.forwardScale = std::lerp(motion.forwardScale, motionTarget.forwardScale, follow);
        motion.lateralSpeed = std::lerp(motion.lateralSpeed, motionTarget.lateralSpeed, follow);
        motion.verticalSpeed = std::lerp(motion.verticalSpeed, motionTarget.verticalSpeed, follow);
    }

    const float forwardScale = (std::abs(motion.forwardScale) < 0.01f)
        ? (motion.forwardScale >= 0.0f ? 0.01f : -0.01f)
        : motion.forwardScale;
    const bool movingBackward = forwardScale < 0.0f;
    const float lateralSpeed = motion.lateralSpeed;
    const float verticalSpeed = motion.verticalSpeed;

    for (auto& star : stars) {
        star.z -= star.speed * deltaSeconds * forwardScale;
        if (!movingBackward) {
            if (star.z <= minDepth) {
                respawn(star, true);
                continue;
            }
        } else {
            if (star.z >= maxDepth) {
                respawn(star, true);
                star.z = minDepth + randomRange(0.25f, 24.0f);
                continue;
            }
        }

        float closeness = 1.0f - std::clamp(star.z / maxDepth, 0.0f, 1.0f);
        float driftScale = (0.35f + closeness * 1.25f);
        star.x += lateralSpeed * deltaSeconds * driftScale;
        star.y += verticalSpeed * deltaSeconds * driftScale;

        float sx = 0.0f;
        float sy = 0.0f;
        if (!project(star, sx, sy)) {
            respawn(star, true);
            continue;
        }

        // If a star projects to (near) the visual center, it can appear perfectly static
        // during straight-line flight. Replace it to avoid the "big static star" artifact.
        if (std::abs(sx - centerX) < 1.25f && std::abs(sy - centerY) < 1.25f) {
            respawn(star, true);
            continue;
        }

        star.prevScreenX = star.screenX;
        star.prevScreenY = star.screenY;
        star.screenX = sx;
        star.screenY = sy;

        float dx = star.screenX - star.prevScreenX;
        float dy = star.screenY - star.prevScreenY;
        float lenSq = dx * dx + dy * dy;
        float maxStreak = std::max(settings.maxTrailLength, 0.0f);
        if (maxStreak > 0.0f && lenSq > maxStreak * maxStreak) {
            float len = std::sqrt(lenSq);
            float scale = maxStreak / len;
            star.prevScreenX = star.screenX - dx * scale;
            star.prevScreenY = star.screenY - dy * scale;
        }
    }

    for (auto it = comets.begin(); it != comets.end();) {
        auto& comet = *it;
        comet.life -= deltaSeconds;
        comet.z -= comet.speed * deltaSeconds * forwardScale;
        bool expired = comet.life <= 0.0f;
        if (!movingBackward) {
            if (comet.z <= minDepth * 0.35f) expired = true;
        } else {
            if (comet.z >= maxDepth + 40.0f) expired = true;
        }

        float closeness = 1.0f - std::clamp(comet.z / maxDepth, 0.0f, 1.0f);
        float driftScale = (0.45f + closeness * 1.6f);
        comet.x += lateralSpeed * deltaSeconds * driftScale;
        comet.y += verticalSpeed * deltaSeconds * driftScale;

        float sx = 0.0f;
        float sy = 0.0f;
        if (!projectPoint(comet.x, comet.y, comet.z, sx, sy)) {
            expired = true;
        } else {
            comet.prevScreenX = comet.screenX;
            comet.prevScreenY = comet.screenY;
            comet.screenX = sx;
            comet.screenY = sy;

            float dx = comet.screenX - comet.prevScreenX;
            float dy = comet.screenY - comet.prevScreenY;
            float lenSq = dx * dx + dy * dy;
            float maxTrail = std::max(comet.trailLength, 0.0f);
            if (maxTrail > 0.0f && lenSq > maxTrail * maxTrail) {
                float len = std::sqrt(lenSq);
                float scale = maxTrail / len;
                comet.prevScreenX = comet.screenX - dx * scale;
                comet.prevScreenY = comet.screenY - dy * scale;
            }
        }

        if (expired) {
            it = comets.erase(it);
        } else {
            ++it;
        }
    }
}

void SpaceWarp::draw(SDL_Renderer* renderer, float alphaScale) {
    if (stars.empty()) {
        return;
    }

    // Add a small, smooth sub-pixel jitter to the visual center used for
    // rendering so a single bright star/comet doesn't permanently sit exactly
    // at the pixel-perfect center of the viewport.
    const float jitterAmp = 1.6f; // maximum jitter in pixels
    const uint32_t now = SDL_GetTicks();
    const float tms = static_cast<float>(now) * 0.001f;
    const float centerJitterX = std::sin(tms * 1.7f) * jitterAmp + std::cos(tms * 0.9f) * 0.4f;
    const float centerJitterY = std::sin(tms * 1.1f + 3.7f) * (jitterAmp * 0.6f);

    SDL_BlendMode previous = SDL_BLENDMODE_NONE;
    SDL_GetRenderDrawBlendMode(renderer, &previous);
    SDL_SetRenderDrawBlendMode(renderer, SDL_BLENDMODE_ADD);

    for (const auto& star : stars) {
        float depthFactor = 1.0f - std::clamp(star.z / maxDepth, 0.0f, 1.0f);
        float alphaBase = std::clamp(settings.minAlpha + depthFactor * settings.alphaDepthBoost, 0.0f, 255.0f);
        Uint8 alpha = static_cast<Uint8>(std::clamp(alphaBase * alphaScale, 0.0f, 255.0f));
        float colorValue = std::clamp(
            star.baseShade * (settings.baseShadeScale + depthFactor * settings.depthColorScale) * star.shade,
            settings.minColor,
            settings.maxColor);
        Uint8 color = static_cast<Uint8>(colorValue);

        if (settings.drawTrails) {
            float trailAlphaFloat = alpha * settings.trailAlphaScale;
            Uint8 trailAlpha = static_cast<Uint8>(std::clamp(trailAlphaFloat, 0.0f, 255.0f));
            SDL_SetRenderDrawColor(renderer, color, color, color, trailAlpha);
            SDL_RenderLine(renderer,
                           star.prevScreenX + centerJitterX, star.prevScreenY + centerJitterY,
                           star.screenX + centerJitterX, star.screenY + centerJitterY);
        }

        float dotSize = std::clamp(settings.minDotSize + depthFactor * (settings.maxDotSize - settings.minDotSize),
                                   settings.minDotSize,
                                   settings.maxDotSize);
        SDL_FRect dot{star.screenX - dotSize * 0.5f + centerJitterX,
                      star.screenY - dotSize * 0.5f + centerJitterY,
                      dotSize, dotSize};
        SDL_SetRenderDrawColor(renderer, color, color, color, alpha);
        SDL_RenderFillRect(renderer, &dot);
    }

    for (const auto& comet : comets) {
        float lifeNorm = std::clamp(comet.life / comet.maxLife, 0.0f, 1.0f);
        Uint8 alpha = static_cast<Uint8>(std::clamp(220.0f * lifeNorm, 0.0f, 255.0f));
        SDL_SetRenderDrawColor(renderer, comet.color.r, comet.color.g, comet.color.b, alpha);
        SDL_RenderLine(renderer,
                   comet.prevScreenX + centerJitterX, comet.prevScreenY + centerJitterY,
                   comet.screenX + centerJitterX, comet.screenY + centerJitterY);

        float size = comet.size * (0.8f + (1.0f - lifeNorm) * 0.6f);
        SDL_FRect head{comet.screenX - size * 0.5f + centerJitterX,
                       comet.screenY - size * 0.5f + centerJitterY,
                       size, size};
        SDL_RenderFillRect(renderer, &head);
    }

    SDL_SetRenderDrawBlendMode(renderer, previous);
}
