RED.diff = (function() {

    var currentDiff = {};
    var diffVisible = false;
    var diffList;

    function init() {

        // RED.actions.add("core:show-current-diff",showLocalDiff);
        RED.actions.add("core:show-remote-diff",showRemoteDiff);
        // RED.keyboard.add("*","ctrl-shift-l","core:show-current-diff");
        // RED.keyboard.add("*","ctrl-shift-r","core:show-remote-diff");


        // RED.actions.add("core:show-test-flow-diff-1",function(){showTestFlowDiff(1)});
        // RED.keyboard.add("*","ctrl-shift-f 1","core:show-test-flow-diff-1");
        //
        // RED.actions.add("core:show-test-flow-diff-2",function(){showTestFlowDiff(2)});
        // RED.keyboard.add("*","ctrl-shift-f 2","core:show-test-flow-diff-2");
        // RED.actions.add("core:show-test-flow-diff-3",function(){showTestFlowDiff(3)});
        // RED.keyboard.add("*","ctrl-shift-f 3","core:show-test-flow-diff-3");

    }
    function createDiffTable(container,CurrentDiff) {
        var diffList = $('<ol class="node-dialog-view-diff-diff"></ol>').appendTo(container);
        diffList.editableList({
            addButton: false,
            height: "auto",
            scrollOnAdd: false,
            addItem: function(container,i,object) {
                var localDiff = object.diff;
                var remoteDiff = object.remoteDiff;
                var tab = object.tab.n;
                var def = object.def;
                var conflicts = CurrentDiff.conflicts;

                var tabDiv = $('<div>',{class:"node-diff-tab"}).appendTo(container);
                tabDiv.addClass('collapsed');
                var titleRow = $('<div>',{class:"node-diff-tab-title"}).appendTo(tabDiv);
                var nodesDiv = $('<div>').appendTo(tabDiv);
                var originalCell = $('<div>',{class:"node-diff-node-entry-cell"}).appendTo(titleRow);
                var localCell = $('<div>',{class:"node-diff-node-entry-cell node-diff-node-local"}).appendTo(titleRow);
                var remoteCell;
                var selectState;

                if (remoteDiff) {
                    remoteCell = $('<div>',{class:"node-diff-node-entry-cell node-diff-node-remote"}).appendTo(titleRow);
                }
                $('<span class="node-diff-chevron"><i class="fa fa-angle-down"></i></span>').appendTo(originalCell);
                createNodeIcon(tab,def).appendTo(originalCell);
                var tabForLabel = (object.newTab || object.tab).n;
                var titleSpan = $('<span>',{class:"node-diff-tab-title-meta"}).appendTo(originalCell);
                if (tabForLabel.type === 'tab') {
                    titleSpan.html(tabForLabel.label||tabForLabel.id);
                } else if (tab.type === 'subflow') {
                    titleSpan.html((tabForLabel.name||tabForLabel.id));
                } else {
                    titleSpan.html(RED._("diff.globalNodes"));
                }
                var flowStats = {
                    local: {
                        addedCount:0,
                        deletedCount:0,
                        changedCount:0,
                        unchangedCount: 0
                    },
                    remote: {
                        addedCount:0,
                        deletedCount:0,
                        changedCount:0,
                        unchangedCount: 0
                    },
                    conflicts: 0
                }
                if (object.newTab || object.remoteTab) {
                    var localTabNode = {
                        node: localDiff.newConfig.all[tab.id],
                        all: localDiff.newConfig.all,
                        diff: localDiff
                    }
                    var remoteTabNode;
                    if (remoteDiff) {
                        remoteTabNode = {
                            node:remoteDiff.newConfig.all[tab.id]||null,
                            all: remoteDiff.newConfig.all,
                            diff: remoteDiff
                        }
                    }
                    if (tab.type !== undefined) {
                        var div = $("<div>",{class:"node-diff-node-entry node-diff-node-props collapsed"}).appendTo(nodesDiv);
                        var row = $("<div>",{class:"node-diff-node-entry-header"}).appendTo(div);
                        var originalNodeDiv = $("<div>",{class:"node-diff-node-entry-cell"}).appendTo(row);
                        var localNodeDiv = $("<div>",{class:"node-diff-node-entry-cell node-diff-node-local"}).appendTo(row);
                        var localChanged = false;
                        var remoteChanged = false;

                        if (!localDiff.newConfig.all[tab.id]) {
                            localNodeDiv.addClass("node-diff-empty");
                        } else if (localDiff.added[tab.id]) {
                            localNodeDiv.addClass("node-diff-node-added");
                            localChanged = true;
                            $('<span class="node-diff-status"><i class="fa fa-plus-square"></i> <span data-i18n="diff.type.added"></span></span>').appendTo(localNodeDiv);
                        } else if (localDiff.changed[tab.id]) {
                            localNodeDiv.addClass("node-diff-node-changed");
                            localChanged = true;
                            $('<span class="node-diff-status"><i class="fa fa-square"></i> <span data-i18n="diff.type.changed"></span></span>').appendTo(localNodeDiv);
                        } else {
                            localNodeDiv.addClass("node-diff-node-unchanged");
                            $('<span class="node-diff-status"><i class="fa fa-square-o"></i> <span data-i18n="diff.type.unchanged"></span></span>').appendTo(localNodeDiv);
                        }

                        var remoteNodeDiv;
                        if (remoteDiff) {
                            remoteNodeDiv = $("<div>",{class:"node-diff-node-entry-cell node-diff-node-remote"}).appendTo(row);
                            if (!remoteDiff.newConfig.all[tab.id]) {
                                remoteNodeDiv.addClass("node-diff-empty");
                                if (remoteDiff.deleted[tab.id]) {
                                    remoteChanged = true;
                                }
                            } else if (remoteDiff.added[tab.id]) {
                                remoteNodeDiv.addClass("node-diff-node-added");
                                remoteChanged = true;
                                $('<span class="node-diff-status"><i class="fa fa-plus-square"></i> <span data-i18n="diff.type.added"></span></span>').appendTo(remoteNodeDiv);
                            } else if (remoteDiff.changed[tab.id]) {
                                remoteNodeDiv.addClass("node-diff-node-changed");
                                remoteChanged = true;
                                $('<span class="node-diff-status"><i class="fa fa-square"></i> <span data-i18n="diff.type.changed"></span></span>').appendTo(remoteNodeDiv);
                            } else {
                                remoteNodeDiv.addClass("node-diff-node-unchanged");
                                $('<span class="node-diff-status"><i class="fa fa-square-o"></i> <span data-i18n="diff.type.unchanged"></span></span>').appendTo(remoteNodeDiv);
                            }
                        }
                        $('<span class="node-diff-chevron"><i class="fa fa-angle-down"></i></span>').appendTo(originalNodeDiv);
                        $('<span>').html(RED._("diff.flowProperties")).appendTo(originalNodeDiv);

                        row.click(function(evt) {
                            evt.preventDefault();
                            $(this).parent().toggleClass('collapsed');
                        });

                        createNodePropertiesTable(def,tab,localTabNode,remoteTabNode,conflicts).appendTo(div);
                        selectState = "";
                        if (conflicts[tab.id]) {
                            flowStats.conflicts++;

                            if (!localNodeDiv.hasClass("node-diff-empty")) {
                                $('<span class="node-diff-node-conflict"><span class="node-diff-status"><i class="fa fa-exclamation"></i></span></span>').prependTo(localNodeDiv);
                            }
                            if (!remoteNodeDiv.hasClass("node-diff-empty")) {
                                $('<span class="node-diff-node-conflict"><span class="node-diff-status"><i class="fa fa-exclamation"></i></span></span>').prependTo(remoteNodeDiv);
                            }
                            div.addClass("node-diff-node-entry-conflict");
                        } else {
                            selectState = CurrentDiff.resolutions[tab.id];
                        }
                        // Tab properties row
                        createNodeConflictRadioBoxes(tab,div,localNodeDiv,remoteNodeDiv,true,!conflicts[tab.id],selectState,CurrentDiff);
                    }
                }
                // var stats = $('<span>',{class:"node-diff-tab-stats"}).appendTo(titleRow);
                var localNodeCount = 0;
                var remoteNodeCount = 0;
                var seen = {};
                object.tab.nodes.forEach(function(node) {
                    seen[node.id] = true;
                    createNodeDiffRow(node,flowStats,CurrentDiff).appendTo(nodesDiv)
                });
                if (object.newTab) {
                    localNodeCount = object.newTab.nodes.length;
                    object.newTab.nodes.forEach(function(node) {
                        if (!seen[node.id]) {
                            seen[node.id] = true;
                            createNodeDiffRow(node,flowStats,CurrentDiff).appendTo(nodesDiv)
                        }
                    });
                }
                if (object.remoteTab) {
                    remoteNodeCount = object.remoteTab.nodes.length;
                    object.remoteTab.nodes.forEach(function(node) {
                        if (!seen[node.id]) {
                            createNodeDiffRow(node,flowStats,CurrentDiff).appendTo(nodesDiv)
                        }
                    });
                }
                titleRow.click(function(evt) {
                    // if (titleRow.parent().find(".node-diff-node-entry:not(.hide)").length > 0) {
                    titleRow.parent().toggleClass('collapsed');
                    if ($(this).parent().hasClass('collapsed')) {
                        $(this).parent().find('.node-diff-node-entry').addClass('collapsed');
                        $(this).parent().find('.debug-message-element').addClass('collapsed');
                    }
                    // }
                })

                if (localDiff.deleted[tab.id]) {
                    $('<span class="node-diff-node-deleted"><span class="node-diff-status"><i class="fa fa-minus-square"></i> <span data-i18n="diff.type.flowDeleted"></span></span></span>').appendTo(localCell);
                } else if (object.newTab) {
                    if (localDiff.added[tab.id]) {
                        $('<span class="node-diff-node-added"><span class="node-diff-status"><i class="fa fa-plus-square"></i> <span data-i18n="diff.type.flowAdded"></span></span></span>').appendTo(localCell);
                    } else {
                        if (tab.id) {
                            if (localDiff.changed[tab.id]) {
                                flowStats.local.changedCount++;
                            } else {
                                flowStats.local.unchangedCount++;
                            }
                        }
                        var localStats = $('<span>',{class:"node-diff-tab-stats"}).appendTo(localCell);
                        $('<span class="node-diff-status"></span>').html(RED._('diff.nodeCount',{count:localNodeCount})).appendTo(localStats);

                        if (flowStats.conflicts + flowStats.local.addedCount + flowStats.local.changedCount + flowStats.local.deletedCount > 0) {
                            $('<span class="node-diff-status"> [ </span>').appendTo(localStats);
                            if (flowStats.conflicts > 0) {
                                $('<span class="node-diff-node-conflict"><span class="node-diff-status"><i class="fa fa-exclamation"></i> '+flowStats.conflicts+'</span></span>').appendTo(localStats);
                            }
                            if (flowStats.local.addedCount > 0) {
                                $('<span class="node-diff-node-added"><span class="node-diff-status"><i class="fa fa-plus-square"></i> '+flowStats.local.addedCount+'</span></span>').appendTo(localStats);
                            }
                            if (flowStats.local.changedCount > 0) {
                                $('<span class="node-diff-node-changed"><span class="node-diff-status"><i class="fa fa-square"></i> '+flowStats.local.changedCount+'</span></span>').appendTo(localStats);
                            }
                            if (flowStats.local.deletedCount > 0) {
                                $('<span class="node-diff-node-deleted"><span class="node-diff-status"><i class="fa fa-minus-square"></i> '+flowStats.local.deletedCount+'</span></span>').appendTo(localStats);
                            }
                            $('<span class="node-diff-status"> ] </span>').appendTo(localStats);
                        }

                    }
                } else {
                    localCell.addClass("node-diff-empty");
                }

                if (remoteDiff) {
                    if (remoteDiff.deleted[tab.id]) {
                        $('<span class="node-diff-node-deleted"><span class="node-diff-status"><i class="fa fa-minus-square"></i> <span data-i18n="diff.type.flowDeleted"></span></span></span>').appendTo(remoteCell);
                    } else if (object.remoteTab) {
                        if (remoteDiff.added[tab.id]) {
                            $('<span class="node-diff-node-added"><span class="node-diff-status"><i class="fa fa-plus-square"></i> <span data-i18n="diff.type.flowAdded"></span></span></span>').appendTo(remoteCell);
                        } else {
                            if (tab.id) {
                                if (remoteDiff.changed[tab.id]) {
                                    flowStats.remote.changedCount++;
                                } else {
                                    flowStats.remote.unchangedCount++;
                                }
                            }
                            var remoteStats = $('<span>',{class:"node-diff-tab-stats"}).appendTo(remoteCell);
                            $('<span class="node-diff-status"></span>').html(RED._('diff.nodeCount',{count:remoteNodeCount})).appendTo(remoteStats);
                            if (flowStats.conflicts + flowStats.remote.addedCount + flowStats.remote.changedCount + flowStats.remote.deletedCount > 0) {
                                $('<span class="node-diff-status"> [ </span>').appendTo(remoteStats);
                                if (flowStats.conflicts > 0) {
                                    $('<span class="node-diff-node-conflict"><span class="node-diff-status"><i class="fa fa-exclamation"></i> '+flowStats.conflicts+'</span></span>').appendTo(remoteStats);
                                }
                                if (flowStats.remote.addedCount > 0) {
                                    $('<span class="node-diff-node-added"><span class="node-diff-status"><i class="fa fa-plus-square"></i> '+flowStats.remote.addedCount+'</span></span>').appendTo(remoteStats);
                                }
                                if (flowStats.remote.changedCount > 0) {
                                    $('<span class="node-diff-node-changed"><span class="node-diff-status"><i class="fa fa-square"></i> '+flowStats.remote.changedCount+'</span></span>').appendTo(remoteStats);
                                }
                                if (flowStats.remote.deletedCount > 0) {
                                    $('<span class="node-diff-node-deleted"><span class="node-diff-status"><i class="fa fa-minus-square"></i> '+flowStats.remote.deletedCount+'</span></span>').appendTo(remoteStats);
                                }
                                $('<span class="node-diff-status"> ] </span>').appendTo(remoteStats);
                            }
                        }
                    } else {
                        remoteCell.addClass("node-diff-empty");
                    }
                    selectState = "";
                    if (flowStats.conflicts > 0) {
                        titleRow.addClass("node-diff-node-entry-conflict");
                    } else {
                        selectState = CurrentDiff.resolutions[tab.id];
                    }
                    if (tab.id) {
                        var hide = !(flowStats.conflicts > 0 &&(localDiff.deleted[tab.id] || remoteDiff.deleted[tab.id]));
                        // Tab parent row
                        createNodeConflictRadioBoxes(tab,titleRow,localCell,remoteCell, false, hide, selectState, CurrentDiff);
                    }
                }

                if (tabDiv.find(".node-diff-node-entry").length === 0) {
                    tabDiv.addClass("node-diff-tab-empty");
                }
                container.i18n();
            }
        });
        return diffList;
    }
    function buildDiffPanel(container,diff,options) {
        var diffPanel = $('<div class="node-dialog-view-diff-panel"></div>').appendTo(container);
        var diffHeaders = $('<div class="node-dialog-view-diff-headers"></div>').appendTo(diffPanel);
        if (options.mode === "merge") {
            diffPanel.addClass("node-dialog-view-diff-panel-merge");
        }
        var diffList = createDiffTable(diffPanel, diff);

        var localDiff = diff.localDiff;
        var remoteDiff = diff.remoteDiff;
        var conflicts = diff.conflicts;

        var currentConfig = localDiff.currentConfig;
        var newConfig = localDiff.newConfig;


        if (remoteDiff !== undefined) {
            diffPanel.addClass('node-diff-three-way');
            var localTitle = options.oldRevTitle || RED._('diff.local');
            var remoteTitle = options.newRevTitle || RED._('diff.remote');
            $('<div></div>').text(localTitle).appendTo(diffHeaders);
            $('<div></div>').text(remoteTitle).appendTo(diffHeaders);
        } else {
            diffPanel.removeClass('node-diff-three-way');
        }

        return {
            list: diffList,
            finish: function() {
                var el = {
                    diff: localDiff,
                    def: {
                        category: 'config',
                        color: '#f0f0f0'
                    },
                    tab: {
                        n: {},
                        nodes: currentConfig.globals
                    },
                    newTab: {
                        n: {},
                        nodes: newConfig.globals
                    }
                };
                if (remoteDiff !== undefined) {
                    el.remoteTab = {
                        n:{},
                        nodes:remoteDiff.newConfig.globals
                    };
                    el.remoteDiff = remoteDiff;
                }
                diffList.editableList('addItem',el);

                var seenTabs = {};

                currentConfig.tabOrder.forEach(function(tabId) {
                    var tab = currentConfig.tabs[tabId];
                    var el = {
                        diff: localDiff,
                        def: RED.nodes.getType('tab'),
                        tab:tab
                    };
                    if (newConfig.tabs.hasOwnProperty(tabId)) {
                        el.newTab = newConfig.tabs[tabId];
                    }
                    if (remoteDiff !== undefined) {
                        el.remoteTab = remoteDiff.newConfig.tabs[tabId];
                        el.remoteDiff = remoteDiff;
                    }
                    seenTabs[tabId] = true;
                    diffList.editableList('addItem',el)
                });
                newConfig.tabOrder.forEach(function(tabId) {
                    if (!seenTabs[tabId]) {
                        seenTabs[tabId] = true;
                        var tab = newConfig.tabs[tabId];
                        var el = {
                            diff: localDiff,
                            def: RED.nodes.getType('tab'),
                            tab:tab,
                            newTab: tab
                        };
                        if (remoteDiff !== undefined) {
                            el.remoteDiff = remoteDiff;
                        }
                        diffList.editableList('addItem',el)
                    }
                });
                if (remoteDiff !== undefined) {
                    remoteDiff.newConfig.tabOrder.forEach(function(tabId) {
                        if (!seenTabs[tabId]) {
                            var tab = remoteDiff.newConfig.tabs[tabId];
                            // TODO how to recognise this is a remotely added flow
                            var el = {
                                diff: localDiff,
                                remoteDiff: remoteDiff,
                                def: RED.nodes.getType('tab'),
                                tab:tab,
                                remoteTab:tab
                            };
                            diffList.editableList('addItem',el)
                        }
                    });
                }
                var subflowId;
                for (subflowId in currentConfig.subflows) {
                    if (currentConfig.subflows.hasOwnProperty(subflowId)) {
                        seenTabs[subflowId] = true;
                        el = {
                            diff: localDiff,
                            def: {
                                defaults:{},
                                icon:"subflow.png",
                                category: "subflows",
                                color: "#da9"
                            },
                            tab:currentConfig.subflows[subflowId]
                        }
                        if (newConfig.subflows.hasOwnProperty(subflowId)) {
                            el.newTab = newConfig.subflows[subflowId];
                        }
                        if (remoteDiff !== undefined) {
                            el.remoteTab = remoteDiff.newConfig.subflows[subflowId];
                            el.remoteDiff = remoteDiff;
                        }
                        diffList.editableList('addItem',el)
                    }
                }
                for (subflowId in newConfig.subflows) {
                    if (newConfig.subflows.hasOwnProperty(subflowId) && !seenTabs[subflowId]) {
                        seenTabs[subflowId] = true;
                        el = {
                            diff: localDiff,
                            def: {
                                defaults:{},
                                icon:"subflow.png",
                                category: "subflows",
                                color: "#da9"
                            },
                            tab:newConfig.subflows[subflowId],
                            newTab:newConfig.subflows[subflowId]
                        }
                        if (remoteDiff !== undefined) {
                            el.remoteDiff = remoteDiff;
                        }
                        diffList.editableList('addItem',el)
                    }
                }
                if (remoteDiff !== undefined) {
                    for (subflowId in remoteDiff.newConfig.subflows) {
                        if (remoteDiff.newConfig.subflows.hasOwnProperty(subflowId) && !seenTabs[subflowId]) {
                            el = {
                                diff: localDiff,
                                remoteDiff: remoteDiff,
                                def: {
                                    defaults:{},
                                    icon:"subflow.png",
                                    category: "subflows",
                                    color: "#da9"
                                },
                                tab:remoteDiff.newConfig.subflows[subflowId],
                                remoteTab: remoteDiff.newConfig.subflows[subflowId]
                            }
                            diffList.editableList('addItem',el)
                        }
                    }
                }
            }
        };
    }
    function formatWireProperty(wires,allNodes) {
        var result = $("<div>",{class:"node-diff-property-wires"})
        var list = $("<ol></ol>");
        var c = 0;
        wires.forEach(function(p,i) {
            var port = $("<li>").appendTo(list);
            if (p && p.length > 0) {
                $("<span>").html(i+1).appendTo(port);
                var links = $("<ul>").appendTo(port);
                p.forEach(function(d) {
                    c++;
                    var entry = $("<li>").appendTo(links);
                    var node = allNodes[d];
                    if (node) {
                        var def = RED.nodes.getType(node.type)||{};
                        createNode(node,def).appendTo(entry);
                    } else {
                        entry.html(d);
                    }
                })
            } else {
                port.html('none');
            }
        })
        if (c === 0) {
            result.html("none");
        } else {
            list.appendTo(result);
        }
        return result;
    }
    function createNodeIcon(node,def) {
        var nodeDiv = $("<div>",{class:"node-diff-node-entry-node"});
        var colour = def.color;
        var icon_url = RED.utils.getNodeIcon(def,node);
        if (node.type === 'tab') {
            colour = "#C0DEED";
        }
        nodeDiv.css('backgroundColor',colour);

        var iconContainer = $('<div/>',{class:"palette_icon_container"}).appendTo(nodeDiv);
        $('<div/>',{class:"palette_icon",style:"background-image: url("+icon_url+")"}).appendTo(iconContainer);

        return nodeDiv;
    }
    function createNode(node,def) {
        var nodeTitleDiv = $("<div>",{class:"node-diff-node-entry-title"})
        createNodeIcon(node,def).appendTo(nodeTitleDiv);
        var contentDiv = $('<div>',{class:"node-diff-node-description"}).appendTo(nodeTitleDiv);
        var nodeLabel = node.label || node.name || node.id;
        $('<span>',{class:"node-diff-node-label"}).html(nodeLabel).appendTo(contentDiv);
        return nodeTitleDiv;
    }
    function createNodeDiffRow(node,stats,CurrentDiff) {
        var localDiff = CurrentDiff.localDiff;
        var remoteDiff = CurrentDiff.remoteDiff;
        var conflicted = CurrentDiff.conflicts[node.id];

        var hasChanges = false; // exists in original and local/remote but with changes
        var unChanged = true; // existing in original,local,remote unchanged
        var localChanged = false;

        if (localDiff.added[node.id]) {
            stats.local.addedCount++;
            unChanged = false;
        }
        if (remoteDiff && remoteDiff.added[node.id]) {
            stats.remote.addedCount++;
            unChanged = false;
        }
        if (localDiff.deleted[node.id]) {
            stats.local.deletedCount++;
            unChanged = false;
        }
        if (remoteDiff && remoteDiff.deleted[node.id]) {
            stats.remote.deletedCount++;
            unChanged = false;
        }
        if (localDiff.changed[node.id]) {
            stats.local.changedCount++;
            hasChanges = true;
            unChanged = false;
        }
        if (remoteDiff && remoteDiff.changed[node.id]) {
            stats.remote.changedCount++;
            hasChanges = true;
            unChanged = false;
        }
        // console.log(node.id,localDiff.added[node.id],remoteDiff.added[node.id],localDiff.deleted[node.id],remoteDiff.deleted[node.id],localDiff.changed[node.id],remoteDiff.changed[node.id])
        var def = RED.nodes.getType(node.type);
        if (def === undefined) {
            if (/^subflow:/.test(node.type)) {
                def = {
                    icon:"subflow.png",
                    category: "subflows",
                    color: "#da9",
                    defaults:{name:{value:""}}
                }
            } else {
                def = {};
            }
        }
        var div = $("<div>",{class:"node-diff-node-entry collapsed"});
        var row = $("<div>",{class:"node-diff-node-entry-header"}).appendTo(div);

        var originalNodeDiv = $("<div>",{class:"node-diff-node-entry-cell"}).appendTo(row);
        var localNodeDiv = $("<div>",{class:"node-diff-node-entry-cell node-diff-node-local"}).appendTo(row);
        var remoteNodeDiv;
        var chevron;
        if (remoteDiff) {
            remoteNodeDiv = $("<div>",{class:"node-diff-node-entry-cell node-diff-node-remote"}).appendTo(row);
        }
        $('<span class="node-diff-chevron"><i class="fa fa-angle-down"></i></span>').appendTo(originalNodeDiv);

        if (unChanged) {
            stats.local.unchangedCount++;
            createNode(node,def).appendTo(originalNodeDiv);
            localNodeDiv.addClass("node-diff-node-unchanged");
            $('<span class="node-diff-status"><i class="fa fa-square-o"></i> <span data-i18n="diff.type.unchanged"></span></span>').appendTo(localNodeDiv);
            if (remoteDiff) {
                stats.remote.unchangedCount++;
                remoteNodeDiv.addClass("node-diff-node-unchanged");
                $('<span class="node-diff-status"><i class="fa fa-square-o"></i> <span data-i18n="diff.type.unchanged"></span></span>').appendTo(remoteNodeDiv);
            }
            div.addClass("node-diff-node-unchanged");
        } else if (localDiff.added[node.id]) {
            localNodeDiv.addClass("node-diff-node-added");
            if (remoteNodeDiv) {
                remoteNodeDiv.addClass("node-diff-empty");
            }
            $('<span class="node-diff-status"><i class="fa fa-plus-square"></i> <span data-i18n="diff.type.added"></span></span>').appendTo(localNodeDiv);
            createNode(node,def).appendTo(originalNodeDiv);
        } else if (remoteDiff && remoteDiff.added[node.id]) {
            localNodeDiv.addClass("node-diff-empty");
            remoteNodeDiv.addClass("node-diff-node-added");
            $('<span class="node-diff-status"><i class="fa fa-plus-square"></i> <span data-i18n="diff.type.added"></span></span>').appendTo(remoteNodeDiv);
            createNode(node,def).appendTo(originalNodeDiv);
        } else {
            createNode(node,def).appendTo(originalNodeDiv);
            if (localDiff.moved[node.id]) {
                var localN = localDiff.newConfig.all[node.id];
                if (!localDiff.deleted[node.z] && node.z !== localN.z && node.z !== "" && !localDiff.newConfig.all[node.z]) {
                    localNodeDiv.addClass("node-diff-empty");
                } else {
                    localNodeDiv.addClass("node-diff-node-moved");
                    var localMovedMessage = "";
                    if (node.z === localN.z) {
                        localMovedMessage = RED._("diff.type.movedFrom",{id:(localDiff.currentConfig.all[node.id].z||'global')});
                    } else {
                        localMovedMessage = RED._("diff.type.movedTo",{id:(localN.z||'global')});
                    }
                    $('<span class="node-diff-status"><i class="fa fa-caret-square-o-right"></i> '+localMovedMessage+'</span>').appendTo(localNodeDiv);
                }
                localChanged = true;
            } else if (localDiff.deleted[node.z]) {
                localNodeDiv.addClass("node-diff-empty");
                localChanged = true;
            } else if (localDiff.deleted[node.id]) {
                localNodeDiv.addClass("node-diff-node-deleted");
                $('<span class="node-diff-status"><i class="fa fa-minus-square"></i> <span data-i18n="diff.type.deleted"></span></span>').appendTo(localNodeDiv);
                localChanged = true;
            } else if (localDiff.changed[node.id]) {
                if (localDiff.newConfig.all[node.id].z !== node.z) {
                    localNodeDiv.addClass("node-diff-empty");
                } else {
                    localNodeDiv.addClass("node-diff-node-changed");
                    $('<span class="node-diff-status"><i class="fa fa-square"></i> <span data-i18n="diff.type.changed"></span></span>').appendTo(localNodeDiv);
                    localChanged = true;
                }
            } else {
                if (localDiff.newConfig.all[node.id].z !== node.z) {
                    localNodeDiv.addClass("node-diff-empty");
                } else {
                    stats.local.unchangedCount++;
                    localNodeDiv.addClass("node-diff-node-unchanged");
                    $('<span class="node-diff-status"><i class="fa fa-square-o"></i> <span data-i18n="diff.type.unchanged"></span></span>').appendTo(localNodeDiv);
                }
            }

            if (remoteDiff) {
                if (remoteDiff.moved[node.id]) {
                    var remoteN = remoteDiff.newConfig.all[node.id];
                    if (!remoteDiff.deleted[node.z] && node.z !== remoteN.z && node.z !== "" && !remoteDiff.newConfig.all[node.z]) {
                        remoteNodeDiv.addClass("node-diff-empty");
                    } else {
                        remoteNodeDiv.addClass("node-diff-node-moved");
                        var remoteMovedMessage = "";
                        if (node.z === remoteN.z) {
                            remoteMovedMessage = RED._("diff.type.movedFrom",{id:(remoteDiff.currentConfig.all[node.id].z||'global')});
                        } else {
                            remoteMovedMessage = RED._("diff.type.movedTo",{id:(remoteN.z||'global')});
                        }
                        $('<span class="node-diff-status"><i class="fa fa-caret-square-o-right"></i> '+remoteMovedMessage+'</span>').appendTo(remoteNodeDiv);
                    }
                } else if (remoteDiff.deleted[node.z]) {
                    remoteNodeDiv.addClass("node-diff-empty");
                } else if (remoteDiff.deleted[node.id]) {
                    remoteNodeDiv.addClass("node-diff-node-deleted");
                    $('<span class="node-diff-status"><i class="fa fa-minus-square"></i> <span data-i18n="diff.type.deleted"></span></span>').appendTo(remoteNodeDiv);
                } else if (remoteDiff.changed[node.id]) {
                    if (remoteDiff.newConfig.all[node.id].z !== node.z) {
                        remoteNodeDiv.addClass("node-diff-empty");
                    } else {
                        remoteNodeDiv.addClass("node-diff-node-changed");
                        $('<span class="node-diff-status"><i class="fa fa-square"></i> <span data-i18n="diff.type.changed"></span></span>').appendTo(remoteNodeDiv);
                    }
                } else {
                    if (remoteDiff.newConfig.all[node.id].z !== node.z) {
                        remoteNodeDiv.addClass("node-diff-empty");
                    } else {
                        stats.remote.unchangedCount++;
                        remoteNodeDiv.addClass("node-diff-node-unchanged");
                        $('<span class="node-diff-status"><i class="fa fa-square-o"></i> <span data-i18n="diff.type.unchanged"></span></span>').appendTo(remoteNodeDiv);
                    }
                }
            }
        }
        var localNode = {
            node: localDiff.newConfig.all[node.id],
            all: localDiff.newConfig.all,
            diff: localDiff
        };
        var remoteNode;
        if (remoteDiff) {
            remoteNode = {
                node:remoteDiff.newConfig.all[node.id]||null,
                all: remoteDiff.newConfig.all,
                diff: remoteDiff
            }
        }
        createNodePropertiesTable(def,node,localNode,remoteNode).appendTo(div);

        var selectState = "";

        if (conflicted) {
            stats.conflicts++;
            if (!localNodeDiv.hasClass("node-diff-empty")) {
                $('<span class="node-diff-node-conflict"><span class="node-diff-status"><i class="fa fa-exclamation"></i></span></span>').prependTo(localNodeDiv);
            }
            if (!remoteNodeDiv.hasClass("node-diff-empty")) {
                $('<span class="node-diff-node-conflict"><span class="node-diff-status"><i class="fa fa-exclamation"></i></span></span>').prependTo(remoteNodeDiv);
            }
            div.addClass("node-diff-node-entry-conflict");
        } else {
            selectState = CurrentDiff.resolutions[node.id];
        }
        // Node row
        createNodeConflictRadioBoxes(node,div,localNodeDiv,remoteNodeDiv,false,!conflicted,selectState,CurrentDiff);
        row.click(function(evt) {
            $(this).parent().toggleClass('collapsed');
        });

        return div;
    }
    function createNodePropertiesTable(def,node,localNodeObj,remoteNodeObj) {
        var propertyElements = {};
        var localNode = localNodeObj.node;
        var remoteNode;
        if (remoteNodeObj) {
            remoteNode = remoteNodeObj.node;
        }

        var nodePropertiesDiv = $("<div>",{class:"node-diff-node-entry-properties"});
        var nodePropertiesTable = $("<table>").appendTo(nodePropertiesDiv);
        var nodePropertiesTableCols = $('<colgroup><col/><col/></colgroup>').appendTo(nodePropertiesTable);
        if (remoteNode !== undefined) {
            $("<col/>").appendTo(nodePropertiesTableCols);
        }
        var nodePropertiesTableBody = $("<tbody>").appendTo(nodePropertiesTable);

        var row;
        var localCell, remoteCell;
        var element;
        var currentValue, localValue, remoteValue;
        var localChanged = false;
        var remoteChanged = false;
        var localChanges = 0;
        var remoteChanges = 0;
        var conflict = false;
        var status;

        row = $("<tr>").appendTo(nodePropertiesTableBody);
        $("<td>",{class:"node-diff-property-cell-label"}).html("id").appendTo(row);
        localCell = $("<td>",{class:"node-diff-property-cell node-diff-node-local"}).appendTo(row);
        if (localNode) {
            localCell.addClass("node-diff-node-unchanged");
            $('<span class="node-diff-status"></span>').appendTo(localCell);
            element = $('<span class="node-diff-element"></span>').appendTo(localCell);
            propertyElements['local.id'] = RED.utils.createObjectElement(localNode.id).appendTo(element);
        } else {
            localCell.addClass("node-diff-empty");
        }
        if (remoteNode !== undefined) {
            remoteCell = $("<td>",{class:"node-diff-property-cell node-diff-node-remote"}).appendTo(row);
            remoteCell.addClass("node-diff-node-unchanged");
            if (remoteNode) {
                $('<span class="node-diff-status"></span>').appendTo(remoteCell);
                element = $('<span class="node-diff-element"></span>').appendTo(remoteCell);
                propertyElements['remote.id'] = RED.utils.createObjectElement(remoteNode.id).appendTo(element);
            } else {
                remoteCell.addClass("node-diff-empty");
            }
        }


        if (node.hasOwnProperty('x')) {
            if (localNode) {
                if (localNode.x !== node.x || localNode.y !== node.y) {
                    localChanged = true;
                    localChanges++;
                }
            }
            if (remoteNode) {
                if (remoteNode.x !== node.x || remoteNode.y !== node.y) {
                    remoteChanged = true;
                    remoteChanges++;
                }
            }
            if ( (remoteChanged && localChanged && (localNode.x !== remoteNode.x || localNode.y !== remoteNode.y)) ||
                (!localChanged && remoteChanged && localNodeObj.diff.deleted[node.id]) ||
                (localChanged && !remoteChanged && remoteNodeObj.diff.deleted[node.id])
            ) {
                conflict = true;
            }
            row = $("<tr>").appendTo(nodePropertiesTableBody);
            $("<td>",{class:"node-diff-property-cell-label"}).html("position").appendTo(row);
            localCell = $("<td>",{class:"node-diff-property-cell node-diff-node-local"}).appendTo(row);
            if (localNode) {
                localCell.addClass("node-diff-node-"+(localChanged?"changed":"unchanged"));
                $('<span class="node-diff-status">'+(localChanged?'<i class="fa fa-square"></i>':'')+'</span>').appendTo(localCell);
                element = $('<span class="node-diff-element"></span>').appendTo(localCell);
                propertyElements['local.position'] = RED.utils.createObjectElement({x:localNode.x,y:localNode.y},
                    {
                        path: "position",
                        exposeApi: true,
                        ontoggle: function(path,state) {
                            if (propertyElements['remote.'+path]) {
                                propertyElements['remote.'+path].prop('expand')(path,state)
                            }
                        }
                    }
                ).appendTo(element);
            } else {
                localCell.addClass("node-diff-empty");
            }

            if (remoteNode !== undefined) {
                remoteCell = $("<td>",{class:"node-diff-property-cell node-diff-node-remote"}).appendTo(row);
                remoteCell.addClass("node-diff-node-"+(remoteChanged?"changed":"unchanged"));
                if (remoteNode) {
                    $('<span class="node-diff-status">'+(remoteChanged?'<i class="fa fa-square"></i>':'')+'</span>').appendTo(remoteCell);
                    element = $('<span class="node-diff-element"></span>').appendTo(remoteCell);
                    propertyElements['remote.position'] = RED.utils.createObjectElement({x:remoteNode.x,y:remoteNode.y},
                        {
                            path: "position",
                            exposeApi: true,
                            ontoggle: function(path,state) {
                                if (propertyElements['local.'+path]) {
                                    propertyElements['local.'+path].prop('expand')(path,state);
                                }
                            }
                        }
                    ).appendTo(element);
                } else {
                    remoteCell.addClass("node-diff-empty");
                }
            }
        }
        //
        localChanged = remoteChanged = conflict = false;
        if (node.hasOwnProperty('wires')) {
            currentValue = JSON.stringify(node.wires);
            if (localNode) {
                localValue = JSON.stringify(localNode.wires);
                if (currentValue !== localValue) {
                    localChanged = true;
                    localChanges++;
                }
            }
            if (remoteNode) {
                remoteValue = JSON.stringify(remoteNode.wires);
                if (currentValue !== remoteValue) {
                    remoteChanged = true;
                    remoteChanges++;
                }
            }
            if ( (remoteChanged && localChanged && (localValue !== remoteValue)) ||
                (!localChanged && remoteChanged && localNodeObj.diff.deleted[node.id]) ||
                (localChanged && !remoteChanged && remoteNodeObj.diff.deleted[node.id])
            ){
                conflict = true;
            }
            row = $("<tr>").appendTo(nodePropertiesTableBody);
            $("<td>",{class:"node-diff-property-cell-label"}).html("wires").appendTo(row);
            localCell = $("<td>",{class:"node-diff-property-cell node-diff-node-local"}).appendTo(row);
            if (localNode) {
                if (!conflict) {
                    localCell.addClass("node-diff-node-"+(localChanged?"changed":"unchanged"));
                    $('<span class="node-diff-status">'+(localChanged?'<i class="fa fa-square"></i>':'')+'</span>').appendTo(localCell);
                } else {
                    localCell.addClass("node-diff-node-conflict");
                    $('<span class="node-diff-status"><i class="fa fa-exclamation"></i></span>').appendTo(localCell);
                }
                formatWireProperty(localNode.wires,localNodeObj.all).appendTo(localCell);
            } else {
                localCell.addClass("node-diff-empty");
            }

            if (remoteNode !== undefined) {
                remoteCell = $("<td>",{class:"node-diff-property-cell node-diff-node-remote"}).appendTo(row);
                if (remoteNode) {
                    if (!conflict) {
                        remoteCell.addClass("node-diff-node-"+(remoteChanged?"changed":"unchanged"));
                        $('<span class="node-diff-status">'+(remoteChanged?'<i class="fa fa-square"></i>':'')+'</span>').appendTo(remoteCell);
                    } else {
                        remoteCell.addClass("node-diff-node-conflict");
                        $('<span class="node-diff-status"><i class="fa fa-exclamation"></i></span>').appendTo(remoteCell);
                    }
                    formatWireProperty(remoteNode.wires,remoteNodeObj.all).appendTo(remoteCell);
                } else {
                    remoteCell.addClass("node-diff-empty");
                }
            }
        }

        var properties = Object.keys(node).filter(function(p) { return p!='inputLabels'&&p!='outputLabels'&&p!='z'&&p!='wires'&&p!=='x'&&p!=='y'&&p!=='id'&&p!=='type'&&(!def.defaults||!def.defaults.hasOwnProperty(p))});
        if (def.defaults) {
            properties = properties.concat(Object.keys(def.defaults));
        }
        if (node.type !== 'tab') {
            properties = properties.concat(['inputLabels','outputLabels']);
        }
        properties.forEach(function(d) {
            localChanged = false;
            remoteChanged = false;
            conflict = false;
            currentValue = JSON.stringify(node[d]);
            if (localNode) {
                localValue = JSON.stringify(localNode[d]);
                if (currentValue !== localValue) {
                    localChanged = true;
                    localChanges++;
                }
            }
            if (remoteNode) {
                remoteValue = JSON.stringify(remoteNode[d]);
                if (currentValue !== remoteValue) {
                    remoteChanged = true;
                    remoteChanges++;
                }
            }

            if ( (remoteChanged && localChanged && (localValue !== remoteValue)) ||
                (!localChanged &&  remoteChanged && localNodeObj.diff.deleted[node.id]) ||
                (localChanged && !remoteChanged && remoteNodeObj.diff.deleted[node.id])
            ){
                conflict = true;
            }

            row = $("<tr>").appendTo(nodePropertiesTableBody);
            var propertyNameCell = $("<td>",{class:"node-diff-property-cell-label"}).html(d).appendTo(row);
            localCell = $("<td>",{class:"node-diff-property-cell node-diff-node-local"}).appendTo(row);
            if (localNode) {
                if (!conflict) {
                    localCell.addClass("node-diff-node-"+(localChanged?"changed":"unchanged"));
                    $('<span class="node-diff-status">'+(localChanged?'<i class="fa fa-square"></i>':'')+'</span>').appendTo(localCell);
                } else {
                    localCell.addClass("node-diff-node-conflict");
                    $('<span class="node-diff-status"><i class="fa fa-exclamation"></i></span>').appendTo(localCell);
                }
                element = $('<span class="node-diff-element"></span>').appendTo(localCell);
                propertyElements['local.'+d] = RED.utils.createObjectElement(localNode[d],
                    {
                        path: d,
                        exposeApi: true,
                        ontoggle: function(path,state) {
                            if (propertyElements['remote.'+d]) {
                                propertyElements['remote.'+d].prop('expand')(path,state)
                            }
                        }
                    }
                ).appendTo(element);
            } else {
                localCell.addClass("node-diff-empty");
            }
            if (remoteNode !== undefined) {
                remoteCell = $("<td>",{class:"node-diff-property-cell node-diff-node-remote"}).appendTo(row);
                if (remoteNode) {
                    if (!conflict) {
                        remoteCell.addClass("node-diff-node-"+(remoteChanged?"changed":"unchanged"));
                        $('<span class="node-diff-status">'+(remoteChanged?'<i class="fa fa-square"></i>':'')+'</span>').appendTo(remoteCell);
                    } else {
                        remoteCell.addClass("node-diff-node-conflict");
                        $('<span class="node-diff-status"><i class="fa fa-exclamation"></i></span>').appendTo(remoteCell);
                    }
                    element = $('<span class="node-diff-element"></span>').appendTo(remoteCell);
                    propertyElements['remote.'+d] = RED.utils.createObjectElement(remoteNode[d],
                        {
                            path: d,
                            exposeApi: true,
                            ontoggle: function(path,state) {
                                if (propertyElements['local.'+d]) {
                                    propertyElements['local.'+d].prop('expand')(path,state)
                                }
                            }
                        }
                    ).appendTo(element);
                } else {
                    remoteCell.addClass("node-diff-empty");
                }
            }
            if (localNode && remoteNode && typeof localNode[d] === "string") {
                if (/\n/.test(localNode[d]) || /\n/.test(remoteNode[d])) {
                    $('<button class="editor-button editor-button-small node-diff-text-diff-button"><i class="fa fa-file-o"> <i class="fa fa-caret-left"></i> <i class="fa fa-caret-right"></i> <i class="fa fa-file-o"></i></button>').click(function() {
                        showTextDiff(localNode[d],remoteNode[d]);
                    }).appendTo(propertyNameCell);
                }
            }


        });
        return nodePropertiesDiv;
    }
    function createNodeConflictRadioBoxes(node,row,localDiv,remoteDiv,propertiesTable,hide,state,diff) {
        var safeNodeId = "node-diff-selectbox-"+node.id.replace(/\./g,'-')+(propertiesTable?"-props":"");
        var className = "";
        if (node.z||propertiesTable) {
            className = "node-diff-selectbox-tab-"+(propertiesTable?node.id:node.z).replace(/\./g,'-');
        }
        var titleRow = !propertiesTable && (node.type === 'tab' || node.type === 'subflow');
        var changeHandler = function(evt) {
            var className;
            if (node.type === undefined) {
                // TODO: handle globals
            } else if (titleRow) {
                className = "node-diff-selectbox-tab-"+node.id.replace(/\./g,'-');
                $("."+className+"-"+this.value).prop('checked',true);
                if (this.value === 'local') {
                    $("."+className+"-"+this.value).closest(".node-diff-node-entry").addClass("node-diff-select-local");
                    $("."+className+"-"+this.value).closest(".node-diff-node-entry").removeClass("node-diff-select-remote");
                } else {
                    $("."+className+"-"+this.value).closest(".node-diff-node-entry").removeClass("node-diff-select-local");
                    $("."+className+"-"+this.value).closest(".node-diff-node-entry").addClass("node-diff-select-remote");
                }
            } else {
                // Individual node or properties table
                var parentId = "node-diff-selectbox-"+(propertiesTable?node.id:node.z).replace(/\./g,'-');
                $('#'+parentId+"-local").prop('checked',false);
                $('#'+parentId+"-remote").prop('checked',false);
                var titleRowDiv = $('#'+parentId+"-local").closest(".node-diff-tab").find(".node-diff-tab-title");
                titleRowDiv.removeClass("node-diff-select-local");
                titleRowDiv.removeClass("node-diff-select-remote");
            }
            if (this.value === 'local') {
                row.removeClass("node-diff-select-remote");
                row.addClass("node-diff-select-local");
            } else if (this.value === 'remote') {
                row.addClass("node-diff-select-remote");
                row.removeClass("node-diff-select-local");
            }
            refreshConflictHeader(diff);
        }

        var localSelectDiv = $('<label>',{class:"node-diff-selectbox",for:safeNodeId+"-local"}).click(function(e) { e.stopPropagation();}).appendTo(localDiv);
        var localRadio = $('<input>',{id:safeNodeId+"-local",type:'radio',value:"local",name:safeNodeId,class:className+"-local"+(titleRow?"":" node-diff-select-node")}).data('node-id',node.id).change(changeHandler).appendTo(localSelectDiv);
        var remoteSelectDiv = $('<label>',{class:"node-diff-selectbox",for:safeNodeId+"-remote"}).click(function(e) { e.stopPropagation();}).appendTo(remoteDiv);
        var remoteRadio = $('<input>',{id:safeNodeId+"-remote",type:'radio',value:"remote",name:safeNodeId,class:className+"-remote"+(titleRow?"":" node-diff-select-node")}).data('node-id',node.id).change(changeHandler).appendTo(remoteSelectDiv);
        if (state === 'local') {
            localRadio.prop('checked',true);
        } else if (state === 'remote') {
            remoteRadio.prop('checked',true);
        }
        if (hide||localDiv.hasClass("node-diff-empty") || remoteDiv.hasClass("node-diff-empty")) {
            localSelectDiv.hide();
            remoteSelectDiv.hide();
        }

    }
    function refreshConflictHeader(currentDiff) {
        var resolutionCount = 0;
        $(".node-diff-selectbox>input:checked").each(function() {
            if (currentDiff.conflicts[$(this).data('node-id')]) {
                resolutionCount++;
            }
            currentDiff.resolutions[$(this).data('node-id')] = $(this).val();
        })
        var conflictCount = Object.keys(currentDiff.conflicts).length;
        if (conflictCount - resolutionCount === 0) {
            $("#node-diff-toolbar-resolved-conflicts").html('<span class="node-diff-node-added"><span class="node-diff-status"><i class="fa fa-check"></i></span></span> '+RED._("diff.unresolvedCount",{count:conflictCount - resolutionCount}));
        } else {
            $("#node-diff-toolbar-resolved-conflicts").html('<span class="node-diff-node-conflict"><span class="node-diff-status"><i class="fa fa-exclamation"></i></span></span> '+RED._("diff.unresolvedCount",{count:conflictCount - resolutionCount}));
        }
        if (conflictCount === resolutionCount) {
            $("#node-diff-view-diff-merge").removeClass('disabled');
            $("#node-diff-view-resolve-diff").removeClass('disabled');
        }
    }
    function getRemoteDiff(callback) {
        $.ajax({
            headers: {
                "Accept":"application/json",
            },
            cache: false,
            url: 'flows',
            success: function(nodes) {
                var localFlow = RED.nodes.createCompleteNodeSet();
                var originalFlow = RED.nodes.originalFlow();
                var remoteFlow = nodes.flows;
                var localDiff = generateDiff(originalFlow,localFlow);
                var remoteDiff = generateDiff(originalFlow,remoteFlow);
                remoteDiff.rev = nodes.rev;
                callback(resolveDiffs(localDiff,remoteDiff))
            }
        });

    }
    // function showLocalDiff() {
    //     var nns = RED.nodes.createCompleteNodeSet();
    //     var originalFlow = RED.nodes.originalFlow();
    //     var diff = generateDiff(originalFlow,nns);
    //     showDiff(diff);
    // }
    function showRemoteDiff(diff) {
        if (diff === undefined) {
            getRemoteDiff(showRemoteDiff);
        } else {
            showDiff(diff,{mode:'merge'});
        }
    }
    function parseNodes(nodeList) {
        var tabOrder = [];
        var tabs = {};
        var subflows = {};
        var globals = [];
        var all = {};

        nodeList.forEach(function(node) {
            all[node.id] = node;
            if (node.type === 'tab') {
                tabOrder.push(node.id);
                tabs[node.id] = {n:node,nodes:[]};
            } else if (node.type === 'subflow') {
                subflows[node.id] = {n:node,nodes:[]};
            }
        });

        nodeList.forEach(function(node) {
            if (node.type !== 'tab' && node.type !== 'subflow') {
                if (tabs[node.z]) {
                    tabs[node.z].nodes.push(node);
                } else if (subflows[node.z]) {
                    subflows[node.z].nodes.push(node);
                } else {
                    globals.push(node);
                }
            }
        });

        return {
            all: all,
            tabOrder: tabOrder,
            tabs: tabs,
            subflows: subflows,
            globals: globals
        }
    }
    function generateDiff(currentNodes,newNodes) {
        var currentConfig = parseNodes(currentNodes);
        var newConfig = parseNodes(newNodes);
        var added = {};
        var deleted = {};
        var changed = {};
        var moved = {};

        Object.keys(currentConfig.all).forEach(function(id) {
            var node = RED.nodes.workspace(id)||RED.nodes.subflow(id)||RED.nodes.node(id);
            if (!newConfig.all.hasOwnProperty(id)) {
                deleted[id] = true;
            } else if (JSON.stringify(currentConfig.all[id]) !== JSON.stringify(newConfig.all[id])) {
                changed[id] = true;

                if (currentConfig.all[id].z !== newConfig.all[id].z) {
                    moved[id] = true;
                }
            }
        });
        Object.keys(newConfig.all).forEach(function(id) {
            if (!currentConfig.all.hasOwnProperty(id)) {
                added[id] = true;
            }
        });

        return {
            currentConfig: currentConfig,
            newConfig: newConfig,
            added: added,
            deleted: deleted,
            changed: changed,
            moved: moved
        }
    }
    function resolveDiffs(localDiff,remoteDiff) {
        var conflicted = {};
        var resolutions = {};

        var diff = {
            localDiff: localDiff,
            remoteDiff: remoteDiff,
            conflicts: conflicted,
            resolutions: resolutions
        }
        var seen = {};
        var id,node;
        for (id in localDiff.currentConfig.all) {
            if (localDiff.currentConfig.all.hasOwnProperty(id)) {
                seen[id] = true;
                var localNode = localDiff.newConfig.all[id];
                if (localDiff.changed[id] && remoteDiff.deleted[id]) {
                    conflicted[id] = true;
                } else if (localDiff.deleted[id] && remoteDiff.changed[id]) {
                    conflicted[id] = true;
                } else if (localDiff.changed[id] && remoteDiff.changed[id]) {
                    var remoteNode = remoteDiff.newConfig.all[id];
                    if (JSON.stringify(localNode) !== JSON.stringify(remoteNode)) {
                        conflicted[id] = true;
                    }
                }
                if (!conflicted[id]) {
                    if (remoteDiff.added[id]||remoteDiff.changed[id]||remoteDiff.deleted[id]) {
                        resolutions[id] = 'remote';
                    } else {
                        resolutions[id] = 'local';
                    }
                }
            }
        }
        for (id in localDiff.added) {
            if (localDiff.added.hasOwnProperty(id)) {
                node = localDiff.newConfig.all[id];
                if (remoteDiff.deleted[node.z]) {
                    conflicted[id] = true;
                    // conflicted[node.z] = true;
                } else {
                    resolutions[id] = 'local';
                }
            }
        }
        for (id in remoteDiff.added) {
            if (remoteDiff.added.hasOwnProperty(id)) {
                node = remoteDiff.newConfig.all[id];
                if (localDiff.deleted[node.z]) {
                    conflicted[id] = true;
                    // conflicted[node.z] = true;
                } else {
                    resolutions[id] = 'remote';
                }
            }
        }
        // console.log(diff.resolutions);
        // console.log(conflicted);
        return diff;
    }

    function showDiff(diff,options) {
        if (diffVisible) {
            return;
        }
        options = options || {};
        var mode = options.mode || 'merge';

        var localDiff = diff.localDiff;
        var remoteDiff = diff.remoteDiff;
        var conflicts = diff.conflicts;
        // currentDiff = diff;

        var trayOptions = {
            title: options.title||"Review Changes", //TODO: nls
            width: Infinity,
            overlay: true,
            buttons: [
                {
                    text: RED._((options.mode === 'merge')?"common.label.cancel":"common.label.close"),
                    click: function() {
                        RED.tray.close();
                    }
                }
            ],
            resize: function(dimensions) {
                // trayWidth = dimensions.width;
            },
            open: function(tray) {
                var trayBody = tray.find('.editor-tray-body');
                var toolbar = $('<div class="node-diff-toolbar">'+
                    '<span><span id="node-diff-toolbar-resolved-conflicts"></span></span> '+
                    '</div>').prependTo(trayBody);
                var diffContainer = $('<div class="node-diff-container"></div>').appendTo(trayBody);
                var diffTable = buildDiffPanel(diffContainer,diff,options);
                diffTable.list.hide();
                if (remoteDiff) {
                    $("#node-diff-view-diff-merge").show();
                    if (Object.keys(conflicts).length === 0) {
                        $("#node-diff-view-diff-merge").removeClass('disabled');
                    } else {
                        $("#node-diff-view-diff-merge").addClass('disabled');
                    }
                } else {
                    $("#node-diff-view-diff-merge").hide();
                }
                refreshConflictHeader(diff);
                // console.log("--------------");
                // console.log(localDiff);
                // console.log(remoteDiff);

                setTimeout(function() {
                    diffTable.finish();
                    diffTable.list.show();
                },300);
                $("#sidebar-shade").show();
            },
            close: function() {
                diffVisible = false;
                $("#sidebar-shade").hide();

            },
            show: function() {

            }
        }
        if (options.mode === 'merge') {
            trayOptions.buttons.push(
                {
                    id: "node-diff-view-diff-merge",
                    text: RED._("deploy.confirm.button.merge"),
                    class: "primary disabled",
                    click: function() {
                        if (!$("#node-diff-view-diff-merge").hasClass('disabled')) {
                            refreshConflictHeader(diff);
                            mergeDiff(diff);
                            RED.tray.close();
                        }
                    }
                }
            );
        }

        RED.tray.show(trayOptions);
    }

    function applyDiff(diff) {
        var currentConfig = diff.localDiff.currentConfig;
        var localDiff = diff.localDiff;
        var remoteDiff = diff.remoteDiff;
        var conflicts = diff.conflicts;
        var resolutions = diff.resolutions;
        var id;

        for (id in conflicts) {
            if (conflicts.hasOwnProperty(id)) {
                if (!resolutions.hasOwnProperty(id)) {
                    console.log(diff);
                    throw new Error("No resolution for conflict on node",id);
                }
            }
        }

        var newConfig = [];
        var node;
        var nodeChangedStates = {};
        var localChangedStates = {};
        for (id in localDiff.newConfig.all) {
            if (localDiff.newConfig.all.hasOwnProperty(id)) {
                node = RED.nodes.node(id);
                if (resolutions[id] === 'local') {
                    if (node) {
                        nodeChangedStates[id] = node.changed;
                    }
                    newConfig.push(localDiff.newConfig.all[id]);
                } else if (resolutions[id] === 'remote') {
                    if (!remoteDiff.deleted[id] && remoteDiff.newConfig.all.hasOwnProperty(id)) {
                        if (node) {
                            nodeChangedStates[id] = node.changed;
                        }
                        localChangedStates[id] = true;
                        newConfig.push(remoteDiff.newConfig.all[id]);
                    }
                } else {
                    console.log("Unresolved",id)
                }
            }
        }
        for (id in remoteDiff.added) {
            if (remoteDiff.added.hasOwnProperty(id)) {
                node = RED.nodes.node(id);
                if (node) {
                    nodeChangedStates[id] = node.changed;
                }
                if (!localDiff.added.hasOwnProperty(id)) {
                    localChangedStates[id] = true;
                    newConfig.push(remoteDiff.newConfig.all[id]);
                }
            }
        }
        return {
            config: newConfig,
            nodeChangedStates: nodeChangedStates,
            localChangedStates: localChangedStates
        }
    }

    function mergeDiff(diff) {
        var appliedDiff = applyDiff(diff);

        var newConfig = appliedDiff.config;
        var nodeChangedStates = appliedDiff.nodeChangedStates;
        var localChangedStates = appliedDiff.localChangedStates;

        var historyEvent = {
            t:"replace",
            config: RED.nodes.createCompleteNodeSet(),
            changed: nodeChangedStates,
            dirty: RED.nodes.dirty(),
            rev: RED.nodes.version()
        }

        RED.history.push(historyEvent);

        RED.nodes.clear();
        var imported = RED.nodes.import(newConfig);
        imported[0].forEach(function(n) {
            if (nodeChangedStates[n.id] || localChangedStates[n.id]) {
                n.changed = true;
            }
        })

        RED.nodes.version(diff.remoteDiff.rev);

        RED.view.redraw(true);
        RED.palette.refresh();
        RED.workspaces.refresh();
        RED.sidebar.config.refresh();
    }
    function showTestFlowDiff(index) {
        if (index === 1) {
            var localFlow = RED.nodes.createCompleteNodeSet();
            var originalFlow = RED.nodes.originalFlow();
            showTextDiff(JSON.stringify(localFlow,null,4),JSON.stringify(originalFlow,null,4))
        } else if (index === 2) {
            var local = "1\n2\n3\n4\n5\nA\n6\n7\n8\n9\n";
            var remote = "1\nA\n2\n3\nD\nE\n6\n7\n8\n9\n";
            showTextDiff(local,remote);
        } else if (index === 3) {
            var local =  "1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n21\n22";
            var remote = "1\nTWO\nTHREE\nEXTRA\n4\n5\n6\n7\n8\n9\n10\n11\n12\nTHIRTEEN\n14\n15\n16\n17\n18\n19\n20\n21\n22";
            showTextDiff(local,remote);
        }
    }

    function showTextDiff(textA,textB) {
        var trayOptions = {
            title: "Compare Changes", //TODO: nls
            width: Infinity,
            overlay: true,
            buttons: [
                {
                    text: RED._("common.label.close"),
                    click: function() {
                        RED.tray.close();
                    }
                }
            ],
            resize: function(dimensions) {
                // trayWidth = dimensions.width;
            },
            open: function(tray) {
                var trayBody = tray.find('.editor-tray-body');
                var diffPanel = $('<div class="node-text-diff"></div>').appendTo(trayBody);

                var codeTable = $("<table>",{class:"node-text-diff-content"}).appendTo(diffPanel);
                $('<colgroup><col width="50"><col width="50%"><col width="50"><col width="50%"></colgroup>').appendTo(codeTable);
                var codeBody = $('<tbody>').appendTo(codeTable);
                var diffSummary = diffText(textA||"",textB||"");
                var aIndex = 0;
                var bIndex = 0;
                var diffLength = Math.max(diffSummary.a.length, diffSummary.b.length);

                var diffLines = [];
                var diffBlocks = [];
                var currentBlock;
                var blockLength = 0;
                var blockType = 0;

                for (var i=0;i<diffLength;i++) {
                    var diffLine = diffSummary[i];
                    var Adiff = (aIndex < diffSummary.a.length)?diffSummary.a[aIndex]:{type:2,line:""};
                    var Bdiff = (bIndex < diffSummary.b.length)?diffSummary.b[bIndex]:{type:2,line:""};
                    if (Adiff.type === 0 && Bdiff.type !== 0) {
                        Adiff = {type:2,line:""};
                        bIndex++;
                    } else if (Bdiff.type === 0 && Adiff.type !== 0) {
                        Bdiff = {type:2,line:""};
                        aIndex++;
                    } else {
                        aIndex++;
                        bIndex++;
                    }
                    diffLines.push({
                        a: Adiff,
                        b: Bdiff
                    });
                    if (currentBlock === undefined) {
                        currentBlock = {start:i,end:i};
                        blockLength = 0;
                        blockType = (Adiff.type === 0 && Bdiff.type === 0)?0:1;
                    } else {
                        if (Adiff.type === 0 && Bdiff.type === 0) {
                            // Unchanged line
                            if (blockType === 0) {
                                // still unchanged - extend the block
                                currentBlock.end = i;
                                blockLength++;
                            } else if (blockType === 1) {
                                // end of a change
                                currentBlock.end = i;
                                blockType = 2;
                                blockLength = 0;
                            } else if (blockType === 2) {
                                // post-change unchanged
                                currentBlock.end = i;
                                blockLength++;
                                if (blockLength === 8) {
                                    currentBlock.end -= 5; // rollback the end
                                    diffBlocks.push(currentBlock);
                                    currentBlock = {start:i-5,end:i-5};
                                    blockType = 0;
                                    blockLength = 0;
                                }
                            }
                        } else {
                            // in a change
                            currentBlock.end = i;
                            blockLength++;
                            if (blockType === 0) {
                                if (currentBlock.end > 3) {
                                    currentBlock.end -= 3;
                                    currentBlock.empty = true;
                                    diffBlocks.push(currentBlock);
                                    currentBlock = {start:i-3,end:i-3};
                                }
                                blockType = 1;
                            } else if (blockType === 2) {
                                // we were in unchanged, but hit a change again
                                blockType = 1;
                            }
                        }
                    }
                }
                if (blockType === 0) {
                    currentBlock.empty = true;
                }
                currentBlock.end = diffLength;
                diffBlocks.push(currentBlock);
                // console.table(diffBlocks);
                var diffRow;
                for (var b = 0; b<diffBlocks.length; b++) {
                    currentBlock = diffBlocks[b];
                    if (currentBlock.empty) {
                        diffRow = createExpandLine(currentBlock.start,currentBlock.end,diffLines).appendTo(codeBody);
                    } else {
                        for (var i=currentBlock.start;i<currentBlock.end;i++) {
                            var row = createDiffLine(diffLines[i]).appendTo(codeBody);
                            if (i === currentBlock.start) {
                                row.addClass("start-block");
                            } else if (i === currentBlock.end-1) {
                                row.addClass("end-block");
                            }
                        }
                    }
                }

            },
            close: function() {
                diffVisible = false;

            },
            show: function() {

            }
        }
        RED.tray.show(trayOptions);
    }

    function createExpandLine(start,end,diffLines) {
        diffRow = $('<tr class="node-text-diff-header node-text-diff-expand">');
        var content = $('<td colspan="4"> <i class="fa fa-arrows-v"></i> </td>').appendTo(diffRow);
        var label = $('<span></span>').appendTo(content);
        if (end < diffLines.length-1) {
            label.text("@@ -"+(diffLines[end-1].a.i+1)+" +"+(diffLines[end-1].b.i+1));
        }
        diffRow.click(function(evt) {
            // console.log(start,end,diffLines.length);
            if (end - start > 20) {
                var startPos = $(this).offset();
                // console.log(startPos);
                if (start > 0) {
                    for (var i=start;i<start+10;i++) {
                        createDiffLine(diffLines[i]).addClass("unchanged").insertBefore($(this));
                    }
                    start += 10;
                }
                if (end < diffLines.length-1) {
                    for (var i=end-1;i>end-11;i--) {
                        createDiffLine(diffLines[i]).addClass("unchanged").insertAfter($(this));
                    }
                    end -= 10;
                }
                if (end < diffLines.length-1) {
                    label.text("@@ -"+(diffLines[end-1].a.i+1)+" +"+(diffLines[end-1].b.i+1));
                }
                var endPos = $(this).offset();
                var delta = endPos.top - startPos.top;
                $(".node-text-diff").scrollTop($(".node-text-diff").scrollTop() + delta);
            } else {
                for (var i=start;i<end;i++) {
                    createDiffLine(diffLines[i]).addClass("unchanged").insertBefore($(this));
                }
                $(this).remove();
            }
        });
        return diffRow;
    }

    function createDiffLine(diffLine) {
        var diffRow = $('<tr>');
        var Adiff = diffLine.a;
        var Bdiff = diffLine.b;
        //console.log(diffLine);
        var cellNo = $('<td class="lineno">').text(Adiff.type === 2?"":Adiff.i).appendTo(diffRow);
        var cellLine = $('<td class="linetext">').text(Adiff.line).appendTo(diffRow);
        if (Adiff.type === 2) {
            cellNo.addClass('blank');
            cellLine.addClass('blank');
        } else if (Adiff.type === 4) {
            cellNo.addClass('added');
            cellLine.addClass('added');
        } else if (Adiff.type === 1) {
            cellNo.addClass('removed');
            cellLine.addClass('removed');
        }
        cellNo = $('<td class="lineno">').text(Bdiff.type === 2?"":Bdiff.i).appendTo(diffRow);
        cellLine = $('<td class="linetext">').text(Bdiff.line).appendTo(diffRow);
        if (Bdiff.type === 2) {
            cellNo.addClass('blank');
            cellLine.addClass('blank');
        } else if (Bdiff.type === 4) {
            cellNo.addClass('added');
            cellLine.addClass('added');
        } else if (Bdiff.type === 1) {
            cellNo.addClass('removed');
            cellLine.addClass('removed');
        }
        return diffRow;
    }

    function diffText(string1, string2,ignoreWhitespace) {
        var lines1 = string1.split(/\r?\n/);
        var lines2 = string2.split(/\r?\n/);
        var i = lines1.length;
        var j = lines2.length;
        var k;
        var m;
        var diffSummary = {a:[],b:[]};
        var diffMap = [];
        for (k = 0; k < i + 1; k++) {
            diffMap[k] = [];
            for (m = 0; m < j + 1; m++) {
                diffMap[k][m] = 0;
            }
        }
        var c = 0;
        for (k = i - 1; k >= 0; k--) {
            for (m = j - 1; m >=0; m--) {
                c++;
                if (compareLines(lines1[k],lines2[m],ignoreWhitespace) !== 1) {
                    diffMap[k][m] = diffMap[k+1][m+1]+1;
                } else {
                    diffMap[k][m] = Math.max(diffMap[(k + 1)][m], diffMap[k][(m + 1)]);
                }
            }
        }
        //console.log(c);
        k = 0;
        m = 0;

        while ((k < i) && (m < j)) {
            var n = compareLines(lines1[k],lines2[m],ignoreWhitespace);
            if (n !== 1) {
                var d = 0;
                if (n===0) {
                    d = 0;
                } else if (n==2) {
                    d = 3;
                }
                diffSummary.a.push({i:k+1,j:m+1,line:lines1[k],type:d});
                diffSummary.b.push({i:m+1,j:k+1,line:lines2[m],type:d});
                k++;
                m++;
            } else if (diffMap[(k + 1)][m] >= diffMap[k][(m + 1)]) {
                diffSummary.a.push({i:k+1,line:lines1[k],type:1});
                k++;
            } else {
                diffSummary.b.push({i:m+1,line:lines2[m],type:4});
                m++;
            }
        }
        while ((k < i) || (m < j)) {
            if (k == i) {
                diffSummary.b.push({i:m+1,line:lines2[m],type:4});
                m++;
            } else if (m == j) {
                diffSummary.a.push({i:k+1,line:lines1[k],type:1});
                k++;
            }
        }
        return diffSummary;
    }

    function compareLines(string1, string2, ignoreWhitespace) {
        if (ignoreWhitespace) {
            if (string1 === string2) {
                return 0;
            }
            return string1.trim() === string2.trime() ? 2 : 1;
        }
        return string1 === string2 ? 0 : 1;
    }

    function createUnifiedDiffTable(files,commitOptions) {
        var diffPanel = $('<div></div>');
        files.forEach(function(file) {
            var hunks = file.hunks;
            var isBinary = file.binary;
            var codeTable = $("<table>",{class:"node-text-diff-content"}).appendTo(diffPanel);
            $('<colgroup><col width="50"><col width="50"><col width="100%"></colgroup>').appendTo(codeTable);
            var codeBody = $('<tbody>').appendTo(codeTable);

            var diffFileRow = $('<tr class="node-text-diff-file-header">').appendTo(codeBody);
            var content = $('<td colspan="3"></td>').appendTo(diffFileRow);

            var chevron = $('<i class="node-diff-chevron fa fa-angle-down"></i>').appendTo(content);
            diffFileRow.click(function(e) {
                diffFileRow.toggleClass("collapsed");
                var isCollapsed = diffFileRow.hasClass("collapsed");
                diffFileRow.nextUntil(".node-text-diff-file-header").toggle(!isCollapsed);
            })
            var label = $('<span class="filename"></span>').text(file.file).appendTo(content);

            var conflictHeader;
            var unresolvedConflicts = 0;
            var resolvedConflicts = 0;
            var conflictResolutions = {};
            if (commitOptions.project.files && commitOptions.project.files.flow === file.file) {
                if (commitOptions.unmerged) {
                    $('<span style="float: right;"><span id="node-diff-toolbar-resolved-conflicts"></span></span>').appendTo(content);
                }
                var diffRow = $('<tr class="node-text-diff-header">').appendTo(codeBody);
                var flowDiffContent = $('<td class="flow-diff" colspan="3"></td>').appendTo(diffRow);

                var projectName = commitOptions.project.name;
                var filename = commitOptions.project.files.flow;
                var commonVersionUrl = "projects/"+projectName+"/files/"+commitOptions.commonRev+"/"+filename;
                var oldVersionUrl = "projects/"+projectName+"/files/"+commitOptions.oldRev+"/"+filename;
                var newVersionUrl = "projects/"+projectName+"/files/"+commitOptions.newRev+"/"+filename;
                var promises = [$.Deferred(),$.Deferred(),$.Deferred()];
                if (commitOptions.commonRev) {
                    var commonVersionUrl = "projects/"+projectName+"/files/"+commitOptions.commonRev+"/"+filename;
                    $.ajax({dataType: "json",url: commonVersionUrl}).then(function(data) { promises[0].resolve(data); }).fail(function() { promises[0].resolve(null);})
                } else {
                    promises[0].resolve(null);
                }

                $.ajax({dataType: "json",url: oldVersionUrl}).then(function(data) { promises[1].resolve(data); }).fail(function() { promises[1].resolve({content:"[]"});})
                $.ajax({dataType: "json",url: newVersionUrl}).then(function(data) { promises[2].resolve(data); }).fail(function() { promises[2].resolve({content:"[]"});})
                $.when.apply($,promises).always(function(commonVersion, oldVersion,newVersion) {
                    var commonFlow;
                    var oldFlow;
                    var newFlow;
                    if (commonVersion) {
                        try {
                            commonFlow = JSON.parse(commonVersion.content||"[]");
                        } catch(err) {
                            console.log("Common Version doesn't contain valid JSON:",commonVersionUrl);
                            console.log(err);
                            return;
                        }
                    }
                    try {
                        oldFlow = JSON.parse(oldVersion.content||"[]");
                    } catch(err) {
                        console.log("Old Version doesn't contain valid JSON:",oldVersionUrl);
                        console.log(err);
                        return;
                    }
                    if (!commonFlow) {
                        commonFlow = oldFlow;
                    }
                    try {
                        newFlow = JSON.parse(newVersion.content||"[]");
                    } catch(err) {
                        console.log("New Version doesn't contain valid JSON:",newFlow);
                        console.log(err);
                        return;
                    }
                    var localDiff = generateDiff(commonFlow,oldFlow);
                    var remoteDiff = generateDiff(commonFlow,newFlow);
                    commitOptions.currentDiff = resolveDiffs(localDiff,remoteDiff);
                    var diffTable = buildDiffPanel(flowDiffContent,commitOptions.currentDiff,{
                        title: filename,
                        mode: commitOptions.commonRev?'merge':'view',
                        oldRevTitle: commitOptions.oldRevTitle,
                        newRevTitle: commitOptions.newRevTitle
                    });
                    diffTable.list.hide();
                    refreshConflictHeader(commitOptions.currentDiff);
                    setTimeout(function() {
                        diffTable.finish();
                        diffTable.list.show();
                    },300);
                    // var flowDiffRow = $("<tr>").insertAfter(diffRow);
                    // var content = $('<td colspan="3"></td>').appendTo(flowDiffRow);
                    // currentDiff = diff;
                    // var diffTable = buildDiffPanel(content,diff,{mode:"view"}).finish();
                });



            } else

            if (isBinary) {
                var diffBinaryRow = $('<tr class="node-text-diff-header">').appendTo(codeBody);
                var binaryContent = $('<td colspan="3"></td>').appendTo(diffBinaryRow);
                $('<span></span>').text("Cannot show binary file contents").appendTo(binaryContent);

            } else {
                if (commitOptions.unmerged) {
                    conflictHeader = $('<span style="float: right;"><span>'+resolvedConflicts+'</span> of <span>'+unresolvedConflicts+'</span> conflicts resolved</span>').appendTo(content);
                }
                hunks.forEach(function(hunk) {
                    var diffRow = $('<tr class="node-text-diff-header">').appendTo(codeBody);
                    var content = $('<td colspan="3"></td>').appendTo(diffRow);
                    var label = $('<span></span>').text(hunk.header).appendTo(content);
                    var isConflict = hunk.conflict;
                    var localLine = hunk.localStartLine;
                    var remoteLine = hunk.remoteStartLine;
                    if (isConflict) {
                        unresolvedConflicts++;
                    }

                    hunk.lines.forEach(function(lineText,lineNumber) {
                        // if (lineText[0] === '\\' || lineText === "") {
                        //     // Comment line - bail out of this hunk
                        //     break;
                        // }

                        var actualLineNumber = hunk.diffStart + lineNumber;
                        var isMergeHeader = isConflict && /^\+\+(<<<<<<<|=======$|>>>>>>>)/.test(lineText);
                        var diffRow = $('<tr>').appendTo(codeBody);
                        var localLineNo = $('<td class="lineno">').appendTo(diffRow);
                        var remoteLineNo;
                        if (!isMergeHeader) {
                            remoteLineNo = $('<td class="lineno">').appendTo(diffRow);
                        } else {
                            localLineNo.attr('colspan',2);
                        }
                        var line = $('<td class="linetext">').appendTo(diffRow);
                        var prefixStart = 0;
                        var prefixEnd = 1;
                        if (isConflict) {
                            prefixEnd = 2;
                        }
                        if (!isMergeHeader) {
                            var changeMarker = lineText[0];
                            if (isConflict && !commitOptions.unmerged && changeMarker === ' ') {
                                changeMarker = lineText[1];
                            }
                            $('<span class="prefix">').text(changeMarker).appendTo(line);
                            var handledlLine = false;
                            if (isConflict && commitOptions.unmerged) {
                                $('<span class="prefix">').text(lineText[1]).appendTo(line);
                                if (lineText[0] === '+') {
                                    localLineNo.text(localLine++);
                                    handledlLine = true;
                                }
                                if (lineText[1] === '+') {
                                    remoteLineNo.text(remoteLine++);
                                    handledlLine = true;
                                }
                            } else {
                                if (lineText[0] === '+' || (isConflict && lineText[1] === '+')) {
                                    localLineNo.addClass("added");
                                    remoteLineNo.addClass("added");
                                    line.addClass("added");
                                    remoteLineNo.text(remoteLine++);
                                    handledlLine = true;
                                } else if (lineText[0] === '-' || (isConflict && lineText[1] === '-')) {
                                    localLineNo.addClass("removed");
                                    remoteLineNo.addClass("removed");
                                    line.addClass("removed");
                                    localLineNo.text(localLine++);
                                    handledlLine = true;
                                }
                            }
                            if (!handledlLine) {
                                line.addClass("unchanged");
                                if (localLine > 0 && lineText[0] !== '\\' && lineText !== "") {
                                    localLineNo.text(localLine++);
                                }
                                if (remoteLine > 0 && lineText[0] !== '\\' && lineText !== "") {
                                    remoteLineNo.text(remoteLine++);
                                }
                            }
                            $('<span>').text(lineText.substring(prefixEnd)).appendTo(line);
                        } else {
                            diffRow.addClass("mergeHeader");
                            var isSeparator = /^\+\+=======$/.test(lineText);
                            if (!isSeparator) {
                                var isOurs = /^..<<<<<<</.test(lineText);
                                if (isOurs) {
                                    $('<span>').text("<<<<<<< Local Changes").appendTo(line);
                                    hunk.localChangeStart = actualLineNumber;
                                } else {
                                    hunk.remoteChangeEnd = actualLineNumber;
                                    $('<span>').text(">>>>>>> Remote Changes").appendTo(line);

                                }
                                diffRow.addClass("mergeHeader-"+(isOurs?"ours":"theirs"));
                                $('<button class="editor-button editor-button-small" style="float: right; margin-right: 20px;"><i class="fa fa-angle-double-'+(isOurs?"down":"up")+'"></i> use '+(isOurs?"local":"remote")+' changes</button>')
                                    .appendTo(line)
                                    .click(function(evt) {
                                        evt.preventDefault();
                                        resolvedConflicts++;
                                        var addedRows;
                                        var midRow;
                                        if (isOurs) {
                                            addedRows = diffRow.nextUntil(".mergeHeader-separator");
                                            midRow = addedRows.last().next();
                                            midRow.nextUntil(".mergeHeader").remove();
                                            midRow.next().remove();
                                        } else {
                                            addedRows = diffRow.prevUntil(".mergeHeader-separator");
                                            midRow = addedRows.last().prev();
                                            midRow.prevUntil(".mergeHeader").remove();
                                            midRow.prev().remove();
                                        }
                                        midRow.remove();
                                        diffRow.remove();
                                        addedRows.find(".linetext").addClass('added');
                                        conflictHeader.empty();
                                        $('<span><span>'+resolvedConflicts+'</span> of <span>'+unresolvedConflicts+'</span> conflicts resolved</span>').appendTo(conflictHeader);

                                        conflictResolutions[file.file] = conflictResolutions[file.file] || {};
                                        conflictResolutions[file.file][hunk.localChangeStart] = {
                                            changeStart: hunk.localChangeStart,
                                            separator: hunk.changeSeparator,
                                            changeEnd: hunk.remoteChangeEnd,
                                            selection: isOurs?"A":"B"
                                        }
                                        if (commitOptions.resolveConflict) {
                                            commitOptions.resolveConflict({
                                                conflicts: unresolvedConflicts,
                                                resolved: resolvedConflicts,
                                                resolutions: conflictResolutions
                                            });
                                        }
                                    })
                            } else {
                                hunk.changeSeparator = actualLineNumber;
                                diffRow.addClass("mergeHeader-separator");
                            }
                        }
                    });
                });
            }
        });
        return diffPanel;
    }

    function showCommitDiff(options) {
        var commit = parseCommitDiff(options.commit);
        var trayOptions = {
            title: "View Commit Changes", //TODO: nls
            width: Infinity,
            overlay: true,
            buttons: [
                {
                    text: RED._("common.label.close"),
                    click: function() {
                        RED.tray.close();
                    }
                }
            ],
            resize: function(dimensions) {
                // trayWidth = dimensions.width;
            },
            open: function(tray) {
                var trayBody = tray.find('.editor-tray-body');
                var diffPanel = $('<div class="node-text-diff"></div>').appendTo(trayBody);

                var codeTable = $("<table>",{class:"node-text-diff-content"}).appendTo(diffPanel);
                $('<colgroup><col width="50"><col width="50"><col width="100%"></colgroup>').appendTo(codeTable);
                var codeBody = $('<tbody>').appendTo(codeTable);

                var diffRow = $('<tr class="node-text-diff-commit-header">').appendTo(codeBody);
                var content = $('<td colspan="3"></td>').appendTo(diffRow);

                $("<h3>").text(commit.title).appendTo(content);
                $('<div class="commit-body"></div>').text(commit.comment).appendTo(content);
                var summary = $('<div class="commit-summary"></div>').appendTo(content);
                $('<div style="float: right">').text("Commit "+commit.sha).appendTo(summary);
                $('<div>').text((commit.authorName||commit.author)+" - "+options.date).appendTo(summary);

                if (commit.files) {
                    createUnifiedDiffTable(commit.files,options).appendTo(diffPanel);
                }


            },
            close: function() {
                diffVisible = false;
            },
            show: function() {

            }
        }
        RED.tray.show(trayOptions);
    }
    function showUnifiedDiff(options) {
        var diff = options.diff;
        var title = options.title;
        var files = parseUnifiedDiff(diff);

        var currentResolution;
        if (options.unmerged) {
            options.resolveConflict = function(results) {
                currentResolution = results;
                if (results.conflicts === results.resolved) {
                    $("#node-diff-view-resolve-diff").removeClass('disabled');
                }
            }
        }

        var trayOptions = {
            title: title||"Compare Changes", //TODO: nls
            width: Infinity,
            overlay: true,
            buttons: [
                {
                    text: RED._((options.unmerged)?"common.label.cancel":"common.label.close"),
                    click: function() {
                        if (options.oncancel) {
                            options.oncancel();
                        }
                        RED.tray.close();
                    }
                }
            ],
            resize: function(dimensions) {
                // trayWidth = dimensions.width;
            },
            open: function(tray) {
                var trayBody = tray.find('.editor-tray-body');
                var diffPanel = $('<div class="node-text-diff"></div>').appendTo(trayBody);
                createUnifiedDiffTable(files,options).appendTo(diffPanel);
            },
            close: function() {
                diffVisible = false;
            },
            show: function() {

            }
        }
        if (options.unmerged) {
            trayOptions.buttons.push(
                {
                    id: "node-diff-view-resolve-diff",
                    text: "Save conflict resolution",
                    class: "primary disabled",
                    click: function() {
                        if (!$("#node-diff-view-resolve-diff").hasClass('disabled')) {
                            if (options.currentDiff) {
                                // This is a flow file. Need to apply the diff
                                // and generate the new flow.
                                var result = applyDiff(options.currentDiff);
                                currentResolution = {
                                    resolutions:{}
                                };
                                currentResolution.resolutions[options.project.files.flow] = JSON.stringify(result.config,"",4);
                            }
                            if (options.onresolve) {
                                options.onresolve(currentResolution);
                            }
                            RED.tray.close();
                        }
                    }
                }
            );
        }
        RED.tray.show(trayOptions);
    }

    function parseCommitDiff(diff) {
        var result = {};
        var lines = diff.split("\n");
        var comment = [];
        for (var i=0;i<lines.length;i++) {
            if (/^commit /.test(lines[i])) {
                result.sha = lines[i].substring(7);
            } else if (/^Author: /.test(lines[i])) {
                result.author = lines[i].substring(8);
                var m = /^(.*) <(.*)>$/.exec(result.author);
                if (m) {
                    result.authorName = m[1];
                    result.authorEmail = m[2];
                }
            } else if (/^Date: /.test(lines[i])) {
                result.date = lines[i].substring(8);
            } else if (/^    /.test(lines[i])) {
                if (!result.title) {
                    result.title = lines[i].substring(4);
                } else {
                    if (lines[i].length !== 4 || comment.length > 0) {
                        comment.push(lines[i].substring(4));
                    }
                }
            } else if (/^diff /.test(lines[i])) {
                result.files = parseUnifiedDiff(lines.slice(i));
                break;
            }
         }
         result.comment = comment.join("\n");
         return result;
    }
    function parseUnifiedDiff(diff) {
        var lines;
        if (Array.isArray(diff)) {
            lines = diff;
        } else {
            lines = diff.split("\n");
        }
        var diffHeader = /^diff (?:(?:--git a\/(.*) b\/(.*))|(?:--cc (.*)))$/;
        var fileHeader = /^\+\+\+ b\/(.*)\t?/;
        var binaryFile = /^Binary files /;
        var hunkHeader = /^@@ -((\d+)(,(\d+))?) \+((\d+)(,(\d+))?) @@ ?(.*)$/;
        var conflictHunkHeader = /^@+ -((\d+)(,(\d+))?) -((\d+)(,(\d+))?) \+((\d+)(,(\d+))?) @+/;
        var files = [];
        var currentFile;
        var hunks = [];
        var currentHunk;
        for (var i=0;i<lines.length;i++) {
            var line = lines[i];
            var diffLine = diffHeader.exec(line);
            if (diffLine) {
                if (currentHunk) {
                    currentFile.hunks.push(currentHunk);
                    files.push(currentFile);
                }
                currentHunk = null;
                currentFile = {
                    file: diffLine[1]||diffLine[3],
                    hunks: []
                }
            } else if (binaryFile.test(line)) {
                if (currentFile) {
                    currentFile.binary = true;
                }
            } else {
                var fileLine = fileHeader.exec(line);
                if (fileLine) {
                    currentFile.file = fileLine[1];
                } else {
                    var hunkLine = hunkHeader.exec(line);
                    if (hunkLine) {
                        if (currentHunk) {
                            currentFile.hunks.push(currentHunk);
                        }
                        currentHunk = {
                            header: line,
                            localStartLine: hunkLine[2],
                            localLength: hunkLine[4]||1,
                            remoteStartLine: hunkLine[6],
                            remoteLength: hunkLine[8]||1,
                            lines: [],
                            conflict: false
                        }
                        continue;
                    }
                    hunkLine = conflictHunkHeader.exec(line);
                    if (hunkLine) {
                        if (currentHunk) {
                            currentFile.hunks.push(currentHunk);
                        }
                        currentHunk = {
                            header: line,
                            localStartLine: hunkLine[2],
                            localLength: hunkLine[4]||1,
                            remoteStartLine: hunkLine[6],
                            remoteLength: hunkLine[8]||1,
                            diffStart: parseInt(hunkLine[10]),
                            lines: [],
                            conflict: true
                        }
                        continue;
                    }
                    if (currentHunk) {
                        currentHunk.lines.push(line);
                    }
                }
            }
        }
        if (currentHunk) {
            currentFile.hunks.push(currentHunk);
        }
        files.push(currentFile);
        return files;
    }

    return {
        init: init,
        getRemoteDiff: getRemoteDiff,
        showRemoteDiff: showRemoteDiff,
        showUnifiedDiff: showUnifiedDiff,
        showCommitDiff: showCommitDiff,
        mergeDiff: mergeDiff
    }
})();