import { Frustum as ThreeFrustum } from 'three';
import AABB2 from '../../src/core/geom/2d/aabb/AABB2.js';
import Vector2 from '../../src/core/geom/Vector2.js';
import { makeScreenScissorFrustum } from "../../src/engine/graphics/camera/makeScreenScissorFrustum.js";
import { Camera } from '../../src/engine/graphics/ecs/camera/Camera.js';
import { ShadedGeometrySystem } from "../../src/engine/graphics/ecs/mesh-v2/ShadedGeometrySystem.js";
import { make_ray_from_viewport_position } from "../../src/engine/graphics/make_ray_from_viewport_position.js";
import View from '../../src/view/View.js';

import SelectionAddAction from '../actions/concrete/SelectionAddAction.js';
import SelectionClearAction from '../actions/concrete/SelectionClearAction.js';
import EditorEntity from "../ecs/EditorEntity.js";
import Tool from './engine/Tool.js';

class SelectionView extends View {
    constructor() {
        super();
        this.el = document.createElement('div');
        const style = this.el.style;

        style.left = "0";
        style.top = "0";
        style.borderWidth = "1px";
        style.borderStyle = "solid";
        style.borderColor = "#02ff00";
        style.background = "rgba(2,256,0,0.1)";
        style.pointerEvents = "none";
        style.zIndex = 1000;
        style.position = "absolute";
    }
}


class SelectionTool extends Tool {
    constructor() {
        super();
        this.name = "marquee_selection";
        this.settings = {};
        this.anchorPoint = new Vector2();
        this.targetPoint = new Vector2();
        this.box = new AABB2();

        this.selectionMarker = new SelectionView();
    }

    readPosition(target) {
        const pointerPosition = this.engine.devices.pointer.position;

        target.copy(pointerPosition).sub(this.engine.gameView.position);
    }

    start() {
        //read mouse position
        //set anchor point
        this.readPosition(this.anchorPoint);
        //run update loop once
        this.update();
        //add view of selection
        document.body.appendChild(this.selectionMarker.el);
    }

    update() {
        this.readPosition(this.targetPoint);
        //update selection marker

        const x0 = Math.min(this.targetPoint.x, this.anchorPoint.x);
        const x1 = Math.max(this.targetPoint.x, this.anchorPoint.x);
        const y0 = Math.min(this.targetPoint.y, this.anchorPoint.y);
        const y1 = Math.max(this.targetPoint.y, this.anchorPoint.y);

        this.box.set(x0, y0, x1, y1);

        this.selectionMarker.position.set(x0, y0).add(this.engine.gameView.position);
        this.selectionMarker.size.set(x1 - x0, y1 - y0);
    }

    stop() {
        const self = this;
        //finish selection

        //remove selection view
        if (this.selectionMarker.el.parentNode === document.body) {
            document.body.removeChild(this.selectionMarker.el);
        }

        /**
         *
         * @type {Engine|null}
         */
        const engine = this.engine;

        //convert selection box to a frustum based on camera view

        let camera = null;
        const em = engine.entityManager;

        /**
         *
         * @param {Camera} c
         * @returns {boolean}
         */
        function visitCamera(c) {
            if (!c.active) {
                //skip inactive camera
                return true;
            }

            camera = c.object;
            //TODO check assumption that first camera is the right one
            return false;
        }

        em.dataset.traverseComponents(Camera, visitCamera);

        if (camera === null) {
            console.error("Couldn't find a camera to project selection box from");
        }

        let selection;

        const editor = this.editor;

        const box = this.box;
        //if box size is 0, it leads to all planes being converged on one point, to correct this we need to ensure that box dimensions are greater than zero
        const dX = Math.abs(box.x0 - box.x1);
        const dY = Math.abs(box.y0 - box.y1);

        if (dX < SELECTION_MIN_SIZE && dY < SELECTION_MIN_SIZE) {

            selection = pickingEntitySelection(new Vector2(box.x0, box.y0), editor.engine, camera);

        } else {

            if (dX < SELECTION_MIN_SIZE) {
                box.x0 -= (SELECTION_MIN_SIZE - dX) / 2;
                box.x1 += (SELECTION_MIN_SIZE - dX) / 2;
            }
            if (dY < SELECTION_MIN_SIZE) {
                box.y0 -= (SELECTION_MIN_SIZE - dY) / 2;
                box.y1 += (SELECTION_MIN_SIZE - dY) / 2;
            }


            selection = marqueeSelection(box, editor, camera);
        }
        console.log("Selected entities", selection);

        const actions = editor.actions;

        actions.mark();
        if (!self.modifiers.shift) {
            actions.do(new SelectionClearAction());
        }
        actions.do(new SelectionAddAction(selection));

    }
}


