package wtf.beatrice.retrorender.engine.render;

import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.Input;
import com.badlogic.gdx.graphics.Cursor;
import com.badlogic.gdx.graphics.Pixmap;
import com.badlogic.gdx.graphics.PerspectiveCamera;
import com.badlogic.gdx.math.Vector3;
import wtf.beatrice.retrorender.engine.config.GameSettings;
import wtf.beatrice.retrorender.engine.world.World3D;

public class FpsCameraController {

    private final PerspectiveCamera camera;
    private final GameSettings settings;

    // movement + look
    private float yaw = 0f;
    private float pitch = 0f;

    private final float eyeHeight = 1.8f;
    private final float jumpSpeed = 12f;
    private final float gravity = -50f;
    private float velocityY = 0f;
    private boolean grounded = false;

    // view bobbing
    private float bobTime    = 0f;
    private float bobOffsetY = 0f;
    private final float bobAmount = 0.04f; // amplitude in world units
    private final float bobSpeed  = 16f;

    // --- approximation toggles
    private final boolean retroQuantization = false;

    // how coarse the rotation is (0.25f = round to 0.25°)
    private final float yawStepDegrees   = 0.2f;
    private final float pitchStepDegrees = 0.2f;

    // how coarse the position is (16f = steps of 1/16 units)
    private final float positionQuantize = 64f;

    private final Vector3 tmpForward = new Vector3();
    private final Vector3 tmpRight = new Vector3();

    // mouse capture / centering
    private boolean mouseCaptured = true;
    private int centerX;
    private int centerY;
    private int captureWarmupFrames = 0;
    private static final int WARMUP_FRAMES = 10; // frames to ignore after capture

    private Cursor invisibleCursor;

    public FpsCameraController(PerspectiveCamera camera, GameSettings settings) {
        this.camera = camera;
        this.settings = settings;

        camera.position.set(0f, eyeHeight, 6f);
        camera.near = 0.1f;
        camera.far = 100f;

        yaw = 180f;
        pitch = -15f;
        updateCameraDirection();

        centerX = Gdx.graphics.getWidth() / 2;
        centerY = Gdx.graphics.getHeight() / 2;

        grounded = true;
    }

    /** Call from Screen.show() */
    public void onShow() {
        captureMouse();
    }

    /** Call from Screen.hide() */
    public void onHide() {
        releaseMouse();
    }

    /** Explicitly capture mouse (used when resuming from pause). */
    public void captureMouse() {
        mouseCaptured = true;
        centerX = Gdx.graphics.getWidth() / 2;
        centerY = Gdx.graphics.getHeight() / 2;
        Gdx.input.setCursorPosition(centerX, centerY);

        ensureInvisibleCursor();
        Gdx.graphics.setCursor(invisibleCursor);

        captureWarmupFrames = WARMUP_FRAMES;
    }

    /** Explicitly release mouse (used when pausing). */
    public void releaseMouse() {
        mouseCaptured = false;
        Gdx.graphics.setSystemCursor(Cursor.SystemCursor.Arrow);
    }

    /** Call from Screen.resize() */
    public void resize(int width, int height) {
        centerX = width / 2;
        centerY = height / 2;
        if (mouseCaptured) {
            Gdx.input.setCursorPosition(centerX, centerY);
        }
        // after resize, skip frames to avoid mouse skew
        captureWarmupFrames = WARMUP_FRAMES;
    }

