// Starfield3D.cpp - 3D Parallax Starfield Implementation
#include "Starfield3D.h"
#include <cmath>
#include <algorithm>

Starfield3D::Starfield3D() : rng(std::random_device{}()), width(800), height(600), centerX(400), centerY(300) {
}

void Starfield3D::init(int w, int h, int starCount) {
    width = w;
    height = h;
    centerX = width * 0.5f;
    centerY = height * 0.5f;
    
    stars.resize(starCount);
    createStarfield();
}

void Starfield3D::resize(int w, int h) {
    width = w;
    height = h;
    centerX = width * 0.5f;
    centerY = height * 0.5f;
}

void Starfield3D::setMagnetTarget(float localX, float localY, float strength) {
    if (strength <= 0.0f) {
        clearMagnetTarget();
        return;
    }
    magnetActive = true;
    magnetStrength = strength;
    magnetX = std::clamp(localX, 0.0f, static_cast<float>(width));
    magnetY = std::clamp(localY, 0.0f, static_cast<float>(height));
}

void Starfield3D::clearMagnetTarget() {
    magnetActive = false;
    magnetStrength = 0.0f;
}

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

int Starfield3D::randomRange(int min, int max) {
    std::uniform_int_distribution<int> dist(min, max - 1);
    return dist(rng);
}

void Starfield3D::setRandomDirection(Star3D& star) {
    star.targetVx = randomFloat(-MAX_VELOCITY, MAX_VELOCITY);
    star.targetVy = randomFloat(-MAX_VELOCITY, MAX_VELOCITY);
    
    // Allow stars to move both toward and away from viewer
    if (randomFloat(0.0f, 1.0f) < REVERSE_PROBABILITY) {
        // Move away from viewer (positive Z)
        star.targetVz = STAR_SPEED * randomFloat(0.5f, 1.0f);
    } else {
        // Move toward viewer (negative Z)
        star.targetVz = -STAR_SPEED * randomFloat(0.7f, 1.3f);
    }
    
    star.changing = true;
    star.changeTimer = randomFloat(30.0f, 120.0f); // Direction change lasts 30-120 frames
}

void Starfield3D::updateStar(int index) {
    Star3D& star = stars[index];

    // Avoid spawning stars on (or very near) the view axis. A star with x≈0 and y≈0
    // projects to the exact center, and when it happens to be bright it looks like a
    // static "big" star.
    constexpr float SPAWN_RANGE = 25.0f;
    constexpr float MIN_AXIS_RADIUS = 2.5f; // in star-space units
    for (int attempt = 0; attempt < 8; ++attempt) {
        star.x = randomFloat(-SPAWN_RANGE, SPAWN_RANGE);
        star.y = randomFloat(-SPAWN_RANGE, SPAWN_RANGE);
        if ((star.x * star.x + star.y * star.y) >= (MIN_AXIS_RADIUS * MIN_AXIS_RADIUS)) {
            break;
        }
    }
    // If we somehow still ended up too close, push it out deterministically.
    if ((star.x * star.x + star.y * star.y) < (MIN_AXIS_RADIUS * MIN_AXIS_RADIUS)) {
        star.x = (star.x < 0.0f ? -1.0f : 1.0f) * MIN_AXIS_RADIUS;
        star.y = (star.y < 0.0f ? -1.0f : 1.0f) * MIN_AXIS_RADIUS;
    }
    star.z = randomFloat(1.0f, MAX_DEPTH);
    
    // Give stars initial velocities in all possible directions
    if (randomFloat(0.0f, 1.0f) < 0.5f) {
        // Half stars start moving toward viewer
        star.vx = randomFloat(-0.1f, 0.1f);
        star.vy = randomFloat(-0.1f, 0.1f);
        star.vz = -STAR_SPEED * randomFloat(0.8f, 1.2f);
    } else {
        // Half stars start moving in random directions
        star.vx = randomFloat(-0.2f, 0.2f);
        star.vy = randomFloat(-0.2f, 0.2f);
        
        // 30% chance to start moving away
        if (randomFloat(0.0f, 1.0f) < 0.3f) {
            star.vz = STAR_SPEED * randomFloat(0.5f, 0.8f);
        } else {
            star.vz = -STAR_SPEED * randomFloat(0.8f, 1.2f);
        }
    }

    // Ensure newly spawned stars have some lateral drift so they don't appear to
    // "stick" near the center line.
    if (std::abs(star.vx) < 0.02f && std::abs(star.vy) < 0.02f) {
        const float sx = (star.x < 0.0f ? -1.0f : 1.0f);
        const float sy = (star.y < 0.0f ? -1.0f : 1.0f);
        star.vx = sx * randomFloat(0.04f, 0.14f);
        star.vy = sy * randomFloat(0.04f, 0.14f);
    }
    
    star.targetVx = star.vx;
    star.targetVy = star.vy;
    star.targetVz = star.vz;
    star.changing = false;
    star.changeTimer = 0.0f;
    star.type = randomRange(0, COLOR_COUNT);
    
    // Give some stars initial direction variations
    if (randomFloat(0.0f, 1.0f) < 0.4f) {
        setRandomDirection(star);
    }
}

