////////////////////////////////////////
// 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.renderer.queue.RenderQueue.ShadowMode;
import com.jme3.scene.Geometry;
import com.jme3.scene.Node;
import com.jme3.scene.Spatial;
import com.jme3.scene.shape.Box;
import com.jme3.scene.shape.Cylinder;
import pp.battleship.client.BattleshipApp;
import pp.battleship.model.Battleship;
import pp.battleship.model.Rotation;
import pp.battleship.model.ShipMap;
import pp.battleship.model.Shot;

import static java.util.Objects.requireNonNull;
import static pp.util.FloatMath.HALF_PI;
import static pp.util.FloatMath.PI;

/**
 * The {@code SeaSynchronizer} class is responsible for synchronizing the graphical
 * representation of the ships and shots on the sea map with the underlying data model.
 * It extends the {@link ShipMapSynchronizer} to provide specific synchronization
 * logic for the sea map.
 */
class SeaSynchronizer extends ShipMapSynchronizer {
    private static final String UNSHADED = "Common/MatDefs/Misc/Unshaded.j3md"; //NON-NLS
    private static final String KING_GEORGE_V_MODEL = "Models/KingGeorgeV/KingGeorgeV.j3o"; //NON-NLS
    private static final String COLOR = "Color"; //NON-NLS
    private static final String SHIP = "ship"; //NON-NLS
    private static final String SHOT = "shot"; //NON-NLS
    private static final ColorRGBA BOX_COLOR = ColorRGBA.Gray;
    private static final ColorRGBA SPLASH_COLOR = new ColorRGBA(0f, 0f, 1f, 0.4f);
    private static final ColorRGBA HIT_COLOR = new ColorRGBA(1f, 0f, 0f, 0.4f);

    private final ShipMap map;
    private final BattleshipApp app;

    /**
     * Constructs a {@code SeaSynchronizer} object with the specified application, root node, and ship map.
     *
     * @param app  the Battleship application
     * @param root the root node to which graphical elements will be attached
     * @param map  the ship map containing the ships and shots
     */
    public SeaSynchronizer(BattleshipApp app, Node root, ShipMap map) {
        super(app.getGameLogic().getOwnMap(), root);
        this.app = app;
        this.map = map;
        addExisting();
    }

    /**
     * Visits a {@link Shot} and creates a graphical representation of it.
     * If the shot is a hit, it attaches the representation to the ship node.
     *
     * @param shot the shot to be represented
     * @return the graphical representation of the shot, or null if the shot is a hit
     * and the representation has been attached to the ship node
     */
    @Override
    public Spatial visit(Shot shot) {
        return shot.isHit() ? handleHit(shot) : createCylinder(shot);
    }

    /**
     * Handles a hit by attaching its representation to the node that
     * contains the ship model as a child so that it moves with the ship.
     *
     * @param shot a hit
     * @return always null to prevent the representation from being attached
     * to the items node as well
     */
    private Spatial handleHit(Shot shot) {
        final Battleship ship = requireNonNull(map.findShipAt(shot), "Missing ship");
        final Node shipNode = requireNonNull((Node) getSpatial(ship), "Missing ship node");

        final Geometry representation = createCylinder(shot);
        representation.getLocalTranslation().subtractLocal(shipNode.getLocalTranslation());
        shipNode.attachChild(representation);

        return null;
    }

    /**
     * Creates a cylinder geometry representing the specified shot.
     * The appearance of the cylinder depends on whether the shot is a hit or a miss.
     *
     * @param shot the shot to be represented
     * @return the geometry representing the shot
     */
    private Geometry createCylinder(Shot shot) {
        final ColorRGBA color = shot.isHit() ? HIT_COLOR : SPLASH_COLOR;
        final float height = shot.isHit() ? 1.2f : 0.1f;

        final Cylinder cylinder = new Cylinder(2, 20, 0.45f, height, true);
        final Geometry geometry = new Geometry(SHOT, cylinder);

        geometry.setMaterial(createColoredMaterial(color));
        geometry.rotate(HALF_PI, 0f, 0f);
        // compute the center of the shot in world coordinates
        geometry.setLocalTranslation(shot.getY() + 0.5f, 0f, shot.getX() + 0.5f);

        return geometry;
    }

