import '../../../../css/editor/EditorView.scss';

import '../../../../css/editor/EntityEditorView.scss';
import { resetSoundEmitterTracks } from "../../model/game/options/resetSoundEmitterTracks.js";
import { assert } from "../src/core/assert.js";
import { downloadAsFile } from "../src/core/binary/downloadAsFile.js";
import { EncodingBinaryBuffer } from "../src/core/binary/EncodingBinaryBuffer.js";
import { EndianType } from "../src/core/binary/EndianType.js";
import List from '../src/core/collection/list/List.js';
import { Color } from "../src/core/color/Color.js";
import Signal from "../src/core/events/signal/Signal.js";
import Quaternion from "../src/core/geom/Quaternion.js";
import Vector1 from "../src/core/geom/Vector1.js";
import Vector2 from "../src/core/geom/Vector2.js";
import Vector3 from "../src/core/geom/Vector3.js";
import Vector4 from "../src/core/geom/Vector4.js";
import { NumericInterval } from "../src/core/math/interval/NumericInterval.js";
import ObservedBoolean from "../src/core/model/ObservedBoolean.js";
import ObservedEnum from "../src/core/model/ObservedEnum.js";
import ObservedInteger from "../src/core/model/ObservedInteger.js";
import ObservedString from "../src/core/model/ObservedString.js";
import { ActionProcessor } from '../src/core/process/undo/ActionProcessor.js';
import WorkerProxy from "../src/core/process/worker/WorkerProxy.js";
import Entity from "../src/engine/ecs/Entity.js";
import { EntityComponentDataset } from "../src/engine/ecs/EntityComponentDataset.js";
import BinaryBufferSerializer from "../src/engine/ecs/storage/BinaryBufferSerializer.js";
import { Transform } from "../src/engine/ecs/transform/Transform.js";
import { Camera } from "../src/engine/graphics/ecs/camera/Camera.js";
import TopDownCameraController from "../src/engine/graphics/ecs/camera/topdown/TopDownCameraController.js";
import TopDownCameraControllerSystem, {
    setCameraControllerFromTransform
} from "../src/engine/graphics/ecs/camera/topdown/TopDownCameraControllerSystem.js";
import { ParameterTrackSet } from "../src/engine/graphics/particles/particular/engine/parameter/ParameterTrackSet.js";
import { Sampler2D } from "../src/engine/graphics/texture/sampler/Sampler2D.js";
import { KeyCodes } from "../src/engine/input/devices/KeyCodes.js";
import InputControllerSystem from "../src/engine/input/ecs/systems/InputControllerSystem.js";
import ComponentAddAction from "./actions/concrete/ComponentAddAction.js";
import EntityCreateAction from "./actions/concrete/EntityCreateAction.js";
import EntityRemoveAction from "./actions/concrete/EntityRemoveAction.js";
import SelectionAddAction from "./actions/concrete/SelectionAddAction.js";
import SelectionClearAction from "./actions/concrete/SelectionClearAction.js";
import { ListEditor } from "./ecs/component/editors/collection/ListEditor.js";
import { ColorEditor } from "./ecs/component/editors/ColorEditor.js";
import { noEditor } from "./ecs/component/editors/common/noEditor.js";
import { ParameterTrackSetEditor } from "./ecs/component/editors/ecs/ParameterTrackSetEditor.js";
import { QuaternionEditor } from "./ecs/component/editors/geom/QuaternionEditor.js";
import { Vector1Editor } from "./ecs/component/editors/geom/Vector1Editor.js";
import { Vector2Editor } from "./ecs/component/editors/geom/Vector2Editor.js";
import { Vector3Editor } from "./ecs/component/editors/geom/Vector3Editor.js";
import { Vector4Editor } from "./ecs/component/editors/geom/Vector4Editor.js";
import { HTMLElementEditor } from "./ecs/component/editors/HTMLElementEditor.js";
import { NumericIntervalEditor } from "./ecs/component/editors/NumericIntervalEditor.js";
import { ObservedBooleanEditor } from "./ecs/component/editors/ObservedBooleanEditor.js";
import { ObservedEnumEditor } from "./ecs/component/editors/ObservedEnumEditor.js";
import { ObservedIntegerEditor } from "./ecs/component/editors/ObservedIntegerEditor.js";
import { ObservedStringEditor } from "./ecs/component/editors/ObservedStringEditor.js";
import { ArrayEditor } from "./ecs/component/editors/primitive/ArrayEditor.js";
import { BooleanEditor } from "./ecs/component/editors/primitive/BooleanEditor.js";
import { FunctionEditor } from "./ecs/component/editors/primitive/FunctionEditor.js";
import { NumberEditor } from "./ecs/component/editors/primitive/NumberEditor.js";
import { ObjectEditor } from "./ecs/component/editors/primitive/ObjectEditor.js";
import { StringEditor } from "./ecs/component/editors/primitive/StringEditor.js";
import { Sampler2DEditor } from "./ecs/component/editors/Sampler2DEditor.js";
import EditorEntity from "./ecs/EditorEntity.js";
import EditorEntitySystem from "./ecs/EditorEntitySystem.js";
import { MeshLibrary } from "./library/MeshLibrary.js";
import { ProcessEngine } from "./process/ProcessEngine.js";
import { SelectionVisualizer } from "./SelectionVisualizer.js";
import ToolEngine from './tools/engine/ToolEngine.js';
import { TransformerMode } from "./tools/TransformTool.js";
import EditorView from './view/EditorView.js';

