Source: mandg.js

var _ = require("./lodash-wrapper.js");
var DepGraph = require("dependency-graph").DepGraph;

/** @todo Mutator and Generator should be derived from a common class... too much cut-and-paste code right now */

class MorGBase extends Function {
    constructor(name, fn, opts) {
        super();
        // check args
        if (typeof name !== "string") {
            throw new TypeError("Mutator constructor expected 'name' to be string, got:" + name);
        }
        if (typeof fn !== "function") {
            throw new TypeError("Mutator constructor expected 'fn' to be function, got:" + typeof fn);
        }
        opts = opts || {};

        this.tags = [];
        this.fn = fn;

        // can't assign to read-only 'name'...
        // ...but we can sure redefine it!
        Object.defineProperty(this, "name", {
            enumerable: false,
            configurable: true,
            writable: false,
            value: name
        });

        // if we are called, turn around and call fn
        return new Proxy(this, {
            apply: (target, thisArg, argumentsList) => {
                var ctx = new MandGTypeManager().getContext();
                // console.log ("inserting ctx:", ctx);
                this.fn.apply(ctx, argumentsList);
            }
        });
    }

    fn(thing) {
        throw new Error(`Mutating ${thing} in ${this.name}: mutator not implemented`);
    }
}

/**
 * Generic Mutator class
 */
class Mutator extends MorGBase {
    constructor(name, fn, opts) {
        // check args
        if (fn.length !== 1) {
            throw new TypeError("Mutator constructor expected 'fn' to have one arg, has: " + fn.length);
        }
        super(name, fn, opts);
    }
}

/**
 * Generic Generator class
 */
class Generator extends MorGBase {
    constructor(name, fn, opts) {
        // check args
        if (fn.length !== 0) {
            throw new TypeError("Generator constructor expected 'fn' to have zero args, has: " + fn.length);
        }
        super(name, fn, opts);
    }
}


/**
 * Mutators and Generators (MandG) container class
 * has all the logic around the specific mutators and generators for a specific type
 */
class MandG {
    constructor(type, check, opts) {
        if (typeof type !== "string") {
            throw new TypeError("MandG constructor expected type, got: " + type);
        }

        if (typeof check !== "function") {
            throw new TypeError("MandG constructor expected 'check' to be a function, got: " + typeof check);
        }

        // set default options
        opts = opts || {};
        var defaults = {};
        _.defaultsDeep(opts, defaults);

        if (!Array.isArray(opts.depends) && opts.depends !== undefined) {
            throw new TypeError("Expected dependencies to be array, got: " + typeof opts.depends);
        }
        if (typeof opts.parent !== "string" && opts.parent !== undefined) {
            throw new TypeError("Expected parent to be string, got: " + typeof opts.parent);
        }

        this.depends = opts.depends || [];
        this.parent = opts.parent || undefined;

        // configure options
        this.type = type;
        this.name = type;
        this.check = check;

        // for future use
        // TODO: use Maps? using _.sample() later may not be map friendly
        this.mutator = {};
        this.generator = {};
        this.subtype = {};
    }

    check(thing) {
        throw new Error(`Adding ${thing}: check not implemented`);
    }

    addMutator(m) {
        // if mutator is a function, convert it to the mutator class
        if (typeof m === "function" && !(m instanceof Mutator)) {
            if (!m.name) {
                throw new TypeError("attempting to add Mutator from function without a name");
            }
            m = new Mutator(m.name, m);
        }

        // check typeof Mutator
        if (!(m instanceof Mutator)) {
            throw new TypeError("addMutator expected type Mutator, got: " + typeof m);
        }

        // TODO: check to make sure mutator doesn't already exist?
        var name = m.name;
        this.mutator[name] = m;
    }

    addGenerator(g) {
        // if generator is a function, convert it to the generator class
        if (typeof g === "function" && !(g instanceof Generator)) {
            if (!g.name) {
                throw new TypeError("attempting to add Generator from function without a name");
            }
            g = new Generator(g.name, g);
        }

        // check typeof Generator
        if (!(g instanceof Generator)) {
            throw new TypeError("addGenerator expected type Generator, got: " + typeof g);
        }

        // TODO: check to make sure generator doesn't already exist?
        var name = g.name;
        this.generator[name] = g;
    }

    /**
     * Reuses a generator that has already been implemented by another module
     */
    borrowGenerator(module, name) {
        // add module as dependency
        // add string as "function" in module list
        // proxy should resolve string to function later...
    }

    /**
     * Reuses a mutator that has already been implemented by another module
     */
    borrowMutator(module, name) {
        // add module as dependency
        // add string as "function" in module list
        // proxy should resolve string to function later...
    }

    addSubtype(s) {
        // check typeof Subtype
        if (!(s instanceof MandG)) {
            throw new TypeError("addSubtype expected type MandG, got: " + typeof s);
        }

        if (s === this) {
            throw new TypeError("attempting to add MandG to itself: recursion not allowed");
        }

        // TODO: check to make sure type doesn't already exist
        var name = s.name;
        if (this.subtype[name] !== undefined) {
            throw new TypeError("trying to add duplicate subtype: " + name);
        }
        this.subtype[name] = s;
    }

    /**
     * filters mutators and generators based on a specific tag
     * @todo not implemented
     */
    filter(tag) {
        throw new Error ("MandG filter not implemented");
    }
}

/**
 * A singleton for managing all MandG types
 */
