/*
 * GameMode.java
 *
 * This is the primary class file for running the game.  You should study this
 * file for ideas on how to structure your own root class. This class follows a
 * model-view-controller pattern fairly strictly.
 *
 * 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.*;
import com.badlogic.gdx.utils.*;
import com.badlogic.gdx.graphics.*;
import edu.cornell.gdiac.util.*;
import edu.cornell.gdiac.assets.*;
import edu.cornell.gdiac.graphics.*;
import com.badlogic.gdx.graphics.g2d.BitmapFont;
import edu.cornell.cis3152.optimization.entity.GameObject;

/**
 * The primary controller class for the game.
 *
 * While GDXRoot is the root class, it delegates all of the work to the scene
 * classes. This is the scene for running the game. In initializes all
 * of the other classes in the game and hooks them together.  It also provides
 * the basic game loop (update-draw).
 */
public class GameScene implements Scene {
    /** The target FPS of this assignment */
    private static final int TARGET_FPS = 60;

    /**
      * Track the current state of the game for the update loop.
      */
    public enum GameState {
        /** Before the game has started */
        INTRO,
        /** While we are playing the game */
        PLAY,
        /** When the ships is dead (but shells still work) */
        OVER
    }

    // Loaded assets
    /** The constants defining the game behavior */
    private JsonValue  constants;
    /** The background image for the game */
    private Texture background;
    /** The font for giving messages to the player */
    private BitmapFont displayFont;
    /** A layout for drawing shell counter */
    private TextLayout counter;
    /** A layout for drawing game over message */
    private TextLayout gameover;
    /** A layout for the FPS meter */
    private TextLayout fpsmeter;

    /** Factor used to compute where we are in scrolling process */
    private float timeModifier;
    /** Offset for the shell counter message on the screen */
    private float counterOffset;
    /** Offset for the FPS meter message on the screen */
    private float fpsOffset;
    /** The vertical position of the background on the screen */
    private float verticalPos;

    /** The drawing camera for this scene (VIEW CLASS) */
    private OrthographicCamera camera;

    /** The width of this scene */
    private int width;
    /** The height of this scene */
    private int height;
    /** The current FPS of this scene */
    private int fps;

    /** Reads input from keyboard or game pad (CONTROLLER CLASS) */
    private InputController inputController;
    /** Handle collision and physics (CONTROLLER CLASS) */
    private CollisionController physicsController;
    /** Constructs the game models and handle basic gameplay (CONTROLLER CLASS) */
    private GameplayController gameplayController;

    /** Variable to track the game state (SIMPLE FIELDS) */
    private GameState gameState;
    /** Variable to track total time played in milliseconds (SIMPLE FIELDS) */
    private float totalTime = 0;
    /** Whether or not this player mode is still active */
    private boolean active;

    /** Listener that will update the player mode when we are done */
    private ScreenListener listener;

    /**
     * Creates a new game with the given drawing context.
     *
     * This constructor initializes the models and controllers for the game. The
     * view has already been initialized by the root class.
     */
    public GameScene(AssetDirectory assets) {
        resize( Gdx.graphics.getWidth(),Gdx.graphics.getHeight() );

        active = false;
        // Null out all pointers, 0 out all ints, etc.
        gameState = GameState.INTRO;


        // Populate the assets
        constants = assets.getEntry("constants", JsonValue.class);
        background  = assets.getEntry("background",Texture.class);
        displayFont = assets.getEntry("times",BitmapFont.class);

        timeModifier = constants.get("background").getFloat("time modifier", 0);
        counterOffset = constants.get("background").getFloat("counter offset", 0);
        verticalPos = constants.get("background").getFloat("vertical pos", 0);
        fpsOffset = constants.get("background").getFloat("fps offset", 0);

        counter = new TextLayout();
        counter.setFont( displayFont );
        counter.setAlignment( TextAlign.topLeft );

        fpsmeter = new TextLayout();
        fpsmeter.setFont( displayFont );
        fpsmeter.setAlignment( TextAlign.topLeft );

        gameover = new TextLayout();
        gameover.setFont( displayFont );
        gameover.setAlignment( TextAlign.middleCenter );
        gameover.setText( "Game Over!\nPress R to Restart" );
        gameover.layout();

        // Create the controllers.
        inputController = new InputController();
        gameplayController = new GameplayController(constants, assets);
        physicsController  = new CollisionController(constants, width, height);
    }