/**
 * @template T
 * @param {T} original
 * @param {EntityManager} em
 * @returns {T}
 */
function cloneComponent(original, em) {
    function newInstance() {
        return new original.constructor();
    }

    const cloneViaMethod = {
        name: 'Clone Method',
        check: function (original) {
            return typeof original.clone === "function";
        },
        execute: function (original) {
            return original.clone();
        }
    };

    const cloneViaSerialization = {
        name: 'Serialization',
        check: function (original) {
            return typeof original.toJSON === "function" && typeof original.fromJSON === "function";
        },
        execute: function (original) {
            //use serialization to create a clone
            const json = original.toJSON();

            const clone = newInstance();

            const system = em.getOwnerSystemByComponentClass(original.constructor);

            clone.fromJSON(json, system);

            return clone;
        }
    };
    const cloneViaInstantiate = {
        name: 'New Instance',
        check: function (original) {
            return true;
        },
        execute: function (original) {
            //no clone method, that's a bummer
            const typeName = original.constructor.typeName;

            console.error(`Component '${typeName}' has no 'clone' method, creating a new instance instead..`);

            return newInstance();
        }
    };


    const cloners = [cloneViaMethod, cloneViaSerialization, cloneViaInstantiate];

    for (let i = 0; i < cloners.length; i++) {
        const cloner = cloners[i];
        const applicable = cloner.check(original);

        if (!applicable) {
            continue;
        }

        let clone;
        try {
            clone = cloner.execute(original);
        } catch (e) {
            continue;
        }

        if (original.constructor !== clone.constructor) {
            console.error(`Cloner ${cloner.name} produced instance with different constructor`, 'original=', original, 'clone=', clone);
            continue;
        }

        return clone;
    }

    throw new Error(`Failed to clone, no cloners worked`);
}

/**
 *
 * @param {Editor} editor
 */
function copySelectedEntities(editor) {
    if (editor.selection.isEmpty()) {
        //clear out copy buffer
        editor.copyBuffer = [];
        return;
    }

    /**
     *
     * @type {EntityManager}
     */
    const em = editor.engine.entityManager;

    const ecd = em.dataset;

    editor.copyBuffer = editor.selection.map(function (entity) {
        const components = ecd.getAllComponents(entity);

        //clone all components to preserve their status
        const clonedComponents = [];
        components.forEach(function (c) {
            if (c === undefined) {
                //skip undefined
                return;
            }

            let clone;

            try {
                clone = cloneComponent(c, em);
            } catch (e) {
                console.error(`Failed to clone component, omitting`, c, e);
                return;
            }

            clonedComponents.push(clone);
        });

        return clonedComponents;
    });
}

