import Stats from "three/examples/jsm/libs/stats.module.js";
import { Color } from "../core/color/Color.js";

import { noop } from "../core/function/noop.js";
import Vector2 from "../core/geom/Vector2.js";
import Vector3 from "../core/geom/Vector3.js";
import EmptyView from "../view/elements/EmptyView.js";
import { JsonAssetLoader } from "./asset/loaders/JsonAssetLoader.js";
import { SerializationMetadata } from "./ecs/components/SerializationMetadata.js";
import Tag from "./ecs/components/Tag.js";
import Entity from "./ecs/Entity.js";
import { TerrainLayer } from "./ecs/terrain/ecs/layers/TerrainLayer.js";
import Terrain from "./ecs/terrain/ecs/Terrain.js";
import TerrainSystem from "./ecs/terrain/ecs/TerrainSystem.js";
import { Transform } from "./ecs/transform/Transform.js";
import Engine from "./Engine.js";
import { EngineConfiguration } from "./EngineConfiguration.js";
import { makeOrbitalCameraController } from "./graphics/camera/makeOrbitalCameraController.js";
import { Camera } from "./graphics/ecs/camera/Camera.js";
import { CameraSystem } from "./graphics/ecs/camera/CameraSystem.js";
import TopDownCameraController from "./graphics/ecs/camera/topdown/TopDownCameraController.js";
import TopDownCameraControllerSystem from "./graphics/ecs/camera/topdown/TopDownCameraControllerSystem.js";
import { Light } from "./graphics/ecs/light/Light.js";
import LightSystem from "./graphics/ecs/light/LightSystem.js";
import Water from "./graphics/ecs/water/Water.js";
import WaterSystem from "./graphics/ecs/water/WaterSystem.js";
import { MouseEvents } from "./input/devices/events/MouseEvents.js";
import KeyboardCameraController from "./input/ecs/controllers/KeyboardCameraController.js";
import InputControllerSystem from "./input/ecs/systems/InputControllerSystem.js";
import { ConsoleLoggerBackend } from "./logging/ConsoleLoggerBackend.js";
import { logger } from "./logging/GlobalLogger.js";
import { getURLHash } from "./platform/GetURLHash.js";
import { WebEnginePlatform } from "./platform/WebEnginePlatform.js";
import SoundListener from "./sound/ecs/SoundListener.js";
import SoundListenerSystem from "./sound/ecs/SoundListenerSystem.js";

/**
 *
 * @param {Engine} engine
 * @returns {Promise}
 */
function setLocale(engine) {

    const urlHash = getURLHash();

    let locale;
    if (urlHash.lang !== undefined) {
        locale = urlHash.lang;
    } else {
        locale = "en-gb";
    }

    return engine.localization.loadLocale(locale);
}

/**
 * Equal to ~ 5500 K light color
 * @type {Readonly<Color>}
 */
const DEFAULT_SUNLIGHT_COLOR = Object.freeze(new Color(1, 0.93, 0.87));

export class EngineHarness {
    constructor() {
        /**
         *
         * @type {Engine}
         */
        this.engine = new Engine(new WebEnginePlatform());

        window.engine = this.engine;

        logger.addBackend(ConsoleLoggerBackend.INSTANCE);

        /**
         * @readonly
         * @type {Promise|null}
         */
        this.p = null;
    }

    /**
     * Quick initialization of the engine in a sensible default configuration
     * @param {(config:EngineConfiguration,engine:Engine)=>*} [configuration]
     * @return {Promise<Engine>}
     */
    static async bootstrap({ configuration } = {}) {
        const harness = new EngineHarness();

        return harness.initialize({ configuration });
    }

    /**
     *
     * @param {Engine} engine
     */
    static addFpsCounter(engine) {
        const stats = new Stats();

        engine.graphics.on.postRender.add(stats.update, stats);

        const view = new EmptyView({
            el: stats.domElement
        });

        engine.viewStack.addChild(view);
    }