    /**
     * Disposes of all (non-static) resources allocated to this mode.
     */
    public void dispose() {
        inputController = null;
        gameplayController = null;
        physicsController  = null;
    }

    /**
     * Updates the game state.
     *
     * We prefer to separate update and draw from one another as separate
     * methods, instead of using the single render() method that LibGDX does.
     * We will talk about why we prefer this in lecture.
     *
     * @param delta Number of seconds since last animation frame
     */
    public void update(float delta) {
        // This makes the measurement more "stable"
        fps = Math.round(1.0f/delta);
        if (fps >= TARGET_FPS-1) {
            fps = TARGET_FPS;
        }

        // Process the game input
        inputController.sync();

        // Test whether to reset the game.
        switch (gameState) {
        case INTRO:
            gameState = GameState.PLAY;
            gameplayController.start(width / 2.0f,
                                     physicsController.getFloorLedge());
            break;
        case OVER:
            if (inputController.didReset()) {
                gameState = GameState.PLAY;
                gameplayController.reset();
                gameplayController.start(width / 2.0f,
                                         physicsController.getFloorLedge());
            } else {
                play(delta);
            }
            break;
        case PLAY:
            play(delta);
            break;
        default:
            break;
        }
    }

    /**
     * Processes a single step in the game loop.
     *
     * @param delta Number of seconds since last animation frame
     */
    protected void play(float delta) {
        // if no player is alive, declare game over
        if (!gameplayController.isAlive()) {
            gameState = GameState.OVER;
        }

        // Add a new shell if time.
        if (RandomGenerator.getInt(0, 25) == 0 || inputController.didFlood()) {
            gameplayController.addShell(width, height);
        }

        // Update objects.
        gameplayController.resolveActions(inputController,delta);

        // Check for collisions
        totalTime += (delta*1000); // Seconds to milliseconds
        float offset =  width - (totalTime * timeModifier) % width;
        physicsController.processCollisions(gameplayController.getObjects(),
                                            (int)offset);

        // Clean up destroyed objects
        gameplayController.garbageCollect();
    }

    /**
     * Draws the scene on to the provided sprite batch
     *
     * There should be no code in this method that alters the game state. All
     * assignments should be to local variables or cache fields only.
     *
     * @param batch The sprite batch
     */
    public void draw(SpriteBatch batch) {
        float offset = -((totalTime * timeModifier) % width);

        ScreenUtils.clear( Color.BLACK );

        batch.begin(camera);
        batch.setColor( Color.WHITE );
        batch.setBlendMode(SpriteBatch.BlendMode.ALPHA_BLEND);

        // Draw the background
        // Have to draw the background twice for continuous scrolling.
        float w = background.getWidth();
        batch.draw(background, offset,   verticalPos);
        batch.draw(background, offset+w, verticalPos);

        // Draw the game objects
        for (GameObject o : gameplayController.getObjects()) {
            o.draw(batch);
        }

        // Output a simple debugging message with the number of shells on screen

        // This formats text to something drawable
        String message = "Current shells: "+gameplayController.getShellCount();
        counter.setText( message );
        counter.layout();
        batch.drawText(counter, counterOffset, height-counterOffset);

        String fpsrate = "FPS: "+fps;
        fpsmeter.setText( fpsrate );
        fpsmeter.layout();
        batch.drawText(fpsmeter, width-fpsOffset, height-counterOffset);

        if (gameState == GameState.OVER) {
            batch.drawText( gameover, width/2.0f, height/2.0f );
        }

        // Flush information to the graphic buffer.
        batch.end();
    }

    /**
     * Called when the Screen is resized.
     *
     * This can happen at any point during a non-paused state but will never
     * happen before a call to show().
     *
     * @param width  The new width in pixels
     * @param height The new height in pixels
     */
    public void resize(int width, int height) {
        this.width  = width;
        this.height = height;

        if (camera == null) {
            camera = new OrthographicCamera();
        }
        camera.setToOrtho( false, width, height );

        // Only physics controller needs to know size
        if (physicsController != null) {
            physicsController.resize(width, height);
        }

    }

    /**
     * Returns true if this scene has completed and wishes to exit
     *
     * @return true if this scene has completed and wishes to exit
     */
    public boolean exitScene() {
        return inputController.didExit();
    }

}