/**
 *
 * @param {Editor} editor
 */
function pasteEntities(editor) {
    const copyBuffer = editor.copyBuffer;
    if (copyBuffer.length === 0) {
        //copy buffer is empty, nothing to do
        return;
    }

    const actions = editor.actions;

    actions.mark('paste entities');

    const createdEntities = copyBuffer.map(function (components) {
        const entityCreateAction = new EntityCreateAction();

        actions.do(entityCreateAction);

        //clone all components to preserve status of the buffer
        const n = components.length;

        for (let i = 0; i < n; i++) {

            const c = components[i];

            const clone = cloneComponent(c, editor.engine.entityManager);

            const componentAddAction = new ComponentAddAction(entityCreateAction.entity, clone);

            try {
                actions.do(componentAddAction);
            } catch (e) {
                console.warn(`Failed to add component.`, e, 'The component might have been added as a result of some EntityObserver');
            }
        }

        return entityCreateAction.entity;
    });

    //select newly created entities
    actions.do(new SelectionClearAction());
    actions.do(new SelectionAddAction(createdEntities));
}

function buildEditorCamera() {
    const cameraEntity = new Entity();
    const camera = new Camera();

    camera.projectionType.set(Camera.ProjectionType.Perspective);

    camera.autoClip = true;
    camera.active.set(true);

    cameraEntity.add(camera);

    const topDownCameraController = new TopDownCameraController();
    topDownCameraController.distanceMin = -Infinity;
    topDownCameraController.distanceMax = Infinity;

    cameraEntity.add(topDownCameraController);
    cameraEntity.add(new Transform());

    cameraEntity.add(new EditorEntity());

    return cameraEntity;
}

/**
 *
 * @param {Entity} cameraEntity
 * @param {EntityComponentDataset} dataset
 * @param {Editor} editor
 */
function activateEditorCamera(cameraEntity, dataset, editor) {

    const camera = cameraEntity.getComponent(Camera);

    let activeCameraEntity = null;

    //find currently active camera
    dataset.traverseEntities([Camera], function (c, entity) {
        if (dataset.getComponent(entity, EditorEntity) !== undefined) {
            return true;
        }

        if (c.active.getValue()) {


            c.active.set(false);

            editor.cleanupTasks.push(function () {
                //remember to restore active camera
                c.active.set(true);
            });

            activeCameraEntity = entity;
        }
    });

    if (activeCameraEntity !== null) {
        /**
         *
         * @type {Transform}
         */
        const acTransform = dataset.getComponent(activeCameraEntity, Transform);

        /**
         *
         * @type {Transform}
         */
        const cameraTransform = cameraEntity.getComponent(Transform);

        if (acTransform !== undefined) {
            cameraTransform.copy(acTransform);
        }

        /**
         *
         * @type {TopDownCameraController}
         */
        const cameraController = cameraEntity.getComponent(TopDownCameraController);

        const active_camera_controller = dataset.getComponent(activeCameraEntity, TopDownCameraController);
        if (active_camera_controller !== undefined) {
            cameraController.target.copy(active_camera_controller.target);
            cameraController.distance = active_camera_controller.distance;
            cameraController.pitch = active_camera_controller.pitch;
            cameraController.yaw = active_camera_controller.yaw;
            cameraController.roll = active_camera_controller.roll;
        } else {
            setCameraControllerFromTransform(cameraTransform, cameraController);
        }
    }

    camera.active.set(true);
}

/**
 *
 * @param {Map<any, TypeEditor>} registry
 */
