Source: core/env.mjs

/**
 * cnodes
 *
 * A representation-agnostic library to define and execute nodes based processes
 * License: MIT
 * Author: Marco Jacovone
 * Year: 2020-2021
 */

import { Program } from "./program.mjs";
import { Enter } from "./enter.mjs";
import { Exit } from "./exit.mjs";
import { Call } from "../nodes/call.mjs";
import { Console } from "../nodes/console.mjs";
import { FGetvar } from "../nodes/fgetvar.mjs";
import { For } from "../nodes/for.mjs";
import { Getvar } from "../nodes/getvar.mjs";
import { Setvar } from "../nodes/setvar.mjs";
import { While } from "../nodes/while.mjs";
import { If } from "../nodes/if.mjs";
import { APush } from "../nodes/array/apush.mjs";
import { FAConst } from "../nodes/array/faconst.mjs";
import { FAMake } from "../nodes/array/famake.mjs";
import { FAGet } from "../nodes/array/faget.mjs";
import { FALength } from "../nodes/array/falength.mjs";
import { FAdd } from "../nodes/math/fadd.mjs";
import { FDiv } from "../nodes/math/fdiv.mjs";
import { FMul } from "../nodes/math/fmul.mjs";
import { FSqrt } from "../nodes/math/fsqrt.mjs";
import { FEqual } from "../nodes/bool/fequal.mjs";
import { FGT } from "../nodes/bool/fgt.mjs";
import { FGTE } from "../nodes/bool/fgte.mjs";
import { FLT } from "../nodes/bool/flt.mjs";
import { FLTE } from "../nodes/bool/flte.mjs";
import { FNotEqual } from "../nodes/bool/fnotequal.mjs";
import {
  InputSocket,
  NextSocket,
  OutputSocket,
  PrevSocket,
  Socket,
} from "./socket.mjs";
import { Node } from "./node.mjs";
import { FSConst } from "../nodes/string/fsconst.mjs";
import { FConcat } from "../nodes/string/fconcat.mjs";
import { FMod } from "../nodes/math/fmod.mjs";
import { FIf } from "../nodes/fif.mjs";
import { FIsNull } from "../nodes/isnull.mjs";
import { FIsUndefined } from "../nodes/isundefined.mjs";
import { FNConst } from "../nodes/math/fnconst.mjs";
import { FOMake } from "../nodes/object/fomake.mjs";
import { FOBreak } from "../nodes/object/fobreak.mjs";
import { OSet, FOSet } from "../nodes/object/oset.mjs";
import { AMap } from "../nodes/array/amap.mjs";
import { AReduce } from "../nodes/array/areduce.mjs";
import { FAMap } from "../nodes/array/famap.mjs";
import { Types } from "./type.mjs";
import { FAReduce } from "../nodes/array/fareduce.mjs";
import { Log } from "../nodes/log.mjs";
import { FTofixed } from "../nodes/math/ftofixed.mjs";
import { Wait } from "../nodes/wait.mjs";
import { FAnd } from "../nodes/bool/fand.mjs";
import { FOr } from "../nodes/bool/f_or.mjs";
import { FNot } from "../nodes/bool/fnot.mjs";
import { FFalse } from "../nodes/bool/ffalse.mjs";
import { FTrue } from "../nodes/bool/ftrue.mjs";

/**
 * This class represents a main global environment for cnodes.
 * The class is a "static" class that is responible for maintaining a global
 * registry of registered nodes. A node registration is a object with three fields: a node name,
 * a category name and a factory, that returns a new instance for that node.
 * The global Env instance must be initialized one-time by calling the Env.init() method,
 * this method register all built-in nodes. Eventual custom nodes must be registered manually
 * via Env.registerNode(name, category, factory).
 */
export class Env {
  /** The internal node registry */
  static #nodeRegistry = new Map();

