text: /**
   * 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('../context');
  const util = require("util");
  
  const redUtil = require("@node-red/util").util;
  const flowUtil = require("./util");
  
  
  const credentials = require("../credentials");
  
  var Log;
  
  /**
   * 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);
  }
  
  /**
   * Compose information object for env var
   */
  function composeInfo(info, val) {
      var result = {
          name: info.name,
          type: info.type,
          value: val,
      };
      if (info.ui) {
          var ui = info.ui;
          result.ui = {
              hasUI: ui.hasUI,
              icon: ui.icon,
              labels: ui.labels,
              type: ui.type
          };
          var retUI = result.ui;
          if (ui.type === "input") {
              retUI.inputTypes = ui.inputTypes;
          }
          if (ui.type === "select") {
              retUI.menu = ui.menu;
          }
          if (ui.type === "spinner") {
              retUI.spinner = ui.spinner;
          }
      }
      return result;
  }
  
  
  /**
   * 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: {},
              subflows: {}
          }
  
          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);
  
          // 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);
  
          this.templateCredentials = credentials.get(subflowDef.id);
          this.instanceCredentials = credentials.get(this.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 = 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]
       */
      start(diff) {
          var self = this;
          // Create a subflow node to accept inbound messages and route appropriately
          var Node = require("../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,
              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);});
          this.node.on("close", function() { this.status({}); })
          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._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];
                              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._originalWires) {
                              node._originalWires = clone(node.wires);
                          }
                          node.wires[wires[j].port] = (node.wires[wires[j].port]||[]).concat(this.subflowInstance.wires[i]);
                      }
                  }
              }
          }
  
          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._originalWires) {
                          node._originalWires = clone(node.wires);
                      }
                      node.wires[wires[j].port] = (node.wires[wires[j].port]||[]);
                      node.wires[wires[j].port].push(subflowStatusId);
                  }
              }
          }
          super.start(diff);
      }
  
  
      /**
       * Get environment variable of subflow
       * @param {String}   name   name of env var
       * @return {Object}  val    value of env var
       */
      getSetting(name) {
          if (!/^\$parent\./.test(name)) {
              var env = this.env;
              var is_info = name.endsWith("_info");
              var is_type = name.endsWith("_type");
              var ename = (is_info || is_type) ? name.substring(0, name.length -5) : name; // 5 = length of "_info"/"_type"
  
              if (env && env.hasOwnProperty(ename)) {
                  var val = env[ename];
                  if (is_type) {
                      return val ? val.type : undefined;
                  }
                  // If this is an env type property we need to be careful not
                  // to get into lookup loops.
                  // 1. if the value to lookup is the same as this one, go straight to parent
                  // 2. otherwise, check if it is a compound env var ("foo $(bar)")
                  //    and if so, substitute any instances of `name` with $parent.name
                  // See https://github.com/node-red/node-red/issues/2099
                  if (val.type !== 'env' || val.value !== name) {
                      let value = val.value;
                      var type = val.type;
                      if (type === 'env') {
                          value = value.replace(new RegExp("\\${"+name+"}","g"),"${$parent."+name+"}");
                      }
                      try {
                          var ret = evaluateInputValue(value, type, this.node);
                          if (is_info) {
                              return composeInfo(val, ret);
                          }
                          return ret;
                      }
                      catch (e) {
                          this.error(e);
                          return undefined;
                      }
                  } else {
                      // This _is_ an env property pointing at itself - go to parent
                  }
              }
          } else {
              // name starts $parent. ... so delegate to parent automatically
              name = name.substring(8);
          }
          var parent = this.parent;
          if (parent) {
              var val = parent.getSetting(name);
              return val;
          }
          return undefined;
      }
  
      /**
       * 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 = 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 (j=0;j<outputs.length;j++) {
                      wires = outputs[j];
                      for (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') && 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;
                          }
                      }
                  }
              }
          }
      }
  }
  
  function createSubflow(parent,globalFlow,subflowDef,subflowInstance) {
      return new Subflow(parent,globalFlow,subflowDef,subflowInstance)
  }
  
  module.exports = {
      init: function(runtime) {
          Log = runtime.log;
      },
      create: createSubflow
  }
  