    /** Update each frame with delta time */
    public void update(float delta, World3D world) {
        if (settings.bobbingEnabled) {
            camera.position.y -= bobOffsetY;
            bobOffsetY = 0f;
        }

        if (!mouseCaptured) {
            return;
        }

        // warmup: ignore mouse for a few frames after capture
        if (captureWarmupFrames > 0) {
            captureWarmupFrames--;
            Gdx.input.setCursorPosition(centerX, centerY);
            return;
        }

        // --- mouse look ---
        int mouseX = Gdx.input.getX();
        int mouseY = Gdx.input.getY();

        int dx = mouseX - centerX;
        int dy = mouseY - centerY;

        // recenter for next frame
        Gdx.input.setCursorPosition(centerX, centerY);

        float deltaX = -dx * settings.mouseSensitivity;
        float deltaY = -dy * settings.mouseSensitivity;

        yaw   += deltaX;
        pitch += deltaY;

        pitch = Math.max(-89f, Math.min(89f, pitch));
        if (yaw >= 360f) yaw -= 360f;
        if (yaw < 0f)    yaw += 360f;

        updateCameraDirection();

        // --- build movement vectors ---
        tmpForward.set(camera.direction.x, 0f, camera.direction.z).nor();
        tmpRight.set(tmpForward.z, 0f, -tmpForward.x).nor();

        camera.fieldOfView = settings.fov;

        float moveAmount = settings.moveSpeed * delta;
        float moveX = 0f;
        float moveZ = 0f;

        if (Gdx.input.isKeyPressed(Input.Keys.W)) {
            moveX += tmpForward.x * moveAmount;
            moveZ += tmpForward.z * moveAmount;
        }
        if (Gdx.input.isKeyPressed(Input.Keys.S)) {
            moveX -= tmpForward.x * moveAmount;
            moveZ -= tmpForward.z * moveAmount;
        }
        if (Gdx.input.isKeyPressed(Input.Keys.A)) {
            moveX += tmpRight.x * moveAmount;
            moveZ += tmpRight.z * moveAmount;
        }
        if (Gdx.input.isKeyPressed(Input.Keys.D)) {
            moveX -= tmpRight.x * moveAmount;
            moveZ -= tmpRight.z * moveAmount;
        }

        boolean jumpHeld = Gdx.input.isKeyPressed(Input.Keys.SPACE);

        float playerRadius = 0.4f;

        // =========================================================
        // 1) VERTICAL INTEGRATION + GROUND SNAP (capsule-based)
        // =========================================================

        // Apply gravity
        velocityY += gravity * delta;

        float oldY      = camera.position.y;
        float proposedY = oldY + velocityY * delta;

        // Capsule ground at current XZ
        float groundY = world.getCapsuleGroundHeight(
            camera.position.x,
            camera.position.z,
            playerRadius,
            0f
        );

        float feetNow   = oldY      - eyeHeight;
        float feetNext  = proposedY - eyeHeight;
        final float GROUND_SNAP_EPS = 0.15f; // max distance to snap down

        if (velocityY <= 0f && feetNext <= groundY + GROUND_SNAP_EPS) {
            // Falling or resting, and we reached (or slightly passed) the ground
            grounded       = true;
            velocityY      = 0f;
            camera.position.y = groundY + eyeHeight;
        } else {
            grounded       = false;
            camera.position.y = proposedY;
        }

        // Handle jump AFTER we've updated grounded state
        if (grounded && jumpHeld) {
            velocityY = jumpSpeed;
            grounded  = false;
        }

        // =========================================================
        // 2) HORIZONTAL MOVEMENT WITH COLLISION + OPTIONAL STEP UP
        // =========================================================

        // We re-query ground at the start of the frame for step logic
        float currentGroundY = world.getCapsuleGroundHeight(
            camera.position.x,
            camera.position.z,
            playerRadius,
            0f
        );

        float curY = camera.position.y;

        // Max height we auto-step; must be < crate height, < house walls, etc
        final float maxStepHeight = 0.6f;   // your crate is 1.0 high, so you must jump
        final float stepSnapEps   = 0.05f;  // tolerance when landing on step

        // --- X axis ---
        if (moveX != 0f) {
            float targetX = camera.position.x + moveX;
            float targetZ = camera.position.z;

            boolean collides = world.collidesAt(
                targetX,
                curY,
                targetZ,
                playerRadius,
                eyeHeight
            );

            if (!collides) {
                camera.position.x = targetX;
            } else if (grounded && velocityY <= 0.01f) {
                // Only try to step up if we are actually on the ground (walking),
                // NOT while jumping or falling.
                float newGroundY = world.getCapsuleGroundHeight(
                    targetX,
                    targetZ,
                    playerRadius,
                    0f
                );
                float step = newGroundY - currentGroundY;

                if (step > 0f && step <= maxStepHeight) {
                    // Step up: teleport onto the higher surface
                    camera.position.x = targetX;
                    camera.position.y = newGroundY + eyeHeight + stepSnapEps;
                    curY              = camera.position.y;
                    currentGroundY    = newGroundY;
                    grounded          = true;
                    velocityY         = 0f;
                }
                // else: it's a real wall or too high → ignore X movement
            }
        }

        // --- Z axis ---
        if (moveZ != 0f) {
            float targetX = camera.position.x;
            float targetZ = camera.position.z + moveZ;

            boolean collides = world.collidesAt(
                targetX,
                curY,
                targetZ,
                playerRadius,
                eyeHeight
            );

            if (!collides) {
                camera.position.z = targetZ;
            } else if (grounded && velocityY <= 0.01f) {
                float newGroundY = world.getCapsuleGroundHeight(
                    targetX,
                    targetZ,
                    playerRadius,
                    0f
                );
                float step = newGroundY - currentGroundY;

                if (step > 0f && step <= maxStepHeight) {
                    camera.position.z = targetZ;
                    camera.position.y = newGroundY + eyeHeight + stepSnapEps;
                    curY              = camera.position.y;
                    currentGroundY    = newGroundY;
                    grounded          = true;
                    velocityY         = 0f;
                }
            }
        }

        // =========================================================
        // 3) RETRO QUANTIZATION
        // =========================================================
        if (retroQuantization) {
            float q = positionQuantize;
            camera.position.x = Math.round(camera.position.x * q) / q;
            camera.position.y = Math.round(camera.position.y * q) / q;
            camera.position.z = Math.round(camera.position.z * q) / q;
        }

        // =========================================================
        // 4) VIEW BOBBING (purely visual)
        // =========================================================
        // horizontal movement magnitude (world units per frame)
        if (settings.bobbingEnabled) {
            float horizLen = (float)Math.sqrt(moveX * moveX + moveZ * moveZ);

            if (grounded && horizLen > 0.0001f) {
                // scale bob speed by how fast you're moving
                float speedFactor = horizLen / (settings.moveSpeed * delta + 1e-6f);
                bobTime += delta * bobSpeed * speedFactor;

                bobOffsetY = (float)Math.sin(bobTime) * bobAmount;
            } else {
                // not moving or in the air → relax bobbing back to zero
                bobTime = 0f;
                // optional: smooth return to 0 instead of snapping
                bobOffsetY += (0f - bobOffsetY) * 10f * delta;
            }

            camera.position.y += bobOffsetY;
        }
    }
    private void updateCameraDirection() {
        // build direction vector from yaw/pitch
        float useYaw   = yaw;
        float usePitch = pitch;

        if (retroQuantization) {
            // snap yaw/pitch to fixed angular steps
            useYaw   = Math.round(useYaw / yawStepDegrees) * yawStepDegrees;
            usePitch = Math.round(usePitch / pitchStepDegrees) * pitchStepDegrees;
        }

        float yawRad   = (float) Math.toRadians(useYaw);
        float pitchRad = (float) Math.toRadians(usePitch);

        float x = (float) (Math.cos(pitchRad) * Math.sin(yawRad));
        float y = (float) Math.sin(pitchRad);
        float z = (float) (Math.cos(pitchRad) * Math.cos(yawRad));

        camera.direction.set(x, y, z).nor();
    }

    private void ensureInvisibleCursor() {
        if (invisibleCursor == null) {
            Pixmap pm = new Pixmap(16, 16, Pixmap.Format.RGBA8888);
            pm.setColor(0, 0, 0, 0);
            pm.fill();
            invisibleCursor = Gdx.graphics.newCursor(pm, 0, 0);
            pm.dispose();
        }
    }

    public void dispose() {
        if (invisibleCursor != null) {
            invisibleCursor.dispose();
            invisibleCursor = null;
        }
    }

    public float getYaw()   { return yaw; }
    public float getPitch() { return pitch; }
}
