/**
 * 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.
 **/

const clone = require("clone");
const Flow = require('./Flow').Flow;
const context = require('../nodes/context');
const util = require("util");
const redUtil = require("@node-red/util").util;
const events = require("@node-red/util").events;
const flowUtil = require("./util");
const credentials = require("../nodes/credentials");

/**
 * Create deep copy of object
 */
function deepCopy(obj) {
    return JSON.parse(JSON.stringify(obj));
}

/**
 * Evaluate Input Value
 */
function evaluateInputValue(value, type, node) {
    if (type === "bool") {
        return (value === "true") || (value === true);
    }
    if (type === "cred") {
        return value;
    }
    return redUtil.evaluateNodeProperty(value, type, node, null, null);
}

/**
 * This class represents a subflow - which is handled as a special type of Flow
 */
class Subflow extends Flow {

    /**
     * Create a Subflow object.
     * This takes a subflow definition and instance node, creates a clone of the
     * definition with unique ids applied and passes to the super class.
     * @param {[type]} parent          [description]
     * @param {[type]} globalFlow      [description]
     * @param {[type]} subflowDef      [description]
     * @param {[type]} subflowInstance [description]
     */
    constructor(parent,globalFlow,subflowDef,subflowInstance) {
        // console.log("CREATE SUBFLOW",subflowDef.id,subflowInstance.id,"alias?",subflowInstance._alias);
        // console.log("SubflowInstance\n"+JSON.stringify(subflowInstance," ",2));
        // console.log("SubflowDef\n"+JSON.stringify(subflowDef," ",2));
        var subflows = parent.flow.subflows;
        var globalSubflows = parent.global.subflows;

        var node_map = {};
        var node;
        var wires;
        var i;

        var subflowInternalFlowConfig = {
            id: subflowInstance.id,
            configs: {},
            nodes: {},
            groups: {},
            subflows: {}
        }

        if (subflowDef.groups) {
            // Clone all of the subflow group definitions and give them new IDs
            for (i in subflowDef.groups) {
                if (subflowDef.groups.hasOwnProperty(i)) {
                    node = createNodeInSubflow(subflowInstance.id,subflowDef.groups[i]);
                    node_map[node._alias] = node;
                    subflowInternalFlowConfig.groups[node.id] = node;
                }
            }
        }
        if (subflowDef.configs) {
            // Clone all of the subflow config node definitions and give them new IDs
            for (i in subflowDef.configs) {
                if (subflowDef.configs.hasOwnProperty(i)) {
                    node = createNodeInSubflow(subflowInstance.id,subflowDef.configs[i]);
                    node_map[node._alias] = node;
                    subflowInternalFlowConfig.configs[node.id] = node;
                }
            }
        }
        if (subflowDef.nodes) {
            // Clone all of the subflow node definitions and give them new IDs
            for (i in subflowDef.nodes) {
                if (subflowDef.nodes.hasOwnProperty(i)) {
                    node = createNodeInSubflow(subflowInstance.id,subflowDef.nodes[i]);
                    node_map[node._alias] = node;
                    subflowInternalFlowConfig.nodes[node.id] = node;
                }
            }
        }

        subflowInternalFlowConfig.subflows = clone(subflowDef.subflows || {});

        remapSubflowNodes(subflowInternalFlowConfig.configs,node_map);
        remapSubflowNodes(subflowInternalFlowConfig.nodes,node_map);
        remapSubflowNodes(subflowInternalFlowConfig.groups,node_map);

        // console.log("Instance config\n",JSON.stringify(subflowInternalFlowConfig,"",2));

        super(parent,globalFlow,subflowInternalFlowConfig);

        this.TYPE = 'subflow';
        this.subflowDef = subflowDef;
        this.subflowInstance = subflowInstance;
        this.node_map = node_map;
        this.path = parent.path+"/"+(subflowInstance._alias||subflowInstance.id);

        let id = subflowInstance.id;
        if (subflowInstance._alias) {
            id = subflowInstance._alias;
        }
        this.templateCredentials = credentials.get(subflowDef.id) || {};
        this.instanceCredentials = credentials.get(id) || {};

        var env = {};
        if (this.subflowDef.env) {
            this.subflowDef.env.forEach(e => {
                env[e.name] = e;
                if (e.type === "cred") {
                    e.value = this.templateCredentials[e.name];
                }
            });
        }
        if (this.subflowInstance.env) {
            this.subflowInstance.env.forEach(e => {
                var old = env[e.name];
                var ui = old ? old.ui : null;
                env[e.name] = e;
                if (ui) {
                    env[e.name].ui = ui;
                }
                if (e.type === "cred") {
                    if (!old || this.instanceCredentials.hasOwnProperty(e.name) ) {
                        e.value = this.instanceCredentials[e.name];
                    } else if (old) {
                        e.value = this.templateCredentials[e.name];
                    }
                }
            });
        }
        this.env = Object.values(env);
    }

