/** @module editorElements.Wire */

import { PolyLine, PolyLinePoints, PolyLinePoint, Group } from '../svgObjects';
import Logic from '../Logic';
import stateClasses from './stateClasses';
import findPath from '../findPath';

import NetworkElement from './NetworkElement';

/**
 * Wire represents connection of two {@link Connector}s.
 * @extends NetworkElement
 */
export default class Wire extends NetworkElement {
    /**
     * @param {App} appInstance  instance of [App](./module-App.html)
     * @param {string}  fromId    id of the first connector this wire will be connected to
     * @param {string}  toId      id of the second connector this wire will be connected to
     * @param {Boolean} [refresh=true] if `true`, the [App](./module-App.html) will refresh after creating this wire
     */
    constructor(appInstance, fromId, toId, refresh = true, route = true) {
        super(appInstance);

        this.gridSize = appInstance.gridSize;

        this.connection = {
            from: {
                id: fromId,
                box: this.appInstance.getBoxByConnectorId(fromId),
                connector: this.appInstance.getConnectorById(fromId)
            },
            to: {
                id: toId,
                box: this.appInstance.getBoxByConnectorId(toId),
                connector: this.appInstance.getConnectorById(toId)
            }
        };

        if (this.connection.from.connector.isOutputConnector) {
            if (this.connection.to.connector.isInputConnector) {
                // desired state
            } else {
                // connecting two output connectors
                throw 'Can not place wire between two output connectors';
            }
        } else {
            if (this.connection.to.connector.isInputConnector) {
                // connecting two input connectors
                throw 'Can not place wire between two input connectors';
            } else {
                // swap them and we are ready to go
                [this.connection.from, this.connection.to] = [
                    this.connection.to,
                    this.connection.from
                ];
            }
        }

        if (route) {
            this.routeWire(true, refresh);
        } else {
            this.temporaryWire();
        }

        this.elementState = Logic.state.unknown;

        this.setState(this.connection.from.connector.state);

        if (refresh) {
            const { connector } = this.connection.to;
            this.appInstance.startNewSimulation(connector, connector.state);
        }

        this.svgObj.$el.addClass('wire');
    }

    get boxes() {
        return [this.connection.from.box, this.connection.to.box];
    }

    get connectors() {
        return [this.connection.from.connector, this.connection.to.connector];
    }

    /**
     * get data of this wire as a JSON-ready object
     * @return {Object} javascript object containing essential data for this wire
     */
    get exportData() {
        return {
            fromId: this.connection.from.id,
            toId: this.connection.to.id
        };
    }

    /**
     * set the state of this wire to match the state of the input connector it is connected to
     * @param {Logic.state} state [description]
     */
    setState(state) {
        this.svgObj.removeClasses(...stateClasses);
        this.svgObj.addClass(stateClasses[state]);

        this.connection.to.connector.setState(state);

        this.elementState = state;
    }

    /**
     * get the current [Logic.state](./modules-Logic.html#.state) of this wire
     * @return {Logic.state}
     */
    get state() {
        return this.elementState;
    }

    /**
     * update the state of this wire
     */
    updateWireState() {
        for (const box of this.boxes) {
            box.refreshState();
        }
    }

    /**
     * get the jQuery element for this wire
     * @return {jQuery.element}
     */
    get() {
        return this.svgObj.get();
    }

    /**
     * get the PolyLine points for a temporary wire placement connecting the two connectors
     * @return {PolyLinePoints} new instance of {@link PolyLinePoints}
     */
    getTemporaryWirePoints() {
        let points = new PolyLinePoints();
        points.append(new PolyLinePoint(this.wireStart.x, this.wireStart.y));
        points.append(new PolyLinePoint(this.wireEnd.x, this.wireEnd.y));
        return points;
    }

    /**
     * route the wire using the temporary wire points
     */
    temporaryWire() {
        this.wireStart = this.appInstance.getConnectorPosition(
            this.connection.from.connector,
            false
        );
        this.wireEnd = this.appInstance.getConnectorPosition(this.connection.to.connector, false);

        this.setWirePath(this.getTemporaryWirePoints());
    }