    /**
     * @param {Object} [params]
     * @param {(config:EngineConfiguration,engine:Engine)=>*} [params.configuration]
     * @param {boolean} [params.enable_localization] Whether or not to load localization data
     * @returns {Promise<Engine>}
     */
    initialize({
                   configuration = noop,
                   enable_localization = false,
               } = {}) {

        if (this.p !== null) {
            return this.p;
        }

        const engine = this.engine;

        const promise = new Promise(async function (resolve, reject) {
            const config = new EngineConfiguration();

            configuration(config, engine);

            if (!config.hasLoader('json')) {
                config.addLoader('json', new JsonAssetLoader());
            }

            await config.apply(engine);

            engine.sceneManager.create("main");
            engine.sceneManager.set("main");

            await engine.start();

            if (enable_localization) {
                try {
                    await setLocale(engine);
                } catch (e) {
                    console.warn('Failed to load localization:', e);
                }
            }

            document.body.appendChild(engine.viewStack.el);
            engine.viewStack.link();

            // set css
            document.body.style.margin = "0";
            document.body.style.overflow = "hidden";

            function handleInput() {
                window.removeEventListener(MouseEvents.Down, handleInput);

                engine.sound.resumeContext();
            }

            window.addEventListener(MouseEvents.Down, handleInput);

            resolve(engine);
        });

        this.p = promise;

        return promise;
    }

    /**
     * @param {Object} param
     * @param {Engine} param.engine
     * @param {EntityComponentDataset} [param.ecd]
     * @param {Vector3} [param.target]
     * @param {number} [param.distance]
     * @param {number} [param.pitch]
     * @param {number} [param.yaw]
     * @param {boolean} [param.autoClip]
     * @param {number} [param.distanceMin]
     * @param {number} [param.distanceMax]
     * @param {number} [param.fieldOfView] in degrees
     * @returns {Entity}
     */
    static buildCamera(
        {
            engine,
            ecd = engine.entityManager.dataset,
            target = new Vector3(),
            distance = 10,
            pitch = 1.4,
            yaw = 0,
            autoClip = true,
            distanceMin = 0.01,
            distanceMax = 1000,
            fieldOfView = 45
        }
    ) {
        const em = engine.entityManager;

        if (em.getSystem(TopDownCameraControllerSystem) === null) {
            em.addSystem(new TopDownCameraControllerSystem());
        }
        if (em.getSystem(CameraSystem) === null) {
            em.addSystem(new CameraSystem(engine.graphics))
        }

        if (em.getSystem(SoundListenerSystem) === null) {
            em.addSystem(new SoundListenerSystem(engine.sound.context));
        }

        if (!ecd.isComponentTypeRegistered(Tag)) {
            ecd.registerComponentType(Tag);
        }

        const transform = new Transform();

        const cameraController = new TopDownCameraController();
        cameraController.pitch = pitch;
        cameraController.yaw = yaw;
        cameraController.distance = distance;
        cameraController.distanceMin = distanceMin;
        cameraController.distanceMax = distanceMax;
        cameraController.target.copy(target);

        const camera = new Camera();

        camera.active.set(true);
        camera.autoClip = autoClip;
        camera.clip_far = distanceMax;
        camera.fov.set(fieldOfView);

        const entityBuilder = new Entity();

        entityBuilder
            .add(transform)
            .add(cameraController)
            .add(camera)
            .add(new SoundListener())
            .add(Tag.fromJSON(["Camera"]))
            .build(ecd);

        console.log("build camera", entityBuilder);

        return entityBuilder;
    }