  /**
   * Initialize the CNodes global environment
   */
  static init() {
    Env.#nodeRegistry = new Map();

    // Core nodes

    Env.registerNode("Program", "Core", Program.instance);
    Env.registerNode("Call", "Core", Call.instance);
    Env.registerNode("Console", "Core", Console.instance);
    Env.registerNode("Log", "Core", Log.instance);
    Env.registerNode("FGetvar", "Core", FGetvar.instance);
    Env.registerNode("For", "Core", For.instance);
    Env.registerNode("Getvar", "Core", Getvar.instance);
    Env.registerNode("If", "Core", If.instance);
    Env.registerNode("FIf", "Core", FIf.instance);
    Env.registerNode("FIsNull", "Core", FIsNull.instance);
    Env.registerNode("FIsUndefined", "Core", FIsUndefined.instance);
    Env.registerNode("Setvar", "Core", Setvar.instance);
    Env.registerNode("While", "Core", While.instance);
    Env.registerNode("Enter", "Core", Enter.instance);
    Env.registerNode("Exit", "Core", Exit.instance);
    Env.registerNode("Wait", "Core", Wait.instance);

    // String nodes
    Env.registerNode("FSConst", "String", FSConst.instance);
    Env.registerNode("FConcat", "String", FConcat.instance);

    // Math nodes
    Env.registerNode("FNConst", "Math", FNConst.instance);
    Env.registerNode("FAdd", "Math", FAdd.instance);
    Env.registerNode("FDiv", "Math", FDiv.instance);
    Env.registerNode("FMod", "Math", FMod.instance);
    Env.registerNode("FMul", "Math", FMul.instance);
    Env.registerNode("FSqrt", "Math", FSqrt.instance);
    Env.registerNode("FTofixed", "Math", FTofixed.instance);

    // Boolean Nodes
    Env.registerNode("FEqual", "Boolean", FEqual.instance);
    Env.registerNode("FGT", "Boolean", FGT.instance);
    Env.registerNode("FGTE", "Boolean", FGTE.instance);
    Env.registerNode("FLT", "Boolean", FLT.instance);
    Env.registerNode("FLTE", "Boolean", FLTE.instance);
    Env.registerNode("FNotEqual", "Boolean", FNotEqual.instance);
    Env.registerNode("FAnd", "Boolean", FAnd.instance);
    Env.registerNode("FOr", "Boolean", FOr.instance);
    Env.registerNode("FNot", "Boolean", FNot.instance);
    Env.registerNode("FFalse", "Boolean", FFalse.instance);
    Env.registerNode("FTrue", "Boolean", FTrue.instance);

    // Arrays Nodes
    Env.registerNode("APush", "Arrays", APush.instance);
    Env.registerNode("FAConst", "Arrays", FAConst.instance);
    Env.registerNode("FAMake", "Arrays", FAMake.instance);
    Env.registerNode("FAGet", "Arrays", FAGet.instance);
    Env.registerNode("FALength", "Arrays", FALength.instance);
    Env.registerNode("AMap", "Arrays", AMap.instance);
    Env.registerNode("FAMap", "Arrays", FAMap.instance);
    Env.registerNode("AReduce", "Arrays", AReduce.instance);
    Env.registerNode("FAReduce", "Arrays", FAReduce.instance);

    // Object Nodes
    Env.registerNode("FOMake", "Objects", FOMake.instance);
    Env.registerNode("FOBreak", "Objects", FOBreak.instance);
    Env.registerNode("OSet", "Objects", OSet.instance);
    Env.registerNode("FOSet", "Objects", FOSet.instance);
  }

  /**
   * Register a node type
   * @param {string} description The name of the node
   * @param {string} category The category of the node
   * @param {any} factory A function that instantiate the node
   */
  static registerNode(description, category, factory) {
    let inst = factory();
    Env.#nodeRegistry.set(inst.name, { description, category, factory });
  }

  /**
   * Unregister a node
   * @param {string} instance The instance static function of the node
   */
  static unregisterNode(instance) {
    Env.#nodeRegistry.delete(instance().name);
  }

  /**
   * Unregister all nodes
   */
  static unregisterAllNodes() {
    Env.#nodeRegistry.clear();
  }

