Home Reference Source

scripts/path-controller/toolsys/toolsys.js

"use strict";
import nstructjs from '../util/struct.js';

/**

 ToolOps are base operators for modifying application state.
 They operate on Contexts and can use the datapath API.
 They make up the undo stack.

 ToolOp subclasses handle undo with their undoPre (run before tool execution)
 and undo methods.  You can set default handlers (most commonly this is just
 saving/reloading the app state) with setDefaultUndoHandlers.

 ToolOps have inputs and outputs (which are ToolProperties) and can also
 be modal.

 ## Rules

 Tools are never, EVER allowed to store direct pointers to the application state,
 with one exception: tools in modal mode may store such pointers, but they must
 delete them when existing modal mode by overriding modalEnd.

 This is to prevent very evil and difficult to debug bugs in the undo stack
 and nasty memory leaks.

 ## Example

 <pre>

 const ExampleEnum = {
  ITEM1 : 0,
  ITEM2 : 1
}

 class MyTool extends ToolOp {
  static tooldef() {
    return {
      uiname     : "Tool Name",
      toolpath   : "my.tool",
      inputs     : {
          input1 : new IntProperty(),
          input2 : new EnumProperty(0, ExampleEnum)
      },
      outputs    : {
          someoutput : new IntProperty
      }
    }
  }

  undoPre(ctx) {
    //run before tool starts
  }

  undo(ctx) {
    //undo handler
  }

  execPre(ctx) {
    //run right before exec
  }
  exec(ctx) {
    //main execution method
  }
  execPost(ctx) {
    //run right after exec
  }
}
 ToolOp.register(MyTool);

 </pre>
 */

import * as events from '../util/events.js';
import {keymap} from '../util/simple_events.js';
import {PropFlags, PropTypes} from './toolprop.js';
import {DataPath} from '../controller/controller_base.js';
import * as util from '../util/util.js';

export let ToolClasses = [];
window._ToolClasses = ToolClasses;

export function setContextClass(cls) {
  console.warn("setContextClass is deprecated");
}

export const ToolFlags = {
  PRIVATE: 1

};


export const UndoFlags = {
  NO_UNDO      : 2,
  IS_UNDO_ROOT : 4,
  UNDO_BARRIER : 8,
  HAS_UNDO_DATA: 16
};

class InheritFlag {
  constructor(slots = {}) {
    this.slots = slots;
  }
}

let modalstack = [];

let defaultUndoHandlers = {
  undoPre(ctx) {
    throw new Error("implement me");
  },
  undo(ctx) {
    throw new Error("implement me");
  }
}

export function setDefaultUndoHandlers(undoPre, undo) {
  if (!undoPre || !undo) {
    throw new Error("invalid parameters to setDefaultUndoHandlers");
  }

  defaultUndoHandlers.undoPre = undoPre;
  defaultUndoHandlers.undo = undo;
}

export class ToolPropertyCache {
  constructor() {
    this.map = new Map();
    this.pathmap = new Map();
    this.accessors = {};

    this.userSetMap = new Set();

    this.api = undefined;
    this.dstruct = undefined;
  }

  static getPropKey(cls, key, prop) {
    return prop.apiname && prop.apiname.length > 0 ? prop.apiname : key;
  }

  _buildAccessors(cls, key, prop, dstruct, api) {
    let tdef = cls._getFinalToolDef();

    this.api = api;
    this.dstruct = dstruct;

    if (!tdef.toolpath) {
      console.warn("Bad tool property", cls, "it's tooldef was missing a toolpath field");
      return;
    }

    let path = tdef.toolpath.trim().split(".").filter(f => f.trim().length > 0);
    let obj = this.accessors;

    let st = dstruct;
    let partial = "";

    for (let i = 0; i < path.length; i++) {
      let k = path[i];
      let pathk = k;

      if (i === 0) {
        pathk = "accessors." + k;
      }

      if (i > 0) {
        partial += ".";
      }
      partial += k;

      if (!(k in obj)) {
        obj[k] = {};
      }

      let st2 = api.mapStruct(obj[k], true, k);
      if (!(k in st.pathmap)) {
        st.struct(pathk, k, k, st2);
      }
      st = st2;

      this.pathmap.set(partial, obj[k]);

      obj = obj[k];
    }

    let name = prop.apiname !== undefined && prop.apiname.length > 0 ? prop.apiname : key;
    let prop2 = prop.copy()

    let dpath = new DataPath(name, name, prop2);
    let uiname = prop.uiname;

    if (!uiname || uiname.trim().length === 0) {
      uiname = prop.apiname;
    }
    if (!uiname || uiname.trim().length === 0) {
      uiname = key;
    }

    uiname = ToolProperty.makeUIName(uiname);

    prop2.uiname = uiname;
    prop2.description = prop2.description || prop2.uiname;

    st.add(dpath);

    obj[name] = prop2.getValue();
  }

  _getAccessor(cls) {
    let toolpath = cls.tooldef().toolpath.trim();
    return this.pathmap.get(toolpath);
  }

