/*
 * Copyright (c) 2014 Adobe Systems Incorporated. All rights reserved.
 *  
 * Permission is hereby granted, free of charge, to any person obtaining a
 * copy of this software and associated documentation files (the "Software"), 
 * to deal in the Software without restriction, including without limitation 
 * the rights to use, copy, modify, merge, publish, distribute, sublicense, 
 * and/or sell copies of the Software, and to permit persons to whom the 
 * Software is furnished to do so, subject to the following conditions:
 *  
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *  
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 
 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 
 * DEALINGS IN THE SOFTWARE.
 * 
 */

define(function (require, exports, module) {
    "use strict";

    var React = require("react"),
        ReactDOM = require("react-dom"),
        Fluxxor = require("fluxxor"),
        FluxMixin = Fluxxor.FluxMixin(React),
        StoreWatchMixin = Fluxxor.StoreWatchMixin,
        Immutable = require("immutable"),
        d3 = require("d3"),
        _ = require("lodash");

    var UI = require("adapter").ps.ui,
        OS = require("adapter").os;

    var system = require("js/util/system"),
        collection = require("js/util/collection"),
        mathUtil = require("js/util/math"),
        keyUtil = require("js/util/key"),
        uiUtil = require("js/util/ui");

    // Used for debouncing the overlay drawing
    var DEBOUNCE_DELAY = 200;

    var SuperselectOverlay = React.createClass({
        mixins: [FluxMixin, StoreWatchMixin("tool", "document", "application", "ui", "panel", "modifier")],

        /**
         * Keeps track of current mouse position so we can rerender the overlaid layers correctly
         * @type {Number}
         */
        _currentMouseX: null,

        /**
         * Keeps track of current mouse position so we can rerender the overlaid layers correctly
         * @type {Number}
         */
        _currentMouseY: null,

        /**
         * Keeps track of whether the mouse is down or up, used for marquee
         *
         * @type {boolean}
         */
        _currentMouseDown: null,

        /**
         * Keeps track of the marquee rectangle so it can be resized
         *
         * @type {SVGElement}
         */
        _marqueeRect: null,

        /**
         * Owner group for all the overlay svg elements
         *
         * @type {SVGElement}
         */
        _scrimGroup: null,

        /**
         * IDs of layers that are being highlighted by marquee select
         *
         * @type {Array.<number>}
         */
        _marqueeResult: null,

        /**
         * UI Scale for drawing strokes visible at all zoom levels
         *
         * @type {number}
         */
        _scale: null,

        /**
         * During certain drags, especially with Artboards, we get a lot of
         * UI updating events, document resizes etc. we debounce drawing functions
         * so our overlays don't play catch up
         *
         * @type {function}
         */
        _drawDebounced: null,

        getStateFromFlux: function () {
            var flux = this.getFlux(),
                applicationStore = flux.store("application"),
                toolStore = flux.store("tool"),
                panelStore = flux.store("panel"),
                panelState = panelStore.getState(),
                modalState = toolStore.getModalToolState(),
                currentDocument = applicationStore.getCurrentDocument(),
                modifierStore = flux.store("modifier"),
                modifiers = modifierStore.getState(),
                leafModifier = system.isMac ? modifiers.command : modifiers.control,
                vectorMaskMode = toolStore.getVectorMode();

            return {
                document: currentDocument,
                marqueeEnabled: panelState.marqueeEnabled,
                marqueeStart: panelState.marqueeStart,
                modalState: modalState,
                leafBounds: leafModifier,
                vectorMaskMode: vectorMaskMode,
                canvasBounds: panelStore.getCloakRect()
            };
        },

        shouldComponentUpdate: function (nextProps, nextState) {
            var getProps = function (state) {
                var document = state.document;
                if (!document || !document.layers) {
                    return null;
                }

                return collection.pluckAll(document.layers.selected, ["id", "bounds"]);
            };

            // We check for the cheaper flags before plucking layers from document models
            if (this.state.marqueeEnabled !== nextState.marqueeEnabled ||
                this.state.leafBounds !== nextState.leafBounds ||
                this.state.vectorMaskMode !== nextState.vectorMaskMode ||
                this.state.modalState !== nextState.modalState ||
                this.props.transformString !== nextProps.transformString) {
                return true;
            }

            var lastLayerProps = getProps(this.state),
                nextLayerProps = getProps(nextState);

            return !Immutable.is(lastLayerProps, nextLayerProps);
        },

        componentWillMount: function () {
            this._drawDebounced = _.debounce(this.drawOverlay, DEBOUNCE_DELAY);
        },

        componentWillUnmount: function () {
            this._drawDebounced.cancel();

            window.removeEventListener("mousemove", this.marqueeUpdater);
            window.removeEventListener("mouseup", this.mouseUpHandler);
            window.removeEventListener("mousedown", this.mouseDownHandler);
            
            OS.removeListener("externalMouseMove", this.mouseMoveHandler);
        },

        componentDidMount: function () {
            this._currentMouseX = null;
            this._currentMouseY = null;
            this._marqueeResult = null;

            this._drawDebounced();
            
            // Marquee mouse handlers
            window.addEventListener("mousemove", this.marqueeUpdater);
            window.addEventListener("mouseup", this.mouseUpHandler);
            window.addEventListener("mousedown", this.mouseDownHandler);
            OS.addListener("externalMouseMove", this.mouseMoveHandler);
        },

        componentDidUpdate: function (prevProps, prevState) {
            // Redraw immediately when the leaf modifier key changes
            // or when we're in a modal state
            if (this.state.leafBounds !== prevState.leafBounds ||
                this.state.modalState) {
                this.drawOverlay();
            } else {
                this._drawDebounced();
            }
        },

        /**
         * Attaches to mouse move events coming from Photoshop
         * so we can set highlights manually. We have to resort to this
         * because external mouse move events do not cause :hover states
         * in DOM elements
         *
         * @param {CustomEvent} event EXTERNAL_MOUSE_MOVE event coming from _spaces.OS
         */
        mouseMoveHandler: function (event) {
            this._currentMouseX = event.location[0];
            this._currentMouseY = event.location[1];
            if (this.state.marqueeEnabled) {
                this.updateMarqueeRect();
            } else {
                this.updateMouseOverHighlights();
            }
        },

        /**
         * Attaches to mouse move events on the window
         * to update the mouse position and the marquee
         *
         * @param {MouseEvent} event
         */
        marqueeUpdater: function (event) {
            this._currentMouseX = event.x;
            this._currentMouseY = event.y;
            if (this.state.marqueeEnabled) {
                this.updateMarqueeRect();
            }
        },

        /**
         * Calls all helper functions to draw super-select overlay
         * Cleans it first
         */
        drawOverlay: function () {
            var currentDocument = this.state.document,
                svg = d3.select(ReactDOM.findDOMNode(this));

            svg.selectAll(".superselect-bounds").remove();
            svg.selectAll(".superselect-marquee").remove();
            
            if (!currentDocument || this.state.modalState) {
                return null;
            }
            
            var currentLayer = currentDocument.layers.selected.first();
                
            if (currentLayer && currentLayer.vectorMaskEmpty && this.state.vectorMaskMode) {
                return null;
            }

            this._scrimGroup = svg.insert("g", ".transform-control-group")
                .classed("superselect-bounds", true)
                .attr("transform", this.props.transformString);

            // Reason we calculate the scale here is to make sure things like strokewidth / rotate area
            // are not scaled with the SVG transform of the overlay
            var transformObj = d3.transform(this._scrimGroup.attr("transform")),
                layerTree = currentDocument.layers;

            this._scale = 1 / transformObj.scale[0];
            
            this.drawBoundRectangles(svg, layerTree);

            if (this.state.marqueeEnabled) {
                this.startSuperselectMarquee(svg);
            }
        },

        /**
         * Draws either superselectable or leaf layer bounding boxes
         * 
         * @param {SVGElement} svg SVG HTML element to draw in
         * @param {LayerTree} layerTree layerTree of the current document
         */
        drawBoundRectangles: function (svg, layerTree) {
            var scale = this._scale,
                renderLayers;

            if (this.state.leafBounds) {
                renderLayers = layerTree.selectableLeaves;

                // We add artboards here, so they are shown selectable
                renderLayers = renderLayers.concat(layerTree.artboards);
            } else {
                renderLayers = layerTree.selectable.reverse();

                // If we have an artboard selected, we quietly insert it here,
                // so children layer that overlap the name badge don't highlight.
                if (layerTree.selected.size === 1 && layerTree.selected.first().isArtboard) {
                    renderLayers = renderLayers.unshift(layerTree.selected.first());
                }
            }

            renderLayers.forEach(function (layer) {
                var bounds = layerTree.childBounds(layer);
                    
                // Skip empty bounds
                if (!bounds || bounds.empty) {
                    return;
                }

                // HACK: For some reason Photoshop's bounds seem to be shifted by ~1px to the
                // bottom-right. See https://github.com/adobe-photoshop/spaces-design/issues/866
                var offset = system.isMac ? 0 : scale;

                if (layer.isArtboard) {
                    // We don't want to draw the artboard bounds if it's the selected artboard     
                    if (!layer.selected) {
                        this._scrimGroup
                            .append("rect")
                            .attr("x", bounds.left + offset)
                            .attr("y", bounds.top + offset)
                            .attr("width", bounds.width)
                            .attr("height", bounds.height)
                            .attr("layer-id", layer.id)
                            .attr("id", "layer-" + layer.id)
                            .classed("layer-bounds", false)
                            .classed("layer-artboard-bounds", true);
                    }

                    var nameBounds = uiUtil.getNameBadgeBounds(bounds, scale),
                        namePointCoords = [
                            { x: nameBounds.left, y: nameBounds.top },
                            { x: nameBounds.right, y: nameBounds.top },
                            { x: nameBounds.right, y: nameBounds.bottom },
                            { x: nameBounds.left, y: nameBounds.bottom }
                        ],
                        namePoints = namePointCoords.map(function (coord) {
                            return coord.x + "," + coord.y;
                        }).join(" ");
                        
                    this._scrimGroup.append("rect")
                        .attr("points", namePoints)
                        .attr("id", "name-badge-" + layer.id)
                        .attr("layer-id", layer.id)
                        .attr("x", nameBounds.left + offset)
                        .attr("y", nameBounds.top + offset)
                        .attr("width", nameBounds.width)
                        .attr("height", nameBounds.height)
                        .classed("artboard-name-rect", true)
                        .classed("layer-artboard-bounds", true);
                } else {
                    this._scrimGroup
                        .append("rect")
                        .attr("x", bounds.left + offset)
                        .attr("y", bounds.top + offset)
                        .attr("width", bounds.width)
                        .attr("height", bounds.height)
                        .attr("layer-id", layer.id)
                        .attr("layer-selected", layer.selected)
                        .attr("id", "layer-" + layer.id)
                        .classed("layer-bounds", true)
                        .classed("layer-artboard-bounds", false)
                        .classed("marqueeable", true);
                }
            }, this);

            this.updateMouseOverHighlights();
        },
        
        /**
         * Starts drawing the superselect marquee at the given location
         *
         * @param {SVGElement} svg
         */
        startSuperselectMarquee: function (svg) {
            // Because we wait for React to tell us to start marquee, sometimes we may reach here
            // after mouse up has happened, so we have to check for it ourselves, and not even bother
            // to start marquee tracking
            if (!this._currentMouseDown) {
                return;
            }

            var rectX = this.state.marqueeStart.x,
                rectY = this.state.marqueeStart.y,
                rectW = this._currentMouseX - rectX,
                rectH = this._currentMouseY - rectY,
                group = svg.insert("g", ".transform-control-group")
                    .classed("superselect-marquee", true);
            
            if (rectW < 0) {
                rectX = rectX + rectW;
                rectW = -rectW;
            }

            if (rectH < 0) {
                rectY = rectY + rectH;
                rectH = -rectH;
            }

            // We store this in a component variable so we can update it
            this._marqueeRect = group.append("rect")
                .attr("x", rectX)
                .attr("y", rectY)
                .attr("width", rectW)
                .attr("height", rectH)
                .classed("superselect-marquee", true);

            this._marqueeResult = [];

            this.updateMarqueeRect();
        },

        /**
         * Calls the action and removes the listener so it's a one off listener
         *
         * @param {MouseEvent} event
         */
        mouseUpHandler: function (event) {
            var runMarquee = !!this._marqueeRect;
            
            this.getFlux().actions.superselect.marqueeSelect(
                this.state.document, runMarquee,
                this._marqueeResult, event.shiftKey
            );
            
            this._currentMouseDown = false;
            this._marqueeRect = null;
            this._marqueeResult = null;
        },

        /**
         * Sets the flag so we can start marquee once the Flux action is complete
         * if the flag is still set
         */
        mouseDownHandler: function () {
            this._currentMouseDown = true;
        },

        /**
         * Checks to see if the given position is under
         * any always_propagate policies, where mouse events should go to PS
         * and we shouldn't highlight layers
         *
         * @param {number} x
         * @param {number} y
         *
         * @return {boolean} True if the pointer is under an always propagate policy
         */
        _positionUnderAlwaysPropagatePolicy: function (x, y) {
            var policyStore = this.getFlux().store("policy"),
                modifierStore = this.getFlux().store("modifier"),
                modifiers = modifierStore.getState(),
                modifierBits = keyUtil.modifiersToBits(modifiers),
                pointerPolicies = policyStore.getMasterPointerPolicyList(),
                underAlways = false;

            // As soon as we find a policy that intersects and matches modifiers, we can return
            pointerPolicies.some(function (policy) {
                if (!policy.area || policy.modifiers !== modifierBits) {
                    return false;
                }

                var area = {
                        left: policy.area[0],
                        top: policy.area[1],
                        right: policy.area[0] + policy.area[2],
                        bottom: policy.area[1] + policy.area[3]
                    },
                    intersects = area.left < x && area.right > x &&
                        area.top < y && area.bottom > y;

                if (intersects) {
                    if (policy.action === UI.policyAction.PROPAGATE_TO_PHOTOSHOP ||
                        policy.action === UI.policyAction.PROPAGATE_BY_ALPHA) {
                        underAlways = true;
                    } else {
                        underAlways = false;
                    }
                }

                return intersects;
            }, this);

            return underAlways;
        },

        /**
         * Goes through all layer bounds and highlights the top one the cursor is on
         */
        updateMouseOverHighlights: function () {
            if (!this.state.document) {
                return;
            }

            var canvasBounds = this.state.canvasBounds,
                mouseX = this._currentMouseX,
                mouseY = this._currentMouseY;

            if (!canvasBounds ||
                mouseX < canvasBounds.left || mouseX > canvasBounds.right ||
                mouseY < canvasBounds.top || mouseY > canvasBounds.bottom) {
                return;
            }

            var marquee = this.state.marqueeEnabled,
                layerTree = this.state.document.layers,
                scale = this._scale,
                uiStore = this.getFlux().store("ui"),
                canvasMouse = uiStore.transformWindowToCanvas(mouseX, mouseY),
                highlightFound = false,
                underPolicy = this._positionUnderAlwaysPropagatePolicy(mouseX, mouseY);

            // Yuck, we gotta traverse backwards, and D3 doesn't offer reverse iteration
            _.forEachRight(d3.selectAll(".artboard-name-rect")[0], function (element) {
                var layer = d3.select(element),
                    layerID = layer.attr("layer-id"),
                    layerLeft = mathUtil.parseNumber(layer.attr("x")),
                    layerTop = mathUtil.parseNumber(layer.attr("y")),
                    layerRight = layerLeft + mathUtil.parseNumber(layer.attr("width")),
                    layerBottom = layerTop + mathUtil.parseNumber(layer.attr("height")),
                    intersects = layerLeft < canvasMouse.x && layerRight > canvasMouse.x &&
                        layerTop < canvasMouse.y && layerBottom > canvasMouse.y;

                if (!underPolicy && !marquee && !highlightFound && intersects) {
                    d3.select("#layer-" + layerID)
                        .classed("layer-bounds-hover", true)
                        .style("stroke-width", 1.0 * scale);
                    highlightFound = true;
                } else {
                    d3.select("#layer-" + layerID)
                        .classed("layer-bounds-hover", false)
                        .style("stroke-width", 0.0);
                }
            });

            uiUtil.hitTestLayers(this.state.document.id, canvasMouse.x, canvasMouse.y)
                .bind(this)
                .then(function (hitLayerIDs) {
                    var selectableLayers = this.state.leafBounds ?
                            this.state.document.layers.selectableLeaves :
                            this.state.document.layers.selectable,
                        selectableLayerIDs = collection.pluck(selectableLayers, "id"),
                        considerIDs = collection.intersection(hitLayerIDs, selectableLayerIDs);

                    return considerIDs.findLast(function (id) {
                        var layer = this.state.document.layers.byID(id);

                        return layer && !layer.isArtboard;
                    }, this);
                })
                .then(function (topID) {
                    // Yuck, we gotta traverse the list backwards, and D3 doesn't offer reverse iteration
                    _.forEachRight(d3.selectAll(".layer-bounds")[0], function (element) {
                        var layer = d3.select(element),
                            layerID = mathUtil.parseNumber(layer.attr("layer-id")),
                            layerSelected = layer.attr("layer-selected") === "true",
                            layerModel = layerTree.byID(layerID);

                        // Sometimes, the DOM elements may be out of date, and be of different documents
                        if (!layerModel) {
                            return;
                        }
                        
                        if (!underPolicy && !marquee && !highlightFound && layerID === topID) {
                            if (!layerSelected) {
                                layer.classed("layer-bounds-hover", true)
                                    .style("stroke-width", 1.0 * scale);
                            }
                            highlightFound = true;
                        } else {
                            layer.classed("layer-bounds-hover", true)
                                .style("stroke-width", 0.0);
                        }
                    });
                });
        },

        /**
         * Updates the marquee rectangle by changing size/location
         * and highlighting the correct layers
         */
        updateMarqueeRect: function () {
            if (!this._marqueeRect) {
                return;
            }

            var uiStore = this.getFlux().store("ui"),
                highlightedIDs = [],
                scale = this._scale,
                left = this.state.marqueeStart.x,
                top = this.state.marqueeStart.y,
                right = this._currentMouseX,
                bottom = this._currentMouseY,
                temp;

            if (left > right) {
                temp = left;
                left = right;
                right = temp;
            }

            if (top > bottom) {
                temp = top;
                top = bottom;
                bottom = temp;
            }

            var start = uiStore.transformWindowToCanvas(left, top),
                end = uiStore.transformWindowToCanvas(right, bottom);
                
            this._marqueeRect
                .attr("x", left)
                .attr("y", top)
                .attr("width", right - left)
                .attr("height", bottom - top);
            
            d3.selectAll(".marqueeable").each(function () {
                var layer = d3.select(this),
                    layerLeft = mathUtil.parseNumber(layer.attr("x")),
                    layerTop = mathUtil.parseNumber(layer.attr("y")),
                    layerRight = layerLeft + mathUtil.parseNumber(layer.attr("width")),
                    layerBottom = layerTop + mathUtil.parseNumber(layer.attr("height")),
                    intersects = layerLeft < end.x && layerRight > start.x &&
                        layerTop < end.y && layerBottom > start.y;

                if (intersects) {
                    layer.classed("marquee-hover", true)
                        .style("stroke-width", 1.0 * scale);
                    highlightedIDs.push(mathUtil.parseNumber(layer.attr("layer-id")));
                } else {
                    layer.classed("marquee-hover", false)
                        .style("stroke-width", 0.0);
                }
            });

            this._marqueeResult = highlightedIDs;
        },

        render: function () {
            return (<g />);
        }
    });

    module.exports = SuperselectOverlay;
});