    /**
     * Start the subflow.
     * This creates a subflow instance node to handle the inbound messages. It also
     * rewires an subflow internal node that is connected to an output so it is connected
     * to the parent flow nodes the subflow instance is wired to.
     * @param  {[type]} diff [description]
     * @return {[type]}      [description]
     */
    async start(diff) {
        var self = this;
        // Create a subflow node to accept inbound messages and route appropriately
        var Node = require("../nodes/Node");

        if (this.subflowDef.status) {
            var subflowStatusConfig = {
                id: this.subflowInstance.id+":status",
                type: "subflow-status",
                z: this.subflowInstance.id,
                _flow: this.parent
            }
            this.statusNode = new Node(subflowStatusConfig);
            this.statusNode.on("input", function(msg) {
                if (msg.payload !== undefined) {
                    if (typeof msg.payload === "string") {
                        // if msg.payload is a String, use it as status text
                        self.node.status({text:msg.payload})
                        return;
                    } else if (Object.prototype.toString.call(msg.payload) === "[object Object]") {
                        if (msg.payload.hasOwnProperty('text') || msg.payload.hasOwnProperty('fill') || msg.payload.hasOwnProperty('shape') || Object.keys(msg.payload).length === 0) {
                            // msg.payload is an object that looks like a status object
                            self.node.status(msg.payload);
                            return;
                        }
                    }
                    // Anything else - inspect it and use as status text
                    var text = util.inspect(msg.payload);
                    if (text.length > 32) { text = text.substr(0,32) + "..."; }
                    self.node.status({text:text});
                } else if (msg.status !== undefined) {
                    // if msg.status exists
                    if (msg.status.hasOwnProperty("text") && msg.status.text.indexOf("common.") === 0) {
                        msg.status.text = "node-red:"+msg.status.text;
                    }
                    self.node.status(msg.status)
                }
            })
        }


        var subflowInstanceConfig = {
            id: this.subflowInstance.id,
            type: this.subflowInstance.type,
            g: this.subflowInstance.g,
            z: this.subflowInstance.z,
            name: this.subflowInstance.name,
            wires: [],
            _flow: this
        }
        if (this.subflowDef.in) {
            subflowInstanceConfig.wires = this.subflowDef.in.map(function(n) { return n.wires.map(function(w) { return self.node_map[w.id].id;})})
            subflowInstanceConfig._originalWires = clone(subflowInstanceConfig.wires);
        }

        this.node = new Node(subflowInstanceConfig);
        this.node.on("input", function(msg) { this.send(msg);});
        // Called when the subflow instance node is being stopped
        this.node.on("close", function(done) {
            this.status({});
            // Stop the complete subflow
            self.stop().finally(done)
        })
        this.node.status = status => this.parent.handleStatus(this.node,status);
        // Create a context instance
        // console.log("Node.context",this.type,"id:",this._alias||this.id,"z:",this.z)
        this._context = context.get(this._alias||this.id,this.z);


        this.node._updateWires = this.node.updateWires;

        this.node.updateWires = function(newWires) {
            // Wire the subflow outputs
            if (self.subflowDef.out) {
                var node,wires,i,j;
                // Restore the original wiring to the internal nodes
                subflowInstanceConfig.wires = clone(subflowInstanceConfig._originalWires);
                for (i=0;i<self.subflowDef.out.length;i++) {
                    wires = self.subflowDef.out[i].wires;
                    for (j=0;j<wires.length;j++) {
                        if (wires[j].id != self.subflowDef.id) {
                            node = self.node_map[wires[j].id];
                            if (node && node._originalWires) {
                                node.wires = clone(node._originalWires);
                            }
                        }
                    }
                }

                var modifiedNodes = {};
                var subflowInstanceModified = false;
                for (i=0;i<self.subflowDef.out.length;i++) {
                    wires = self.subflowDef.out[i].wires;
                    for (j=0;j<wires.length;j++) {
                        if (wires[j].id === self.subflowDef.id) {
                            subflowInstanceConfig.wires[wires[j].port] = subflowInstanceConfig.wires[wires[j].port].concat(newWires[i]);
                            subflowInstanceModified = true;
                        } else {
                            node = self.node_map[wires[j].id];
                            if (node) {
                                node.wires[wires[j].port] = node.wires[wires[j].port].concat(newWires[i]);
                                modifiedNodes[node.id] = node;
                            }
                        }
                    }
                }
                Object.keys(modifiedNodes).forEach(function(id) {
                    var node = modifiedNodes[id];
                    self.activeNodes[id].updateWires(node.wires);
                });

                if (subflowInstanceModified) {
                    self.node._updateWires(subflowInstanceConfig.wires);
                }
            }
        };

        // Wire the subflow outputs
        if (this.subflowDef.out) {
            for (var i=0;i<this.subflowDef.out.length;i++) {
                // i: the output index
                // This is what this Output is wired to
                var wires = this.subflowDef.out[i].wires;
                for (var j=0;j<wires.length;j++) {
                    if (wires[j].id === this.subflowDef.id) {
                        // A subflow input wired straight to a subflow output
                        subflowInstanceConfig.wires[wires[j].port] = subflowInstanceConfig.wires[wires[j].port].concat(this.subflowInstance.wires[i])
                        this.node._updateWires(subflowInstanceConfig.wires);
                    } else {
                        var node = self.node_map[wires[j].id];
                        if (node) {
                            if (!node._originalWires) {
                                node._originalWires = clone(node.wires);
                            }
                            node.wires[wires[j].port] = (node.wires[wires[j].port]||[]).concat(this.subflowInstance.wires[i]);
                        } else {
                            this.error("Unknown node referenced inside subflow: " + wires[j].id)
                        }
                    }
                }
            }
        }

        if (this.subflowDef.status) {
            var subflowStatusId = this.statusNode.id;
            wires = this.subflowDef.status.wires;
            for (var j=0;j<wires.length;j++) {
                if (wires[j].id === this.subflowDef.id) {
                    // A subflow input wired straight to a subflow output
                    subflowInstanceConfig.wires[wires[j].port].push(subflowStatusId);
                    this.node._updateWires(subflowInstanceConfig.wires);
                } else {
                    var node = self.node_map[wires[j].id];
                    if (node) {
                        if (!node._originalWires) {
                            node._originalWires = clone(node.wires);
                        }
                        node.wires[wires[j].port] = (node.wires[wires[j].port]||[]);
                        node.wires[wires[j].port].push(subflowStatusId);
                    } else {
                        this.error("Unknown node referenced inside subflow: " + wires[j].id)
                    }
                }
            }
        }
        return super.start(diff);
    }