  useDefault(cls, key, prop) {
    key = this.userSetMap.has(cls.tooldef().trim() + "." + this.constructor.getPropKey(key));
    key = key.trim();

    return key;
  }

  has(cls, key, prop) {
    let obj = this._getAccessor(cls);

    key = this.constructor.getPropKey(cls, key, prop);
    return obj && key in obj;
  }

  get(cls, key, prop) {
    if (cls === ToolMacro) {
      return;
    }

    let obj = this._getAccessor(cls);
    key = this.constructor.getPropKey(cls, key, prop);

    if (obj) {
      return obj[key];
    }

    return undefined;
  }

  set(cls, key, prop) {
    if (cls === ToolMacro) {
      return;
    }

    let toolpath = cls.tooldef().toolpath.trim();
    let obj = this._getAccessor(cls);

    if (!obj) {
      console.warn("Warning, toolop " + cls.name + " was not in the default map; unregistered?");
      this._buildAccessors(cls, key, prop, this.dstruct, this.api);

      obj = this.pathmap.get(toolpath);
    }

    if (!obj) {
      console.error("Malformed toolpath in toolop definition: " + toolpath);
      return;
    }

    key = this.constructor.getPropKey(cls, key, prop);

    //copy prop first in case we're a non-primitive-value type, e.g. vector properties
    obj[key] = prop.copy().getValue();

    let path = toolpath + "." + key;
    this.userSetMap.add(path);

    return this;
  }
}

export const SavedToolDefaults = new ToolPropertyCache();

export class ToolOp extends events.EventHandler {
  /**
   Main ToolOp constructor.  It reads the inputs/outputs properteis from
   this.constructor.tooldef() and copies them to build this.inputs and this.outputs.
   If inputs or outputs are wrapped in ToolOp.inherit(), it will walk up the class
   chain to fetch parent class properties.


   Default input values are loaded from SavedToolDefaults.  If initialized (buildToolSysAPI
   has been called) SavedToolDefaults will have a copy of all the default
   property values of all registered ToolOps.
   **/

  constructor() {
    super();

    this._overdraw = undefined;
    this.__memsize = undefined;

    var def = this.constructor.tooldef();

    if (def.undoflag !== undefined) {
      this.undoflag = def.undoflag;
    }

    if (def.flag !== undefined) {
      this.flag = def.flag;
    }

    this._accept = this._reject = undefined;
    this._promise = undefined;

    for (var k in def) {
      this[k] = def[k];
    }

    let getSlots = (slots, key) => {
      if (slots === undefined)
        return {};

      if (!(slots instanceof InheritFlag)) {
        return slots;
      }

      slots = {};
      let p = this.constructor;
      let lastp = undefined;

      while (p !== undefined && p !== Object && p !== ToolOp && p !== lastp) {
        if (p.tooldef) {
          let def = p.tooldef();

          if (def[key] !== undefined) {
            let slots2 = def[key];
            let stop = !(slots2 instanceof InheritFlag);

            if (slots2 instanceof InheritFlag) {
              slots2 = slots2.slots;
            }

            for (let k in slots2) {
              if (!(k in slots)) {
                slots[k] = slots2[k];
              }
            }

            if (stop) {
              break;
            }
          }
        }

        lastp = p;
        p = p.prototype.__proto__.constructor;
      }

      return slots;
    };

    let dinputs = getSlots(def.inputs, "inputs");
    let doutputs = getSlots(def.outputs, "outputs");

    this.inputs = {};
    this.outputs = {};

    if (dinputs) {
      for (let k in dinputs) {
        let prop = dinputs[k].copy();
        prop.apiname = prop.apiname && prop.apiname.length > 0 ? prop.apiname : k;

        if (!this.hasDefault(prop, k)) {
          this.inputs[k] = prop;
          continue;
        }

        try {
          prop.setValue(this.getDefault(prop, k));
        } catch (error) {
          console.log(error.stack);
          console.log(error.message);
        }

        prop.wasSet = false;
        this.inputs[k] = prop;
      }
    }

    if (doutputs) {
      for (let k in doutputs) {
        let prop = doutputs[k].copy();
        prop.apiname = prop.apiname && prop.apiname.length > 0 ? prop.apiname : k;

        this.outputs[k] = prop;
      }
    }

    this.drawlines = [];
  }

  /**
   ToolOp definition.

   An example:
   <pre>
   static tooldef() {
    return {
      uiname   : "Tool Name",
      toolpath : "logical_module.tool", //logical_module need not match up to a real module
      icon     : -1, //tool's icon, or -1 if there is none
      description : "tooltip",
      is_modal : false, //tool is interactive and takes control of events
      hotkey   : undefined,
      undoflag : 0, //see UndoFlags
      flag     : 0,
      inputs   : ToolOp.inherit({
        f32val : new Float32Property(1.0),
        path   : new StringProperty("./path");
      }),
      outputs  : {}
      }
    }
   </pre>
   */
  static tooldef() {
    if (this === ToolOp) {
      throw new Error("Tools must implemented static tooldef() methods!");
    }

    return {};
  }