    /**
     * @param {Object} params
     * @param {Engine} params.engine
     * @param {{x:number,y:number,z:number}} [params.focus]
     * @param [params.heightMap]
     * @param [params.heightRange]
     * @param {number} [params.pitch]
     * @param {number} [params.yaw]
     * @param {number} [params.distance]
     * @param {{x:number, y:number}} [params.terrainSize]
     * @param {number} [params.terrainResolution]
     * @param {boolean} [params.enableWater]
     * @param {boolean} [params.enableTerrain]
     * @param {boolean} [params.enableLights=true]
     * @param {boolean} [params.enableShadows=true] if lights are enabled, enable shadows for sun
     * @param {number} [params.cameraFieldOfView]
     * @param {number} [params.cameraFarDistance]
     * @param {boolean} [params.cameraController=true]
     * @param {boolean} [params.cameraAutoClip]
     * @param {number} [params.shadowmapResolution]
     * @param {boolean} [params.showFps]
     */
    static async buildBasics({
                                 engine,
                                 focus = new Vector3(10, 0, 10),
                                 heightMap,
                                 heightRange,
                                 pitch = 0.7,
                                 yaw = -1.2,
                                 distance = 10,
                                 terrainSize = new Vector2(10, 10),
                                 terrainResolution = 10,
                                 enableWater = true,
                                 enableTerrain = true,
                                 enableLights = true,
                                 enableShadows = true,
                                 cameraFieldOfView,
                                 cameraFarDistance,
                                 cameraController = true,
                                 cameraAutoClip = true,
                                 shadowmapResolution,
                                 showFps = true
                             }) {

        if (showFps) {
            EngineHarness.addFpsCounter(engine);
        }

        if (enableLights) {
            await EngineHarness.buildLights({
                engine: engine,
                shadowmapResolution,
                castShadow: enableShadows,
            });
        }

        const camera = EngineHarness.buildCamera({
            engine,
            target: focus,
            pitch,
            yaw,
            distance,
            fieldOfView: cameraFieldOfView,
            distanceMax: cameraFarDistance,
            autoClip: cameraAutoClip
        });

        const cameraEntity = camera.id;

        if (enableTerrain) {
            await EngineHarness.buildTerrain({
                engine,
                heightMap,
                heightRange,
                resolution: terrainResolution,
                size: terrainSize,
                enableWater
            });
        }

        if (cameraController) {
            EngineHarness.buildOrbitalCameraController({
                engine,
                cameraEntity: cameraEntity
            });

            const ecd = engine.entityManager.dataset;
            const keyboardCameraController = new KeyboardCameraController({
                component: ecd.getComponent(cameraEntity, TopDownCameraController),
                entity: cameraEntity
            }, ecd);

            keyboardCameraController.build(ecd);
        }
    }


    /**
     * @param {Object} param
     * @param {Engine} param.engine
     * @param {EntityComponentDataset} [param.ecd]
     * @param {number} [param.shadowmapResolution]
     * @param {boolean} [param.castShadow]
     * @param {Color} [param.sun]
     * @param {number} [param.sunIntensity]
     * @param {Vector3} [param.sunDirection]
     * @param {Color} [param.ambient]
     * @param {number} [param.ambientIntensity]
     * @param {number} [param.sunShadowDistance] Maximum draw distance for sun's shadow. Allows better use of shadowmap resolution.
     */
    static async buildLights({
                                 engine,
                                 ecd = engine.entityManager.dataset,
                                 shadowmapResolution = 1024,
                                 castShadow = true,
                                 sun = DEFAULT_SUNLIGHT_COLOR,
                                 sunIntensity = 0.9,
                                 sunDirection = new Vector3(0.17, -1, 0.17),
                                 sunShadowDistance = 1000,
                                 ambient = Color.white,
                                 ambientIntensity = 0.1
                             }) {
        const em = engine.entityManager;

        if (!em.hasSystem(LightSystem)) {
            await em.addSystem(new LightSystem(engine, {
                shadowResolution: shadowmapResolution
            }));
        }

        if (!ecd.isComponentTypeRegistered(Tag)) {
            ecd.registerComponentType(Tag);
        }

        const key = new Light();
        key.type.set(Light.Type.DIRECTION);
        key.color.copy(sun);
        key.maxShadowDistance.set(sunShadowDistance);
        key.intensity.set(sunIntensity);
        key.castShadow.set(castShadow);

        const transform = new Transform();
        transform.position.set(30, 70, 30);
        transform.rotation.lookRotation(sunDirection);

        new Entity()
            .add(key)
            .add(transform)
            .add(Tag.fromJSON(["Light", "Key"]))
            .build(ecd);


        const fill = new Light();
        fill.type.set(Light.Type.AMBIENT);
        fill.color.copy(ambient);
        fill.intensity.set(ambientIntensity);


        new Entity()
            .add(fill)
            .add(new Transform())
            .add(Tag.fromJSON(["Light", "Ambient"]))
            .build(ecd);
    }

