/*
 * GameplayController.java
 *
 * For many of you, this class will seem like the most unusual one in the entire
 * project. It implements a lot of functionality that looks like it should go
 * into the various GameObject subclasses. However, a lot of this functionality
 * involves the creation or destruction of objects. We cannot do this without a
 * lot of cyclic dependencies, which are bad.
 *
 * You will notice that gameplay-wise, most of the features in this class are
 * interactions, not actions. This demonstrates why a software developer needs
 * to understand the difference between these two.
 *
 * You will definitely need to modify this file in Part 2 of the lab. However,
 * you are free to modify any file you want.  You are also free to add new
 * classes and assets.
 *
 * 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.graphics.Texture;

import edu.cornell.gdiac.assets.*;
import edu.cornell.gdiac.graphics.*;
import edu.cornell.cis3152.optimization.entity.*;
import edu.cornell.gdiac.util.RandomGenerator;

/**
 * Controller to handle gameplay interactions.
 * </summary>
 * <remarks>
 * This controller also acts as the root class for all the models.
 */
public class GameplayController {
    // Graphics assets for the entities
    /** Sprite sheet for all ships, as they look the same */
    private SpriteSheet beetleSprite;
    /** Sprite sheet for all stars, as they look the same */
    private SpriteSheet starSprite;
    /** Sprite sheet for all bullets, as they look the same */
    private SpriteSheet bulletSprite;
    /** Sprite sheet for green shells, as they look the same */
    private SpriteSheet greenSprite;
    /** Sprite sheet for red shells, as they look the same */
    private SpriteSheet redSprite;

    /** Gameplay constants */
    private JsonValue constants;

    /** Reference to player (need to change to allow multiple players) */
    private Ship player;
    /** Shell count for the display in window corner */
    private int shellCount;

    // List of objects with the garbage collection set.
    /** The currently active object */
    private Array<GameObject> objects;
    /** The backing set for garbage collection */
    private Array<GameObject> backing;

    /**
     * Creates a new GameplayController with no active elements.
     */
    public GameplayController(JsonValue constants, AssetDirectory directory) {
        this.constants = constants;

        player = null;
        shellCount = 0;
        objects = new Array<GameObject>();
        backing = new Array<GameObject>();

        beetleSprite = directory.getEntry("beetle.animation", SpriteSheet.class);
        bulletSprite = directory.getEntry("bullet.animation", SpriteSheet.class);
        starSprite  = directory.getEntry("star.animation", SpriteSheet.class);
        redSprite   = directory.getEntry("red.animation", SpriteSheet.class);
        greenSprite = directory.getEntry("green.animation", SpriteSheet.class);

        // Initialize the constants for the entity classes
        Bullet.setConstants(constants.get("bullet"));
        Shell.setConstants(constants.get("shell"));
        Ship.setConstants(constants.get("ship"));
        Star.setConstants(constants.get("star"));
    }

    /**
     * Returns the list of the currently active (not destroyed) game objects
     *
     * As this method returns a reference and Lists are mutable, other classes
     * can technical modify this list. That is a very bad idea. Other classes
     * should only mark objects as destroyed and leave list management to this
     * class.
     *
     * @return the list of the currently active (not destroyed) game objects
     */
    public Array<GameObject> getObjects() {
        return objects;
    }

    /**
     * Returns a reference to the currently active player.
     *
     * This property needs to be modified if you want multiple players.
     *
     * @return a reference to the currently active player.
     */
    public Ship getPlayer() {
        return player;
    }

    /**
     * Returns true if the currently active player is alive.
     *
     * This property needs to be modified if you want multiple players.
     *
     * @return true if the currently active player is alive.
     */
    public boolean isAlive() {
        return player != null;
    }

    /**
     * Returns the number of shells currently active on the screen.
     *
     * @return the number of shells currently active on the screen.
     */
    public int getShellCount() {
        return shellCount;
    }