  static Equals(a, b) {
    if (!a || !b) return false;
    if (a.constructor !== b.constructor) return false;

    let bad = false;

    for (let k in a.inputs) {
      bad = bad || !(k in b.inputs);
      bad = bad || a.inputs[k].constructor !== b.inputs[k];
      bad = bad || !a.inputs[k].equals(b.inputs[k]);

      if (bad) {
        break;
      }
    }

    return !bad;
  }

  static inherit(slots = {}) {
    return new InheritFlag(slots);
  }

  /**

   Creates a new instance of this toolop from args and a context.
   This is often use to fill properties with default arguments
   stored somewhere in the context.

   */
  static invoke(ctx, args) {
    let tool = new this();

    for (let k in args) {
      if (!(k in tool.inputs)) {
        console.warn("Unknown tool argument " + k);
        continue;
      }

      let prop = tool.inputs[k];
      let val = args[k];

      if ((typeof val === "string") && prop.type & (PropTypes.ENUM | PropTypes.FLAG)) {
        if (val in prop.values) {
          val = prop.values[val];
        } else {
          console.warn("Possible invalid enum/flag:", val);
          continue;
        }
      }

      tool.inputs[k].setValue(val);
    }

    return tool;
  }

  static register(cls) {
    if (ToolClasses.indexOf(cls) >= 0) {
      console.warn("Tried to register same ToolOp class twice:", cls.name, cls);
      return;
    }

    ToolClasses.push(cls);
  }

  static _regWithNstructjs(cls, structName = cls.name) {
    if (nstructjs.isRegistered(cls)) {
      return;
    }

    let parent = cls.prototype.__proto__.constructor;

    if (!cls.hasOwnProperty("STRUCT")) {
      if (parent !== ToolOp && parent !== ToolMacro && parent !== Object) {
        this._regWithNstructjs(parent);
      }

      cls.STRUCT = nstructjs.inherit(cls, parent) + '}\n';
    }

    nstructjs.register(cls);
  }

  static isRegistered(cls) {
    return ToolClasses.indexOf(cls) >= 0;
  }

  static unregister(cls) {
    if (ToolClasses.indexOf(cls) >= 0) {
      ToolClasses.remove(cls);
    }
  }

  static _getFinalToolDef() {
    let def = this.tooldef();

    let getSlots = (slots, key) => {
      if (slots === undefined)
        return {};

      if (!(slots instanceof InheritFlag)) {
        return slots;
      }

      slots = {};
      let p = this

      while (p !== undefined && p !== Object && p !== ToolOp) {
        if (p.tooldef) {
          let def = p.tooldef();

          if (def[key] !== undefined) {
            let slots2 = def[key];
            let stop = !(slots2 instanceof InheritFlag);

            if (slots2 instanceof InheritFlag) {
              slots2 = slots2.slots;
            }

            for (let k in slots2) {
              if (!(k in slots)) {
                slots[k] = slots2[k];
              }
            }

            if (stop) {
              break;
            }
          }

        }
        p = p.prototype.__proto__.constructor;
      }

      return slots;
    };

    let dinputs = getSlots(def.inputs, "inputs");
    let doutputs = getSlots(def.outputs, "outputs");

    def.inputs = dinputs;
    def.outputs = doutputs;

    return def;
  }

  static onTick() {
    for (let toolop of modalstack) {
      toolop.on_tick();
    }
  }

  static searchBoxOk(ctx) {
    let flag = this.tooldef().flag;
    let ret = !(flag && (flag & ToolFlags.PRIVATE));
    ret = ret && this.canRun(ctx);

    return ret;
  }

  //toolop is an optional instance of this class, may be undefined
  static canRun(ctx, toolop = undefined) {
    return true;
  }

  /** Called when the undo system needs to destroy
   *  this toolop to save memory*/
  onUndoDestroy() {

  }

  /** Used by undo system to limit memory */
  calcMemSize(ctx) {
    if (this.__memsize !== undefined) {
      return this.__memsize;
    }

    let tot = 0;

    for (let step = 0; step < 2; step++) {
      let props = step ? this.outputs : this.inputs;

      for (let k in props) {
        let prop = props[k];

        let size = prop.calcMemSize();

        if (isNaN(size) || !isFinite(size)) {
          console.warn("Got NaN when calculating mem size for property", prop);
          continue;
        }

        tot += size;
      }
    }

    let size = this.calcUndoMem(ctx);

    if (isNaN(size) || !isFinite(size)) {
      console.warn("Got NaN in calcMemSize", this);
    } else {
      tot += size;
    }

    this.__memsize = tot;

    return tot;
  }

  loadDefaults(force = true) {
    for (let k in this.inputs) {
      let prop = this.inputs[k];

      if (!force && prop.wasSet) {
        continue;
      }

      if (this.hasDefault(prop, k)) {
        prop.setValue(this.getDefault(prop, k));
        prop.wasSet = false;
      }
    }

    return this;
  }