void Starfield3D::createStarfield() {
    for (size_t i = 0; i < stars.size(); ++i) {
        updateStar(static_cast<int>(i));
    }
}

void Starfield3D::update(float deltaTime) {
    const float frameRate = 60.0f; // Target 60 FPS for consistency
    const float frameMultiplier = deltaTime * frameRate;
    
    for (size_t i = 0; i < stars.size(); ++i) {
        Star3D& star = stars[i];
        
        // Randomly change direction occasionally
        if (!star.changing && randomFloat(0.0f, 1.0f) < DIRECTION_CHANGE_PROBABILITY * frameMultiplier) {
            setRandomDirection(star);
        }
        
        // Update velocities to approach target values
        if (star.changing) {
            // Smoothly transition to target velocities
            const float change = VELOCITY_CHANGE * frameMultiplier;
            star.vx += (star.targetVx - star.vx) * change;
            star.vy += (star.targetVy - star.vy) * change;
            star.vz += (star.targetVz - star.vz) * change;
            
            // Decrement change timer
            star.changeTimer -= frameMultiplier;
            if (star.changeTimer <= 0.0f) {
                star.changing = false;
            }
        }
        
        // Update position using current velocity
        star.x += star.vx * frameMultiplier;
        star.y += star.vy * frameMultiplier;
        star.z += star.vz * frameMultiplier;
        
        // Handle boundaries - reset star if it moves out of bounds, too close, or too far
        if (star.z <= MIN_Z || 
            star.z >= MAX_Z || 
            std::abs(star.x) > 50.0f || 
            std::abs(star.y) > 50.0f) {
            updateStar(static_cast<int>(i));
        }
    }
}

void Starfield3D::drawStar(SDL_Renderer* renderer, float x, float y, SDL_Color color, float alphaScale) {
    Uint8 alpha = static_cast<Uint8>(std::clamp(color.a * alphaScale, 0.0f, 255.0f));
    SDL_SetRenderDrawColor(renderer, color.r, color.g, color.b, alpha);
    
    // Draw star as a small rectangle (1x1 pixel)
    SDL_FRect rect{x, y, 1.0f, 1.0f};
    SDL_RenderFillRect(renderer, &rect);
}

void Starfield3D::draw(SDL_Renderer* renderer, float offsetX, float offsetY, float alphaScale, bool grayscale) {
    // Small visual jitter applied to the visual center so a single bright star
    // doesn't remain perfectly fixed at the exact pixel center of the viewport.
    const float jitterAmp = 1.6f; // max pixel offset
    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);

    const bool useMagnet = magnetActive && magnetStrength > 0.0f;
    for (const Star3D& star : stars) {
        // Calculate perspective projection factor
        const float k = DEPTH_FACTOR / star.z;
        
        // Calculate screen position with perspective
        float px = star.x * k + centerX + centerJitterX;
        float py = star.y * k + centerY + centerJitterY;

        if (useMagnet) {
            float dx = magnetX - px;
            float dy = magnetY - py;
            float dist = std::sqrt(dx * dx + dy * dy);
            float pull = magnetStrength / (magnetStrength + dist + 1.0f);
            pull = std::clamp(pull, 0.0f, 0.35f);
            px += dx * pull;
            py += dy * pull;
        }
        
        // Only draw stars that are within the viewport
        if (px >= 0.0f && px <= static_cast<float>(width) && 
            py >= 0.0f && py <= static_cast<float>(height)) {
            SDL_Color baseColor = STAR_COLORS[star.type % COLOR_COUNT];
            if (grayscale) {
                Uint8 gray = static_cast<Uint8>(0.299f * baseColor.r + 0.587f * baseColor.g + 0.114f * baseColor.b);
                baseColor.r = baseColor.g = baseColor.b = gray;
            }
            drawStar(renderer, px + offsetX, py + offsetY, baseColor, alphaScale);
        }
    }
}