    /**
     * Visits a {@link Battleship} and creates a graphical representation of it.
     * The representation is either a 3D model or a simple box depending on the
     * type of battleship.
     *
     * @param ship the battleship to be represented
     * @return the node containing the graphical representation of the battleship
     */
    @Override
    public Spatial visit(Battleship ship) {
        final Node node = new Node(SHIP);
        node.attachChild(createShip(ship));
        // compute the center of the ship in world coordinates
        final float x = 0.5f * (ship.getMinY() + ship.getMaxY() + 1f);
        final float z = 0.5f * (ship.getMinX() + ship.getMaxX() + 1f);
        node.setLocalTranslation(x, 0f, z);
        node.addControl(new ShipControl(ship));
        return node;
    }

    /**
     * Creates the appropriate graphical representation of the specified battleship.
     * The representation is either a detailed model or a simple box based on the length of the ship.
     *
     * @param ship the battleship to be represented
     * @return the spatial representing the battleship
     */
    private Spatial createShip(Battleship ship) {
        return ship.getLength() == 4 ? createBattleship(ship) : createBox(ship);
    }

    /**
     * Creates a simple box to represent a battleship that is not of the "King George V" type.
     *
     * @param ship the battleship to be represented
     * @return the geometry representing the battleship as a box
     */
    private Spatial createBox(Battleship ship) {
        final Box box = new Box(0.5f * (ship.getMaxY() - ship.getMinY()) + 0.3f,
                                0.3f,
                                0.5f * (ship.getMaxX() - ship.getMinX()) + 0.3f);
        final Geometry geometry = new Geometry(SHIP, box);
        geometry.setMaterial(createColoredMaterial(BOX_COLOR));
        geometry.setShadowMode(ShadowMode.CastAndReceive);

        return geometry;
    }

    /**
     * Creates a new {@link Material} with the specified color.
     * If the color includes transparency (i.e., alpha value less than 1),
     * the material's render state is set to use alpha blending, allowing for
     * semi-transparent rendering.
     *
     * @param color the {@link ColorRGBA} to be applied to the material. If the alpha value
     *              of the color is less than 1, the material will support transparency.
     * @return a {@link Material} instance configured with the specified color and,
     * if necessary, alpha blending enabled.
     */
    private Material createColoredMaterial(ColorRGBA color) {
        final Material material = new Material(app.getAssetManager(), UNSHADED);
        if (color.getAlpha() < 1f)
            material.getAdditionalRenderState().setBlendMode(BlendMode.Alpha);
        material.setColor(COLOR, color);
        return material;
    }

    /**
     * Creates a detailed 3D model to represent a "King George V" battleship.
     *
     * @param ship the battleship to be represented
     * @return the spatial representing the "King George V" battleship
     */
    private Spatial createBattleship(Battleship ship) {
        final Spatial model = app.getAssetManager().loadModel(KING_GEORGE_V_MODEL);

        model.rotate(-HALF_PI, calculateRotationAngle(ship.getRot()), 0f);
        model.scale(1.48f);
        model.setShadowMode(ShadowMode.CastAndReceive);

        return model;
    }

    /**
     * Calculates the rotation angle for the specified rotation.
     *
     * @param rot the rotation of the battleship
     * @return the rotation angle in radians
     */
    private static float calculateRotationAngle(Rotation rot) {
        return switch (rot) {
            case RIGHT -> HALF_PI;
            case DOWN -> 0f;
            case LEFT -> -HALF_PI;
            case UP -> PI;
        };
    }
}
