/*
 * 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 d3, define, module, require, exports */

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

    var nfConnectable;
    var nfDraggable;
    var nfSelectable;
    var nfContextMenu;

    var PREVIEW_NAME_LENGTH = 30;

    var dimensions = {
        width: 384,
        height: 176
    };

    // ----------------------------
    // process groups currently on the graph
    // ----------------------------

    var processGroupMap;

    // -----------------------------------------------------------
    // cache for components that are added/removed from the canvas
    // -----------------------------------------------------------

    var removedCache;
    var addedCache;

    // --------------------
    // component containers
    // --------------------

    var processGroupContainer;

    // --------------------------
    // privately scoped functions
    // --------------------------

    /**
     * Determines whether the specified process group is under version control.
     *
     * @param d
     */
    var isUnderVersionControl = function (d) {
        return nfCommon.isDefinedAndNotNull(d.versionedFlowState);
    };

    /**
     * Selects the process group elements against the current process group map.
     */
    var select = function () {
        return processGroupContainer.selectAll('g.process-group').data(processGroupMap.values(), function (d) {
            return d.id;
        });
    };

    /**
     * Renders the process groups in the specified selection.
     *
     * @param {selection} entered           The selection of process groups to be rendered
     * @param {boolean} selected            Whether the process group should be selected
     * @return the entered selection
     */
    var renderProcessGroups = function (entered, selected) {
        if (entered.empty()) {
            return entered;
        }

        var processGroup = entered.append('g')
            .attrs({
                'id': function (d) {
                    return 'id-' + d.id;
                },
                'class': 'process-group component'
            })
            .classed('selected', selected)
            .call(nfCanvasUtils.position);

        // ----
        // body
        // ----

        // process group border
        processGroup.append('rect')
            .attrs({
                'class': 'border',
                'width': function (d) {
                    return d.dimensions.width;
                },
                'height': function (d) {
                    return d.dimensions.height;
                },
                'fill': 'transparent',
                'stroke': 'transparent'
            });

        // process group body
        processGroup.append('rect')
            .attrs({
                'class': 'body',
                'width': function (d) {
                    return d.dimensions.width;
                },
                'height': function (d) {
                    return d.dimensions.height;
                },
                'filter': 'url(#component-drop-shadow)',
                'stroke-width': 0
            });

        // process group name background
        processGroup.append('rect')
            .attrs({
                'width': function (d) {
                    return d.dimensions.width;
                },
                'height': 32,
                'fill': '#b8c6cd'
            });

        // process group name
        processGroup.append('text')
            .attrs({
                'x': 10,
                'y': 20,
                'width': 300,
                'height': 16,
                'class': 'process-group-name'
            });

        // process group name
        processGroup.append('text')
            .attrs({
                'x': 10,
                'y': 21,
                'class': 'version-control'
            });

        // always support selecting and navigation
        processGroup.on('dblclick', function (d) {
            // enter this group on double click
            nfProcessGroup.enterGroup(d.id);
        })
            .call(nfSelectable.activate).call(nfContextMenu.activate);

        // only support dragging, connection, and drag and drop if appropriate
        processGroup.filter(function (d) {
            return d.permissions.canWrite && d.permissions.canRead;
        })
            .on('mouseover.drop', function (d) {
                // Using mouseover/out to workaround chrome issue #122746

                // get the target and ensure its not already been marked for drop
                var target = d3.select(this);
                if (!target.classed('drop')) {
                    var targetData = target.datum();

                    // see if there is a selection being dragged
                    var drag = d3.select('rect.drag-selection');
                    if (!drag.empty()) {
                        // filter the current selection by this group
                        var selection = nfCanvasUtils.getSelection().filter(function (d) {
                            return targetData.id === d.id;
                        });

                        // ensure this group isn't in the selection
                        if (selection.empty()) {
                            // mark that we are hovering over a drop area if appropriate
                            target.classed('drop', function () {
                                // get the current selection and ensure its disconnected
                                return nfConnection.isDisconnected(nfCanvasUtils.getSelection());
                            });
                        }
                    }
                }
            })
            .on('mouseout.drop', function (d) {
                // mark that we are no longer hovering over a drop area unconditionally
                d3.select(this).classed('drop', false);
            })
            .call(nfDraggable.activate)
            .call(nfConnectable.activate);

        return processGroup;
    };

    // attempt of space between component count and icon for process group contents
    var CONTENTS_SPACER = 10;
    var CONTENTS_VALUE_SPACER = 5;

    /**
     * Updates the process groups in the specified selection.
     *
     * @param {selection} updated               The process groups to be updated
     */
    var updateProcessGroups = function (updated) {
        if (updated.empty()) {
            return;
        }

        // process group border authorization
        updated.select('rect.border')
            .classed('unauthorized', function (d) {
                return d.permissions.canRead === false;
            });

        // process group body authorization
        updated.select('rect.body')
            .classed('unauthorized', function (d) {
                return d.permissions.canRead === false;
            });

        updated.each(function (processGroupData) {
            var processGroup = d3.select(this);
            var details = processGroup.select('g.process-group-details');

            // update the component behavior as appropriate
            nfCanvasUtils.editable(processGroup, nfConnectable, nfDraggable);

            // if this processor is visible, render everything
            if (processGroup.classed('visible')) {
                if (details.empty()) {
                    details = processGroup.append('g').attr('class', 'process-group-details');

                    // -------------------
                    // contents background
                    // -------------------

                    details.append('rect')
                        .attrs({
                            'x': 0,
                            'y': 32,
                            'width': function () {
                                return processGroupData.dimensions.width
                            },
                            'height': 24,
                            'fill': '#e3e8eb'
                        });

                    details.append('rect')
                        .attrs({
                            'x': 0,
                            'y': function () {
                                return processGroupData.dimensions.height - 24;
                            },
                            'width': function () {
                                return processGroupData.dimensions.width;
                            },
                            'height': 24,
                            'fill': '#e3e8eb'
                        });

                    // --------
                    // contents
                    // --------

                    // transmitting icon
                    details.append('text')
                        .attrs({
                            'x': 10,
                            'y': 49,
                            'class': 'process-group-transmitting process-group-contents-icon',
                            'font-family': 'FontAwesome'
                        })
                        .text('\uf140')
                        .append("title")
                        .text("Transmitting Remote Process Groups");


                    // transmitting count
                    details.append('text')
                        .attrs({
                            'y': 49,
                            'class': 'process-group-transmitting-count process-group-contents-count'
                        });

                    // not transmitting icon
                    details.append('text')
                        .attrs({
                            'y': 49,
                            'class': 'process-group-not-transmitting process-group-contents-icon',
                            'font-family': 'flowfont'
                        })
                        .text('\ue80a')
                        .append("title")
                        .text("Not Transmitting Remote Process Groups");

                    // not transmitting count
                    details.append('text')
                        .attrs({
                            'y': 49,
                            'class': 'process-group-not-transmitting-count process-group-contents-count'
                        });

                    // running icon
                    details.append('text')
                        .attrs({
                            'y': 49,
                            'class': 'process-group-running process-group-contents-icon',
                            'font-family': 'FontAwesome'
                        })
                        .text('\uf04b')
                        .append("title")
                        .text("Running Components");

                    // running count
                    details.append('text')
                        .attrs({
                            'y': 49,
                            'class': 'process-group-running-count process-group-contents-count'
                        });

                    // stopped icon
                    details.append('text')
                        .attrs({
                            'y': 49,
                            'class': 'process-group-stopped process-group-contents-icon',
                            'font-family': 'FontAwesome'
                        })
                        .text('\uf04d')
                        .append("title")
                        .text("Stopped Components");

                    // stopped count
                    details.append('text')
                        .attrs({
                            'y': 49,
                            'class': 'process-group-stopped-count process-group-contents-count'
                        });

                    // invalid icon
                    details.append('text')
                        .attrs({
                            'y': 49,
                            'class': 'process-group-invalid process-group-contents-icon',
                            'font-family': 'FontAwesome'
                        })
                        .text('\uf071')
                        .append("title")
                        .text("Invalid Components");

                    // invalid count
                    details.append('text')
                        .attrs({
                            'y': 49,
                            'class': 'process-group-invalid-count process-group-contents-count'
                        });

                    // disabled icon
                    details.append('text')
                        .attrs({
                            'y': 49,
                            'class': 'process-group-disabled process-group-contents-icon',
                            'font-family': 'flowfont'
                        })
                        .text('\ue802')
                        .append("title")
                        .text("Disabled Components");

                    // disabled count
                    details.append('text')
                        .attrs({
                            'y': 49,
                            'class': 'process-group-disabled-count process-group-contents-count'
                        });

                    // up to date icon
                    details.append('text')
                        .attrs({
                            'x': 10,
                            'y': function () {
                                return processGroupData.dimensions.height - 7;
                            },
                            'class': 'process-group-up-to-date process-group-contents-icon',
                            'font-family': 'FontAwesome'
                        })
                        .text('\uf00c')
                        .append("title")
                        .text("Up to date Versioned Process Groups");

                    // up to date count
                    details.append('text')
                        .attrs({
                            'y': function () {
                                return processGroupData.dimensions.height - 7;
                            },
                            'class': 'process-group-up-to-date-count process-group-contents-count'
                        });

                    // locally modified icon
                    details.append('text')
                        .attrs({
                            'y': function () {
                                return processGroupData.dimensions.height - 7;
                            },
                            'class': 'process-group-locally-modified process-group-contents-icon',
                            'font-family': 'FontAwesome'
                        })
                        .text('\uf069')
                        .append("title")
                        .text("Locally modified Versioned Process Groups");

                    // locally modified count
                    details.append('text')
                        .attrs({
                            'y': function () {
                                return processGroupData.dimensions.height - 7;
                            },
                            'class': 'process-group-locally-modified-count process-group-contents-count'
                        });

                    // stale icon
                    details.append('text')
                        .attrs({
                            'y': function () {
                                return processGroupData.dimensions.height - 7;
                            },
                            'class': 'process-group-stale process-group-contents-icon',
                            'font-family': 'FontAwesome'
                        })
                        .text('\uf0aa')
                        .append("title")
                        .text("Stale Versioned Process Groups");

                    // stale count
                    details.append('text')
                        .attrs({
                            'y': function () {
                                return processGroupData.dimensions.height - 7;
                            },
                            'class': 'process-group-stale-count process-group-contents-count'
                        });

                    // locally modified and stale icon
                    details.append('text')
                        .attrs({
                            'y': function () {
                                return processGroupData.dimensions.height - 7;
                            },
                            'class': 'process-group-locally-modified-and-stale process-group-contents-icon',
                            'font-family': 'FontAwesome'
                        })
                        .text('\uf06a')
                        .append("title")
                        .text("Locally modified and stale Versioned Process Groups");

                    // locally modified and stale count
                    details.append('text')
                        .attrs({
                            'y': function () {
                                return processGroupData.dimensions.height - 7;
                            },
                            'class': 'process-group-locally-modified-and-stale-count process-group-contents-count'
                        });

                    // sync failure icon
                    details.append('text')
                        .attrs({
                            'y': function () {
                                return processGroupData.dimensions.height - 7;
                            },
                            'class': 'process-group-sync-failure process-group-contents-icon',
                            'font-family': 'FontAwesome'
                        })
                        .text('\uf128')
                        .append("title")
                        .text("Sync failure Versioned Process Groups");

                    // sync failure count
                    details.append('text')
                        .attrs({
                            'y': function () {
                                return processGroupData.dimensions.height - 7;
                            },
                            'class': 'process-group-sync-failure-count process-group-contents-count'
                        });

                    // ----------------
                    // stats background
                    // ----------------

                    // queued
                    details.append('rect')
                        .attrs({
                            'width': function () {
                                return processGroupData.dimensions.width;
                            },
                            'height': 19,
                            'x': 0,
                            'y': 66,
                            'fill': '#f4f6f7'
                        });

                    // border
                    details.append('rect')
                        .attrs({
                            'width': function () {
                                return processGroupData.dimensions.width;
                            },
                            'height': 1,
                            'x': 0,
                            'y': 84,
                            'fill': '#c7d2d7'
                        });

                    // in
                    details.append('rect')
                        .attrs({
                            'width': function () {
                                return processGroupData.dimensions.width;
                            },
                            'height': 19,
                            'x': 0,
                            'y': 85,
                            'fill': '#ffffff'
                        });

                    // border
                    details.append('rect')
                        .attrs({
                            'width': function () {
                                return processGroupData.dimensions.width;
                            },
                            'height': 1,
                            'x': 0,
                            'y': 103,
                            'fill': '#c7d2d7'
                        });

                    // read/write
                    details.append('rect')
                        .attrs({
                            'width': function () {
                                return processGroupData.dimensions.width;
                            },
                            'height': 19,
                            'x': 0,
                            'y': 104,
                            'fill': '#f4f6f7'
                        });

                    // border
                    details.append('rect')
                        .attrs({
                            'width': function () {
                                return processGroupData.dimensions.width;
                            },
                            'height': 1,
                            'x': 0,
                            'y': 122,
                            'fill': '#c7d2d7'
                        });

                    // out
                    details.append('rect')
                        .attrs({
                            'width': function () {
                                return processGroupData.dimensions.width;
                            },
                            'height': 19,
                            'x': 0,
                            'y': 123,
                            'fill': '#ffffff'
                        });

                    // -----
                    // stats
                    // -----

                    // stats label container
                    var processGroupStatsLabel = details.append('g')
                        .attrs({
                            'transform': 'translate(6, 75)'
                        });

                    // queued label
                    processGroupStatsLabel.append('text')
                        .attrs({
                            'width': 73,
                            'height': 10,
                            'x': 4,
                            'y': 5,
                            'class': 'stats-label'
                        })
                        .text('Queued');

                    // in label
                    processGroupStatsLabel.append('text')
                        .attrs({
                            'width': 73,
                            'height': 10,
                            'x': 4,
                            'y': 24,
                            'class': 'stats-label'
                        })
                        .text('In');

                    // read/write label
                    processGroupStatsLabel.append('text')
                        .attrs({
                            'width': 73,
                            'height': 10,
                            'x': 4,
                            'y': 42,
                            'class': 'stats-label'
                        })
                        .text('Read/Write');

                    // out label
                    processGroupStatsLabel.append('text')
                        .attrs({
                            'width': 73,
                            'height': 10,
                            'x': 4,
                            'y': 60,
                            'class': 'stats-label'
                        })
                        .text('Out');

                    // stats value container
                    var processGroupStatsValue = details.append('g')
                        .attrs({
                            'transform': 'translate(95, 75)'
                        });

                    // queued value
                    var queuedText = processGroupStatsValue.append('text')
                        .attrs({
                            'width': 180,
                            'height': 10,
                            'x': 4,
                            'y': 5,
                            'class': 'process-group-queued stats-value'
                        });

                    // queued count
                    queuedText.append('tspan')
                        .attrs({
                            'class': 'count'
                        });

                    // queued size
                    queuedText.append('tspan')
                        .attrs({
                            'class': 'size'
                        });

                    // in value
                    var inText = processGroupStatsValue.append('text')
                        .attrs({
                            'width': 180,
                            'height': 10,
                            'x': 4,
                            'y': 24,
                            'class': 'process-group-in stats-value'
                        });

                    // in count
                    inText.append('tspan')
                        .attrs({
                            'class': 'count'
                        });

                    // in size
                    inText.append('tspan')
                        .attrs({
                            'class': 'size'
                        });

                    // in
                    inText.append('tspan')
                        .attrs({
                            'class': 'ports'
                        });

                    // in (remote)
                    inText.append('tspan')
                        .attrs({
                            'class': 'public-ports'
                        });

                    // read/write value
                    processGroupStatsValue.append('text')
                        .attrs({
                            'width': 180,
                            'height': 10,
                            'x': 4,
                            'y': 42,
                            'class': 'process-group-read-write stats-value'
                        });

                    // out value
                    var outText = processGroupStatsValue.append('text')
                        .attrs({
                            'width': 180,
                            'height': 10,
                            'x': 4,
                            'y': 60,
                            'class': 'process-group-out stats-value'
                        });

                    // out ports
                    outText.append('tspan')
                        .attrs({
                            'class': 'ports'
                        });

                    // out ports (remote)
                    outText.append('tspan')
                        .attrs({
                            'class': 'public-ports'
                        });

                    // out count
                    outText.append('tspan')
                        .attrs({
                            'class': 'count'
                        });

                    // out size
                    outText.append('tspan')
                        .attrs({
                            'class': 'size'
                        });

                    // stats value container
                    var processGroupStatsInfo = details.append('g')
                        .attrs({
                            'transform': 'translate(335, 75)'
                        });

                    // in info
                    processGroupStatsInfo.append('text')
                        .attrs({
                            'width': 25,
                            'height': 10,
                            'x': 4,
                            'y': 24,
                            'class': 'stats-info'
                        })
                        .text('5 min');

                    // read/write info
                    processGroupStatsInfo.append('text')
                        .attrs({
                            'width': 25,
                            'height': 10,
                            'x': 4,
                            'y': 42,
                            'class': 'stats-info'
                        })
                        .text('5 min');

                    // out info
                    processGroupStatsInfo.append('text')
                        .attrs({
                            'width': 25,
                            'height': 10,
                            'x': 4,
                            'y': 60,
                            'class': 'stats-info'
                        })
                        .text('5 min');

                    // --------
                    // comments
                    // --------

                    details.append('path')
                        .attrs({
                            'class': 'component-comments',
                            'transform': 'translate(' + (processGroupData.dimensions.width - 2) + ', ' + (processGroupData.dimensions.height - 10) + ')',
                            'd': 'm0,0 l0,8 l-8,0 z'
                        });

                    // -------------------
                    // active thread count
                    // -------------------

                    // active thread count
                    details.append('text')
                        .attrs({
                            'class': 'active-thread-count-icon',
                            'y': 20
                        })
                        .text('\ue83f');

                    // active thread icon
                    details.append('text')
                        .attrs({
                            'class': 'active-thread-count',
                            'y': 20
                        });

                    // ---------
                    // bulletins
                    // ---------

                    // bulletin background
                    details.append('rect')
                        .attrs({
                            'class': 'bulletin-background',
                            'x': function () {
                                return processGroupData.dimensions.width - 24;
                            },
                            'y': 32,
                            'width': 24,
                            'height': 24
                        });

                    // bulletin icon
                    details.append('text')
                        .attrs({
                            'class': 'bulletin-icon',
                            'x': function () {
                                return processGroupData.dimensions.width - 17;
                            },
                            'y': 49
                        })
                        .text('\uf24a');
                }

                // update transmitting
                var transmitting = details.select('text.process-group-transmitting')
                    .classed('transmitting', function (d) {
                        return d.permissions.canRead && d.activeRemotePortCount > 0;
                    })
                    .classed('zero', function (d) {
                        return d.permissions.canRead && d.activeRemotePortCount === 0;
                    });
                var transmittingCount = details.select('text.process-group-transmitting-count')
                    .attr('x', function () {
                        var transmittingCountX = parseInt(transmitting.attr('x'), 10);
                        return transmittingCountX + Math.round(transmitting.node().getComputedTextLength()) + CONTENTS_VALUE_SPACER;
                    })
                    .text(function (d) {
                        return d.activeRemotePortCount;
                    });
                transmittingCount.append("title").text("Transmitting Remote Process Groups");

                // update not transmitting
                var notTransmitting = details.select('text.process-group-not-transmitting')
                    .classed('not-transmitting', function (d) {
                        return d.permissions.canRead && d.inactiveRemotePortCount > 0;
                    })
                    .classed('zero', function (d) {
                        return d.permissions.canRead && d.inactiveRemotePortCount === 0;
                    })
                    .attr('x', function () {
                        var transmittingX = parseInt(transmittingCount.attr('x'), 10);
                        return transmittingX + Math.round(transmittingCount.node().getComputedTextLength()) + CONTENTS_SPACER;
                    });
                var notTransmittingCount = details.select('text.process-group-not-transmitting-count')
                    .attr('x', function () {
                        var notTransmittingCountX = parseInt(notTransmitting.attr('x'), 10);
                        return notTransmittingCountX + Math.round(notTransmitting.node().getComputedTextLength()) + CONTENTS_VALUE_SPACER;
                    })
                    .text(function (d) {
                        return d.inactiveRemotePortCount;
                    });
                notTransmittingCount.append("title").text("Not transmitting Remote Process Groups")

                // update running
                var running = details.select('text.process-group-running')
                    .classed('running', function (d) {
                        return d.permissions.canRead && d.component.runningCount > 0;
                    })
                    .classed('zero', function (d) {
                        return d.permissions.canRead && d.component.runningCount === 0;
                    })
                    .attr('x', function () {
                        var notTransmittingX = parseInt(notTransmittingCount.attr('x'), 10);
                        return notTransmittingX + Math.round(notTransmittingCount.node().getComputedTextLength()) + CONTENTS_SPACER;
                    });
                var runningCount = details.select('text.process-group-running-count')
                    .attr('x', function () {
                        var runningCountX = parseInt(running.attr('x'), 10);
                        return runningCountX + Math.round(running.node().getComputedTextLength()) + CONTENTS_VALUE_SPACER;
                    })
                    .text(function (d) {
                        return d.runningCount;
                    });
                runningCount.append("title").text("Running Components");

                // update stopped
                var stopped = details.select('text.process-group-stopped')
                    .classed('stopped', function (d) {
                        return d.permissions.canRead && d.component.stoppedCount > 0;
                    })
                    .classed('zero', function (d) {
                        return d.permissions.canRead && d.component.stoppedCount === 0;
                    })
                    .attr('x', function () {
                        var runningX = parseInt(runningCount.attr('x'), 10);
                        return runningX + Math.round(runningCount.node().getComputedTextLength()) + CONTENTS_SPACER;
                    });
                var stoppedCount = details.select('text.process-group-stopped-count')
                    .attr('x', function () {
                        var stoppedCountX = parseInt(stopped.attr('x'), 10);
                        return stoppedCountX + Math.round(stopped.node().getComputedTextLength()) + CONTENTS_VALUE_SPACER;
                    })
                    .text(function (d) {
                        return d.stoppedCount;
                    });
                stoppedCount.append("title").text("Stopped Components");

                // update invalid
                var invalid = details.select('text.process-group-invalid')
                    .classed('invalid', function (d) {
                        return d.permissions.canRead && d.component.invalidCount > 0;
                    })
                    .classed('zero', function (d) {
                        return d.permissions.canRead && d.component.invalidCount === 0;
                    })
                    .attr('x', function () {
                        var stoppedX = parseInt(stoppedCount.attr('x'), 10);
                        return stoppedX + Math.round(stoppedCount.node().getComputedTextLength()) + CONTENTS_SPACER;
                    });
                var invalidCount = details.select('text.process-group-invalid-count')
                    .attr('x', function () {
                        var invalidCountX = parseInt(invalid.attr('x'), 10);
                        return invalidCountX + Math.round(invalid.node().getComputedTextLength()) + CONTENTS_VALUE_SPACER;
                    })
                    .text(function (d) {
                        return d.invalidCount;
                    });
                invalidCount.append("title").text("Invalid Components");

                // update disabled
                var disabled = details.select('text.process-group-disabled')
                    .classed('disabled', function (d) {
                        return d.permissions.canRead && d.component.disabledCount > 0;
                    })
                    .classed('zero', function (d) {
                        return d.permissions.canRead && d.component.disabledCount === 0;
                    })
                    .attr('x', function () {
                        var invalidX = parseInt(invalidCount.attr('x'), 10);
                        return invalidX + Math.round(invalidCount.node().getComputedTextLength()) + CONTENTS_SPACER;
                    });
                var disabledCount = details.select('text.process-group-disabled-count')
                    .attr('x', function () {
                        var disabledCountX = parseInt(disabled.attr('x'), 10);
                        return disabledCountX + Math.round(disabled.node().getComputedTextLength()) + CONTENTS_VALUE_SPACER;
                    })
                    .text(function (d) {
                        return d.disabledCount;
                    });
                disabledCount.append("title").text("Disabled Components");

                // up to date current
                var upToDate = details.select('text.process-group-up-to-date')
                    .classed('up-to-date', function (d) {
                        return d.permissions.canRead && d.component.upToDateCount > 0;
                    })
                    .classed('zero', function (d) {
                        return d.permissions.canRead && d.component.upToDateCount === 0;
                    });
                var upToDateCount = details.select('text.process-group-up-to-date-count')
                    .attr('x', function () {
                        var updateToDateCountX = parseInt(upToDate.attr('x'), 10);
                        return updateToDateCountX + Math.round(upToDate.node().getComputedTextLength()) + CONTENTS_VALUE_SPACER;
                    })
                    .text(function (d) {
                        return d.upToDateCount;
                    });
                upToDateCount.append("title").text("Up to date Versioned Process Groups");

                // update locally modified
                var locallyModified = details.select('text.process-group-locally-modified')
                    .classed('locally-modified', function (d) {
                        return d.permissions.canRead && d.component.locallyModifiedCount > 0;
                    })
                    .classed('zero', function (d) {
                        return d.permissions.canRead && d.component.locallyModifiedCount === 0;
                    })
                    .attr('x', function () {
                        var upToDateX = parseInt(upToDateCount.attr('x'), 10);
                        return upToDateX + Math.round(upToDateCount.node().getComputedTextLength()) + CONTENTS_SPACER;
                    });
                var locallyModifiedCount = details.select('text.process-group-locally-modified-count')
                    .attr('x', function () {
                        var locallyModifiedCountX = parseInt(locallyModified.attr('x'), 10);
                        return locallyModifiedCountX + Math.round(locallyModified.node().getComputedTextLength()) + CONTENTS_VALUE_SPACER;
                    })
                    .text(function (d) {
                        return d.locallyModifiedCount;
                    });
                locallyModifiedCount.append("title").text("Locally modified Versioned Process Groups");

                // update stale
                var stale = details.select('text.process-group-stale')
                    .classed('stale', function (d) {
                        return d.permissions.canRead && d.component.staleCount > 0;
                    })
                    .classed('zero', function (d) {
                        return d.permissions.canRead && d.component.staleCount === 0;
                    })
                    .attr('x', function () {
                        var locallyModifiedX = parseInt(locallyModifiedCount.attr('x'), 10);
                        return locallyModifiedX + Math.round(locallyModifiedCount.node().getComputedTextLength()) + CONTENTS_SPACER;
                    });
                var staleCount = details.select('text.process-group-stale-count')
                    .attr('x', function () {
                        var staleCountX = parseInt(stale.attr('x'), 10);
                        return staleCountX + Math.round(stale.node().getComputedTextLength()) + CONTENTS_VALUE_SPACER;
                    })
                    .text(function (d) {
                        return d.staleCount;
                    });
                staleCount.append("title").text("Stale Versioned Process Groups");

                // update locally modified and stale
                var locallyModifiedAndStale = details.select('text.process-group-locally-modified-and-stale')
                    .classed('locally-modified-and-stale', function (d) {
                        return d.permissions.canRead && d.component.locallyModifiedAndStaleCount > 0;
                    })
                    .classed('zero', function (d) {
                        return d.permissions.canRead && d.component.locallyModifiedAndStaleCount === 0;
                    })
                    .attr('x', function () {
                        var staleX = parseInt(staleCount.attr('x'), 10);
                        return staleX + Math.round(staleCount.node().getComputedTextLength()) + CONTENTS_SPACER;
                    });
                var locallyModifiedAndStaleCount = details.select('text.process-group-locally-modified-and-stale-count')
                    .attr('x', function () {
                        var locallyModifiedAndStaleCountX = parseInt(locallyModifiedAndStale.attr('x'), 10);
                        return locallyModifiedAndStaleCountX + Math.round(locallyModifiedAndStale.node().getComputedTextLength()) + CONTENTS_VALUE_SPACER;
                    })
                    .text(function (d) {
                        return d.locallyModifiedAndStaleCount;
                    });
                locallyModifiedAndStaleCount.append("title").text("Locally modified and stale Versioned Process Groups");

                // update sync failure
                var syncFailure = details.select('text.process-group-sync-failure')
                    .classed('sync-failure', function (d) {
                        return d.permissions.canRead && d.component.syncFailureCount > 0;
                    })
                    .classed('zero', function (d) {
                        return d.permissions.canRead && d.component.syncFailureCount === 0;
                    })
                    .attr('x', function () {
                        var syncFailureX = parseInt(locallyModifiedAndStaleCount.attr('x'), 10);
                        return syncFailureX + Math.round(locallyModifiedAndStaleCount.node().getComputedTextLength()) + CONTENTS_SPACER - 2;
                    });
                var syncFailureCount = details.select('text.process-group-sync-failure-count')
                    .attr('x', function () {
                        var syncFailureCountX = parseInt(syncFailure.attr('x'), 10);
                        return syncFailureCountX + Math.round(syncFailure.node().getComputedTextLength()) + CONTENTS_VALUE_SPACER;
                    })
                    .text(function (d) {
                        return d.syncFailureCount;
                    });
                syncFailureCount.append("title").text("Sync failure Versioned Process Groups");

                // update version control information
                var versionControl = processGroup.select('text.version-control')
                    .styles({
                        'visibility': isUnderVersionControl(processGroupData) ? 'visible' : 'hidden',
                        'fill': function () {
                            if (isUnderVersionControl(processGroupData)) {
                                var vciState = processGroupData.versionedFlowState;
                                if (vciState === 'SYNC_FAILURE') {
                                    return '#666666';
                                } else if (vciState === 'LOCALLY_MODIFIED_AND_STALE') {
                                    return '#BA554A';
                                } else if (vciState === 'STALE') {
                                    return '#BA554A';
                                } else if (vciState === 'LOCALLY_MODIFIED') {
                                    return '#666666';
                                } else {
                                    return '#1A9964';
                                }
                            } else {
                                return '#000';
                            }
                        }
                    })
                    .text(function () {
                        if (isUnderVersionControl(processGroupData)) {
                            var vciState = processGroupData.versionedFlowState;
                            if (vciState === 'SYNC_FAILURE') {
                                return '\uf128'
                            } else if (vciState === 'LOCALLY_MODIFIED_AND_STALE') {
                                return '\uf06a';
                            } else if (vciState === 'STALE') {
                                return '\uf0aa';
                            } else if (vciState === 'LOCALLY_MODIFIED') {
                                return '\uf069';
                            } else {
                                return '\uf00c';
                            }
                        } else {
                            return '';
                        }
                    });

                if (processGroupData.permissions.canRead) {
                    // version control tooltip
                    versionControl.each(function () {
                            // get the tip
                            var tip = d3.select('#version-control-tip-' + processGroupData.id);

                            // if there are validation errors generate a tooltip
                            if (isUnderVersionControl(processGroupData)) {
                                // create the tip if necessary
                                if (tip.empty()) {
                                    tip = d3.select('#process-group-tooltips').append('div')
                                        .attr('id', function () {
                                            return 'version-control-tip-' + processGroupData.id;
                                        })
                                        .attr('class', 'tooltip nifi-tooltip');
                                }

                                // update the tip
                                tip.html(function () {
                                    var vci = processGroupData.component.versionControlInformation;
                                    var versionControlTip = $('<div></div>').text('Tracking to "' + vci.flowName + '" version ' + vci.version + ' in "' + vci.registryName + ' - ' + vci.bucketName + '"');
                                    var versionControlStateTip = $('<div></div>').text(nfCommon.getVersionControlTooltip(vci));
                                    return $('<div></div>').append(versionControlTip).append('<br/>').append(versionControlStateTip).html();
                                });

                                // add the tooltip
                                nfCanvasUtils.canvasTooltip(tip, d3.select(this));
                            } else {
                                // remove the tip if necessary
                                if (!tip.empty()) {
                                    tip.remove();
                                }
                            }
                        });

                    // update the process group comments
                    processGroup.select('path.component-comments')
                        .style('visibility', nfCommon.isBlank(processGroupData.component.comments) ? 'hidden' : 'visible')
                        .each(function () {
                            // get the tip
                            var tip = d3.select('#comments-tip-' + processGroupData.id);

                            // if there are validation errors generate a tooltip
                            if (nfCommon.isBlank(processGroupData.component.comments)) {
                                // remove the tip if necessary
                                if (!tip.empty()) {
                                    tip.remove();
                                }
                            } else {
                                // create the tip if necessary
                                if (tip.empty()) {
                                    tip = d3.select('#process-group-tooltips').append('div')
                                        .attr('id', function () {
                                            return 'comments-tip-' + processGroupData.id;
                                        })
                                        .attr('class', 'tooltip nifi-tooltip');
                                }

                                // update the tip
                                tip.text(processGroupData.component.comments);

                                // add the tooltip
                                nfCanvasUtils.canvasTooltip(tip, d3.select(this));
                            }
                        });

                    // update the process group name
                    processGroup.select('text.process-group-name')
                        .attrs({
                            'x': function () {
                                if (isUnderVersionControl(processGroupData)) {
                                    var versionControlX = parseInt(versionControl.attr('x'), 10);
                                    return versionControlX + Math.round(versionControl.node().getComputedTextLength()) + CONTENTS_VALUE_SPACER;
                                } else {
                                    return 10;
                                }
                            },
                            'width': function () {
                                if (isUnderVersionControl(processGroupData)) {
                                    var versionControlX = parseInt(versionControl.attr('x'), 10);
                                    var processGroupNameX = parseInt(d3.select(this).attr('x'), 10);
                                    return 300 - (processGroupNameX - versionControlX);
                                } else {
                                    return 300;
                                }
                            }
                        })
                        .each(function (d) {
                            var processGroupName = d3.select(this);

                            // reset the process group name to handle any previous state
                            processGroupName.text(null).selectAll('title').remove();

                            // apply ellipsis to the process group name as necessary
                            nfCanvasUtils.ellipsis(processGroupName, d.component.name);
                        })
                        .append('title')
                        .text(function (d) {
                            return d.component.name;
                        });
                } else {
                    // clear the process group comments
                    processGroup.select('path.component-comments').style('visibility', 'hidden');

                    // clear the process group name
                    processGroup.select('text.process-group-name')
                        .attrs({
                            'x': 10,
                            'width': 316
                        })
                        .text(null);

                    // clear tooltips
                    processGroup.call(removeTooltips);
                }

                // populate the stats
                processGroup.call(updateProcessGroupStatus);
            } else {
                if (processGroupData.permissions.canRead) {
                    // update the process group name
                    processGroup.select('text.process-group-name')
                        .text(function (d) {
                            var name = d.component.name;
                            if (name.length > PREVIEW_NAME_LENGTH) {
                                return name.substring(0, PREVIEW_NAME_LENGTH) + String.fromCharCode(8230);
                            } else {
                                return name;
                            }
                        });
                } else {
                    // clear the process group name
                    processGroup.select('text.process-group-name').text(null);
                }

                // remove the tooltips
                processGroup.call(removeTooltips);

                // remove the details if necessary
                if (!details.empty()) {
                    details.remove();
                }
            }
        });
    };

    /**
     * Updates the process group status.
     *
     * @param {selection} updated           The process groups to be updated
     */
    var updateProcessGroupStatus = function (updated) {
        if (updated.empty()) {
            return;
        }

        // queued count value
        updated.select('text.process-group-queued tspan.count')
            .text(function (d) {
                return nfCommon.substringBeforeFirst(d.status.aggregateSnapshot.queued, ' ');
            });

        // queued size value
        updated.select('text.process-group-queued tspan.size')
            .text(function (d) {
                return ' ' + nfCommon.substringAfterFirst(d.status.aggregateSnapshot.queued, ' ');
            });

        // in count value
        updated.select('text.process-group-in tspan.count')
            .text(function (d) {
                return nfCommon.substringBeforeFirst(d.status.aggregateSnapshot.input, ' ');
            });

        // in size value
        updated.select('text.process-group-in tspan.size')
            .text(function (d) {
                return ' ' + nfCommon.substringAfterFirst(d.status.aggregateSnapshot.input, ' ');
            });

        // in ports value
        updated.select('text.process-group-in tspan.ports')
            .text(function (d) {
                return ' ' + String.fromCharCode(8594) + ' ' + d.inputPortCount;
            });

        // in ports value (remote)
        updated.select('text.process-group-in tspan.public-ports')
            .text(function (d) {
                return d.publicInputPortCount > 0 ? ' (' + d.publicInputPortCount + ' remote)' : '';
            });

        // read/write value
        updated.select('text.process-group-read-write')
            .text(function (d) {
                return d.status.aggregateSnapshot.read + ' / ' + d.status.aggregateSnapshot.written;
            });

        // out ports value
        updated.select('text.process-group-out tspan.ports')
            .text(function (d) {
                return d.outputPortCount;
            });

        // out ports value (remote)
        updated.select('text.process-group-out tspan.public-ports')
            .text(function (d) {
                return d.publicOutputPortCount > 0 ? ' (' + d.publicOutputPortCount + ' remote) ' : '';
            });

        // out count value
        updated.select('text.process-group-out tspan.count')
            .text(function (d) {
                return  ' ' + String.fromCharCode(8594) + ' ' + nfCommon.substringBeforeFirst(d.status.aggregateSnapshot.output, ' ');
            });

        // out size value
        updated.select('text.process-group-out tspan.size')
            .text(function (d) {
                return ' ' + nfCommon.substringAfterFirst(d.status.aggregateSnapshot.output, ' ');
            });

        updated.each(function (d) {
            var processGroup = d3.select(this);
            var offset = 0;

            // -------------------
            // active thread count
            // -------------------

            nfCanvasUtils.activeThreadCount(processGroup, d, function (off) {
                offset = off;
            });

            // ---------
            // bulletins
            // ---------

            processGroup.select('rect.bulletin-background').classed('has-bulletins', function () {
                return !nfCommon.isEmpty(d.status.aggregateSnapshot.bulletins);
            });

            nfCanvasUtils.bulletins(processGroup, d, function () {
                return d3.select('#process-group-tooltips');
            }, offset);
        });
    };

    /**
     * Removes the process groups in the specified selection.
     *
     * @param {selection} removed               The process groups to be removed
     */
    var removeProcessGroups = function (removed) {
        if (removed.empty()) {
            return;
        }

        removed.call(removeTooltips).remove();
    };

    /**
     * Removes the tooltips for the process groups in the specified selection.
     *
     * @param {selection} removed
     */
    var removeTooltips = function (removed) {
        removed.each(function (d) {
            // remove any associated tooltips
            $('#bulletin-tip-' + d.id).remove();
            $('#version-control-tip-' + d.id).remove();
            $('#comments-tip-' + d.id).remove();
        });
    };

    var nfProcessGroup = {
        /**
         * Initializes of the Process Group handler.
         *
         * @param nfConnectableRef   The nfConnectable module.
         * @param nfDraggableRef   The nfDraggable module.
         * @param nfSelectableRef   The nfSelectable module.
         * @param nfContextMenuRef   The nfContextMenu module.
         */
        init: function (nfConnectableRef, nfDraggableRef, nfSelectableRef, nfContextMenuRef) {
            nfConnectable = nfConnectableRef;
            nfDraggable = nfDraggableRef;
            nfSelectable = nfSelectableRef;
            nfContextMenu = nfContextMenuRef;

            processGroupMap = d3.map();
            removedCache = d3.map();
            addedCache = d3.map();

            // create the process group container
            processGroupContainer = d3.select('#canvas').append('g')
                .attrs({
                    'pointer-events': 'all',
                    'class': 'process-groups'
                });
        },

        /**
         * Adds the specified process group entity.
         *
         * @param processGroupEntities       The process group
         * @param options           Configuration options
         */
        add: function (processGroupEntities, options) {
            var selectAll = false;
            if (nfCommon.isDefinedAndNotNull(options)) {
                selectAll = nfCommon.isDefinedAndNotNull(options.selectAll) ? options.selectAll : selectAll;
            }

            // get the current time
            var now = new Date().getTime();

            var add = function (processGroupEntity) {
                addedCache.set(processGroupEntity.id, now);

                // add the process group
                processGroupMap.set(processGroupEntity.id, $.extend({
                    type: 'ProcessGroup',
                    dimensions: dimensions
                }, processGroupEntity));
            };

            // determine how to handle the specified process groups
            if ($.isArray(processGroupEntities)) {
                $.each(processGroupEntities, function (_, processGroupEntity) {
                    add(processGroupEntity);
                });
            } else if (nfCommon.isDefinedAndNotNull(processGroupEntities)) {
                add(processGroupEntities);
            }

            // select
            var selection = select();

            // enter
            var entered = renderProcessGroups(selection.enter(), selectAll);

            // update
            updateProcessGroups(selection.merge(entered));
        },

        /**
         * Populates the graph with the specified process groups.
         *
         * @argument {object | array} processGroupEntities                    The process groups to add
         * @argument {object} options                Configuration options
         */
        set: function (processGroupEntities, options) {
            var selectAll = false;
            var transition = false;
            var overrideRevisionCheck = false;
            if (nfCommon.isDefinedAndNotNull(options)) {
                selectAll = nfCommon.isDefinedAndNotNull(options.selectAll) ? options.selectAll : selectAll;
                transition = nfCommon.isDefinedAndNotNull(options.transition) ? options.transition : transition;
                overrideRevisionCheck = nfCommon.isDefinedAndNotNull(options.overrideRevisionCheck) ? options.overrideRevisionCheck : overrideRevisionCheck;
            }

            var set = function (proposedProcessGroupEntity) {
                var currentProcessGroupEntity = processGroupMap.get(proposedProcessGroupEntity.id);

                // set the process group if appropriate due to revision and wasn't previously removed
                if ((nfClient.isNewerRevision(currentProcessGroupEntity, proposedProcessGroupEntity) && !removedCache.has(proposedProcessGroupEntity.id)) || overrideRevisionCheck === true) {
                    processGroupMap.set(proposedProcessGroupEntity.id, $.extend({
                        type: 'ProcessGroup',
                        dimensions: dimensions
                    }, proposedProcessGroupEntity));
                }
            };

            // determine how to handle the specified process groups
            if ($.isArray(processGroupEntities)) {
                $.each(processGroupMap.keys(), function (_, key) {
                    var currentProcessGroupEntity = processGroupMap.get(key);
                    var isPresent = $.grep(processGroupEntities, function (proposedProcessGroupEntity) {
                        return proposedProcessGroupEntity.id === currentProcessGroupEntity.id;
                    });

                    // if the current process group is not present and was not recently added, remove it
                    if (isPresent.length === 0 && !addedCache.has(key)) {
                        processGroupMap.remove(key);
                    }
                });
                $.each(processGroupEntities, function (_, processGroupEntity) {
                    set(processGroupEntity);
                });
            } else if (nfCommon.isDefinedAndNotNull(processGroupEntities)) {
                set(processGroupEntities);
            }

            // select
            var selection = select();

            // enter
            var entered = renderProcessGroups(selection.enter(), selectAll);

            // update
            var updated = selection.merge(entered);
            updated.call(updateProcessGroups).call(nfCanvasUtils.position, transition);

            // exit
            selection.exit().call(removeProcessGroups);
        },

        /**
         * If the process group id is specified it is returned. If no process group id
         * specified, all process groups are returned.
         *
         * @param {string} id
         */
        get: function (id) {
            if (nfCommon.isUndefined(id)) {
                return processGroupMap.values();
            } else {
                return processGroupMap.get(id);
            }
        },

        /**
         * If the process group id is specified it is refresh according to the current
         * state. If no process group id is specified, all process groups are refreshed.
         *
         * @param {string} id      Optional
         */
        refresh: function (id) {
            if (nfCommon.isDefinedAndNotNull(id)) {
                d3.select('#id-' + id).call(updateProcessGroups);
            } else {
                d3.selectAll('g.process-group').call(updateProcessGroups);
            }
        },

        /**
         * Refreshes the components necessary after a pan event.
         */
        pan: function () {
            d3.selectAll('g.process-group.entering, g.process-group.leaving').call(updateProcessGroups);
        },

        /**
         * Reloads the process group state from the server and refreshes the UI.
         * If the process group is currently unknown, this function reloads the canvas.
         *
         * @param {string} id The process group id
         */
        reload: function (id) {
            if (processGroupMap.has(id)) {
                var processGroupEntity = processGroupMap.get(id);
                return $.ajax({
                    type: 'GET',
                    url: processGroupEntity.uri,
                    dataType: 'json'
                }).done(function (response) {
                    nfProcessGroup.set(response);
                });
            }
        },

        /**
         * Positions the component.
         *
         * @param {string} id   The id
         */
        position: function (id) {
            d3.select('#id-' + id).call(nfCanvasUtils.position);
        },

        /**
         * Removes the specified process group.
         *
         * @param {string} processGroupIds      The process group id(s)
         */
        remove: function (processGroupIds) {
            var now = new Date().getTime();

            if ($.isArray(processGroupIds)) {
                $.each(processGroupIds, function (_, processGroupId) {
                    removedCache.set(processGroupId, now);
                    processGroupMap.remove(processGroupId);
                });
            } else {
                removedCache.set(processGroupIds, now);
                processGroupMap.remove(processGroupIds);
            }

            // apply the selection and handle all removed process groups
            select().exit().call(removeProcessGroups);
        },

        /**
         * Removes all process groups.
         */
        removeAll: function () {
            nfProcessGroup.remove(processGroupMap.keys());
        },

        /**
         * Expires the caches up to the specified timestamp.
         *
         * @param timestamp
         */
        expireCaches: function (timestamp) {
            var expire = function (cache) {
                cache.each(function (entryTimestamp, id) {
                    if (timestamp > entryTimestamp) {
                        cache.remove(id);
                    }
                });
            };

            expire(addedCache);
            expire(removedCache);
        },

        /**
         * Enters the specified group.
         *
         * @param {string} groupId
         */
        enterGroup: function (groupId) {

            // hide the context menu
            nfContextMenu.hide();

            // reload the graph
            return nfCanvasUtils.reload(groupId).done(function () {

                // attempt to restore the view
                var viewRestored = nfCanvasUtils.restoreUserView();

                // if the view was not restore attempt to fit
                if (viewRestored === false) {
                    nfCanvasUtils.fitCanvas();
                }

                // update URL deep linking params
                nfCanvasUtils.setURLParameters(groupId, d3.select());

            }).fail(function () {
                nfDialog.showOkDialog({
                    headerText: 'Process Group',
                    dialogContent: 'Unable to enter the selected group.'
                });
            });
        }
    };

    return nfProcessGroup;
}));