  hasDefault(toolprop, key = toolprop.apiname) {
    return SavedToolDefaults.has(this.constructor, key, toolprop);
  }

  getDefault(toolprop, key = toolprop.apiname) {
    let cls = this.constructor;

    if (SavedToolDefaults.has(cls, key, toolprop)) {
      return SavedToolDefaults.get(cls, key, toolprop);
    } else {
      return toolprop.getValue();
    }
  }

  saveDefaultInputs() {
    for (let k in this.inputs) {
      let prop = this.inputs[k];

      if (prop.flag & PropFlags.SAVE_LAST_VALUE) {
        SavedToolDefaults.set(this.constructor, k, prop);
      }
    }

    return this;
  }

  genToolString() {
    let def = this.constructor.tooldef();
    let path = def.toolpath + "(";

    for (let k in this.inputs) {
      let prop = this.inputs[k];

      path += k + "=";
      if (prop.type === PropTypes.STRING)
        path += "'";

      if (prop.type === PropTypes.FLOAT) {
        path += prop.getValue().toFixed(3);
      } else {
        path += prop.getValue();
      }

      if (prop.type === PropTypes.STRING)
        path += "'";
      path += " ";
    }
    path += ")";
    return path;
  }

  on_tick() {

  }

  /**default on_keydown implementation for modal tools,
   no need to call super() to execute this if you don't want to*/
  on_keydown(e) {
    switch (e.keyCode) {
      case keymap["Enter"]:
      case keymap["Space"]:
        this.modalEnd(false);
        break;
      case keymap["Escape"]:
        this.modalEnd(true);
        break;
    }
  }

  //called after undoPre
  calcUndoMem(ctx) {
    console.warn("ToolOp.prototype.calcUndoMem: implement me!");
    return 0;
  }

  undoPre(ctx) {
    throw new Error("implement me!");
  }

  undo(ctx) {
    throw new Error("implement me!");
    //_appstate.loadUndoFile(this._undo);
  }

  redo(ctx) {
    this._was_redo = true; //also set by toolstack.redo

    this.undoPre(ctx);
    this.execPre(ctx);
    this.exec(ctx);
    this.execPost(ctx);
  }

  //for compatibility with fairmotion
  exec_pre(ctx) {
    this.execPre(ctx);
  }

  execPre(ctx) {
  }

  exec(ctx) {
  }

  execPost(ctx) {

  }

  /**for use in modal mode only*/
  resetTempGeom() {
    var ctx = this.modal_ctx;

    for (var dl of this.drawlines) {
      dl.remove();
    }

    this.drawlines.length = 0;
  }

  error(msg) {
    console.warn(msg);
  }

  getOverdraw() {
    if (this._overdraw === undefined) {
      this._overdraw = document.createElement("overdraw-x");
      this._overdraw.start(this.modal_ctx.screen);
    }

    return this._overdraw;
  }

  /**for use in modal mode only*/
  makeTempLine(v1, v2, style) {
    let line = this.getOverdraw().line(v1, v2, style);
    this.drawlines.push(line);
    return line;
  }

  pushModal(node) {
    throw new Error("cannot call this; use modalStart")
  }

  popModal() {
    throw new Error("cannot call this; use modalEnd");
  }

  /**returns promise to be executed on modalEnd*/
  modalStart(ctx) {
    if (this.modalRunning) {
      console.warn("Warning, tool is already in modal mode consuming events");
      return this._promise;
    }

    this.modal_ctx = ctx;
    this.modalRunning = true

    this._promise = new Promise((accept, reject) => {
      this._accept = accept;
      this._reject = reject;

      modalstack.push(this);
      super.pushModal(ctx.screen);
    });

    return this._promise;
  }

  /*eek, I've not been using this.
    guess it's a non-enforced contract, I've been naming
    cancel methods 'cancel' all this time.

    XXX fix
  */
  toolCancel() {
  }

  modalEnd(was_cancelled) {
    if (this._modalstate) {
      modalstack.pop();
    }

    if (this._overdraw !== undefined) {
      this._overdraw.end();
      this._overdraw = undefined;
    }

    if (was_cancelled && this._on_cancel !== undefined) {
      if (this._accept) {
        this._accept(this.modal_ctx, true);
      }
      this._on_cancel(this);
    }

    this.resetTempGeom();

    var ctx = this.modal_ctx;

    this.modal_ctx = undefined;
    this.modalRunning = false;
    this.is_modal = false;

    super.popModal();

    this._promise = undefined;

    if (this._accept) {
      this._accept(ctx, false);//Context, was_cancelled
      this._accept = this._reject = undefined;
    }

    this.saveDefaultInputs();
  }

  loadSTRUCT(reader) {
    reader(this);

    let outs = this.outputs;
    let ins = this.inputs;

    this.inputs = {};
    this.outputs = {};

    for (let pair of ins) {
      this.inputs[pair.key] = pair.val;
    }

    for (let pair of outs) {
      this.outputs[pair.key] = pair.val;
    }
  }