const SELECTION_MIN_SIZE = 0.01;

/**
 *
 * @param {int} entity
 * @param {EntityComponentDataset} dataset
 * @returns {int}
 */
function dereferenceEntity(entity, dataset) {
    if (dataset.componentTypeMap.indexOf(EditorEntity) === -1) {
        //EditorEntity is not registered
        return entity;
    }

    const editorEntity = dataset.getComponent(entity, EditorEntity);

    if (editorEntity === undefined) {
        return entity;
    }

    if (editorEntity.referenceEntity !== -1) {
        //reference found, deference
        return editorEntity.referenceEntity;
    } else {
        //no reference, just ignore this one
        return -1;
    }
}

/**
 *
 * @param {LeafNode|BinaryNode} node
 * @returns {number|undefined}
 */
function findEntityOfNode(node) {
    const entity = node.entity;
    if (typeof entity === "number") {
        return entity;
    } else {
        const parentNode = node.parentNode;
        if (parentNode !== null) {
            return findEntityOfNode(parentNode);
        }
    }
}


/**
 *
 * @param {Vector2} point
 * @param {Engine} engine
 * @param {THREE.Camera} camera
 * @returns {number[]} entities
 */
export function pickingEntitySelection(point, engine, camera) {

    const em = engine.entityManager;
    const dataset = em.dataset;

    // push source by a small amount along the way to prevent selection of elements directly on the NEAR plane
    const ray_offset = 0.0001;

    const ray = make_ray_from_viewport_position(engine,point);

    const ray_origin = ray.origin;
    const ray_direction = ray.direction;

    ray_origin._add(
        ray_direction.x * ray_offset,
        ray_direction.y * ray_offset,
        ray_direction.z * ray_offset,
    )

    let bestCandidate = null;
    let bestDistance = Infinity;


    /**
     *
     * @param {number} entity
     * @param {Vector3} contact
     */
    function tryAddEntity(entity, contact) {
        const distance = ray.origin.distanceSqrTo(contact);

        if (distance < bestDistance) {
            bestDistance = distance;

            entity = dereferenceEntity(entity, dataset);

            if (entity === -1) {
                return;
            }

            bestCandidate = entity;
        }
    }

    /**
     *
     * @type {ShadedGeometrySystem|undefined}
     */
    const sgm = em.getSystem(ShadedGeometrySystem);

    if (sgm !== undefined && sgm !== null) {
        const hits = sgm.raycast(
            ray_origin.x, ray_origin.y, ray_origin.y,
            ray_direction.x, ray_direction.y, ray_direction.z
        );

        for (let i = 0; i < hits.length; i++) {
            const hit = hits[i];

            tryAddEntity(hit.entity, hit.contact.position);
        }
    }

    return bestCandidate !== null ? [bestCandidate] : [];
}

/**
 *
 * @param {AABB2} box in screen-space, in pixels
 * @param {Editor} editor
 * @param {THREE.Camera} camera
 */
function marqueeSelection(box, editor, camera) {
    const engine = editor.engine;

    const dataset = engine.entityManager.dataset;

    const normalizedBox = box.clone();
    /**
     * @type {Vector2}
     */
    const viewportSize = engine.graphics.viewport.size;
    /*
     need to convert pixel size into normalized viewport coordinates, they range from -1 to 1 and have inverse Y axis compared to html
     */
    normalizedBox.x0 = (box.x0 / viewportSize.x) * 2 - 1;
    normalizedBox.x1 = (box.x1 / viewportSize.x) * 2 - 1;
    normalizedBox.y0 = -(box.y0 / viewportSize.y) * 2 + 1;
    normalizedBox.y1 = -(box.y1 / viewportSize.y) * 2 + 1;

    const frustum = new ThreeFrustum();

    makeScreenScissorFrustum(frustum, normalizedBox, camera);

    const selection = [];

    return selection;
}

export default SelectionTool;
