/*
 * CollisionController.java
 *
 * This controller implements basic collision detection as described in
 * the instructions.  All objects in this game are treated as circles,
 * and a collision happens when circles intersect.
 *
 * This controller is EXTREMELY ineffecient.  To improve its performance,
 * you will need to use collision cells, as described in the instructions.
 * You should not need to modify any method other than the constructor
 * and processCollisions.  However, you will need to add your own methods.
 *
 * This and 'constants.json' are the only files that you need to modify as the
 * first part of the lab.
 *
 * Based on the original Optimization Lab by Don Holden, 2007
 *
 * @author: Walker M. White
 * @version: 2/2/2025
 */
package edu.cornell.cis3152.optimization;

import com.badlogic.gdx.utils.*;
import com.badlogic.gdx.math.*;
import edu.cornell.cis3152.optimization.entity.*;

/**
 * Controller implementing simple game physics.
 *
 * This is a very inefficient physics engine. Part of this lab is determining
 * how to make it more efficient.
 */
public class CollisionController {

    /** Width of the collision geometry */
    private float width;
    /** Height of the collision geometry */
    private float height;

    /** Physics constants */
    private JsonValue constants;

    /** Restitution for colliding with box in the terrain */
    private float boxRestitution;
    /** Restitution for colliding  bump in the terrain */
    private float bumpRestitution;
    /** (Scaled) distance of the floor ledge from bottom */
    private float bottomOffset;
    /** (Scaled) position of the box center */
    private float boxCenter;
    /** (Scaled) position of the bump center */
    private float bumpCenter;

    /** (Scaled) value of the box width */
    private float boxWidth;
    /** (Scaled) value of the box height from bottom of screen */
    private float boxHeight;
    /** (Scaled) value of the bump radius */
    private float bumpRadius;

    // Cache objects for collision calculations
    private Vector2 temp1;
    private Vector2 temp2;

    /// ACCESSORS

    /**
     * Returns width of the game window (necessary to detect out of bounds)
     *
     * @return width of the game window
     */
    public float getWidth() {
        return width;
    }

    /**
     * Returns height of the game window (necessary to detect out of bounds)
     *
     * @return height of the game window
     */
    public float getHeight() {
        return height;
    }

    /**
     * Returns the height of the floor ledge.
     *
     * The floor ledge supports the player ship, and is what all of the shells
     * bounce off of.  It is raised slightly higher than the bottom of the
     * screen.
     *
     * @return the height of the floor ledge.
     */
    public float getFloorLedge() {
        return bottomOffset*height;
    }

    /**
     * Returns the x-coordinate of the center of the square box
     *
     * This box is part of the background image.
     *
     * @return the x-coordinate of the center of the square box
     */
    public float getBoxX() {
        return boxCenter * width;
    }

    /**
     * Returns half of the width of the square box.
     *
     * This box is part of the background image. The box edges are x+/-
     * this width.
     *
     * @return half of the width of the square box
     */
    public float getBoxRadius() {
        return boxWidth * width/2.0f;
    }

    /**
     * Returns the height of the square box in the background image
     *
     * Height is measured from the bottom of the screen, not the ledge.
     *
     * @return the height of the square box in the background image
     */
    public float getBoxHeight() {
        return boxHeight * height;
    }

    /**
     * Returns x-coordinate of the center of the semicircular bump
     *
     * This bump is part of the background image
     *
     * @return x-coordinate of the center of the semicircular bump
     */
    protected float getBumpX() {
        return bumpCenter * width;
    }

    /**
     * Returns radius of the semicircular bump in the background image
     *
     * @return radius of the semicircular bump
     */
    protected float getBumpRadius() {
        return bumpRadius * width;
    }

    //#region Initialization (MODIFY THIS CODE)
    /**
     * Creates a CollisionController with the given size
     *
     * @param constants The physics constants to use
     * @param width     Width of the screen
     * @param height    Height of the screen
     */
    public CollisionController(JsonValue constants, float width, float height) {
        // Initialize cache objects
        temp1 = new Vector2();
        temp2 = new Vector2();

        // Set the physics constants.
        // ADD NEW VALUES TO constants.json IF YOU NEED NEW CONSTANTS
        bottomOffset = constants.get("background").getFloat("ledge height");
        boxCenter = constants.get("box").getFloat("position");
        boxWidth = constants.get("box").getFloat("width");
        boxHeight = constants.get("box").getFloat("height");
        boxRestitution = constants.get("box").getFloat("restitution");
        bumpCenter = constants.get("bump").getFloat("position");
        bumpRadius = constants.get("bump").getFloat("radius");
        bumpRestitution = constants.get("bump").getFloat("restitution");

        resize(width,height);
    }