    /**
     * @param {Object} param
     * @param {number} [param.cameraEntity]
     * @param {Engine} param.engine
     * @param {number} [param.sensitivity]
     * @param {EntityComponentDataset} [param.ecd]
     * @returns {Promise<Entity>}
     */
    static async buildOrbitalCameraController({
                                                  cameraEntity,
                                                  engine,
                                                  ecd = engine.entityManager.dataset,
                                                  sensitivity = 1
                                              }) {

        if (cameraEntity === undefined) {
            cameraEntity = ecd.getAnyComponent(Camera).entity;
        }

        const em = engine.entityManager;

        if (em.getSystem(InputControllerSystem) === null) {
            await em.addSystem(new InputControllerSystem(engine.devices));
        }

        if (!ecd.isComponentTypeRegistered(SerializationMetadata)) {
            ecd.registerComponentType(SerializationMetadata);
        }

        const domElement = engine.graphics.domElement;


        domElement.addEventListener("contextmenu", (event) => {
            event.preventDefault();
        });

        const eb = makeOrbitalCameraController({
            camera_entity: cameraEntity,
            ecd,
            dom_element: domElement,
            sensitivity
        });

        eb.add(Tag.fromJSON(['CameraController']));
        eb.add(SerializationMetadata.Transient);

        eb.build(ecd);

        return eb;
    }


    /**
     *
     * @param {Engine} engine
     * @param {Vector2} [size]
     * @param {String} [diffuse0]
     * @param heightMap
     * @param {number} [resolution]
     * @param {number} [waterLevel]
     * @param {boolean} [enableWater]
     * @returns {Terrain}
     */
    static async buildTerrain(
        {
            engine,
            size = new Vector2(10, 10),
            diffuse0 = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAAAXNSR0IArs4c6QAAAA1JREFUGFdj+P///38ACfsD/QVDRcoAAAAASUVORK5CYII=",
            resolution = 10,
            waterLevel = 0,
            enableWater = true,
        }
    ) {

        const em = engine.entityManager;

        if (em.getSystem(TerrainSystem) === null) {
            await em.addSystem(new TerrainSystem(engine.graphics, engine.assetManager));
        }

        const terrain = new Terrain();

        terrain.size.copy(size);
        terrain.resolution = resolution;
        terrain.gridScale = 2;
        terrain.layers.addLayer(TerrainLayer.from(
            diffuse0,
            5,
            5
        ));
        terrain.splat.resize(1, 1, 1);
        terrain.splat.fillLayerWeights(0, 255);

        const eb = new Entity();

        eb.add(new Transform())
        eb.add(terrain);

        if (enableWater) {

            if (em.getSystem(WaterSystem) === null) {
                em.addSystem(new WaterSystem(engine.graphics));
            }

            const water = new Water();

            water.level.set(waterLevel);

            eb.add(water);
        }

        eb.build(engine.entityManager.dataset);

        return terrain;

    }
}

let singleton = null;

/**
 *
 * @returns {EngineHarness}
 */
EngineHarness.getSingleton = function () {
    if (singleton === null) {
        singleton = new EngineHarness();
    }

    return singleton;
};