    /**
     * Stop this subflow.
     * The `stopList` argument helps define what needs to be stopped in the case
     * of a modified-nodes/flows type deploy.
     * @param  {[type]} stopList    [description]
     * @param  {[type]} removedList [description]
     * @return {[type]}             [description]
     */
    stop(stopList, removedList) {
        const nodes = Object.keys(this.activeNodes);
        return super.stop(stopList, removedList).then(res => {
            nodes.forEach(id => {
                events.emit("node-status",{
                    id: id
                });
            })
            return res;
        })

    }
    /**
     * Get environment variable of subflow
     * @param {String}   key   name of env var
     * @return {Object}  val    value of env var
     */
    getSetting(key) {
        const node = this.subflowInstance;
        if (node) {
            if (key === "NR_NODE_NAME" || key === "NR_SUBFLOW_NAME") {
                return node.name;
            }
            if (key === "NR_NODE_ID" || key === "NR_SUBFLOW_ID") {
                return node.id;
            }
            if (key === "NR_NODE_PATH" || key === "NR_SUBFLOW_PATH") {
                return node._path;
            }
        }
        if (!key.startsWith("$parent.")) {
            if (this._env.hasOwnProperty(key)) {
                return (this._env[key] && Object.hasOwn(this._env[key], 'value') && this._env[key].__clone__) ? clone(this._env[key].value) : this._env[key]
            }
        } else {
            key = key.substring(8);
        }
        // Push the request up to the parent.
        // Unlike a Flow, the parent of a Subflow could be a Group
        if (node.g) {
            return this.parent.getGroupNode(node.g).getSetting(key)
        }
        return this.parent.getSetting(key)
    }

    /**
     * Get a node instance from this subflow.
     * If the subflow has a status node, check for that, otherwise use
     * the super-class function
     * @param  {String} id [description]
     * @param  {Boolean} cancelBubble    if true, prevents the flow from passing the request to the parent
     *                                   This stops infinite loops when the parent asked this Flow for the
     *                                   node to begin with.
     * @return {[type]}    [description]
     */
    getNode(id, cancelBubble) {
        if (this.statusNode && this.statusNode.id === id) {
            return this.statusNode;
        }
        return super.getNode(id,cancelBubble);
    }