function initialize_basic_registry(registry) {

    registry.set(Number, new NumberEditor());
    registry.set(Boolean, new BooleanEditor());
    registry.set(String, new StringEditor());
    registry.set(Function, new FunctionEditor());
    registry.set(Array, new ArrayEditor());
    registry.set(Object, new ObjectEditor());

    registry.set(ObservedInteger, new ObservedIntegerEditor());
    registry.set(ObservedBoolean, new ObservedBooleanEditor());
    registry.set(ObservedEnum, new ObservedEnumEditor());
    registry.set(ObservedString, new ObservedStringEditor());

    registry.set(NumericInterval, new NumericIntervalEditor());
    registry.set(List, new ListEditor());
    registry.set(Color, new ColorEditor());

    registry.set(Vector4, new Vector4Editor());
    registry.set(Vector3, new Vector3Editor());
    registry.set(Vector2, new Vector2Editor());
    registry.set(Vector1, new Vector1Editor());
    registry.set(Quaternion, new QuaternionEditor());

    registry.set(Sampler2D, new Sampler2DEditor());
    registry.set(ParameterTrackSet, new ParameterTrackSetEditor());

    registry.set(HTMLElement, new HTMLElementEditor());
    registry.set(HTMLCanvasElement, new HTMLElementEditor());

    registry.set(WorkerProxy, noEditor());
    registry.set(Signal, noEditor());

    registry.set(Uint8Array, noEditor());
    registry.set(Uint16Array, noEditor());
    registry.set(Uint32Array, noEditor());

    registry.set(Int8Array, noEditor());
    registry.set(Int16Array, noEditor());
    registry.set(Int32Array, noEditor());

    registry.set(Float32Array, noEditor());
    registry.set(Float64Array, noEditor());

    registry.set(CanvasRenderingContext2D, noEditor());

}

/**
 *
 * @constructor
 * @property {List.<Number>} selection represents list of currently selected entities
 * @property {List.<Object>} history list of applied actions
 */