  _save_inputs() {
    let ret = [];
    for (let k in this.inputs) {
      ret.push(new PropKey(k, this.inputs[k]));
    }

    return ret;
  }

  _save_outputs() {
    let ret = [];
    for (let k in this.outputs) {
      ret.push(new PropKey(k, this.outputs[k]));
    }

    return ret;
  }
}

ToolOp.STRUCT = `
toolsys.ToolOp {
  inputs  : array(toolsys.PropKey) | this._save_inputs();
  outputs : array(toolsys.PropKey) | this._save_outputs();
}
`;
nstructjs.register(ToolOp);

class PropKey {
  constructor(key, val) {
    this.key = key;
    this.val = val;
  }
}

PropKey.STRUCT = `
toolsys.PropKey {
  key : string;
  val : abstract(ToolProperty);
}
`;
nstructjs.register(PropKey);

export class MacroLink {
  constructor(sourcetool_idx, srckey, srcprops = "outputs", desttool_idx, dstkey, dstprops = "inputs") {
    this.source = sourcetool_idx;
    this.dest = desttool_idx;

    this.sourceProps = srcprops;
    this.destProps = dstprops;

    this.sourcePropKey = srckey;
    this.destPropKey = dstkey;
  }
}

MacroLink.STRUCT = `
toolsys.MacroLink {
  source         : int;
  dest           : int;
  sourcePropKey  : string;
  destPropKey    : string;
  sourceProps    : string;
  destProps      : string; 
}
`;
nstructjs.register(MacroLink);

export const MacroClasses = {};
window._MacroClasses = MacroClasses;

let macroidgen = 0;


export class ToolMacro extends ToolOp {
  constructor() {
    super();

    this.tools = [];
    this.curtool = 0;
    this.has_modal = false;
    this.connects = [];
    this.connectLinks = [];

    this._macro_class = undefined;
  }

  static tooldef() {
    return {
      uiname: "Tool Macro"
    }
  }

  //toolop is an optional instance of this class, may be undefined
  static canRun(ctx, toolop = undefined) {
    return true;
  }

  _getTypeClass() {
    if (this._macro_class && this._macro_class.ready) {
      return this._macro_class;
    }

    if (!this._macro_class) {
      this._macro_class = class MacroTypeClass extends ToolOp {
        static tooldef() {
          return this.__tooldef;
        }
      }

      this._macro_class.__tooldef = {
        toolpath: this.constructor.tooldef().toolpath || ''
      };
      this._macro_class.ready = false;
    }

    if (!this.tools || this.tools.length === 0) {
      /* We've been invoked by ToolOp constructor,
      *  for now just return an empty class  */
      return this._macro_class;
    }

    let key = "";
    for (let tool of this.tools) {
      key = tool.constructor.name + ":";
    }

    /* Handle child classes of ToolMacro */
    if (this.constructor !== ToolMacro) {
      key += ":" + this.constructor.tooldef().toolpath;
    }

    for (let k in this.inputs) {
      key += k + ":";
    }

    if (key in MacroClasses) {
      this._macro_class = MacroClasses[key];
      return this._macro_class;
    }

    let name = "Macro("
    let i = 0;
    let is_modal;

    for (let tool of this.tools) {
      let def = tool.constructor.tooldef();

      if (i > 0) {
        name += ", ";
      } else {
        is_modal = def.is_modal;
      }

      if (def.uiname) {
        name += def.uiname;
      } else if (def.toolpath) {
        name += def.toolpath;
      } else {
        name += tool.constructor.name;
      }

      i++;
    }

    let inputs = {};

    for (let k in this.inputs) {
      inputs[k] = this.inputs[k].copy().clearEventCallbacks();
      inputs[k].wasSet = false;
    }

    let tdef = {
      uiname  : name,
      toolpath: key,
      inputs,
      outputs : {},
      is_modal
    };

    let cls = this._macro_class;
    cls.__tooldef = tdef;
    cls._macroTypeId = macroidgen++;
    cls.ready = true;

    /*
    let cls = {
      name : key,
      tooldef() {
        return tdef
      },
      _getFinalToolDef() {
        return this.tooldef();
      }
    };//*/

    MacroClasses[key] = cls;

    return cls;
  }

  saveDefaultInputs() {
    for (let k in this.inputs) {
      let prop = this.inputs[k];

      if (prop.flag & PropFlags.SAVE_LAST_VALUE) {
        SavedToolDefaults.set(this._getTypeClass(), k, prop);
      }
    }

    return this;
  }

  hasDefault(toolprop, key = toolprop.apiname) {
    return SavedToolDefaults.has(this._getTypeClass(), key, toolprop);
  }

  getDefault(toolprop, key = toolprop.apiname) {
    let cls = this._getTypeClass();

    if (SavedToolDefaults.has(cls, key, toolprop)) {
      return SavedToolDefaults.get(cls, key, toolprop);
    } else {
      return toolprop.getValue();
    }
  }