    /**
     * Handle a status event from a node within this flow.
     * @param  {Node}    node          The original node that triggered the event
     * @param  {Object}  statusMessage The status object
     * @param  {Node}    reportingNode The node emitting the status event.
     *                                 This could be a subflow instance node when the status
     *                                 is being delegated up.
     * @param  {boolean} muteStatus    Whether to emit the status event
     * @return {[type]}               [description]
     */
    handleStatus(node,statusMessage,reportingNode,muteStatus) {
        let handled = super.handleStatus(node,statusMessage,reportingNode,muteStatus);
        if (!handled) {
            if (!this.statusNode || node === this.node) {
                // No status node on this subflow caught the status message.
                // AND there is no Subflow Status node - so the user isn't
                // wanting to manage status messages themselves
                // Pass up to the parent with this subflow's instance as the
                // reporting node
                handled = this.parent.handleStatus(node,statusMessage,this.node,true);
            }
        }
        return handled;

    }

    /**
     * Handle an error event from a node within this flow. If there are no Catch
     * nodes within this flow, pass the event to the parent flow.
     * @param  {[type]} node          [description]
     * @param  {[type]} logMessage    [description]
     * @param  {[type]} msg           [description]
     * @param  {[type]} reportingNode [description]
     * @return {[type]}               [description]
     */
    handleError(node,logMessage,msg,reportingNode) {
        let handled = super.handleError(node,logMessage,msg,reportingNode);
        if (!handled) {
            // No catch node on this subflow caught the error message.
            // Pass up to the parent with the subflow's instance as the
            // reporting node.
            handled = this.parent.handleError(node,logMessage,msg,this.node);
        }
        return handled;
    }
}


/**
 * Clone a node definition for use within a subflow instance.
 * Give the node a new id and set its _alias property to record
 * its association with the original node definition.
 * @param  {[type]} subflowInstanceId [description]
 * @param  {[type]} def               [description]
 * @return {[type]}                   [description]
 */
function createNodeInSubflow(subflowInstanceId, def) {
    let node = clone(def);
    let nid = `${subflowInstanceId}-${node.id}` //redUtil.generateId();
    // console.log("Create Node In subflow",node._alias, "--->",nid, "(",node.type,")")
    // node_map[node.id] = node;
    node._alias = node.id;
    node.id = nid;
    node.z = subflowInstanceId;
    return node;
}


/**
 * Given an object of {id:nodes} and a map of {old-id:node}, modifiy all
 * properties in the nodes object to reference the new node ids.
 * This handles:
 *  - node.wires,
 *  - node.scope of Complete, Catch and Status nodes,
 *  - node.XYZ for any property where XYZ is recognised as an old property
 * @param  {[type]} nodes   [description]
 * @param  {[type]} nodeMap [description]
 * @return {[type]}         [description]
 */
function remapSubflowNodes(nodes,nodeMap) {
    for (var id in nodes) {
        if (nodes.hasOwnProperty(id)) {
            var node = nodes[id];
            if (node.wires) {
                var outputs = node.wires;
                for (var j=0;j<outputs.length;j++) {
                    var wires = outputs[j];
                    for (var k=0;k<wires.length;k++) {
                        if (nodeMap[outputs[j][k]]) {
                            outputs[j][k] = nodeMap[outputs[j][k]].id
                        } else {
                            outputs[j][k] = null;
                        }
                    }
                }
            }
            if ((node.type === 'complete' || node.type === 'catch' || node.type === 'status') && Array.isArray(node.scope)) {
                node.scope = node.scope.map(function(id) {
                    return nodeMap[id]?nodeMap[id].id:""
                })
            } else {
                for (var prop in node) {
                    if (node.hasOwnProperty(prop) && prop !== '_alias') {
                        if (nodeMap[node[prop]]) {
                            node[prop] = nodeMap[node[prop]].id;
                        }
                    }
                }
            }
        }
    }
}

class SubflowModule extends Subflow {
    /**
     * Create a Subflow Module object.
     * This is a node that has been published as a subflow.
     * @param {[type]} parent          [description]
     * @param {[type]} globalFlow      [description]
     * @param {[type]} subflowDef      [description]
     * @param {[type]} subflowInstance [description]
     */
    constructor(type, parent,globalFlow,subflowDef,subflowInstance) {
        super(parent,globalFlow,subflowDef,subflowInstance);
        this.TYPE = `module:${type}`;
        this.subflowType = type;
    }

    /**
     * [log description]
     * @param  {[type]} msg [description]
     * @return {[type]}     [description]
     */
    log(msg) {
        if (msg.id) {
            msg.id = this.id
        }
        if (msg.type) {
            msg.type = this.subflowType
        }
        super.log(msg);
    }

}

function createSubflow(parent,globalFlow,subflowDef,subflowInstance) {
    return new Subflow(parent,globalFlow,subflowDef,subflowInstance)
}


function createModuleInstance(type, parent,globalFlow,subflowDef,subflowInstance) {
    return new SubflowModule(type, parent,globalFlow,subflowDef,subflowInstance);
}

module.exports = {
    init: function(runtime) {},
    create: createSubflow,
    createModuleInstance: createModuleInstance
}