function Editor() {
    this.processEngine = new ProcessEngine();
    this.toolEngine = new ToolEngine();
    this.selection = new List();
    this.actions = new ActionProcessor(this);
    this.selectionVistualizer = new SelectionVisualizer(this);
    this.meshLibrary = new MeshLibrary();

    /**
     *
     * @type {Map<any, TypeEditor>}
     */
    this.type_editor_registry = new Map();

    initialize_basic_registry(this.type_editor_registry);

    this.cameraEntity = buildEditorCamera();

    this.editorEntitySystem = new EditorEntitySystem();

    this.view = null;

    this.copyBuffer = [];


    const self = this;

    /**
     *
     * @param keyCode
     * @param {KeyboardEvent} event
     */
    function processCtrlCombinations(keyCode, event) {
        if (keyCode === KeyCodes.z) {
            event.preventDefault();
            event.stopPropagation();

            self.actions.undo();
        } else if (keyCode === KeyCodes.y) {
            event.preventDefault();
            event.stopPropagation();

            self.actions.redo();
        } else if (keyCode === KeyCodes.c) {
            event.preventDefault();
            event.stopPropagation();

            //copy
            copySelectedEntities(self);
        } else if (keyCode === KeyCodes.v) {
            event.preventDefault();
            event.stopPropagation();

            //paste
            pasteEntities(self);
        } else if (keyCode === KeyCodes.s) {
            event.preventDefault();
            event.stopPropagation();

            const entityManager = self.engine.entityManager;
            const currentDataset = entityManager.dataset;

            //clone dataset
            const dataset = new EntityComponentDataset();
            dataset.setComponentTypeMap(currentDataset.getComponentTypeMap());
            dataset.maskedCopy(currentDataset, currentDataset.getComponentTypeMap());

            //remove all Editor entities
            dataset.traverseComponents(EditorEntity, function (c, entity) {
                dataset.removeEntity(entity);
            });

            //clone and re-enable the camera
            dataset.traverseComponents(Camera, function (c, entity) {
                const clone = c.clone();
                clone.active.set(true);

                dataset.removeComponentFromEntity(entity, Camera);
                dataset.addComponentToEntity(entity, clone);
            });

            // Set music tracks back to time=0
            resetSoundEmitterTracks(dataset);

            const serializer = new BinaryBufferSerializer();
            serializer.engine = self.engine;
            serializer.registry = engine.binarySerializationRegistry;

            const state = new EncodingBinaryBuffer();
            state.endianness = EndianType.BigEndian;

            try {
                serializer.process(state, dataset);
            } catch (e) {
                //failed to serialize game state
                console.error("Failed to serialize game state", e);
            }

            state.trim();

            downloadAsFile(state.data, "level.bin");
        }
    }

    /**
     *
     * @param keyCode
     * @param {KeyboardEvent} event
     */
    function processSingleKey(keyCode, event) {
        if (keyCode === KeyCodes.delete || keyCode === KeyCodes.x) {
            event.preventDefault();
            event.stopPropagation();

            self.actions.mark('delete selected entities');


            const removeActions = self.selection.map(function (entity) {

                return new EntityRemoveAction(entity);
            });

            self.actions.do(new SelectionClearAction());

            removeActions.forEach(function (a) {
                self.actions.do(a);
            });
        } else if (keyCode === KeyCodes.q) {
            let activeTool = self.toolEngine.active.getValue();
            if (activeTool === null || activeTool.name !== "marquee_selection") {
                //activate selection tool
                self.toolEngine.activate("marquee_selection");
            } else {
                //activate transform tool
                self.toolEngine.activate("spatial_transform");
            }
        } else if (keyCode === KeyCodes.g) {
            //activate camera tool
            self.toolEngine.activate("camera_control");
        } else if (keyCode === KeyCodes.d && event.shiftKey) {
            if (!self.selection.isEmpty()) {
                //copy
                copySelectedEntities(self);
                //paste
                pasteEntities(self);
            }
        } else if (keyCode === KeyCodes.w) {
            self.toolEngine.activate("spatial_transform");
            self.toolEngine.active.getValue().mode.set(TransformerMode.Translation);
        } else if (keyCode === KeyCodes.e) {
            self.toolEngine.activate("spatial_transform");
            self.toolEngine.active.getValue().mode.set(TransformerMode.Rotation);
        } else if (keyCode === KeyCodes.r) {
            self.toolEngine.activate("spatial_transform");
            self.toolEngine.active.getValue().mode.set(TransformerMode.Scale);
        }
    }

    function isViewFocused(view) {
        function checkChild(el) {
            if (el === view.el) {
                return true;
            } else {
                const children = el.children;
                const numChildren = children.length;
                for (let i = 0; i < numChildren; i++) {
                    const child = children[i];
                    if (checkChild(child)) {
                        return true;
                    }
                }
            }

            return false;
        }

        return checkChild(document.activeElement);
    }

    /**
     *
     * @param {KeyboardEvent} event
     */
    function handleKeyDownEvent(event) {
        //check that game view has focus
        if (!isViewFocused(self.engine.gameView)) {
            return;
        }

        const keyCode = event.keyCode;
        if (event.ctrlKey) {
            processCtrlCombinations(keyCode, event);
        } else {
            processSingleKey(keyCode, event)
        }

        const activeTool = self.toolEngine.active.getValue();

        if (activeTool !== null && activeTool !== undefined) {
            //pass event to active tool
            activeTool.handleKeyboardEvent(event);
        }
    }

    this.handlers = {
        keyDown: handleKeyDownEvent
    };
}

Editor.prototype.initialize = function () {
    this.toolEngine.initialize();
    this.processEngine.initialize(this);
    this.view = new EditorView(this);

    this.copyBuffer = [];

    this.disabledSystems = [];

    this.lastTool = "marquee_selection";


    this.cleanupTasks = [];
};

/**
 * Attempt to focus camera on the entity
 * @param {int} entity
 */