  connect(srctool, srcoutput, dsttool, dstinput, srcprops = "outputs", dstprops = "inputs") {
    if (typeof dsttool === "function") {
      return this.connectCB(...arguments);
    }

    let i1 = this.tools.indexOf(srctool);
    let i2 = this.tools.indexOf(dsttool);

    if (i1 < 0 || i2 < 0) {
      throw new Error("tool not in macro");
    }

    //remove linked properties from this.inputs
    if (srcprops === "inputs") {
      let tool = this.tools[i1];

      let prop = tool.inputs[srcoutput];
      if (prop === this.inputs[srcoutput]) {
        delete this.inputs[srcoutput];
      }
    }

    if (dstprops === "inputs") {
      let tool = this.tools[i2];
      let prop = tool.inputs[dstinput];

      if (this.inputs[dstinput] === prop) {
        delete this.inputs[dstinput];
      }
    }

    this.connectLinks.push(new MacroLink(i1, srcoutput, srcprops, i2, dstinput, dstprops));
    return this;
  }

  connectCB(srctool, dsttool, callback, thisvar) {
    this.connects.push({
      srctool : srctool,
      dsttool : dsttool,
      callback: callback,
      thisvar : thisvar
    });

    return this;
  }

  add(tool) {
    if (tool.is_modal) {
      this.is_modal = true;
    }

    for (let k in tool.inputs) {
      let prop = tool.inputs[k];

      if (!(prop.flag & PropFlags.PRIVATE)) {
        this.inputs[k] = prop;
      }
    }

    this.tools.push(tool);

    return this;
  }

  _do_connections(tool) {
    let i = this.tools.indexOf(tool);

    for (let c of this.connectLinks) {
      if (c.source === i) {
        let tool2 = this.tools[c.dest];

        tool2[c.destProps][c.destPropKey].setValue(tool[c.sourceProps][c.sourcePropKey].getValue());
      }
    }

    for (var c of this.connects) {
      if (c.srctool === tool) {
        c.callback.call(c.thisvar, c.srctool, c.dsttool);
      }
    }
  }

  /*
  canRun(ctx) {
    if (this.tools.length == 0)
      return false;

    //poll first tool only in list
    return this.tools[0].constructor.canRun(ctx);
  }//*/

  modalStart(ctx) {
    //macros obviously can't call loadDefaults in the constructor
    //like normal tool ops can.
    this.loadDefaults(false);

    this._promise = new Promise((function (accept, reject) {
      this._accept = accept;
      this._reject = reject;
    }).bind(this));

    this.curtool = 0;

    let i;

    for (i = 0; i < this.tools.length; i++) {
      if (this.tools[i].is_modal)
        break;

      this.tools[i].undoPre(ctx);
      this.tools[i].execPre(ctx);
      this.tools[i].exec(ctx);
      this.tools[i].execPost(ctx);
      this._do_connections(this.tools[i]);
    }

    var on_modal_end = (function on_modal_end() {
      this._do_connections(this.tools[this.curtool]);
      this.curtool++;

      while (this.curtool < this.tools.length &&
      !this.tools[this.curtool].is_modal) {
        this.tools[this.curtool].undoPre(ctx);
        this.tools[this.curtool].execPre(ctx);
        this.tools[this.curtool].exec(ctx);
        this.tools[this.curtool].execPost(ctx);
        this._do_connections(this.tools[this.curtool]);

        this.curtool++;
      }

      if (this.curtool < this.tools.length) {
        this.tools[this.curtool].undoPre(ctx);
        this.tools[this.curtool].modalStart(ctx).then(on_modal_end);
      } else {
        this._accept(this, false);
      }
    }).bind(this);

    if (i < this.tools.length) {
      this.curtool = i;
      this.tools[this.curtool].undoPre(ctx);
      this.tools[this.curtool].modalStart(ctx).then(on_modal_end);
    }

    return this._promise;
  }

  loadDefaults(force = true) {
    return super.loadDefaults(force);
  }

  exec(ctx) {
    //macros obviously can't call loadDefaults in the constructor
    //like normal tool ops can.
    //note that this will detect if the user changes property values

    this.loadDefaults(false);

    for (var i = 0; i < this.tools.length; i++) {
      this.tools[i].undoPre(ctx);
      this.tools[i].execPre(ctx);
      this.tools[i].exec(ctx);
      this.tools[i].execPost(ctx);
      this._do_connections(this.tools[i]);
    }
  }

  calcUndoMem(ctx) {
    let tot = 0;

    for (let tool of this.tools) {
      tot += tool.calcUndoMem(ctx);
    }

    return tot;
  }

  calcMemSize(ctx) {
    let tot = 0;

    for (let tool of this.tools) {
      tot += tool.calcMemSize(ctx);
    }

    return tot;
  }

  undoPre() {
    return; //undoPre is handled in exec() or modalStart()
  }