var mandgTypeManagerSingleton;
class MandGTypeManager {
    constructor(opts) {
        // class is a singleton
        if (mandgTypeManagerSingleton) return mandgTypeManagerSingleton;
        mandgTypeManagerSingleton = this;

        // set default options
        opts = opts || {};
        var defaults = {
            mandgModuleList: ["array", "boolean", "date", "function", "null", "number", "object", "regexp", "string", "undef"]
        };
        _.defaultsDeep(opts, defaults);

        // the context for all MandG function calls
        this.callContext = {};
        // a flat list of all types
        this.typeIndex = {};
        // root of the heirarchy of types
        this.root = new MandG("everything", function() {
            return true;
        });
        this.typeIndex.everything = this.root;

        // load all the built-in types
        if (opts.mandgReplacementModules) {
            this.mandgModuleList = opts.mandgReplacementModules;
        } else {
            this.mandgModuleList = opts.mandgModuleList;
        }

        var mandg;
        var dependencyGraph = new DepGraph();
        for (let idx in this.mandgModuleList) {
            mandg = null;

            // try to load a module that is available in `node_modules` or similar
            try {
                mandg = require(this.mandgModuleList[idx]);
            } catch (err) {}

            // try to load the module out of our local types directory
            if (!mandg) try {
                mandg = require(`./types/${this.mandgModuleList[idx]}.js`);
            } catch (err) {}

            // if (!(mandg instanceof MandG)) { // XXX TODO for some reason this is broken
            if (!mandg) {
                throw TypeError("Error loading mandg module: " + this.mandgModuleList[idx]);
            }

            // add the node to the dependency graph
            // note that all nodes have to exist before dependencies can be added
            dependencyGraph.addNode(mandg.type, mandg);
        }

        // map out dependencies
        // TODO: save this order? replace module list?
        var list = dependencyGraph.overallOrder();
        for (let node of list) {
            mandg = dependencyGraph.getNodeData(node);

            if (typeof mandg.parent !== "string" && mandg.parent !== undefined) {
                throw new TypeError (`Expected parent to be string when building dependencies for ${mandg.type}. Got: ${typeof mandg.parent}`);
            }

            // try to add parent as a dependency
            if (mandg.parent) {
                try {
                    dependencyGraph.addDependency(mandg.type, mandg.parent);
                } catch (err) {
                    throw new TypeError(`parent '${mandg.parent}' not found when loading '${mandg.type}'`);
                }
            }

            // try to add all depends as dependencies
            for (let depName of mandg.depends) {
                try {
                    dependencyGraph.addDependency(mandg.type, depName);
                } catch (err) {
                    throw new TypeError(`dependency '${depName}' not found when loading '${mandg.type}'`);
                }
            }
        }

        for (let node of dependencyGraph.overallOrder()) {
            mandg = dependencyGraph.getNodeData(node);
            this.registerType(mandg);
        }

        this.addToContext ("utils", this.typeIndex);
    }

    /**
     * Kills the singleton so that a new one will be created.
     * Mostly useful for testing, but maybe other things too.
     */
    forceReset() {
        // console.log ("RESETTING MANDGTYPEMANAGER");
        mandgTypeManagerSingleton = null;
    }

    /**
     * registers a new mandg type
     * @param newType {MandG} - the new MandG type to register
     */
    registerType(newType) {
        if (!(newType instanceof MandG)) {
            throw new TypeError("expected 'newType' to be of type MandG, was: " + typeof mandg);
        }
        var name = newType.type;
        var parent = newType.parent || "everything";

        if (this.typeIndex[name] !== undefined) {
            throw new TypeError("can't register type twice: " + name);
        }

        this.typeIndex[name] = newType;
        var parentType = this.typeIndex[parent];
        if (parentType === undefined) {
            throw new TypeError(`registerType couldn't find parent '${parent}' when adding type ${newType.type}`);
        }
        parentType.addSubtype(newType);
        newType.parent = parentType;
        // console.log (`Added ${newType.type} to ${parentType.type}`)
    }

    /**
     * Turns a thing (string, object, null, etc.) into a mandg type
     * @param thing {any} - the thing to find the type of, using the registered types
     */
    resolveType(thing) {
        // recursively see if some 'type' in 'typeList' returns true when checking 'obj'
        // if it does, that's our type... check the subtypes to make sure there's not something
        // more specific
        function findType(typeList, obj) {
            var mandgList = _.map(typeList, function(m, t) {
                if (m === undefined || t === undefined) return;
                // console.log ("typeof m:", typeof m);
                // console.log ("m:", m);
                if (m.check(obj)) {
                    return findType(m.subtype, obj) || m;
                }
            });

            // type = _.flattenDeep (_.remove(type, _.isUndefined));
            var mandg;
            _.remove(mandgList, _.isUndefined);
            if (mandgList.length > 1) {
                throw new Error("found too many matching types");
            }
            if (mandgList.length === 0) mandg = undefined;
            if (Array.isArray(mandgList)) {
                mandg = _.flattenDeep(mandgList)[0];
            }

            return mandg;
        }

        return findType(this.root.subtype, thing);
    }

    /**
     * Returns a context that every function should be called with
     */
    getContext() {
        return this.callContext;
    }

    /**
     * Adds an item to the calling context for all MandG function calls
     * @param prop {string} - the new property in the context
     * @param val {any} - the value to assign to the property in the context
     */
    addToContext(prop, val) {
        Object.defineProperty(this.callContext, prop, {
            writeable: false,
            configurable: true,
            enumerable: true,
            value: val
        });

        // this.callContext[prop] = val;
        // TODO: configure as read only?
    }
}

module.exports = {
    // classes
    MandGTypeManager: MandGTypeManager,
    MandG: MandG,
    Mutator: Mutator,
    Generator: Generator
};