    /**
     * Resizes the bounds of the collision controller.
     *
     * If you need to make any support data structures at initialization,
     * they should be created here. That way, they are reinitialized whenever
     * the window size changes.
     *
     * @param width     Width of the screen
     * @param height    Height of the screen
     */
    public void resize(float width, float height) {
        this.width = width;
        this.height = height;
    }

    /**
     * This is the main (incredibly unoptimized) collision detetection method.
     *
     * @param objects List of live objects to check
     * @param offset  Offset of the box and bump
     */
    public void processCollisions(Array<GameObject> objects, int offset) {
        // For each shell, check for collisions with the terrain elements
        for (GameObject o : objects) {
            // Make sure object is in bounds.
            // For shells, this handles the box and bump.
            processBounds(o, offset);

            //#region REPLACE THIS CODE
            /* This is the slow code that must be replaced. */
            for (int ii = 0; ii < objects.size; ii++) {
                if (objects.get(ii) != o) {
                    processCollision(o,objects.get(ii));
                }
            }
            //#endregion
        }
    }
    //#endregion

    //#region Cell Management (INSERT CODE HERE)

    //#endregion

    //#region Collision Handlers (DO NOT MODIFY FOR PART 1)

    /**
     * Check if a GameObject is out of bounds and take action.
     *
     * Obviously an object off-screen is out of bounds.  In the case of shells,
     * the box and bump are also out of bounds.
     *
     * @param o      Object to check
     * @param offset Offset of the box and bump
     */
    private void processBounds(GameObject o, int offset) {
        // Dispatch the appropriate helper for each type
        switch (o.getType()) {
        case SHELL:
            // Only shells care about the offset
            handleBounds((Shell)o, offset);
            break;
        case STAR:
            handleBounds((Star)o);
            break;
        case BULLET:
            handleBounds((Bullet)o);
            break;
        case SHIP:
            handleBounds((Ship)o);
            break;
        default:
            break;
        }
    }

    /**
     * Check a shell for being out-of-bounds.
     *
     * Obviously an shell off-screen is out of bounds.  In addition, shells
     * cannot penetrate the box and bump.
     *
     * @param sh     Shell to check
     * @param offset Offset of the box and bump
     */
    private void handleBounds(Shell sh, int offset) {
        // Hit the rectangular step
        // (done three times to account for the fact that it could be on the
        // right side of the screen but also appearing on the left due to
        // scrolling, or on the left side but also appearing on the right)
        hitBox(sh, offset + getBoxX() - getWidth());
        hitBox(sh, offset + getBoxX());
        hitBox(sh, offset + getBoxX() + getWidth());

        // Hit the circular bump
        hitBump(sh, offset + getBumpX() - getWidth());
        hitBump(sh, offset + getBumpX());
        hitBump(sh, offset + getBumpX() + getWidth());

        // Check if off right side
        if (sh.getX() > getWidth() - sh.getRadius()) {
            // Set within bounds on right and swap velocity
            sh.setX(2 * (getWidth() - sh.getRadius()) - sh.getX());
            sh.setVX(-sh.getVX());
        }
        // Check if off left side
        else if (sh.getX() < sh.getRadius()) {
            // Set within bounds on left and swap velocity
            sh.setX(2 * sh.getRadius() - sh.getX());
            sh.setVX(-sh.getVX());
        }

        // Check for in bounds on bottom
        if (sh.getY()-sh.getRadius() < getFloorLedge()) {
            // Set within bounds on bottom and swap velocity
            sh.setY(getFloorLedge()+sh.getRadius());
            sh.setVY(-sh.getVY());

            // Constrict velocity
            sh.setVY((float)Math.max(sh.getMinVY(),
                                     sh.getVY() * sh.getFriction()));
        }
    }

    /**
     * Check a star for being out-of-bounds (currently does nothing).
     *
     * @param st Star to check
     */
    private void handleBounds(Star st) {
        // DO NOTHING (You may change for Part 2)
    }

    /**
     * Check a bullet for being out-of-bounds.
     *
     * @param bu Bullet to check
     */
    private void handleBounds(Bullet bu) {
        // Destroy a bullet once off screen.
        if (bu.getY() <= 0) {
            bu.setDestroyed(true);
        }
    }