  undo(ctx) {
    for (var i = this.tools.length - 1; i >= 0; i--) {
      this.tools[i].undo(ctx);
    }
  }
}

ToolMacro.STRUCT = nstructjs.inherit(ToolMacro, ToolOp, "toolsys.ToolMacro") + `
  tools        : array(abstract(toolsys.ToolOp));
  connectLinks : array(toolsys.MacroLink);
}
`;
nstructjs.register(ToolMacro);

export class ToolStack extends Array {
  constructor(ctx) {
    super();

    this.memLimit = 512*1024*1024;
    this.enforceMemLimit = false;

    this.cur = -1;
    this.ctx = ctx;

    this.modalRunning = 0;

    this._undo_branch = undefined; //used to save undo branch in case of tool cancel
  }

  get head() {
    return this[this.cur];
  }

  limitMemory(maxmem = this.memLimit, ctx = this.ctx) {
    if (maxmem === undefined) {
      throw new Error("maxmem cannot be undefined");
    }

    let size = this.calcMemSize();

    let start = 0;

    while (start < this.cur - 2 && size > maxmem) {
      size -= this[start].calcMemSize(ctx);
      start++;
    }

    if (start === 0) {
      return size;
    }

    for (let i = 0; i < start; i++) {
      this[i].onUndoDestroy();
    }

    this.cur -= start;

    for (let i = 0; i < this.length - start; i++) {
      this[i] = this[i + start];
    }
    this.length -= start;

    return this.calcMemSize(ctx);
  }

  calcMemSize(ctx = this.ctx) {
    let tot = 0;

    for (let tool of this) {
      try {
        tot += tool.calcMemSize();
      } catch (error) {
        util.print_stack(error);
        console.error("Failed to execute a calcMemSize method");
      }
    }

    return tot;
  }

  setRestrictedToolContext(ctx) {
    this.toolctx = ctx;
  }

  reset(ctx) {
    if (ctx !== undefined) {
      this.ctx = ctx;
    }

    this.modalRunning = 0;
    this.cur = -1;
    this.length = 0;
  }

  /**
   * runs .undo,.redo if toolstack head is same as tool
   *
   * otherwise, .execTool(ctx, tool) is called.
   *
   * @param compareInputs : check if toolstack head has identical input values, defaults to false
   * */
  execOrRedo(ctx, tool, compareInputs = false) {
    let head = this.head;

    let ok = compareInputs ? ToolOp.Equals(head, tool) : head && head.constructor === tool.constructor;

    tool.__memsize = undefined; //reset cache memsize

    if (ok) {
      //console.warn("Same tool detected");

      this.undo();

      //can inputs differ? in that case, execute new tool
      if (!compareInputs) {
        this.execTool(ctx, tool);
      } else {
        this.rerun();
      }

      return false;
    } else {
      this.execTool(ctx, tool);
      return true;
    }
  }

  execTool(ctx, toolop) {
    if (this.enforceMemLimit) {
      this.limitMemory(this.memLimit, ctx);
    }

    if (!toolop.constructor.canRun(ctx, toolop)) {
      console.log("toolop.constructor.canRun returned false");
      return;
    }

    let tctx = ctx.toLocked();

    let undoflag = toolop.constructor.tooldef().undoflag;
    if (toolop.undoflag !== undefined) {
      undoflag = toolop.undoflag;
    }
    undoflag = undoflag === undefined ? 0 : undoflag;

    //if (!(undoflag & UndoFlags.IS_UNDO_ROOT) && !(undoflag & UndoFlags.NO_UNDO)) {
    //tctx = new SavedContext(ctx, ctx.datalib);
    //}

    toolop.execCtx = tctx;

    if (!(undoflag & UndoFlags.NO_UNDO)) {
      this.cur++;

      //save branch for if tool cancel
      this._undo_branch = this.slice(this.cur + 1, this.length);

      //truncate
      this.length = this.cur + 1;

      this[this.cur] = toolop;
      toolop.undoPre(tctx);
    }

    if (toolop.is_modal) {
      ctx = toolop.modal_ctx = ctx;

      this.modal_running = true;

      toolop._on_cancel = (function (toolop) {
        if (!(toolop.undoflag & UndoFlags.NO_UNDO)) {
          this.pop_i(this.cur);
          this.cur--;
        }
      }).bind(this);

      //will handle calling .exec itself
      toolop.modalStart(ctx);
    } else {
      toolop.execPre(tctx);
      toolop.exec(tctx);
      toolop.execPost(tctx);
      toolop.saveDefaultInputs();
    }
  }

  toolCancel(ctx, tool) {
    if (tool._was_redo) { //also set by toolstack.redo
      //ignore tool cancel requests on redo
      return;
    }

    if (tool !== this[this.cur]) {
      console.warn("toolCancel called in error", this, tool);
      return;
    }

    this.undo();
    this.length = this.cur + 1;

    if (this._undo_branch !== undefined) {
      for (let item of this._undo_branch) {
        this.push(item);
      }
    }
  }

