////////////////////////////////////////
// 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.app.Application;
import com.jme3.app.state.AppStateManager;
import com.jme3.asset.AssetManager;
import com.jme3.light.AmbientLight;
import com.jme3.light.DirectionalLight;
import com.jme3.material.Material;
import com.jme3.math.ColorRGBA;
import com.jme3.math.Vector2f;
import com.jme3.math.Vector3f;
import com.jme3.renderer.Camera;
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.shadow.DirectionalLightShadowRenderer;
import com.jme3.shadow.EdgeFilteringMode;
import com.jme3.texture.Texture;
import com.jme3.util.SkyFactory;
import com.jme3.util.TangentBinormalGenerator;
import pp.battleship.client.BattleshipAppState;
import pp.battleship.model.ShipMap;
import pp.util.FloatMath;

import static pp.util.FloatMath.TWO_PI;
import static pp.util.FloatMath.cos;
import static pp.util.FloatMath.sin;
import static pp.util.FloatMath.sqrt;

/**
 * Manages the rendering and visual aspects of the sea and sky in the Battleship game.
 * This state is responsible for setting up and updating the sea, sky, and lighting
 * conditions, and controls the camera to create a dynamic view of the game environment.
 */
public class SeaAppState extends BattleshipAppState {
    /**
     * The path to the sea texture material.
     */
    private static final String SEA_TEXTURE = "Textures/Terrain/Water/Water.j3m"; //NON-NLS
    private static final float ABOVE_SEA_LEVEL = 4f;
    private static final float INCLINATION = 2.5f;

    /**
     * The root node for all visual elements in this state.
     */
    private final Node viewNode = new Node("view"); //NON-NLS

    /**
     * The node containing the scene elements, such as the sea surface.
     */
    private final Node sceneNode = new Node("scene"); //NON-NLS

    /**
     * Synchronizes the sea's visual representation with the game logic.
     */
    private SeaSynchronizer synchronizer;

    /**
     * The current angle of the camera around the center of the map.
     */
    private float cameraAngle;

    /**
     * Initializes the state by setting up the sky, lights, and other visual components.
     * This method is called when the state is first attached to the state manager.
     *
     * @param stateManager the state manager
     * @param application  the application
     */
    @Override
    public void initialize(AppStateManager stateManager, Application application) {
        super.initialize(stateManager, application);
        viewNode.attachChild(sceneNode);
        setupLights();
        setupSky();
    }

    /**
     * Enables the sea and sky state, setting up the scene and registering any necessary listeners.
     * This method is called when the state is set to active.
     */
    @Override
    protected void enableState() {
        sceneNode.detachAllChildren();
        setupScene();
        if (synchronizer == null) {
            synchronizer = new SeaSynchronizer(getApp(), sceneNode, getGameLogic().getOwnMap());
            getGameLogic().addListener(synchronizer);
        }
        getApp().getRootNode().attachChild(viewNode);
    }

    /**
     * Disables the sea and sky state, removing visual elements from the scene and unregistering listeners.
     * This method is called when the state is set to inactive.
     */
    @Override
    protected void disableState() {
        getApp().getRootNode().detachChild(viewNode);
        if (synchronizer != null) {
            getGameLogic().removeListener(synchronizer);
            synchronizer = null;
        }
    }

    /**
     * Updates the state each frame, moving the camera to simulate it circling around the map.
     *
     * @param tpf the time per frame (seconds)
     */
    @Override
    public void update(float tpf) {
        super.update(tpf);
        cameraAngle += TWO_PI * 0.05f * tpf;
        adjustCamera();
    }

    /**
     * Sets up the lighting for the scene, including directional and ambient lights.
     * Also configures shadows to enhance the visual depth of the scene.
     */
    private void setupLights() {
        final AssetManager assetManager = getApp().getAssetManager();
        final DirectionalLightShadowRenderer shRend = new DirectionalLightShadowRenderer(assetManager, 2048, 3);
        shRend.setLambda(0.55f);
        shRend.setShadowIntensity(0.6f);
        shRend.setEdgeFilteringMode(EdgeFilteringMode.Bilinear);
        getApp().getViewPort().addProcessor(shRend);

        final DirectionalLight sun = new DirectionalLight();
        sun.setDirection(new Vector3f(-1f, -0.7f, -1f).normalizeLocal());
        viewNode.addLight(sun);
        shRend.setLight(sun);

        final AmbientLight ambientLight = new AmbientLight(new ColorRGBA(0.3f, 0.3f, 0.3f, 0f));
        viewNode.addLight(ambientLight);
    }

    /**
     * Sets up the sky in the scene using a skybox with textures for all six directions.
     * This creates a realistic and immersive environment for the sea.
     */
    private void setupSky() {
        final AssetManager assetManager = getApp().getAssetManager();
        final Texture west = assetManager.loadTexture("Textures/Sky/Lagoon/lagoon_west.jpg"); //NON-NLS
        final Texture east = assetManager.loadTexture("Textures/Sky/Lagoon/lagoon_east.jpg"); //NON-NLS
        final Texture north = assetManager.loadTexture("Textures/Sky/Lagoon/lagoon_north.jpg"); //NON-NLS
        final Texture south = assetManager.loadTexture("Textures/Sky/Lagoon/lagoon_south.jpg"); //NON-NLS
        final Texture up = assetManager.loadTexture("Textures/Sky/Lagoon/lagoon_up.jpg"); //NON-NLS
        final Texture down = assetManager.loadTexture("Textures/Sky/Lagoon/lagoon_down.jpg"); //NON-NLS
        final Spatial sky = SkyFactory.createSky(assetManager, west, east, north, south, up, down);
        sky.rotate(0f, FloatMath.PI, 0f);
        viewNode.attachChild(sky);
    }

    /**
     * Sets up the sea surface in the scene. This includes creating the sea mesh,
     * applying textures, and enabling shadows.
     */
    private void setupScene() {
        final ShipMap ownMap = getGameLogic().getOwnMap();
        final float x = 0.5f * ownMap.getWidth();
        final float y = 0.5f * ownMap.getHeight();
        final Box seaMesh = new Box(y + 0.5f, 0.1f, x + 0.5f);
        final Geometry seaGeo = new Geometry("sea", seaMesh); //NON-NLS
        seaGeo.setLocalTranslation(new Vector3f(y, -0.1f, x));
        seaMesh.scaleTextureCoordinates(new Vector2f(4f, 4f));
        final Material seaMat = getApp().getAssetManager().loadMaterial(SEA_TEXTURE);
        seaGeo.setMaterial(seaMat);
        seaGeo.setShadowMode(ShadowMode.CastAndReceive);
        TangentBinormalGenerator.generate(seaGeo);
        sceneNode.attachChild(seaGeo);
    }

    /**
     * Adjusts the camera position and orientation to create a circular motion around
     * the center of the map. This provides a dynamic view of the sea and surrounding environment.
     */
    private void adjustCamera() {
        final ShipMap ownMap = getGameLogic().getOwnMap();
        final float mx = 0.5f * ownMap.getWidth();
        final float my = 0.5f * ownMap.getHeight();
        final float radius = 2f * sqrt(mx * mx + my + my);
        final float cos = radius * cos(cameraAngle);
        final float sin = radius * sin(cameraAngle);
        final float x = mx - cos;
        final float y = my - sin;
        final Camera camera = getApp().getCamera();
        camera.setLocation(new Vector3f(y, ABOVE_SEA_LEVEL, x));
        camera.getRotation().lookAt(new Vector3f(sin, -INCLINATION, cos),
                                    Vector3f.UNIT_Y);
        camera.update();
    }
}
