package MV3500Cohort2024JulySeptember.homework3.Smith;

import edu.nps.moves.dis7.enumerations.VariableRecordType;
import edu.nps.moves.dis7.pdus.*;
import edu.nps.moves.dis7.utilities.DisChannel;
import edu.nps.moves.dis7.utilities.PduFactory;
import java.util.logging.Level;
import java.util.logging.Logger;

/**
 * The purpose of this inheritable class is to provide an easily modifiable
 * example simulation program that includes DIS-capable entities performing
 * tasks of interest, and then reporting activity via PDUs to the network.
 * Default program initialization includes PDU recording turned on by default.
 */
public class ExampleSimulationProgram {

    // Constants for the Battleship game
    private static final int GRID_SIZE = 10; // 10x10 grid for Battleship
    private static final int MAX_TURNS = 200; // Limit turns to avoid infinite games

    // Grids for each player
    private char[][] player1Grid = new char[GRID_SIZE][GRID_SIZE];
    private char[][] player2Grid = new char[GRID_SIZE][GRID_SIZE];

    // To track whose turn it is
    private boolean isPlayer1Turn = true;

    // Ship positions (simple example positions for each player's ships)
    private final int[][] player1Ships = {{0, 0}, {0, 1}, {0, 2}}; // Positions for player 1's ships
    private final int[][] player2Ships = {{5, 5}, {5, 6}, {5, 7}}; // Positions for player 2's ships

    // DIS utilities
    private String descriptor = this.getClass().getSimpleName();
    protected DisChannel disChannel;
    protected PduFactory pduFactory;
    protected CommentPdu gridStatusPdu;
    protected FirePdu firePduPlayer1;
    protected FirePdu firePduPlayer2;
    protected MunitionDescriptor munitionDescriptor1 = new MunitionDescriptor();

    // Simulation time settings
    private double simulationTimeStepDuration = 1.0; // seconds
    private double simulationTimeSeconds = 0.0;

    // Tracking indices for Player 1's systematic search
    private int player1SearchRow = 0;
    private int player1SearchCol = 0;

    /**
     * Constructor to create an instance of this class. Design goal: additional
     * built-in initialization conveniences can go here to keep your efforts
     * focused on the runSimulation() method.
     */
    public ExampleSimulationProgram() {
        initialize();
    }

    /**
     * Initialize the simulation program, setting up the DIS channel, PDUs, and
     * the Battleship game grids.
     */
    private void initialize() {
        initializeDisChannel();
        initializeSimulationEntities();
        setupGrids();
        disChannel.join();
    }

    /**
     * Initialize the DIS channel for communication, setting verbose output for
     * debugging and creating the PduFactory.
     */
    private void initializeDisChannel() {
        disChannel = new DisChannel();
        pduFactory = disChannel.getPduFactory();
        disChannel.setDescriptor(this.getClass().getSimpleName());
        disChannel.setUpNetworkInterface();
        disChannel.getDisNetworkInterface().setVerbose(true); // sending and receipt
        disChannel.getPduRecorder().setVerbose(true);
    }

    /**
     * Initialize simulation entities by creating PDUs for firing events.
     */
    private void initializeSimulationEntities() {
        firePduPlayer1 = pduFactory.makeFirePdu();
        firePduPlayer2 = pduFactory.makeFirePdu();
        gridStatusPdu = pduFactory.makeCommentPdu();
    }

    /**
     * Set up the game grids for each player, marking empty water and ship
     * positions.
     */
    private void setupGrids() {
        // Initialize grids with empty water '~'
        for (int i = 0; i < GRID_SIZE; i++) {
            for (int j = 0; j < GRID_SIZE; j++) {
                player1Grid[i][j] = '~';
                player2Grid[i][j] = '~';
            }
        }

        // Place ships on the grids ('P' denotes a ship)
        for (int[] ship : player1Ships) {
            player1Grid[ship[0]][ship[1]] = 'P';
        }
        for (int[] ship : player2Ships) {
            player2Grid[ship[0]][ship[1]] = 'P';
        }
    }

    /**
     * This runSimulationLoops() method is for you, a customizable
     * programmer-modifiable code block for defining and running a new
     * simulation of interest.
     */
    public void runSimulationLoops() {
        int turnCount = 0;
        while (turnCount < MAX_TURNS) {
            if (isPlayer1Turn) {
                // Player 1 systematically searches Player 2's grid
                int[] target = selectNextTargetForPlayer1();
                handleFire(firePduPlayer1, player2Grid, target, "Player 1");
            } else {
                // Player 2 fires at Player 1 randomly
                int[] target = selectRandomTarget();
                handleFire(firePduPlayer2, player1Grid, target, "Player 2");
            }

            // Switch turns and increment turn counter
            isPlayer1Turn = !isPlayer1Turn;
            turnCount++;

            // Check for win conditions
            if (checkWinCondition(player1Grid)) {
                System.out.println("Player 2 wins!");
                break;
            } else if (checkWinCondition(player2Grid)) {
                System.out.println("Player 1 wins!");
                break;
            }

            // Wait for the next simulation step
            try {
                Thread.sleep((long) (simulationTimeStepDuration * 1000)); // units of seconds * (1000 msec/sec) = milliseconds
            } catch (InterruptedException iex) {
                Logger.getLogger(ExampleSimulationProgram.class.getSimpleName()).log(Level.SEVERE, null, iex);
            }

            // Increment simulation time
            simulationTimeSeconds += simulationTimeStepDuration;
        }
    }