    /**
     * Check a bullet for being out-of-bounds.
     *
     * @param sh Ship to check
     */
    private void handleBounds(Ship sh) {
        // Do not let the ship go off screen.
        if (sh.getX() <= sh.getRadius()) {
            sh.setX(sh.getRadius());
        } else if (sh.getX() >= getWidth() - sh.getRadius()) {
            sh.setX(getWidth() - sh.getRadius());
        }
    }

    /**
     * Detect collision with rectangle step in the terrain.
     *
     * @param o Object to check
     * @param x Offset of the box
     */
    private void hitBox(GameObject o, float x) {
        if (Math.abs(o.getX() - x) < getBoxRadius() &&
                     o.getY() < getBoxHeight()) {
            if (o.getX()+o.getRadius() > x+getBoxRadius()) {
                o.setX(x+getBoxRadius()+o.getRadius());
                o.setVX(-o.getVX());
            } else if (o.getX()-o.getRadius() < x-getBoxRadius()) {
                o.setX(x-getBoxRadius()-o.getRadius());
                o.setVX(-o.getVX());
            } else {
                o.setVY(-o.getVY() * boxRestitution);
                o.setY(getBoxHeight()+o.getRadius());
            }
        }
    }

    /**
     * Detect collision with semicircular bump in the terrain.
     *
     * @param o Object to check
     * @param x Offset of the bump
     */
    public void hitBump(GameObject o, float x) {
        // Make sure to not just change the velocity but also move the
        // object so that it no longer penetrates the terrain.
        float dx = o.getX() - x;
        float dy = o.getY() - getFloorLedge();
        float dist = (float)Math.sqrt(dx * dx + dy * dy);
        if (dist < 0.1f * width) {
            float norm_x = dx / dist;
            float norm_y = Math.abs(dy / dist);
            float tmp = (o.getVX() * norm_x +
                         o.getVY() * norm_y)*bumpRestitution;
            o.getVelocity().sub(norm_x * tmp, norm_y * tmp);
            o.setY(getFloorLedge() + norm_y * getBumpRadius());
        }
    }

    /**
     * Detect and resolve collisions between two game objects
     *
     * @param o1 First object
     * @param o2 Second object
     */
    private void processCollision(GameObject o1, GameObject o2) {
        // Dispatch the appropriate helper for each type
        switch (o1.getType()) {
        case SHELL:
            switch (o2.getType()) {
            case SHELL:
                resolveCollision((Shell)o1, (Shell)o2);
                break;
            case STAR:
                resolveCollision((Shell)o1, (Star)o2);
                break;
            case BULLET:
                resolveCollision((Shell)o1, (Bullet)o2);
                break;
            case SHIP:
                resolveCollision((Shell)o1, (Ship)o2);
                break;
            default:
                break;
            }
            break;
        case STAR:
            switch (o2.getType()) {
            case SHELL:
                // Reuse shell helper
                resolveCollision((Shell)o2, (Star)o1);
                break;
            case STAR:
                resolveCollision((Star)o1, (Star)o2);
                break;
            case BULLET:
                resolveCollision((Star)o1, (Bullet)o2);
                break;
            case SHIP:
                resolveCollision((Star)o1, (Ship)o2);
                break;
            default:
                break;
            }
            break;
        case BULLET:
            switch (o2.getType()) {
            case SHELL:
                // Reuse shell helper
                resolveCollision((Shell)o2, (Bullet)o1);
                break;
            case STAR:
                // Reuse star helper
                resolveCollision((Star)o2, (Bullet)o1);
                break;
            case BULLET:
                resolveCollision((Bullet)o1, (Bullet)o2);
                break;
            case SHIP:
                resolveCollision((Bullet)o1, (Ship)o2);
                break;
            default:
                break;
            }
            break;
        case SHIP:
            switch (o2.getType()) {
            case SHELL:
                // Reuse shell helper
                resolveCollision((Shell)o2, (Ship)o1);
                break;
            case STAR:
                // Reuse star helper
                resolveCollision((Star)o2, (Ship)o1);
                break;
            case BULLET:
                // Reuse bullet helper
                resolveCollision((Bullet)o2, (Ship)o1);
                break;
            case SHIP:
                resolveCollision((Ship)o1, (Ship)o2);
                break;
            default:
                break;
            }
            break;
        default:
            break;
        }
    }