  undo() {
    if (this.enforceMemLimit) {
      this.limitMemory(this.memLimit);
    }

    if (this.cur >= 0 && !(this[this.cur].undoflag & UndoFlags.IS_UNDO_ROOT)) {
      let tool = this[this.cur];

      tool.undo(tool.execCtx);

      this.cur--;
    }
  }

  //reruns a tool if it's at the head of the stack
  rerun(tool) {
    if (this.enforceMemLimit) {
      this.limitMemory(this.memLimit);
    }

    if (tool === this[this.cur]) {
      tool._was_redo = false;

      if (!tool.execCtx) {
        tool.execCtx = this.ctx;
      }

      tool.undo(tool.execCtx);

      tool._was_redo = true; //also set by toolstack.redo

      tool.undoPre(tool.execCtx);
      tool.execPre(tool.execCtx);
      tool.exec(tool.execCtx);
      tool.execPost(tool.execCtx);
    } else {
      console.warn("Tool wasn't at head of stack", tool);
    }
  }

  redo() {
    if (this.enforceMemLimit) {
      this.limitMemory(this.memLimit);
    }

    if (this.cur >= -1 && this.cur + 1 < this.length) {
      //console.log("redo!", this.cur, this.length);

      this.cur++;
      let tool = this[this.cur];


      if (!tool.execCtx) {
        tool.execCtx = this.ctx;
      }

      tool._was_redo = true;
      tool.redo(tool.execCtx);

      tool.saveDefaultInputs();
    }
  }

  save() {
    let data = [];
    nstructjs.writeObject(data, this);
    return data;
  }

  rewind() {
    while (this.cur >= 0) {
      let last = this.cur;
      this.undo();

      //prevent infinite loops
      if (last === this.cur) {
        break;
      }
    }

    return this;
  }

  /**cb is a function(ctx), if it returns the value false then playback stops
   promise will still be fulfilled.

   onstep is a callback, if it returns a promise that promise will be
   waited on, otherwise execution is queue with window.setTimeout().
   */
  replay(cb, onStep) {
    let cur = this.cur;

    this.rewind();

    let last = this.cur;

    let start = util.time_ms();

    return new Promise((accept, reject) => {
      let next = () => {
        last = this.cur;

        if (cb && cb(ctx) === false) {
          accept();
          return;
        }

        if (this.cur < this.length) {
          this.cur++;
          this.rerun();
        }

        if (last === this.cur) {
          console.warn("time:", (util.time_ms() - start)/1000.0);
          accept(this);
        } else {
          if (onStep) {
            let ret = onStep();

            if (ret && ret instanceof Promise) {
              ret.then(() => {
                next();
              });
            } else {
              window.setTimeout(() => {
                next();
              })
            }
          }
        }
      }

      next();
    });
  }

  loadSTRUCT(reader) {
    reader(this);

    for (let item of this._stack) {
      this.push(item);
    }

    delete this._stack;
  }

  //note that this makes sure tool classes are registered with nstructjs
  //during save
  _save() {
    for (let tool of this) {
      let cls = tool.constructor;

      if (!nstructjs.isRegistered(cls)) {
        cls._regWithNstructjs(cls);
      }
    }

    return this;
  }
}

ToolStack.STRUCT = `
toolsys.ToolStack {
  cur    : int;
  _stack : array(abstract(toolsys.ToolOp)) | this._save();
}
`;
nstructjs.register(ToolStack);

window._testToolStackIO = function () {
  let data = [];
  let cls = _appstate.toolstack.constructor;

  nstructjs.writeObject(data, _appstate.toolstack);
  data = new DataView(new Uint8Array(data).buffer);

  let toolstack = nstructjs.readObject(data, cls);

  _appstate.toolstack.rewind();

  toolstack.cur = -1;
  toolstack.ctx = _appstate.toolstack.ctx;
  _appstate.toolstack = toolstack;

  return toolstack;
}

export function buildToolSysAPI(api, registerWithNStructjs = true, rootCtxStruct = undefined) {
  let datastruct = api.mapStruct(ToolPropertyCache, true);

  for (let cls of ToolClasses) {
    let def = cls._getFinalToolDef();

    for (let k in def.inputs) {
      let prop = def.inputs[k];

      if (!(prop.flag & (PropFlags.PRIVATE | PropFlags.READ_ONLY))) {
        SavedToolDefaults._buildAccessors(cls, k, prop, datastruct, api);
      }
    }
  }

  if (rootCtxStruct) {
    rootCtxStruct.struct("toolDefaults", "toolDefaults", "Tool Defaults", api.mapStruct(ToolPropertyCache));
  }

  if (!registerWithNStructjs) {
    return;
  }

  //register tools with nstructjs
  for (let cls of ToolClasses) {
    try {
      if (!nstructjs.isRegistered(cls)) {
        ToolOp._regWithNstructjs(cls);
      }
    } catch (error) {
      console.log(error.stack);
      console.error("Failed to register a tool with nstructjs");
    }
  }
}