    /**
     * Systematically select the next target for Player 1 in a row-by-row
     * manner.
     *
     * @return the coordinates of the next target cell.
     */
    private int[] selectNextTargetForPlayer1() {
        // Systematically select the next target in a row-by-row manner
        int[] target = {player1SearchRow, player1SearchCol};

        // Move to the next column
        player1SearchCol++;
        // If end of row is reached, move to the next row and reset column
        if (player1SearchCol >= GRID_SIZE) {
            player1SearchCol = 0;
            player1SearchRow++;
        }

        // Ensure search stays within grid bounds
        if (player1SearchRow >= GRID_SIZE) {
            player1SearchRow = 0; // Reset search if bounds are exceeded
        }

        return target;
    }

    /**
     * Randomly select a target on the grid for Player 2's turn.
     *
     * @return the coordinates of the randomly selected target cell.
     */
    private int[] selectRandomTarget() {
        int x = (int) (Math.random() * GRID_SIZE);
        int y = (int) (Math.random() * GRID_SIZE);
        return new int[]{x, y};
    }

    /**
     * Handles the firing action, setting relevant data in the FirePdu, checking
     * if the target is a hit or miss, updating the grid, and sending the
     * appropriate PDU.
     *
     * @param firePdu the FirePdu object to use for the shot.
     * @param grid the grid of the player being fired at.
     * @param target the coordinates of the target cell.
     * @param player the name of the player firing the shot.
     */
    private void handleFire(FirePdu firePdu, char[][] grid, int[] target, String player) {
        int x = target[0];
        int y = target[1];

        // Set relevant details in the FirePdu
        firePdu.setFiringEntityID(new EntityID().setEntityID(player.equals("Player 1") ? 1 : 2));
        firePdu.setTargetEntityID(new EntityID().setEntityID(player.equals("Player 1") ? 1 : 2));
        Vector3Double fireLoc = new Vector3Double(); //annoying that there isn't a Vector3Double(x, y, z) constructor...
        fireLoc.setX(x);
        fireLoc.setY(y);
        fireLoc.setZ(0.0);
        firePdu.setLocationInWorldCoordinates(fireLoc);
        firePdu.setDescriptor(munitionDescriptor1); // Not needed I guess
        firePdu.setRange(100.0f);// Not needed I gueess

        // Print the firing action
        System.out.println(player + " fires at (" + x + ", " + y + ").");

        // Determine hit or miss and update the game grid
        if (grid[x][y] == 'P') {
            grid[x][y] = 'H'; // Mark as Hit
            firePdu.setFireMissionIndex(1); // Indicate that this was a successful hit with an index of 1
            System.out.println(player + " hits a ship at (" + x + ", " + y + ")!");
        } else if (grid[x][y] == '~') {
            grid[x][y] = 'M'; // Mark as Miss
            firePdu.setFireMissionIndex(0); // Indicate that this was a miss with an index of 0
            System.out.println(player + " misses at (" + x + ", " + y + ").");
        } else {
            System.out.println(player + " fires at (" + x + ", " + y + ") but it's already hit/missed.");
        }

        // Send the FirePdu to represent the shot
        disChannel.sendSinglePdu(simulationTimeSeconds, firePdu);

        // Send the updated grid status of both players
        sendGridStatus();
    }

    /**
     * Checks the win condition by verifying if all ships on the grid have been
     * hit.
     *
     * @param grid the grid to check.
     * @return true if all ships are hit; false otherwise.
     */
    private boolean checkWinCondition(char[][] grid) {
        // Check if all ships have been hit
        for (int i = 0; i < GRID_SIZE; i++) {
            for (int j = 0; j < GRID_SIZE; j++) {
                if (grid[i][j] == 'P') {
                    return false; // There are still ships left
                }
            }
        }
        return true; // All ships have been hit
    }

    /**
     * Sends the current grid status of both players as a CommentPdu.
     */
    private void sendGridStatus() {
        // Construct a string representation of both grids
        StringBuilder gridStatus = new StringBuilder();
        gridStatus.append("Player 1 Grid:\n");
        appendGridToString(gridStatus, player1Grid);
        gridStatus.append("Player 2 Grid:\n");
        appendGridToString(gridStatus, player2Grid);

        // Set the grid status in the CommentPdu
        gridStatusPdu.getVariableDatums().clear(); // Clear previous comments
        gridStatusPdu.getVariableDatums().add(new VariableDatum()
                .setVariableDatumID(VariableRecordType.OTHER)
                .setVariableDatumValue(gridStatus.toString().getBytes())
                .setVariableDatumLengthInBytes(gridStatus.toString().getBytes().length));

        // Send the CommentPdu containing the grid status
        disChannel.sendSinglePdu(simulationTimeSeconds, gridStatusPdu);
    }

    /**
     * Appends the current grid status to a StringBuilder.
     *
     * @param sb the StringBuilder to append to.
     * @param grid the grid to represent.
     */
    private void appendGridToString(StringBuilder sb, char[][] grid) {
        for (char[] row : grid) {
            for (char cell : row) {
                sb.append(cell).append(' ');
            }
            sb.append('\n');
        }
    }

    /**
     * Main method is first executed when a program instance is loaded.
     *
     * @param args command-line parameters: network address and port.
     * Command-line arguments are an array of optional String parameters that
     * are passed from execution environment during invocation
     */
    public static void main(String[] args) {
        ExampleSimulationProgram game = new ExampleSimulationProgram();
        game.runSimulationLoops();
        System.out.println("Game Over");
    }
}