////////////////////////////////////////
// Programming project code
// UniBw M, 2022, 2023, 2024
// www.unibw.de/inf2
// (c) Mark Minas (mark.minas@unibw.de)
////////////////////////////////////////

package pp.battleship.client.gui;

import com.jme3.material.Material;
import com.jme3.material.RenderState.BlendMode;
import com.jme3.math.ColorRGBA;
import com.jme3.math.Vector2f;
import com.jme3.math.Vector3f;
import com.jme3.scene.Geometry;
import com.jme3.scene.Node;
import com.jme3.scene.Spatial.CullHint;
import com.jme3.scene.shape.Quad;
import pp.battleship.client.BattleshipApp;
import pp.battleship.model.IntPoint;
import pp.battleship.model.ShipMap;
import pp.util.FloatPoint;
import pp.util.Position;

/**
 * Represents the visual view of a {@link ShipMap}, used to display the map structure such as the player's map, harbor,
 * and opponent's map. This class handles the graphical representation of the map, including background setup, grid lines,
 * and interaction between the model and the view.
 */
class MapView {
    public static final float FIELD_SIZE = 40f;
    private static final float GRID_LINE_WIDTH = 2f;
    private static final float BACKGROUND_DEPTH = -4f;
    private static final float GRID_DEPTH = -1f;
    private static final ColorRGBA BACKGROUND_COLOR = new ColorRGBA(0, 0.05f, 0.05f, 0.5f);
    private static final ColorRGBA GRID_COLOR = ColorRGBA.Green;

    // Reference to the main application and the ship map being visualized
    private final BattleshipApp app;
    private final Node mapNode = new Node("map"); // NON-NLS
    private final ShipMap map;
    private final MapViewSynchronizer synchronizer;

    /**
     * Constructs a new MapView for a given {@link ShipMap} and {@link BattleshipApp}.
     * Initializes the view by setting up the background and registering a synchronizer to listen to changes in the map.
     *
     * @param map the ship map to visualize
     * @param app the main application instance
     */
    MapView(ShipMap map, BattleshipApp app) {
        this.map = map;
        this.app = app;
        this.synchronizer = new MapViewSynchronizer(this);
        setupBackground();
        app.getGameLogic().addListener(synchronizer);
    }

    /**
     * Unregisters the {@link MapViewSynchronizer} from the listener list of the ClientGameLogic,
     * stopping the view from receiving updates when the underlying {@link ShipMap} changes.
     * After calling this method, this MapView instance should no longer be used.
     */
    void unregister() {
        app.getGameLogic().removeListener(synchronizer);
    }

    /**
     * Gets the {@link ShipMap} associated with this view.
     *
     * @return the ship map
     */
    public ShipMap getMap() {
        return map;
    }

    /**
     * Gets the {@link BattleshipApp} instance associated with this view.
     *
     * @return the main application instance
     */
    public BattleshipApp getApp() {
        return app;
    }

    /**
     * Sets up the background of the map view using a quad geometry.
     * The background is configured with a semi-transparent color and placed at a specific depth.
     */
    private void setupBackground() {
        final Material mat = new Material(app.getAssetManager(), "Common/MatDefs/Misc/Unshaded.j3md"); // NON-NLS
        mat.setColor("Color", BACKGROUND_COLOR); // NON-NLS
        mat.getAdditionalRenderState().setBlendMode(BlendMode.Alpha);
        final Position corner = modelToView(map.getWidth(), map.getHeight());
        final Geometry background = new Geometry("MapBackground", new Quad(corner.getX(), corner.getY()));
        background.setMaterial(mat);
        background.setLocalTranslation(0f, 0f, BACKGROUND_DEPTH);
        background.setCullHint(CullHint.Never);
        mapNode.attachChild(background);
    }

    /**
     * Adds grid lines to the map view to visually separate the fields within the map.
     * The grid lines are drawn based on the dimensions of the ship map.
     */
    public void addGrid() {
        for (int x = 0; x <= map.getWidth(); x++) {
            final Position f = modelToView(x, 0);
            final Position t = modelToView(x, map.getHeight());
            mapNode.attachChild(gridLine(f, t));
        }
        for (int y = 0; y <= map.getHeight(); y++) {
            final Position f = modelToView(0, y);
            final Position t = modelToView(map.getWidth(), y);
            mapNode.attachChild(gridLine(f, t));
        }
    }

    /**
     * Gets the root node containing all visual elements in this map view.
     *
     * @return the root node for the map view
     */
    public Node getNode() {
        return mapNode;
    }

    /**
     * Gets the total width of the map in view coordinates.
     *
     * @return the width of the map in view coordinates
     */
    public float getWidth() {
        return FIELD_SIZE * map.getWidth();
    }

    /**
     * Gets the total height of the map in view coordinates.
     *
     * @return the height of the map in view coordinates
     */
    public float getHeight() {
        return FIELD_SIZE * map.getHeight();
    }

    /**
     * Converts coordinates from view coordinates to model coordinates.
     *
     * @param x the x-coordinate in view space
     * @param y the y-coordinate in view space
     * @return the corresponding model coordinates as an {@link IntPoint}
     */
    public IntPoint viewToModel(float x, float y) {
        return new IntPoint((int) Math.floor(x / FIELD_SIZE), (int) Math.floor(y / FIELD_SIZE));
    }

    /**
     * Converts coordinates from model coordinates to view coordinates.
     *
     * @param x the x-coordinate in model space
     * @param y the y-coordinate in model space
     * @return the corresponding view coordinates as a {@link Position}
     */
    public Position modelToView(float x, float y) {
        return new FloatPoint(x * FIELD_SIZE, y * FIELD_SIZE);
    }

    /**
     * Converts the mouse position to model coordinates.
     * This method takes into account the map's transformation in the 3D scene.
     *
     * @param pos the 2D vector representing the mouse position in the view
     * @return the corresponding model coordinates as an {@link IntPoint}
     */
    public IntPoint mouseToModel(Vector2f pos) {
        final Vector3f world = new Vector3f(pos.getX(), pos.getY(), 0f);
        final Vector3f view = mapNode.getWorldTransform().transformInverseVector(world, null);
        return viewToModel(view.getX(), view.getY());
    }

    /**
     * Creates a visual representation of a grid line between two positions.
     *
     * @param p1 the start position of the grid line
     * @param p2 the end position of the grid line
     * @return a {@link Geometry} representing the grid line
     */
    private Geometry gridLine(Position p1, Position p2) {
        return app.getDraw().makeFatLine(p1, p2, GRID_DEPTH, GRID_COLOR, GRID_LINE_WIDTH);
    }
}