    /**
     * Collide a shell with a shell.
     *
     * @param s1 First shell
     * @param s2 Second shell
     */
    private void resolveCollision(Shell s1, Shell s2) {
        if (s1.isDestroyed() || s2.isDestroyed()) {
            return;
        }

        // Find the axis of "collision"
        temp1.set(s1.getPosition()).sub(s2.getPosition());
        float dist = temp1.len();

        // Too far away
        if (dist > s1.getRadius() + s2.getRadius()) {
            return;
        }

        // Push the shells out so that they do not collide
        float distToPush = 0.01f + (s1.getRadius() + s2.getRadius() - dist) / 2;
        temp1.nor();
        temp1.scl(distToPush);
        s1.getPosition().add(temp1);
        s2.getPosition().sub(temp1);

        // Compute the new velocities
        // Unit vector for w1
        temp1.set(s2.getPosition()).sub(s1.getPosition()).nor();
        // Unit vector for w2
        temp2.set(s1.getPosition()).sub(s2.getPosition()).nor();

        temp1.scl(temp1.dot(s1.getVelocity())); // Scaled to w1
        temp2.scl(temp2.dot(s2.getVelocity())); // Scaled to w2

        // You can remove this to add friction.
        // We find friction has nasty feedback.
        //temp1.scl(s1.getFriction());
        //temp2.scl(s2.getFriction());

        // Apply to the objects
        s1.getVelocity().sub(temp1).add(temp2);
        s2.getVelocity().sub(temp2).add(temp1);
    }

    /**
     * Collide a shell with a star.
     *
     * @param se The shell
     * @param st The star
     */
    private void resolveCollision(Shell se, Star st) {
        if (se.isDestroyed() || st.isDestroyed()) {
            return;
        }

        temp1.set(se.getPosition()).sub(st.getPosition());
        float dist = temp1.len();

        // Too far away
        if (dist > se.getRadius() + st.getRadius()) {
            return;
        }

        // Knock back shell
        temp1.nor();
        float dot = temp1.dot(se.getVelocity());
        temp1.scl(dot);
        se.getVelocity().sub(temp1.scl(bumpRestitution));

        // Destroy objects
        se.setDestroyed(true);
        st.setDestroyed(true);
    }

    /**
     * Collide a shell with a bullet.
     *
     * @param se The shell
     * @param bu The bullet
     */
    private void resolveCollision(Shell se, Bullet bu) {
        if (se.isDestroyed() || bu.isDestroyed()) {
            return;
        }

        temp1.set(se.getPosition()).sub(bu.getPosition());
        float dist = temp1.len();

        // Too far away
        if (dist > se.getRadius() + bu.getRadius()) {
            return;
        }

        // Knock back shell
        temp1.nor();
        float dot = temp1.dot(se.getVelocity());
        temp1.scl(dot);
        se.getVelocity().sub(temp1.scl(bumpRestitution));

        // Destroy objects
        se.setDestroyed(true);
        bu.setDestroyed(true);
    }

    /**
     * Collide a shell with a ship.
     *
     * @param se The shell
     * @param sh The ship
     */
    private void resolveCollision(Shell se, Ship sh) {
        if (se.isDestroyed() || sh.isDestroyed()) {
            return;
        }

        // Kill the ship
        temp1.set(se.getPosition()).sub(sh.getPosition());
        float dist = temp1.len();

        // Too far away
        if (dist > se.getRadius() + sh.getRadius()) {
            return;
        }

        // Destroy objects
        se.setDestroyed(true);
        sh.setDestroyed(true);
    }

    /**
     * Collide a star with a star.
     *
     * @param s1 First star
     * @param s2 Second star
     */
    private void resolveCollision(Star s1, Star s2) {
        // Nothing happens!
    }

    /**
     * Collide a star with a bullet.
     *
     * @param st The star
     * @param bu The bullet
     */
    private void resolveCollision(Star st, Bullet bu) {
        // Nothing happens!
    }

    /**
     * Collide a star with a ship.
     *
     * @param st The star
     * @param sh The ship
     */
    private void resolveCollision(Star st, Ship sh) {
        // Nothing happens!
    }

    /**
     * Collide a bullet with a bullet.
     *
     * @param b1 First bullet
     * @param b2 Second bullet
     */
    private void resolveCollision(Bullet b1, Bullet b2) {
        // Nothing happens!
    }

    /**
     * Collide a bullet with a ship.
     *
     * @param bu The bullet
     * @param sh The ship
     */
    private void resolveCollision(Bullet bu, Ship sh) {
        // Nothing happens!
    }

    /**
     * Collide a ship with a ship (only useful if you add a 2nd ship)
     *
     * @param s1 First ship
     * @param s2 Second ship
     */
    private void resolveCollision(Ship s1, Ship s2) {
        // Prevent them from moving into each other
    }

    //#endregion
}