Editor.prototype.focusEntity = function (entity) {
    const em = this.engine.entityManager;

    const ecd = em.dataset;

    //try focus camera on it
    const transform = ecd.getComponent(entity, ecd.getComponentClassByName("Transform"));

    if (transform !== null) {

        const TopDownCameraController = ecd.getComponentClassByName("TopDownCameraController");
        const topDownCameraControllerSystem = em.getSystem(TopDownCameraControllerSystem);

        ecd.traverseComponents(TopDownCameraController, function (camera) {
            const target = camera.target;
            target.set(transform.position.x, target.y, transform.position.z);
        });

        let originalValue = topDownCameraControllerSystem.enabled.get();

        if (!originalValue) {
            topDownCameraControllerSystem.enabled.set(true);
        }

        topDownCameraControllerSystem.update(0);
        topDownCameraControllerSystem.enabled.set(originalValue);
    }
};

/**
 *
 * @param {Engine} engine
 */
Editor.prototype.attach = async function (engine) {
    assert.defined(engine, 'engine');
    assert.ok(engine.isEngine, 'engine.isEngine');


    this.engine = engine;

    //validate selection
    const missingEntities = this.selection.filter(function (entity) {
        return !engine.entityManager.dataset.entityExists(entity);
    });

    //drop missing entities from selection
    this.selection.removeAll(missingEntities);

    //find interaction system and disable it
    this.disableSystem(TopDownCameraControllerSystem);
    this.disableSystem(InputControllerSystem);

    //attach EditorEntity system
    await engine.entityManager.addSystem(this.editorEntitySystem);

    const dataset = engine.entityManager.dataset;

    const cameraEntity = this.cameraEntity;

    activateEditorCamera(this.cameraEntity, dataset, this);


    cameraEntity.build(dataset);

    this.toolEngine.startup(engine, this);
    this.toolEngine.activate(this.lastTool);

    this.processEngine.startup();

    try {
        this.selectionVistualizer.startup();
    } catch (e) {
        console.error("Failed to start selection visualizer:", e);
    }

    //attach view
    engine.viewStack.push(this.view, "Editor");

    //map keys
    window.addEventListener('keydown', this.handlers.keyDown);

};

/**
 *
 * @param {Class} systemClass
 */
Editor.prototype.disableSystem = function (systemClass) {

    const entityManager = this.engine.entityManager;

    const system = entityManager.getSystem(systemClass);

    if (system === null) {
        console.log('System not found', systemClass);
        return;
    }

    const originalState = system.enabled.get();

    system.enabled.set(false);
    this.disabledSystems.push({
        name: name,
        originalState: originalState,
        system: system
    });
};

Editor.prototype.restoreDisableSystem = function () {
    this.disabledSystems.forEach(function (disabledSystem) {
        disabledSystem.system.enabled.set(disabledSystem.originalState);
    });

    this.disabledSystems = [];
};

Editor.prototype.detach = function () {
    this.selectionVistualizer.shutdown();

    this.lastTool = this.toolEngine.active.get().name;

    this.toolEngine.shutdown();

    this.processEngine.shutdown();

    //pop own view from the engine's view stack restoring the original state
    this.engine.viewStack.pop();

    //unmap keys
    window.removeEventListener('keydown', this.handlers.keyDown);

    //enable interactions system if it was disabled
    this.restoreDisableSystem();

    //remove all editor entities
    const dataset = this.engine.entityManager.dataset;
    dataset.traverseComponents(EditorEntity, function (component, entity) {
        dataset.removeEntity(entity);
    });

    //remove system
    this.engine.entityManager.removeSystem(this.editorEntitySystem);


    this.cleanupTasks.forEach(t => t());

    this.cleanupTasks = [];
};

/**
 *
 * @param {int} entity
 * @returns {boolean}
 */
Editor.prototype.isEditorEntity = function (entity) {
    return isEditorOwnedEntity(entity, this.engine.entityManager.dataset);
};

/**
 *
 * @param {int} entity
 * @param {EntityComponentDataset} dataset
 */
function isEditorOwnedEntity(entity, dataset) {
    return dataset.getComponent(entity, EditorEntity) !== undefined;
}

export default Editor;
