////////////////////////////////////////
// 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.effect.ParticleEmitter;
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.Shell;
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 BOAT_SMALL_MODEL = "Models/BoatSmall/12219_boat_v2_L2.j3o"; //NON-NLS
    private static final String CV_MODEL = "Models/CV/CV.j3o"; //NON-NLS
    private static final String BATTLE_MODEL = "Models/Battle/Battle.j3o"; //NON-NLS
    private static final String LIGHTING = "Common/MatDefs/Light/Lighting.j3md";
    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;
    private final ParticleEffectFactory particleFactory;

    /**
     * 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;
        this.particleFactory = new ParticleEffectFactory(app);
        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) : handleMiss(shot);
    }

    /**
     * Handles a miss by representing it with a blue cylinder
     * and attaching a water splash effect to it.
     * @param shot the shot to be processed
     * @return a Spatial simulating a miss with water splash effect
     */
    private Spatial handleMiss(Shot shot) {
        Node shotNode = new Node("ShotNode");
        Geometry shotCylinder = createCylinder(shot);
        shotNode.attachChild(shotCylinder);
        ParticleEmitter waterSplash = particleFactory.createWaterSplash();
        waterSplash.setLocalTranslation(shot.getY() + 0.5f, 0f, shot.getX() + 0.5f);
        shotNode.attachChild(waterSplash);
        waterSplash.emitAllParticles();

        return shotNode;
    }

    /**
     * Handles the sinking animation and removal of ship if destroyed
     * @param ship the ship to be sunk
     */
    private void sinkAndRemoveShip(Battleship ship) {
        Battleship wilkeningklaunichtmeinencode = ship;
        final Node shipNode = (Node) getSpatial(wilkeningklaunichtmeinencode);
        if (shipNode == null) return;

        // Add sinking control to animate the sinking
        shipNode.addControl(new SinkingControl(shipNode));

        // Add particle effects
        ParticleEmitter bubbles = particleFactory.createWaterSplash();
        bubbles.setLocalTranslation(shipNode.getLocalTranslation());
        shipNode.attachChild(bubbles);
        bubbles.emitAllParticles();
    }

    /**
     * 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");



        // Create a new node specifically for the hit effects
        Node hitEffectNode = new Node("HitEffectNode");

        // Create particle effects
        ParticleEmitter flame = particleFactory.createFlame();
        ParticleEmitter flash = particleFactory.createFlash();
        ParticleEmitter spark = particleFactory.createSpark();
        ParticleEmitter roundSpark = particleFactory.createRoundSpark();
        ParticleEmitter smokeTrail = particleFactory.createSmokeTrail();
        ParticleEmitter debris = particleFactory.createDebris();
        ParticleEmitter shockwave = particleFactory.createShockwave();
        ParticleEmitter movingSmoke = particleFactory.createMovingSmokeEmitter();

        // Attach all effects to the hitEffectNode
        hitEffectNode.attachChild(flame);
        hitEffectNode.attachChild(flash);
        hitEffectNode.attachChild(spark);
        hitEffectNode.attachChild(roundSpark);
        hitEffectNode.attachChild(smokeTrail);
        hitEffectNode.attachChild(debris);
        hitEffectNode.attachChild(shockwave);
        hitEffectNode.attachChild(movingSmoke);

        // Set the local translation for the hit effect to the point of impact
        hitEffectNode.setLocalTranslation(shot.getY() + 0.5f - shipNode.getLocalTranslation().x,
                                          0.5f, // Adjust as needed for height above the ship
                                          shot.getX() + 0.5f - shipNode.getLocalTranslation().z);

        // Attach the hitEffectNode to the shipNode so it moves with the ship
        shipNode.attachChild(hitEffectNode);

        // Emit particles when the hit happens
        flash.emitAllParticles();
        spark.emitAllParticles();
        smokeTrail.emitAllParticles();
        debris.emitAllParticles();
        shockwave.emitAllParticles();
        flame.emitAllParticles();
        roundSpark.emitAllParticles();

        //Checks if ship is destroyed and triggers animation accordingly
        if (ship.isDestroyed()) {
            sinkAndRemoveShip(ship);
        }

        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) {
        switch (ship.getLength()) {
            case 4: return createBattleship(ship);
            case 3: return createCV(ship);
            case 2: return createBattle(ship);
            case 1: return createSmallship(ship);
            default: return 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.scale(0.0007f);
        model.setShadowMode(ShadowMode.CastAndReceive);

        return model;
    }
    
    /**
     * Creates a detailed 3D model to represent a small tug boat.
     *
     * @param ship the battleship to be represented
     * @return the spatial representing a small tug boat
     */
    private Spatial createSmallship(Battleship ship) {
        final Spatial model = app.getAssetManager().loadModel(BOAT_SMALL_MODEL);

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

        return model;
    }

    /**
     * Creates a detailed 3D model to represent a "German WWII UBoat".
     *
     * @param ship the battleship to be represented
     * @return the spatial representing the "German WWII UBoat"
     */
    private Spatial createCV(Battleship ship) {
        final Spatial model = app.getAssetManager().loadModel(CV_MODEL);

        model.rotate(0f, calculateRotationAngle(ship.getRot()), 0f);
        model.move(0f, 0.25f, 0f);
        model.scale(0.85f);
        model.setShadowMode(ShadowMode.CastAndReceive);

        return model;
    }

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

        model.rotate(-HALF_PI, calculateRotationAngle(ship.getRot()), 0f);
        model.move(0f, -0.06f, 0f);
        model.scale(0.27f);
        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;
        };
    }

    /**
     * Creates and returns a 3D model representation of the given {@code Shell} object
     * for visualization in the game.
     *
     * @param shell The {@code Shell} object to be visualized.
     * @return A {@code Spatial} object representing the 3D model of the shell.
     */
    @Override
    public Spatial visit(Shell shell) {
        final Spatial model = app.getAssetManager().loadModel("Models/Shell/shell.j3o");
        model.setLocalScale(.05f);
        model.setShadowMode(ShadowMode.CastAndReceive);
        Material mat = new Material(app.getAssetManager(), LIGHTING);
        mat.setTexture("DiffuseMap", app.getAssetManager().loadTexture("Models/Shell/shell_color.png"));
        mat.setReceivesShadows(true);
        model.setMaterial(mat);

        model.addControl(new ShellControl(shell));
        return model;
    }
}