    /**
     * Starts a new game.
     *
     * This method creates a single player, but does nothing else.
     *
     * @param x Starting x-position for the player
     * @param y Starting y-position for the player
     */
    public void start(float x, float y) {
        // Create the player's ship
        player = new Ship(x,y);
        player.setSpriteSheet(beetleSprite);

        // Player must be in object list.
        objects.add(player);
    }

    /**
     * Resets the game, deleting all objects.
     */
    public void reset() {
        player = null;
        shellCount = 0;
        objects.clear();
    }

    /**
     * Adds a new shell to the game.
     *
     * A shell is generated at the top with a random horizontal position. Notice
     * that this allocates memory to the heap. If we were REALLY worried about
     * performance, we would use a memory pool here.
     *
     * @param width  Current game width
     * @param height Current game height
     */
    public void addShell(float width, float height) {
        // Add a new shell
        Shell b = new Shell(RandomGenerator.getFloat(0, width), height);

        if (RandomGenerator.getInt(0, 2) == 0) {
            // Needs two shots to kill
            b.setSpriteSheet(redSprite);
            b.setDamagedSprite(greenSprite);
        } else {
            //  Needs one shot to kill
            b.setSpriteSheet(greenSprite);
            b.setDamagedSprite(null);
        }

        objects.add(b);
        shellCount++;
    }

    /**
     * Garbage collects all deleted objects.
     *
     * This method works on the principle that it is always cheaper to copy live
     * objects than to delete dead ones.  Deletion restructures the list and is
     * O(n^2) if the number of deletions is high.  Since Add() is O(1), copying
     * is O(n).
     */
    public void garbageCollect() {
        // INVARIANT: backing and objects are disjoint
        for (GameObject o : objects) {
            if (o.isDestroyed()) {
                destroy(o);
            } else {
                backing.add(o);
            }
        }

        // Swap the backing store and the objects.
        // This is essentially stop-and-copy garbage collection
        Array<GameObject> tmp = backing;
        backing = objects;
        objects = tmp;
        backing.clear();
    }

    /**
     * Process specialized destruction functionality
     *
     * Some objects do something special (e.g. explode) on destruction. That is
     * handled in this method.
     *
     * Notice that this allocates memory to the heap. If we were REALLY worried
     * about performance, we would use a memory pool here.
     *
     * @param o Object to destroy
     */
    protected void destroy(GameObject o) {
        switch(o.getType()) {
        case SHIP:
            player = null;
            break;
        case SHELL:
            // Create some stars
            for (int j = 0; j < 6; j++) {
                Star s = new Star(o.getX(),o.getY());
                s.initVelocity( o.getVX(), o.getVY() );
                s.setSpriteSheet(starSprite);
                backing.add(s);
            }
            shellCount--;
            break;
        default:
            break;
        }
    }

    /**
     * Resolve the actions of all game objects (player and shells)
     *
     * You will probably want to modify this heavily in Part 2.
     *
     * @param input  Reference to the input controller
     * @param delta  Number of seconds since last animation frame
     */
    public void resolveActions(InputController input, float delta) {
        // Process the player
        if (player != null) {
            resolvePlayer(input,delta);
        }

        // Process the other (non-ship) objects.
        for (GameObject o : objects) {
            o.update(delta);
        }
    }

    /**
     * Process the player's actions.
     *
     * Notice that firing bullets allocates memory to the heap. If we were
     * REALLY worried about performance, we would use a memory pool here.
     *
     * @param input  Reference to the input controller
     * @param delta  Number of seconds since last animation frame
     */
    public void resolvePlayer(InputController input, float delta) {
        player.setMovement(input.getMovement());
        player.setFiring(input.didFire());
        player.update(delta);
        if (!player.isFiring()) {
            return;
        }

        // Create a new bullet
        Bullet b = new Bullet(player.getX(),player.getY()+player.getRadius());
        b.setSpriteSheet(bulletSprite);
        backing.add(b); // Bullet added NEXT frame.

        // Prevent player from firing immediately afterwards.
        player.resetCooldown();
    }
}
