/**
 * Copyright JS Foundation and other contributors, http://js.foundation
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 **/
var clone = require("clone");
var redUtil = require("@node-red/util").util;
var Log = require("@node-red/util").log;
var subflowInstanceRE = /^subflow:(.+)$/;
var typeRegistry = require("@node-red/registry");

var envVarExcludes = {};

function diffNodes(oldNode,newNode) {
    if (oldNode == null) {
        return true;
    }
    var oldKeys = Object.keys(oldNode).filter(function(p) { return p != "x" && p != "y" && p != "wires" });
    var newKeys = Object.keys(newNode).filter(function(p) { return p != "x" && p != "y" && p != "wires" });
    if (oldKeys.length != newKeys.length) {
        return true;
    }
    for (var i=0;i<newKeys.length;i++) {
        var p = newKeys[i];
        if (!redUtil.compareObjects(oldNode[p],newNode[p])) {
            return true;
        }
    }

    return false;
}

var EnvVarPropertyRE_old = /^\$\((\S+)\)$/;
var EnvVarPropertyRE = /^\${(\S+)}$/;

function mapEnvVarProperties(obj,prop,flow) {
    var v = obj[prop];
    if (Buffer.isBuffer(v)) {
        return;
    } else if (Array.isArray(v)) {
        for (var i=0;i<v.length;i++) {
            mapEnvVarProperties(v,i,flow);
        }
    } else if (typeof obj[prop] === 'string') {
        if (obj[prop][0] === "$" && (EnvVarPropertyRE_old.test(v) || EnvVarPropertyRE.test(v)) ) {
            var envVar = v.substring(2,v.length-1);
            var r = flow.getSetting(envVar);
            obj[prop] = r!==undefined?r:obj[prop];
        }
    } else {
        for (var p in v) {
            if (v.hasOwnProperty(p)) {
                mapEnvVarProperties(v,p,flow);
            }
        }
    }
}

