    import { Group, Rectangle, SvgImage } from '../svgObjects';

    import NetworkElement from './NetworkElement';
    import InputConnector from './InputConnector';
    import OutputConnector from './OutputConnector';
    import Transform from './Transform';

    /** @module editorElements.Box */

    /**
     * Parent class for gates and input and output boxes. Defines all the factors
     * that the boxes have in common (svgObj structure, draggability and rotatability...)
     * @extends NetworkElement
     */
    export default class Box extends NetworkElement {
        /**
         * @param {App} appInstance  instance of [App](./module-App.html)
         * @param {string} name       name of the element (input, output, and, or, xor...)
         * @param {string} category   type of the element (io, gate)
         * @param {number} gridWidth  width of the element in grid pixels
         * @param {number} gridHeight height of the element in grid pixels
         */
        constructor(appInstance, name, category, gridWidth, gridHeight) {
            super(appInstance);

            /**
             * specifies the box type within the category (input/output in io, and/or/... in gate)
             * @type {string}
             */
            this.name = name;

            /**
             * specifies the box category (io for input or output, gate for logic gates)
             * @type {string}
             */
            this.category = category;

            /**
             * size of the grid in SVG pixels
             * @type {number}
             */
            this.gridSize = this.appInstance.gridSize;

            /**
             * array of connectors of this box
             * @type {Array}
             */
            this.connectors = [];

            /**
             * svgObj containing all SVG data used to display this box
             * @type {svgObj}
             */
            this.svgObj = new Group();

            /**
             * width of this element in SVG pixels
             * @type {number}
             */
            this.width = gridWidth * this.gridSize;
            /**
             * height of this element in SVG pixels
             * @type {number}
             */
            this.height = gridHeight * this.gridSize;

            /**
             * width of this element in grid pixels
             * @type {number}
             */
            this.gridWidth = gridWidth;
            /**
             * height of this element in grid pixels
             * @type {number}
             */
            this.gridHeight = gridHeight;

            // transparent background rectangle
            let rectangle = new Rectangle(0, 0, this.width, this.height, 'none', 'none');
            rectangle.$el.addClass('rect');

            this.svgObj.addChild(rectangle);

            // image of the element
            this.image = new SvgImage(0, 0, this.width, this.height, this.url);
            this.svgObj.addChild(this.image);

            // add type="gate", used in special callbacks in contextmenu
            this.svgObj.addAttr({ type: category });

            this.svgObj.$el.addClass('box');
            this.svgObj.$el.addClass(category);
        }

        /**
         * url of the image depicting this object
         * @type {string}
         */
        get url() {
            const category = this.category || '',
                name = this.name || '',
                suffix = this.imgSuffix || '';

            return `img/svg/${category}/${name}${suffix}.svg`;
        }

        /**
         * get all input connectors of this box
         * @return {Array} array of input connectors
         */
        get inputConnectors() {
            return this.connectors.filter(conn => conn.isInputConnector);
        }

        /**
         * get all output connectors of this box
         * @return {Array} array of output connectors
         */
        get outputConnectors() {
            return this.connectors.filter(conn => conn.isOutputConnector);
        }

        /**
         * get data of this box as a JSON-ready object
         * @return {Object} javascript object containing essential data for this box
         */
        get exportData() {
            let connections = [];

            // go through all connectors
            let counter = 0;
            for (const conn of this.connectors) {
                // go through each its wire id
                for (const item of conn.wireIds) {
                    let thisWireId;
                    if (!this.appInstance.exportWireIdMap.has(item)) {
                        // if the wire id is not in the map, add it and assign new arbitrary id
                        this.appInstance.exportWireIdMap.set(item, this.appInstance.exportWireId);
                        thisWireId = this.appInstance.exportWireId;
                        this.appInstance.exportWireId++;
                    } else {
                        // else get id from the map
                        thisWireId = this.appInstance.exportWireIdMap.get(item);
                    }

                    // add this connection to the list
                    connections[connections.length] = {
                        index: counter,
                        type: conn.type,
                        wireId: thisWireId
                    };
                }
                counter++;
            }

            return {
                name: this.name,
                category: this.category,
                transform: this.getTransform(true),
                connections: connections
            };
        }

        /**
         * get set of nodes that are not suitable for wire routing
         * @param  {Number} [marginTop=0]    top margin of the element (distance from the element that should be also blocked)
         * @param  {Number} [marginRight=0]  right margin of the element
         * @param  {Number} [marginBottom=0] bottom margin of the element
         * @param  {Number} [marginLeft=0]   left margin of the element
         * @param  {Number} specialNodes     additional nodes that should be added to the set
         * @return {Set}                     set of not suitable nodes
         */
        generateBlockNodes(
            marginTop = 0,
            marginRight = 0,
            marginBottom = 0,
            marginLeft = 0,
            ...specialNodes
        ) {
            this.blockedNodes = new Set();
            for (let x = marginLeft; x <= this.gridWidth - marginRight; x++) {
                for (let y = marginTop; y <= this.gridHeight - marginBottom; y++) {
                    this.blockedNodes.add({
                        x: x,
                        y: y
                    });
                }
            }

            for (let node of specialNodes) {
                this.blockedNodes.add(node);
            }
        }

        /**
         * empty function, redefined in inherited elements
         * refreshState takes input connector values and sets output values accordingly
         */
        refreshState() {
            console.warn('Calling the virtual function refreshState has no effect.');
        }

        /**
         * change image to another one that ends with a specified suffix
         *
         * *usage:* `changeImage("abc")` changes image url to `image-abc.svg`,
         * `changeImage()` changes image url to the default one (`image.svg`)
         * @param  {string} [suffix] new suffix for the image
         */
        changeImage(suffix) {
            if (suffix === undefined || suffix === '') {
                this.imgSuffix = '';
            } else {
                this.imgSuffix = '-' + suffix;
            }

            this.image.changeUrl(this.url);
        }

        /**
         * get a jQuery element representing this box
         * @return {jQuery.element}
         */
        get() {
            return this.svgObj.get();
        }

        /**
         * rotate the set of blocked nodes by 90 degrees to the right or to the left, depending on the parameter
         *
         * used to rotate the nodes when the object itself is rotated
         * @param  {boolean} right rotate clockwise if true, counterclockwise if false
         */
        rotateBlockedNodes(center, right) {
            if (this.rotationParity === undefined) {
                this.rotationParity = false;
            }

            this.rotationParity = !this.rotationParity;

            let newBlockedNodes = new Set();

            // rotate the node
            console.log('center:', center);

            for (const node of this.blockedNodes) {
                let newNode;

                const parityFactor = this.rotationParity ? 1 : -1;

                if (right) {
                    newNode = {
                        x: -node.y + this.gridHeight + (center.x - center.y) * parityFactor,
                        y: node.x + (center.y - center.x) * parityFactor
                    };
                } else {
                    newNode = {
                        x: node.y + (center.x - center.y) * parityFactor
                    };

                    if (this.rotationParity) {
                        newNode.y =
                            -node.x +
                            this.gridWidth +
                            (this.gridHeight - center.y - (this.gridWidth - center.x));
                    } else {
                        newNode.y = -node.x + this.gridHeight + (center.y - center.x);
                    }
                }

                newBlockedNodes.add(newNode);
            }

            this.blockedNodes = newBlockedNodes;
        }

        /**
         * rotate the set of blocked nodes to the right
         *
         * used to rotate the nodes when the object itself is rotated
         */
        rotateBlockedNodesRight(center) {
            this.rotateBlockedNodes(center, true);
        }

        /**
         * rotate the set of blocked nodes to the right
         *
         * used to rotate the nodes when the object itself is rotated
         */
        rotateBlockedNodesLeft(center) {
            this.rotateBlockedNodes(center, false);
        }

        rotate(clockWise) {
            // get the transform value for this box and convert it to grid pixels
            // (so we don't have to convert between SVG and grid pixels manually)
            let transform = this.getTransform();
            transform.toGridPixels(this.appInstance);

            // calculate the center of the box
            const realCenter = {
                x: Math.round(this.gridWidth / 2),
                y: Math.round(this.gridHeight / 2)
            };

            // swap the coordinates when the rotation parity is 1
            const center = this.rotationParity
                ? {
                      x: realCenter.y,
                      y: realCenter.x
                  }
                : realCenter;

            // apply the rotation to the transform object
            if (clockWise) {
                transform.rotateRight(center.x, center.y);
            } else {
                transform.rotateLeft(center.x, center.y);
            }

            // rotate the blocked nodes as well
            if (clockWise) {
                this.rotateBlockedNodesRight(center);
            } else {
                this.rotateBlockedNodesLeft(center);
            }

            // convert the modified transform back to SVG pixels
            // and apply it to the svgObj
            transform.toSVGPixels(this.appInstance);
            this.svgObj.addAttr({ transform: transform.get() });

            // update the wires
            this.updateWires();

            // if tutorial exists, call the tutorial callback
            if (this.appInstance.tutorial) {
                this.appInstance.tutorial.onBoxRotated();
            }
        }

        /**
         * add a connector to the element on the specified position
         * @param {number}  left             horizontal distance from the left edge of the element
         * @param {number}  top              vertical distance from the top edge of the element
         * @param {Boolean} isInputConnector whether or not should this connector an input connector (`true` for input connector, `false` for output connector)
         */
        addConnector(left, top, isInputConnector) {
            let index = this.connectors.length;
            if (isInputConnector) {
                this.connectors[index] = new InputConnector(this.appInstance, left, top);
            } else {
                this.connectors[index] = new OutputConnector(this.appInstance, left, top);
            }
            this.svgObj.addChild(this.connectors[index].get());
        }

        /**
         * add an input connector to the element on the specified position
         * @param {number} left horizontal distance from the left edge of the element
         * @param {number} top  vertical distance from the top edge of the element
         */
        addInputConnector(left, top) {
            return this.addConnector(left, top, true);
        }

        /**
         * add an output connector to the element on the specified position
         * @param {number} left horizontal distance from the left edge of the element
         * @param {number} top  vertical distance from the top edge of the element
         */
        addOutputConnector(left, top) {
            return this.addConnector(left, top, false);
        }

        /**
         * get the connector object based on its id
         * @param  {string} connectorId ID of the {@link Connector}
         * @return {Connector}             instance of the {@link Connector} or `undefined` if not found
         */
        getConnectorById(connectorId) {
            for (let i = 0; i < this.connectors.length; i++) {
                if (this.connectors[i].id === connectorId) {
                    return this.connectors[i];
                }
            }
            // if connector not found, return undefined
            return undefined;
        }

        /**
         * get the instance of {@link Transform} representing the state of the transform attribute of this element
         * @param  {Boolean} [gridPixels=false] if `true`, function will return the result in grid pixels instead of SVG pixels
         * @return {Transform}                  {@link Transform} of the element
         */
        getTransform(gridPixels = false) {
            let transform;
            if (!this.svgObj.$el.attr('transform')) {
                // the element does not have a "transform" property --> create it
                transform = new Transform();
                transform.setTranslate(0, 0);
                this.svgObj.addAttr({ transform: transform.get() });
            } else {
                // the element does have a "transform" property --> change it
                transform = new Transform(this.svgObj.$el.attr('transform'));
            }

            // convert values to grid pixels
            if (gridPixels) {
                transform.toGridPixels(this.appInstance);
            }

            return transform;
        }

        /**
         * get the instance of {@link Transform} representing the state of the transform attribute of this element _with lenght units in grid pixels_
         * @return {Transform} {@link Transform} of the element
         */
        getGridPixelTransform() {
            return this.getTransform(true);
        }

        /**
         * set the transform attribute of this element
         * @param {Transform} transform {@link Transform} of the element (with lengths specified in SVG pixels)
         */
        setTransform(transform) {
            this.svgObj.addAttr({ transform: transform.get() });
        }

        /**
         * function that is called on every mouse down on this element
         *
         * moves the element to the front and calls onMouseDownLeft if applicable
         * @param  {jQuery.MouseEvent} event
         */
        onMouseDown(event) {
            this.mouseLeft = false;
            if (event.which === 1) {
                this.mouseLeft = true;
                this.onMouseDownLeft(event);

                // move the DOM element to front
                this.appInstance.moveToFrontById(this.svgObj.id);
            }
        }

        /**
         * function that is called on every left mouse down on this element
         *
         * prepares element for the "click" and "drag and drop" actions
         * @param  {jQuery.MouseEvent} event
         */
        onMouseDownLeft(event) {
            this.mouseMoved = false;

            let transform = this.getTransform();

            // save the current item position into a variable
            let currentPosition = transform.getTranslate();

            let { pageX, pageY } = this.appInstance.viewbox.transformEvent(event);

            // calculate mouse offset from the object origin
            this.offset = {
                x: pageX - currentPosition.x,
                y: pageY - currentPosition.y
            };
        }

        /**
         * function that is called on every left mouse move with this element
         * applies the correct transform values to provide the "drag and drop" functionality
         * @param  {jQuery.MouseEvent} event
         */
        onMouseMove(event) {
            if (this.mouseLeft) {
                this.svgObj.$el.addClass('grabbed');

                this.mouseMoved = true;

                let { pageX, pageY } = this.appInstance.viewbox.transformEvent(event);

                const left = pageX - this.offset.x;
                const top = pageY - this.offset.y;

                let transform = this.getTransform();
                transform.setTranslate(left, top);

                this.setTransform(transform);

                this.updateWires(true);
            }
        }

        /**
         * function that is called on every mouse up on this element
         * provides the "click" functionality and calls the onDrop handler for the "drag and drop" functionality
         * @param  {jQuery.MouseEvent} event
         */
        onMouseUp(event) {
            if (event.which === 1) {
                if (this.mouseMoved) {
                    this.onDrop(event);
                } else {
                    this.onClick();
                }
            } else if (event.which === 2) {
                this.onClickMiddle(event);
            }

            this.svgObj.$el.removeClass('grabbed');
        }

        /**
         * called by onMouseUp when the mouse has been moved between onMouseDown and onMouseUp
         *
         * applies grid snapping of the element on the end of the "drag and drop" action
         * @param  {jQuery.MouseEvent} event
         */
        onDrop(event) {
            let { pageX, pageY } = this.appInstance.viewbox.transformEvent(event);

            let left = pageX - this.offset.x;
            let top = pageY - this.offset.y;

            left = this.appInstance.snapToGrid(left);
            top = this.appInstance.snapToGrid(top);

            let transform = this.getTransform();
            transform.setTranslate(left, top);

            this.setTransform(transform);

            this.updateWires();

            // if tutorial exists, call tutorial callback
            if (this.appInstance.tutorial) {
                this.appInstance.tutorial.onBoxMoved();
            }
        }

        /**
         * empty function, will be redefined in InputBox
         */
        onClick() {}

        /**
         * custom callback function for middle click that rotates the box by 90 degrees to the right
         */
        onClickMiddle(event) {
            if (event.ctrlKey) {
                this.rotate(false);
            } else {
                this.rotate(true);
            }
        }

        /**
         * Updates all wires connected to this box. Iterates over all wires that are connected to this box
         * and calls routeWire (or temporaryWire if the `temporary` parameter is set to true) to update the wire routing
         * @param  {Boolean} [temporary=false] [description]
         */
        updateWires(temporary = false) {
            this.connectors.forEach(conn => {
                conn.wireIds.forEach(wireId => {
                    let wire = this.appInstance.getWireById(wireId);
                    if (temporary) {
                        wire.temporaryWire();
                    } else {
                        wire.routeWire();
                    }
                });
            });
        }
    }