/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

/* global define, module, require, exports */

(function (root, factory) {
    if (typeof define === 'function' && define.amd) {
        define(['jquery',
                'd3',
                'nf.Storage',
                'nf.Connection',
                'nf.Birdseye',
                'nf.CanvasUtils',
                'nf.Common',
                'nf.Dialog',
                'nf.Client',
                'nf.ErrorHandler'],
            function ($, d3, nfStorage, nfConnection, nfBirdseye, nfCanvasUtils, nfCommon, nfDialog, nfClient, nfErrorHandler) {
                return (nf.Draggable = factory($, d3, nfStorage, nfConnection, nfBirdseye, nfCanvasUtils, nfCommon, nfDialog, nfClient, nfErrorHandler));
            });
    } else if (typeof exports === 'object' && typeof module === 'object') {
        module.exports = (nf.Draggable =
            factory(require('jquery'),
                require('d3'),
                require('nf.Storage'),
                require('nf.Connection'),
                require('nf.Birdseye'),
                require('nf.CanvasUtils'),
                require('nf.Common'),
                require('nf.Dialog'),
                require('nf.Client'),
                require('nf.ErrorHandler')));
    } else {
        nf.Draggable = factory(root.$,
            root.d3,
            root.nf.Storage,
            root.nf.Connection,
            root.nf.Birdseye,
            root.nf.CanvasUtils,
            root.nf.Common,
            root.nf.Dialog,
            root.nf.Client,
            root.nf.ErrorHandler);
    }
}(this, function ($, d3, nfStorage, nfConnection, nfBirdseye, nfCanvasUtils, nfCommon, nfDialog, nfClient, nfErrorHandler) {
    'use strict';

    var nfCanvas;
    var drag;
    var snapAlignmentPixels = 8;
    var snapEnabled = true;

    /**
     * Updates the positioning of all selected components.
     *
     * @param {selection} dragSelection The current drag selection
     */
    var updateComponentsPosition = function (dragSelection) {
        var updates = new Map();

        // determine the drag delta
        var dragData = dragSelection.datum();
        var delta = {
            x: dragData.x - dragData.original.x,
            y: dragData.y - dragData.original.y
        };

        // if the component didn't move, return
        if (delta.x === 0 && delta.y === 0) {
            return;
        }

        var selectedConnections = d3.selectAll('g.connection.selected');
        var selectedComponents = d3.selectAll('g.component.selected');

        // ensure every component is writable
        if (nfCanvasUtils.canModify(selectedConnections) === false || nfCanvasUtils.canModify(selectedComponents) === false) {
            nfDialog.showOkDialog({
                headerText: 'Component Position',
                dialogContent: 'Must be authorized to modify every component selected.'
            });
            return;
        }

        // go through each selected connection
        selectedConnections.each(function (d) {
            var connectionUpdate = nfDraggable.updateConnectionPosition(d, delta);
            if (connectionUpdate !== null) {
                updates.set(d.id, connectionUpdate);
            }
        });

        // go through each selected component
        selectedComponents.each(function (d) {
            // consider any self looping connections
            var connections = nfConnection.getComponentConnections(d.id);
            $.each(connections, function (_, connection) {
                if (!updates.has(connection.id) && nfCanvasUtils.getConnectionSourceComponentId(connection) === nfCanvasUtils.getConnectionDestinationComponentId(connection)) {
                    var connectionUpdate = nfDraggable.updateConnectionPosition(nfConnection.get(connection.id), delta);
                    if (connectionUpdate !== null) {
                        updates.set(connection.id, connectionUpdate);
                    }
                }
            });

            // consider the component itself
            updates.set(d.id, nfDraggable.updateComponentPosition(d, delta));
        });

        nfDraggable.refreshConnections(updates);
    };

    /**
     * Updates the parent group of all selected components.
     *
     * @param {selection} the destination group
     */
    var updateComponentsGroup = function (group) {
        // get the selection and deselect the components being moved
        var selection = d3.selectAll('g.component.selected, g.connection.selected').classed('selected', false);

        if (nfCanvasUtils.canModify(selection) === false) {
            nfDialog.showOkDialog({
                headerText: 'Component Position',
                dialogContent: 'Must be authorized to modify every component selected.'
            });
            return;
        }
        if (nfCanvasUtils.canModify(group) === false) {
            nfDialog.showOkDialog({
                headerText: 'Component Position',
                dialogContent: 'Not authorized to modify the destination group.'
            });
            return;
        }

        // move the seleciton into the group
        nfCanvasUtils.moveComponents(selection, group);
    };

    var nfDraggable = {
        init: function (canvas) {
            nfCanvas = canvas;

            // handle component drag events
            drag = d3.drag()
                .on('start', function (event) {
                    // stop further propagation
                    event.sourceEvent.stopPropagation();
                })
                .on('drag', function (event) {
                    var dragSelection = d3.select('rect.drag-selection');

                    // lazily create the drag selection box
                    if (dragSelection.empty()) {
                        // get the current selection
                        var selection = d3.selectAll('g.component.selected');

                        // determine the appropriate bounding box
                        var minX = null, maxX = null, minY = null, maxY = null;
                        selection.each(function (d) {
                            if (minX === null || d.position.x < minX) {
                                minX = d.position.x;
                            }
                            if (minY === null || d.position.y < minY) {
                                minY = d.position.y;
                            }
                            var componentMaxX = d.position.x + d.dimensions.width;
                            var componentMaxY = d.position.y + d.dimensions.height;
                            if (maxX === null || componentMaxX > maxX) {
                                maxX = componentMaxX;
                            }
                            if (maxY === null || componentMaxY > maxY) {
                                maxY = componentMaxY;
                            }
                        });

                        // create a selection box for the move
                        d3.select('#canvas').append('rect')
                            .attr('rx', 6)
                            .attr('ry', 6)
                            .attr('x', minX)
                            .attr('y', minY)
                            .attr('class', 'drag-selection')
                            .attr('pointer-events', 'none')
                            .attr('width', maxX - minX)
                            .attr('height', maxY - minY)
                            .attr('stroke-width', function () {
                                return 1 / nfCanvasUtils.getCanvasScale();
                            })
                            .attr('stroke-dasharray', function () {
                                return 4 / nfCanvasUtils.getCanvasScale();
                            })
                            .datum({
                                original: {
                                    x: minX,
                                    y: minY
                                },
                                x: minX,
                                y: minY
                            });
                    } else {
                        // update the position of the drag selection
                        // snap align the position unless the user is holding shift
                        snapEnabled = !event.sourceEvent.shiftKey;
                        dragSelection.attr('x', function (d) {
                            d.x += event.dx;
                            return snapEnabled ? (Math.round(d.x/snapAlignmentPixels) * snapAlignmentPixels) : d.x;
                        }).attr('y', function (d) {
                                d.y += event.dy;
                                return snapEnabled ? (Math.round(d.y/snapAlignmentPixels) * snapAlignmentPixels) : d.y;
                        });
                     }
                })
                .on('end', function (event) {
                    // stop further propagation
                    event.sourceEvent.stopPropagation();

                    // get the drag selection
                    var dragSelection = d3.select('rect.drag-selection');

                    // ensure we found a drag selection
                    if (dragSelection.empty()) {
                        return;
                    }

                    // get the destination group if applicable... remove the drop flag if necessary to prevent
                    // subsequent drop events from triggering prior to this move's completion
                    var group = d3.select('g.drop').classed('drop', false);

                    // either move or update the selections group as appropriate
                    if (group.empty()) {
                        updateComponentsPosition(dragSelection);
                    } else {
                        updateComponentsGroup(group);
                    }

                    // remove the drag selection
                    dragSelection.remove();
                });
        },

        /**
         * Update the component's position
         *
         * @param d     The component
         * @param delta The change in position
         * @returns {*}
         */
        updateComponentPosition: function (d, delta) {
            var newPosition = {
                'x': snapEnabled ? (Math.round((d.position.x + delta.x)/snapAlignmentPixels) * snapAlignmentPixels) : d.position.x + delta.x,
                'y': snapEnabled ? (Math.round((d.position.y + delta.y)/snapAlignmentPixels) * snapAlignmentPixels) : d.position.y + delta.y
            };

            // build the entity
            var entity = {
                'revision': nfClient.getRevision(d),
                'disconnectedNodeAcknowledged': nfStorage.isDisconnectionAcknowledged(),
                'component': {
                    'id': d.id,
                    'position': newPosition
                }
            };

            // update the component positioning
            return $.Deferred(function (deferred) {
                $.ajax({
                    type: 'PUT',
                    url: d.uri,
                    data: JSON.stringify(entity),
                    dataType: 'json',
                    contentType: 'application/json'
                }).done(function (response) {
                    // update the component
                    nfCanvasUtils.getComponentByType(d.type).set(response);

                    // resolve with an object so we can refresh when finished
                    deferred.resolve({
                        type: d.type,
                        id: d.id
                    });
                }).fail(function (xhr, status, error) {
                    nfErrorHandler.handleAjaxError(xhr, status, error);
                    deferred.reject();
                });
            }).promise();
        },

        /**
         * Update the connection's position
         *
         * @param d     The connection
         * @param delta The change in position
         * @returns {*}
         */
        updateConnectionPosition: function (d, delta) {
            // only update if necessary
            if (d.bends.length === 0) {
                return null;
            }

            // calculate the new bend points
            var newBends = $.map(d.bends, function (bend) {
                return {
                    x: bend.x + delta.x,
                    y: bend.y + delta.y
                };
            });

            var entity = {
                'revision': nfClient.getRevision(d),
                'disconnectedNodeAcknowledged': nfStorage.isDisconnectionAcknowledged(),
                'component': {
                    id: d.id,
                    bends: newBends
                }
            };

            // update the component positioning
            return $.Deferred(function (deferred) {
                $.ajax({
                    type: 'PUT',
                    url: d.uri,
                    data: JSON.stringify(entity),
                    dataType: 'json',
                    contentType: 'application/json'
                }).done(function (response) {
                    // update the component
                    nfConnection.set(response);

                    // resolve with an object so we can refresh when finished
                    deferred.resolve({
                        type: d.type,
                        id: d.id
                    });
                }).fail(function (xhr, status, error) {
                    nfErrorHandler.handleAjaxError(xhr, status, error);

                    deferred.reject();
                });
            }).promise();
        },

        /**
         * Refresh the connections after dragging a component
         *
         * @param updates
         */
        refreshConnections: function (updates) {
            if (updates.size > 0) {
                // wait for all updates to complete
                $.when.apply(window, Array.from(updates.values())).done(function () {
                    var dragged = $.makeArray(arguments);
                    var connections = new Set();

                    // refresh this component
                    $.each(dragged, function (_, component) {
                        // check if the component in question is a connection
                        if (component.type === 'Connection') {
                            connections.add(component.id);
                        } else {
                            // get connections that need to be refreshed because its attached to this component
                            var componentConnections = nfConnection.getComponentConnections(component.id);
                            $.each(componentConnections, function (_, connection) {
                                connections.add(connection.id);
                            });
                        }
                    });

                    // refresh the connections
                    connections.forEach(function (connectionId) {
                        nfConnection.refresh(connectionId);
                    });
                }).always(function () {
                    nfBirdseye.refresh();
                });
            }
        },

        /**
         * Activates the drag behavior for the components in the specified selection.
         *
         * @param {selection} components
         */
        activate: function (components) {
            components.classed('moveable', true).call(drag);
        },

        /**
         * Deactivates the drag behavior for the components in the specified selection.
         *
         * @param {selection} components
         */
        deactivate: function (components) {
            components.classed('moveable', false).on('.drag', null);
        }
    };

    return nfDraggable;
}));