module.exports = {
    init: function(runtime) {
        envVarExcludes = {};
        if (runtime.settings.hasOwnProperty('envVarExcludes') && Array.isArray(runtime.settings.envVarExcludes)) {
            runtime.settings.envVarExcludes.forEach(v => envVarExcludes[v] = true);
        }
    },
    getEnvVar: function(k) {
        return !envVarExcludes[k]?process.env[k]:undefined
    },
    diffNodes: diffNodes,
    mapEnvVarProperties: mapEnvVarProperties,

    parseConfig: function(config) {
        var flow = {};
        flow.allNodes = {};
        flow.subflows = {};
        flow.configs = {};
        flow.flows = {};
        flow.groups = {};
        flow.missingTypes = [];

        config.forEach(function(n) {
            flow.allNodes[n.id] = clone(n);
            if (n.type === 'tab') {
                flow.flows[n.id] = n;
                flow.flows[n.id].subflows = {};
                flow.flows[n.id].configs = {};
                flow.flows[n.id].nodes = {};
            }
            if (n.type === 'group') {
                flow.groups[n.id] = n;
            }
        });

        // TODO: why a separate forEach? this can be merged with above
        config.forEach(function(n) {
            if (n.type === 'subflow') {
                flow.subflows[n.id] = n;
                flow.subflows[n.id].configs = {};
                flow.subflows[n.id].nodes = {};
                flow.subflows[n.id].instances = [];
            }
        });
        var linkWires = {};
        var linkOutNodes = [];
        config.forEach(function(n) {
            if (n.type !== 'subflow' && n.type !== 'tab' && n.type !== 'group') {
                var subflowDetails = subflowInstanceRE.exec(n.type);

                if ( (subflowDetails && !flow.subflows[subflowDetails[1]]) || (!subflowDetails && !typeRegistry.get(n.type)) ) {
                    if (flow.missingTypes.indexOf(n.type) === -1) {
                        flow.missingTypes.push(n.type);
                    }
                }
                var container = null;
                if (flow.flows[n.z]) {
                    container = flow.flows[n.z];
                } else if (flow.subflows[n.z]) {
                    container = flow.subflows[n.z];
                }
                if (n.hasOwnProperty('x') && n.hasOwnProperty('y')) {
                    if (subflowDetails) {
                        var subflowType = subflowDetails[1]
                        n.subflow = subflowType;
                        flow.subflows[subflowType].instances.push(n)
                    }
                    if (container) {
                        container.nodes[n.id] = n;
                    }
                } else {
                    if (container) {
                        container.configs[n.id] = n;
                    } else {
                        flow.configs[n.id] = n;
                        flow.configs[n.id]._users = [];
                    }
                }
                if (n.type === 'link in' && n.links) {
                    // Ensure wires are present in corresponding link out nodes
                    n.links.forEach(function(id) {
                        linkWires[id] = linkWires[id]||{};
                        linkWires[id][n.id] = true;
                    })
                } else if (n.type === 'link out' && n.links) {
                    linkWires[n.id] = linkWires[n.id]||{};
                    n.links.forEach(function(id) {
                        linkWires[n.id][id] = true;
                    })
                    linkOutNodes.push(n);
                }
            }
        });
        linkOutNodes.forEach(function(n) {
            var links = linkWires[n.id];
            var targets = Object.keys(links);
            n.wires = [targets];
        });


        var addedTabs = {};
        config.forEach(function(n) {
            if (n.type !== 'subflow' && n.type !== 'tab' && n.type !== 'group') {
                for (var prop in n) {
                    if (n.hasOwnProperty(prop) && prop !== 'id' && prop !== 'wires' && prop !== 'type' && prop !== '_users' && flow.configs.hasOwnProperty(n[prop])) {
                        // This property references a global config node
                        flow.configs[n[prop]]._users.push(n.id)
                    }
                }
                if (n.z && !flow.subflows[n.z]) {

                    if (!flow.flows[n.z]) {
                        flow.flows[n.z] = {type:'tab',id:n.z};
                        flow.flows[n.z].subflows = {};
                        flow.flows[n.z].configs = {};
                        flow.flows[n.z].nodes = {};
                        addedTabs[n.z] = flow.flows[n.z];
                    }
                    if (addedTabs[n.z]) {
                        if (n.hasOwnProperty('x') && n.hasOwnProperty('y')) {
                            addedTabs[n.z].nodes[n.id] = n;
                        } else {
                            addedTabs[n.z].configs[n.id] = n;
                        }
                    }
                }
            }
        });
        return flow;
    },

    diffConfigs: function(oldConfig, newConfig) {
        var id;
        var node;
        var nn;
        var wires;
        var j,k;

        if (!oldConfig) {
            oldConfig = {
                flows:{},
                allNodes:{}
            }
        }
        var changedSubflows = {};

        var added = {};
        var removed = {};
        var changed = {};
        var wiringChanged = {};

        var linkMap = {};

        var changedTabs = {};

        // Look for tabs that have been removed
        for (id in oldConfig.flows) {
            if (oldConfig.flows.hasOwnProperty(id) && (!newConfig.flows.hasOwnProperty(id))) {
                removed[id] = oldConfig.allNodes[id];
            }
        }

        // Look for tabs that have been disabled
        for (id in oldConfig.flows) {
            if (oldConfig.flows.hasOwnProperty(id) && newConfig.flows.hasOwnProperty(id)) {
                var originalState = oldConfig.flows[id].disabled||false;
                var newState = newConfig.flows[id].disabled||false;
                if (originalState !== newState) {
                    changedTabs[id] = true;
                    if (originalState) {
                        added[id] = oldConfig.allNodes[id];
                    } else {
                        removed[id] = oldConfig.allNodes[id];
                    }
                }
            }
        }

        for (id in oldConfig.allNodes) {
            if (oldConfig.allNodes.hasOwnProperty(id)) {
                node = oldConfig.allNodes[id];
                if (node.type !== 'tab') {
                    // build the map of what this node was previously wired to
                    if (node.wires) {
                        linkMap[node.id] = linkMap[node.id] || [];
                        for (j=0;j<node.wires.length;j++) {
                            wires = node.wires[j];
                            for (k=0;k<wires.length;k++) {
                                linkMap[node.id].push(wires[k]);
                                nn = oldConfig.allNodes[wires[k]];
                                if (nn) {
                                    linkMap[nn.id] = linkMap[nn.id] || [];
                                    linkMap[nn.id].push(node.id);
                                }
                            }
                        }
                    }
                    // This node has been removed or its flow disabled
                    if (removed[node.z] || !newConfig.allNodes.hasOwnProperty(id)) {
                        removed[id] = node;
                        // Mark the container as changed
                        if (!removed[node.z] && newConfig.allNodes[removed[id].z]) {
                            changed[removed[id].z] = newConfig.allNodes[removed[id].z];
                            if (changed[removed[id].z].type === "subflow") {
                                changedSubflows[removed[id].z] = changed[removed[id].z];
                                //delete removed[id];
                            }
                        }
                    } else {
                        if (added[node.z]) {
                            added[id] = node;
                        } else {
                            var currentState = node.d;
                            var newState = newConfig.allNodes[id].d;
                            if (!currentState && newState) {
                                removed[id] = node;
                            }
                            // This node has a material configuration change
                            if (diffNodes(node,newConfig.allNodes[id]) || newConfig.allNodes[id].credentials) {
                                changed[id] = newConfig.allNodes[id];
                                if (changed[id].type === "subflow") {
                                    changedSubflows[id] = changed[id];
                                }
                                // Mark the container as changed
                                if (newConfig.allNodes[changed[id].z]) {
                                    changed[changed[id].z] = newConfig.allNodes[changed[id].z];
                                    if (changed[changed[id].z].type === "subflow") {
                                        changedSubflows[changed[id].z] = changed[changed[id].z];
                                        delete changed[id];
                                    }
                                }
                            }
                            // This node's wiring has changed
                            if (!redUtil.compareObjects(node.wires,newConfig.allNodes[id].wires)) {
                                wiringChanged[id] = newConfig.allNodes[id];
                                // Mark the container as changed
                                if (newConfig.allNodes[wiringChanged[id].z]) {
                                    changed[wiringChanged[id].z] = newConfig.allNodes[wiringChanged[id].z];
                                    if (changed[wiringChanged[id].z].type === "subflow") {
                                        changedSubflows[wiringChanged[id].z] = changed[wiringChanged[id].z];
                                        delete wiringChanged[id];
                                    }
                                }
                            }
                        }
                    }
                }
            }
        }
        // Look for added nodes
        for (id in newConfig.allNodes) {
            if (newConfig.allNodes.hasOwnProperty(id)) {
                node = newConfig.allNodes[id];
                // build the map of what this node is now wired to
                if (node.wires) {
                    linkMap[node.id] = linkMap[node.id] || [];
                    for (j=0;j<node.wires.length;j++) {
                        wires = node.wires[j];
                        for (k=0;k<wires.length;k++) {
                            if (linkMap[node.id].indexOf(wires[k]) === -1) {
                                linkMap[node.id].push(wires[k]);
                            }
                            nn = newConfig.allNodes[wires[k]];
                            if (nn) {
                                linkMap[nn.id] = linkMap[nn.id] || [];
                                if (linkMap[nn.id].indexOf(node.id) === -1) {
                                    linkMap[nn.id].push(node.id);
                                }
                            }
                        }
                    }
                }
                // This node has been added
                if (!oldConfig.allNodes.hasOwnProperty(id)) {
                    added[id] = node;
                    // Mark the container as changed
                    if (newConfig.allNodes[added[id].z]) {
                        changed[added[id].z] = newConfig.allNodes[added[id].z];
                        if (changed[added[id].z].type === "subflow") {
                            changedSubflows[added[id].z] = changed[added[id].z];
                            delete added[id];
                        }
                    }
                }
            }
        }

        var madeChange;
        // Loop through the nodes looking for references to changed config nodes
        // Repeat the loop if anything is marked as changed as it may need to be
        // propagated to parent nodes.
        // TODO: looping through all nodes every time is a bit inefficient - could be more targeted
        do {
            madeChange = false;
            for (id in newConfig.allNodes) {
                if (newConfig.allNodes.hasOwnProperty(id)) {
                    node = newConfig.allNodes[id];
                    for (var prop in node) {
                        if (node.hasOwnProperty(prop) && prop != "z" && prop != "id" && prop != "wires") {
                            // This node has a property that references a changed/removed node
                            // Assume it is a config node change and mark this node as
                            // changed.
                            if (changed[node[prop]] || removed[node[prop]]) {
                                if (!changed[node.id]) {
                                    madeChange = true;
                                    changed[node.id] = node;
                                    // This node exists within subflow template
                                    // Mark the template as having changed
                                    if (newConfig.allNodes[node.z]) {
                                        changed[node.z] = newConfig.allNodes[node.z];
                                        if (changed[node.z].type === "subflow") {
                                            changedSubflows[node.z] = changed[node.z];
                                        }
                                    }
                                }
                            }
                        }
                    }
                }
            }
        } while (madeChange===true)

        // Find any nodes that exist on a subflow template and remove from changed
        // list as the parent subflow will now be marked as containing a change
        for (id in newConfig.allNodes) {
            if (newConfig.allNodes.hasOwnProperty(id)) {
                node = newConfig.allNodes[id];
                if (newConfig.allNodes[node.z] && newConfig.allNodes[node.z].type === "subflow") {
                    delete changed[node.id];
                }
            }
        }

        // Recursively mark all instances of changed subflows as changed
        var changedSubflowStack = Object.keys(changedSubflows);
        while (changedSubflowStack.length > 0) {
            var subflowId = changedSubflowStack.pop();
            for (id in newConfig.allNodes) {
                if (newConfig.allNodes.hasOwnProperty(id)) {
                    node = newConfig.allNodes[id];
                    if (node.type === 'subflow:'+subflowId) {
                        if (!changed[node.id]) {
                            changed[node.id] = node;
                            if (!changed[changed[node.id].z] && newConfig.allNodes[changed[node.id].z]) {
                                changed[changed[node.id].z] = newConfig.allNodes[changed[node.id].z];
                                if (newConfig.allNodes[changed[node.id].z].type === "subflow") {
                                    // This subflow instance is inside a subflow. Add the
                                    // containing subflow to the stack to mark
                                    changedSubflowStack.push(changed[node.id].z);
                                    delete changed[node.id];
                                }
                            }
                        }
                    }
                }
            }
        }

        var diff = {
            added:Object.keys(added),
            changed:Object.keys(changed),
            removed:Object.keys(removed),
            rewired:Object.keys(wiringChanged),
            linked:[]
        }

        // Traverse the links of all modified nodes to mark the connected nodes
        var modifiedNodes = diff.added.concat(diff.changed).concat(diff.removed).concat(diff.rewired);
        var visited = {};
        while (modifiedNodes.length > 0) {
            node = modifiedNodes.pop();
            if (!visited[node]) {
                visited[node] = true;
                if (linkMap[node]) {
                    if (!changed[node] && !added[node] && !removed[node] && !wiringChanged[node]) {
                        diff.linked.push(node);
                    }
                    modifiedNodes = modifiedNodes.concat(linkMap[node]);
                }
            }
        }
        // console.log(diff);
        // for (id in newConfig.allNodes) {
        //     console.log(
        //         (added[id]?"a":(changed[id]?"c":" "))+(wiringChanged[id]?"w":" ")+(diff.linked.indexOf(id)!==-1?"l":" "),
        //         newConfig.allNodes[id].type.padEnd(10),
        //         id.padEnd(16),
        //         (newConfig.allNodes[id].z||"").padEnd(16),
        //         newConfig.allNodes[id].name||newConfig.allNodes[id].label||""
        //     );
        // }
        // for (id in removed) {
        //     console.log(
        //         "- "+(diff.linked.indexOf(id)!==-1?"~":" "),
        //         id,
        //         oldConfig.allNodes[id].type,
        //         oldConfig.allNodes[id].name||oldConfig.allNodes[id].label||""
        //     );
        // }

        return diff;
    },

    /**
     * Create a new instance of a node
     * @param  {Flow} flow     The containing flow
     * @param  {object} config The node configuration object
     * @return {Node}          The instance of the node
     */
    createNode: function(flow,config) {
        var newNode = null;
        var type = config.type;
        try {
            var nodeTypeConstructor = typeRegistry.get(type);
            if (nodeTypeConstructor) {
                var conf = clone(config);
                delete conf.credentials;
                for (var p in conf) {
                    if (conf.hasOwnProperty(p)) {
                        mapEnvVarProperties(conf,p,flow);
                    }
                }
                try {
                    Object.defineProperty(conf,'_flow', {value: flow, enumerable: false, writable: true })
                    newNode = new nodeTypeConstructor(conf);
                } catch (err) {
                    Log.log({
                        level: Log.ERROR,
                        id:conf.id,
                        type: type,
                        msg: err
                    });
                }
            } else {
                Log.error(Log._("nodes.flow.unknown-type", {type:type}));
            }
        } catch(err) {
            Log.error(err);
        }
        return newNode;
    }
}