package wtf.beatrice.retrorender.engine;

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;

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;

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

    // 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 = 6; // 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 we don't own the mouse, just don't move camera / player
        if (!mouseCaptured) {
            return;
        }

        // skip a few frames after capture to avoid mouse skew
        // teleport cursor to center
        if (captureWarmupFrames > 0) {
            captureWarmupFrames--;
            Gdx.input.setCursorPosition(centerX, centerY);
            return;
        }

        // --- mouse look: keep cursor centred
        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;

        // clamp pitch to avoid flipping
        pitch = Math.max(-89f, Math.min(89f, pitch));

        // keep yaw in [0, 360)
        if (yaw >= 360f) yaw -= 360f;
        if (yaw < 0f) yaw += 360f;

        updateCameraDirection();

        // --- keyboard movement
        // forward vector (flattened on XZ)
        tmpForward.set(camera.direction.x, 0f, camera.direction.z).nor();
        // right vector (perpendicular)
        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;
        }

        float playerRadius = 0.4f;
        // --- try to move in X/Z with collision
        float newX = camera.position.x + moveX;
        float newZ = camera.position.z + moveZ;
        float curY = camera.position.y;

        if (!world.collidesAt(newX, curY, camera.position.z, playerRadius, eyeHeight)) {
            camera.position.x = newX;
        }

        if (!world.collidesAt(camera.position.x, curY, newZ, playerRadius, eyeHeight)) {
            camera.position.z = newZ;
        }
        boolean jumpHeld = Gdx.input.isKeyPressed(Input.Keys.SPACE);

        // apply gravity
        velocityY += gravity * delta;
        float proposedY = camera.position.y + velocityY * delta;

        // feet position if we accept this Y
        float feetY = proposedY - eyeHeight;

        // find ground at current X/Z
        float groundY = world.getGroundHeightAt(camera.position.x, camera.position.z, 0f);

        if (feetY <= groundY) {
            // ground or other surface below feet
            grounded = true;
            velocityY = 0f;
            camera.position.y = groundY + eyeHeight;
        } else {
            grounded = false;
            camera.position.y = proposedY;
        }

        // start jump again if holding down key
        if (grounded && jumpHeld) {
            velocityY = jumpSpeed;
            grounded = false;
        }

        // snap position to quantization steps
        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;
        }
    }

    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; }
}