  /**
   * Return the list of unique registered categories
   */
  static getCategories() {
    let categoryMap = new Map();
    Array.from(this.#nodeRegistry.values()).forEach((element) => {
      categoryMap.set(element.category, 0);
    });
    return Array.from(categoryMap.keys());
  }

  /**
   * Return an array of registrations for nodes.
   * Registrations have the sign: {name, category, factory}
   * @param {string} category The category for which seacrh registrations
   */
  static getCategoryNodes(category) {
    let registrations = [];
    Array.from(this.#nodeRegistry.entries()).forEach((entry) => {
      if (entry[1].category === category) {
        registrations.push({
          name: entry[0],
          category: entry[1].category,
          description: entry[1].description,
          factory: entry[1].factory,
        });
      }
    });
    return registrations;
  }

  /**
   * Instantiate a node by name
   * @param {string} name The name of the node
   */
  static getInstance(name) {
    let reg = this.#nodeRegistry.get(name);
    if (reg) {
      return reg.factory();
    } else {
      return null;
    }
  }

  /**
   * Create helper maker nodes to support user with dealing with
   * specific object structures. This method accepts optional
   * options that let you specify what exactly create:
   * {
   *   recursive: true,
   *   fillValues: true,
   *   forceTypes: true
   *   editableInputs: true
   * }
   *
   * @param {any} obj The object structure to consider whiel create nodes
   * @param {any} opts The options on create nodes
   */
  static registerMaker(name, obj, opts = {}) {
    /**
     * This function will be registered as creator for
     * instances of the OMake node for the user object
     */
    let createMake = function () {
      let makeNode = new FOMake();
      makeNode.name = makeNode.name + "::" + name;
      makeNode.title = name;
      makeNode.inputs = [];

      for (let field in obj) {
        let is = new InputSocket(field, makeNode, Types.ANY, 0);

        if (opts.editableInputs) {
          is.canEditName = true;
          is.canEditType = true;
        }

        switch (typeof obj[field]) {
          case "string":
            is.type = opts.forceTypes ? Types.STRING : Types.ANY;
            is.value = opts.fillValues ? obj[field] : "";
            break;
          case "number":
            is.type = opts.forceTypes ? Types.NUMBER : Types.ANY;
            is.value = opts.fillValues ? obj[field] : opts.forceTypes ? 0 : "";
            break;
          case "boolean":
            is.type = opts.forceTypes ? Types.BOOLEAN : Types.ANY;
            is.value = opts.fillValues
              ? obj[field]
              : opts.forceTypes
              ? false
              : "";
            break;
          case "object":
            if (obj[field] instanceof Array) {
              is.type = opts.forceTypes ? Types.ARRAY : Types.ANY;
              is.value = opts.fillValues
                ? obj[field]
                : opts.forceTypes
                ? []
                : "";
            } else if (obj[field] instanceof Object) {
              is.type = opts.forceTypes ? Types.OBJECT : Types.ANY;
              is.value = opts.fillValues
                ? obj[field]
                : opts.forceTypes
                ? {}
                : "";
            } else {
              throw new Error("Unknown field type: " + field);
            }
            break;
          default:
            throw new Error("Unknown field type: " + field);
        }
        makeNode.inputs.push(is);
      }

      return makeNode;
    };

    // Reigister factory objects
    Env.registerNode(name, "Custom", createMake);

    if (opts.recursive) {
      for (let field in obj) {
        if (typeof obj[field] === "object" && !(obj[field] instanceof Array)) {
          Env.registerMaker(name + "." + field, obj[field], opts);
        }
      }
    }
  }

  /**
   * Create helper breaker nodes to support user with dealing with
   * specific object structures. This method accepts optional
   * options that let you specify what exactly create:
   * {
   *   recursive: true,
   *   forceTypes: true,
   *   editableOutputs: true
   * }
   *
   * @param {any} obj The object structure to consider whiel create nodes
   * @param {any} opts The options on create nodes
   */
  static registerBreaker(name, obj, opts = {}) {
    /**
     * This function will be registered as creator for
     * instances of the OBreak node for the user object
     */
    let createBreak = function () {
      let breakNode = new FOBreak();
      breakNode.name = breakNode.name + "::" + name;
      breakNode.title = name;
      breakNode.outputs = [];

      for (let field in obj) {
        let os = new OutputSocket(field, breakNode, Types.ANY, 0);
        if (opts.editableOutputs) {
          os.canEditName = true;
          os.canEditType = true;
        }

        switch (typeof obj[field]) {
          case "string":
            os.type = opts.forceTypes ? Types.STRING : Types.ANY;
            break;
          case "number":
            os.type = opts.forceTypes ? Types.NUMBER : Types.ANY;
            break;
          case "boolean":
            os.type = opts.forceTypes ? Types.BOOLEAN : Types.ANY;
            break;
          case "object":
            if (obj[field] instanceof Array) {
              os.type = opts.forceTypes ? Types.ARRAY : Types.ANY;
            } else if (obj[field] instanceof Object) {
              os.type = opts.forceTypes ? Types.OBJECT : Types.ANY;
            } else {
              throw new Error("Unknown field type: " + field);
            }
            break;
          default:
            throw new Error("Unknown field type: " + field);
        }
        breakNode.outputs.push(os);
      }

      return breakNode;
    };

    // Reigister factory objects
    Env.registerNode(name, "Custom", createBreak);

    if (opts.recursive) {
      for (let field in obj) {
        if (typeof obj[field] === "object" && !(obj[field] instanceof Array)) {
          Env.registerBreaker(name + "." + field, obj[field], opts);
        }
      }
    }
  }

  /**
   * Create both helper maker and breaker nodes to support user with dealing with
   * specific object structures. This method accepts optional
   * options that let you specify what exactly create:
   * {
   *   recursive: true,
   *   fillValues: true,
   *   forceTypes: true,
   *   editableInputs: true
   *   editableOutputs: true
   * }
   *
   * @param {any} obj The object structure to consider whiel create nodes
   * @param {any} opts The options on create nodes
   */
  static registerObject(name, obj, opts = {}) {
    Env.registerMaker("Make " + name, obj, opts);
    Env.registerBreaker("Break " + name, obj, opts);
  }

  /**
   * Creates and returns a JSON representation of the entire program
   * @param {Program} program The program to export
   */
  static export(program) {
    let exp = {
      id: program.id,
      version: Program.version,
      lastNodeIndex: Node.lastNodeIdIndex,
      lastSocketIndex: Socket.lastSocketIdIndex,
      enter: program.enter.id,
      exit: program.exit.id,
      nodes: [],
      connections: [],
    };

    for (let node of program.nodes) {
      let nodeExp = {
        id: node.id,
        name: node.name,
        title: node.title,
        functional: node.functional,
        meta: JSON.parse(JSON.stringify(node.meta)),
        program: node instanceof Program ? Env.export(node) : undefined,
        inputs: node.inputs.map((inp) => {
          return {
            id: inp.id,
            name: inp.name,
            node: null,
            type: inp.type,
            value: inp.value,
            canEditName: inp.canEditName,
            canEditType: inp.canEditType,
            peer: null,
          };
        }),
        outputs: node.outputs.map((outp) => {
          return {
            id: outp.id,
            name: outp.name,
            node: null,
            type: outp.type,
            value: outp.value,
            cached: outp.cached,
            canEditName: outp.canEditName,
            canEditType: outp.canEditType,
            peers: [],
          };
        }),
        prev: !node.prev
          ? null
          : {
              id: node.prev.id,
              name: node.prev.name,
              node: null,
              peers: [],
            },
        nexts: node.nexts.map((next) => {
          return {
            id: next.id,
            name: next.name,
            node: null,
            peer: null,
          };
        }),
      };

      exp.nodes.push(nodeExp);
    }

    /**
     * Define a inner-function that prevent duplicates connections
     * @param {any} connection The connection to push
     */
    function pushConnection(connection) {
      if (
        exp.connections.findIndex(
          (c) =>
            c.type === connection.type &&
            c.sourceNode === connection.sourceNode &&
            c.sourceSocket === connection.sourceSocket &&
            c.targetNode === connection.targetNode &&
            c.targetSocket === connection.targetSocket
        ) === -1
      ) {
        exp.connections.push(connection);
      }
    }

    for (let node of program.nodes) {
      if (node.prev?.peers?.length > 0) {
        for (let peer of node.prev.peers) {
          let connectionExp = {
            type: "pn",
            sourceNode: peer.node.id,
            sourceSocket: peer.id,
            targetNode: node.id,
            targetSocket: node.prev.id,
          };
          pushConnection(connectionExp);
        }
      }
      for (let inp of node.inputs) {
        if (inp.peer) {
          let connectionExp = {
            type: "io",
            sourceNode: inp.peer.node.id,
            sourceSocket: inp.peer.id,
            targetNode: node.id,
            targetSocket: inp.id,
          };
          pushConnection(connectionExp);
        }
      }
      for (let outp of node.outputs) {
        for (let peer of outp.peers) {
          if (peer) {
            let connectionExp = {
              type: "io",
              sourceNode: node.id,
              sourceSocket: outp.id,
              targetNode: peer.node.id,
              targetSocket: peer.id,
            };
            pushConnection(connectionExp);
          }
        }
      }
      for (let next of node.nexts) {
        if (next.peer) {
          let connectionExp = {
            type: "pn",
            sourceNode: node.id,
            sourceSocket: next.id,
            targetNode: next.peer.node.id,
            targetSocket: next.peer.id,
          };
          pushConnection(connectionExp);
        }
      }
    }

    return exp;
  }

  /**
   * Create a program instance based on export data created with export() method
   * @param {any} data A object with the export data format
   */
  static import(data) {
    if (data.version !== 1) {
      throw new Error("Imported data must have version 1");
    }

    let p = new Program();

    // Removes enter and exit auto-nodes, these
    // will be re-created by import procedure
    p.removeNode(p.enter);
    p.removeNode(p.exit);

    p.id = data.id;
    Program.version = data.version;

    // Now import nodes without connections
    for (let nodeData of data.nodes) {
      let node;

      // If this node is a program node, let the import
      // procedure to create the node
      if (nodeData.program) {
        node = Env.import(nodeData.program);
      } else {
        // Otherwise import the node
        node = Env.getInstance(nodeData.name);
      }
      if (!node) {
        throw new Error(`Node type '${nodeData.name}' is not registered`);
      }
      // Delete default sockets (created by getInstance())
      node.inputs = [];
      node.outputs = [];
      node.prev = null;
      node.nexts = [];

      node.title = nodeData.title;
      node.id = nodeData.id;
      node.functional = nodeData.functional;
      node.meta = JSON.parse(JSON.stringify(nodeData.meta));
      for (let inpData of nodeData.inputs) {
        let inp = new InputSocket(
          inpData.name,
          node,
          inpData.type,
          inpData.value
        );
        inp.id = inpData.id;
        inp.canEditName = inpData.canEditName;
        node.inputs.push(inp);
      }
      for (let outpData of nodeData.outputs) {
        let outp = new OutputSocket(
          outpData.name,
          node,
          outpData.type,
          outpData.value,
          outpData.cached
        );
        outp.canEditName = outpData.canEditName;
        outp.id = outpData.id;
        node.outputs.push(outp);
      }
      if (nodeData.prev) {
        let prev = new PrevSocket(nodeData.prev.name, node);
        prev.id = nodeData.prev.id;
        node.prev = prev;
      }
      for (let nextData of nodeData.nexts) {
        let next = new NextSocket(nextData.name, node);
        next.id = nextData.id;
        node.nexts.push(next);
      }

      p.addNode(node);
    }

    p.enter = p.nodes.find((n) => n.id === data.enter);
    p.exit = p.nodes.find((n) => n.id === data.exit);

    // Now import connections
    for (let connectionData of data.connections) {
      let sourceNode = p.nodes.find((n) => n.id === connectionData.sourceNode);
      let targetNode = p.nodes.find((n) => n.id === connectionData.targetNode);

      let sourceSocket =
        connectionData.type === "pn"
          ? sourceNode.nexts.find((n) => n.id === connectionData.sourceSocket)
          : sourceNode.outputs.find(
              (o) => o.id === connectionData.sourceSocket
            );
      let targetSocket =
        connectionData.type === "pn"
          ? targetNode.prev
          : targetNode.inputs.find((i) => i.id === connectionData.targetSocket);

      sourceSocket.connect(targetSocket);
    }

    // These two static variable must be assigned at the end because
    // the new InputSocket(), new OutputSocket(), ... increment it during
    // the import phase
    Node.lastNodeIdIndex = data.lastNodeIndex;
    Socket.lastSocketIdIndex = data.lastSocketIndex;

    return p;
  }
}