    /**
     * route the wire using the modified A* wire routing algorithm
     */
    routeWire(snapToGrid = true, refresh = true) {
        this.wireStart = this.appInstance.getConnectorPosition(
            this.connection.from.connector,
            snapToGrid
        );
        this.wireEnd = this.appInstance.getConnectorPosition(
            this.connection.to.connector,
            snapToGrid
        );

        this.points = this.findRoute(
            {
                x: this.wireStart.x / this.gridSize,
                y: this.wireStart.y / this.gridSize
            },
            {
                x: this.wireEnd.x / this.gridSize,
                y: this.wireEnd.y / this.gridSize
            }
        );

        this.setWirePath(this.points);

        if (refresh) this.updateWireState();

        // regenerate inconvenient nodes
        this.generateInconvenientNodes();
    }

    /**
     * set the wire to follow the specified points
     * @param {PolyLinePoints} points instance of {@link PolyLinePoints}
     */
    setWirePath(points) {
        // set the line
        if (this.svgObj !== undefined) {
            // this.svgObj.updatePoints(points);
            for (let child of this.svgObj.children) {
                child.updatePoints(points);
            }
        } else {
            this.svgObj = new Group();

            let hitbox = new PolyLine(points, 10, 'white');
            hitbox.addClass('hitbox');
            hitbox.addAttr({ opacity: 0 });
            this.svgObj.addChild(hitbox);

            let mainLine = new PolyLine(points, 2);
            mainLine.addClass('main', 'stateUnknown');
            this.svgObj.addChild(mainLine);
        }
    }

    pathToPolyLine(path) {
        let totalPath = new PolyLinePoints();
        for (const point of path) {
            totalPath.append(new PolyLinePoint(point.x * this.gridSize, point.y * this.gridSize));
        }
        return totalPath;
    }

    /**
     * find a nice route for the wire
     * @param  {Object} start object containing numeric attributes `x` and `y` that represent the first endpoint of the wire in grid pixel
     * @param  {Object} end   object containing numeric attributes `x` and `y` that represent the second endpoint of the wire in grid pixels
     * @return {PolyLinePoints}       [description]
     */
    findRoute(start, end) {
        let nonRoutable = this.appInstance.getNonRoutableNodes();

        let punishedButRoutable;
        if (this.svgObj === undefined) {
            punishedButRoutable = this.appInstance.getInconvenientNodes();
        } else {
            punishedButRoutable = this.appInstance.getInconvenientNodes(this.svgObj.id);
        }

        let path = findPath(start, end, nonRoutable, punishedButRoutable, this.gridSize);

        if (path) {
            return this.pathToPolyLine(path);
        }

        // if a path was not found, try again but don't take into account the punished and non routable node
        path = findPath(start, end, new Set(), new Set(), this.gridSize);

        if (path) {
            return this.pathToPolyLine(path);
        }

        // if the path was still not found, give up and return temporary points
        return this.getTemporaryWirePoints();
    }

    /**
     * generate a set of nodes, that are inconvenient for wiring, but can be used, just are not preferred
     * @return {Set} set of nodes (objects containing x and y coordinates) that are not preferred for wiring
     */
    generateInconvenientNodes() {
        this.inconvenientNodes = new Set();

        let prevPoint;

        this.points.forEach(point => {
            const x = this.appInstance.SVGToGrid(point.x),
                y = this.appInstance.SVGToGrid(point.y);

            if (prevPoint === undefined) {
                // if the prevPoint is undefined, add the first point
                this.inconvenientNodes.add({ x, y });
            } else {
                // else add all the point between the prevPoint (excluded) and point (included)

                if (prevPoint.x === x) {
                    // if the line is horizontal
                    let from = Math.min(prevPoint.y, y);
                    let to = Math.max(prevPoint.y, y);

                    while (from <= to) {
                        this.inconvenientNodes.add({ x: x, y: from });
                        from++;
                    }
                } else if (prevPoint.y === y) {
                    // if the line is vertical
                    let from = Math.min(prevPoint.x, x);
                    let to = Math.max(prevPoint.x, x);

                    while (from <= to) {
                        this.inconvenientNodes.add({ x: from, y: y });
                        from++;
                    }
                } else {
                    // line is neither horizontal nor vertical, throw an error for better future debugging
                    // console.error("getInconvenientNodes: line between two points is neither horizontal nor vertical");
                }
            }

            // set new prevPoint
            prevPoint = { x, y };
        });
    }
}
