var Ap = Array.prototype;
var slice = Ap.slice;
var map = Ap.map;
var each = Ap.forEach;
var Op = Object.prototype;
var objToStr = Op.toString;
var funObjStr = objToStr.call(function(){});
var strObjStr = objToStr.call("");
var hasOwn = Op.hasOwnProperty;

module.exports = function () {

    var exports = {};

    // A type is an object with a .check method that takes a value and returns
    // true or false according to whether the value matches the type.

    function Type(check, name) {
        var self = this;
        if (!(self instanceof Type)) {
            throw new Error("Type constructor cannot be invoked without 'new'");
        }

        // Unfortunately we can't elegantly reuse isFunction and isString,
        // here, because this code is executed while defining those types.
        if (objToStr.call(check) !== funObjStr) {
            throw new Error(check + " is not a function");
        }

        // The `name` parameter can be either a function or a string.
        var nameObjStr = objToStr.call(name);
        if (!(nameObjStr === funObjStr ||
          nameObjStr === strObjStr)) {
            throw new Error(name + " is neither a function nor a string");
        }

        Object.defineProperties(self, {
            name: {value: name},
            check: {
                value: function (value, deep) {
                    var result = check.call(self, value, deep);
                    if (!result && deep && objToStr.call(deep) === funObjStr)
                        deep(self, value);
                    return result;
                }
            }
        });
    }

    var Tp = Type.prototype;

    // Throughout this file we use Object.defineProperty to prevent
    // redefinition of exported properties.
    exports.Type = Type;

    // Like .check, except that failure triggers an AssertionError.
    Tp.assert = function (value, deep) {
        if (!this.check(value, deep)) {
            var str = shallowStringify(value);
            throw new Error(str + " does not match type " + this);
        }
        return true;
    };

    function shallowStringify(value) {
        if (isObject.check(value))
            return "{" + Object.keys(value).map(function (key) {
                  return key + ": " + value[key];
              }).join(", ") + "}";

        if (isArray.check(value))
            return "[" + value.map(shallowStringify).join(", ") + "]";

        return JSON.stringify(value);
    }

    Tp.toString = function () {
        var name = this.name;

        if (isString.check(name))
            return name;

        if (isFunction.check(name))
            return name.call(this) + "";

        return name + " type";
    };

    var builtInCtorFns = [];
    var builtInCtorTypes = [];
    var builtInTypes = {};
    exports.builtInTypes = builtInTypes;

    function defBuiltInType(example, name) {
        var objStr = objToStr.call(example);

        var type = new Type(function (value) {
            return objToStr.call(value) === objStr;
        }, name);

        builtInTypes[name] = type;

        if (example && typeof example.constructor === "function") {
            builtInCtorFns.push(example.constructor);
            builtInCtorTypes.push(type);
        }

        return type;
    }

    // These types check the underlying [[Class]] attribute of the given
    // value, rather than using the problematic typeof operator. Note however
    // that no subtyping is considered; so, for instance, isObject.check
    // returns false for [], /./, new Date, and null.
    var isString = defBuiltInType("truthy", "string");
    var isFunction = defBuiltInType(function () {}, "function");
    var isArray = defBuiltInType([], "array");
    var isObject = defBuiltInType({}, "object");
    var isRegExp = defBuiltInType(/./, "RegExp");
    var isDate = defBuiltInType(new Date, "Date");
    var isNumber = defBuiltInType(3, "number");
    var isBoolean = defBuiltInType(true, "boolean");
    var isNull = defBuiltInType(null, "null");
    var isUndefined = defBuiltInType(void 0, "undefined");

    // There are a number of idiomatic ways of expressing types, so this
    // function serves to coerce them all to actual Type objects. Note that
    // providing the name argument is not necessary in most cases.
    function toType(from, name) {
        // The toType function should of course be idempotent.
        if (from instanceof Type)
            return from;

        // The Def type is used as a helper for constructing compound
        // interface types for AST nodes.
        if (from instanceof Def)
            return from.type;

        // Support [ElemType] syntax.
        if (isArray.check(from))
            return Type.fromArray(from);

        // Support { someField: FieldType, ... } syntax.
        if (isObject.check(from))
            return Type.fromObject(from);

        if (isFunction.check(from)) {
            var bicfIndex = builtInCtorFns.indexOf(from);
            if (bicfIndex >= 0) {
                return builtInCtorTypes[bicfIndex];
            }

            // If isFunction.check(from), and from is not a built-in
            // constructor, assume from is a binary predicate function we can
            // use to define the type.
            return new Type(from, name);
        }

        // As a last resort, toType returns a type that matches any value that
        // is === from. This is primarily useful for literal values like
        // toType(null), but it has the additional advantage of allowing
        // toType to be a total function.
        return new Type(function (value) {
            return value === from;
        }, isUndefined.check(name) ? function () {
            return from + "";
        } : name);
    }

    // Returns a type that matches the given value iff any of type1, type2,
    // etc. match the value.
    Type.or = function (/* type1, type2, ... */) {
        var types = [];
        var len = arguments.length;
        for (var i = 0; i < len; ++i)
            types.push(toType(arguments[i]));

        return new Type(function (value, deep) {
            for (var i = 0; i < len; ++i)
                if (types[i].check(value, deep))
                    return true;
            return false;
        }, function () {
            return types.join(" | ");
        });
    };

    Type.fromArray = function (arr) {
        if (!isArray.check(arr)) {
            throw new Error("");
        }
        if (arr.length !== 1) {
            throw new Error("only one element type is permitted for typed arrays");
        }
        return toType(arr[0]).arrayOf();
    };

    Tp.arrayOf = function () {
        var elemType = this;
        return new Type(function (value, deep) {
            return isArray.check(value) && value.every(function (elem) {
                  return elemType.check(elem, deep);
              });
        }, function () {
            return "[" + elemType + "]";
        });
    };

    Type.fromObject = function (obj) {
        var fields = Object.keys(obj).map(function (name) {
            return new Field(name, obj[name]);
        });

        return new Type(function (value, deep) {
            return isObject.check(value) && fields.every(function (field) {
                  return field.type.check(value[field.name], deep);
              });
        }, function () {
            return "{ " + fields.join(", ") + " }";
        });
    };

    function Field(name, type, defaultFn, hidden) {
        var self = this;

        if (!(self instanceof Field)) {
            throw new Error("Field constructor cannot be invoked without 'new'");
        }
        isString.assert(name);

        type = toType(type);

        var properties = {
            name: {value: name},
            type: {value: type},
            hidden: {value: !!hidden}
        };

        if (isFunction.check(defaultFn)) {
            properties.defaultFn = {value: defaultFn};
        }

        Object.defineProperties(self, properties);
    }

    var Fp = Field.prototype;

    Fp.toString = function () {
        return JSON.stringify(this.name) + ": " + this.type;
    };

    Fp.getValue = function (obj) {
        var value = obj[this.name];

        if (!isUndefined.check(value))
            return value;

        if (this.defaultFn)
            value = this.defaultFn.call(obj);

        return value;
    };

    // Define a type whose name is registered in a namespace (the defCache) so
    // that future definitions will return the same type given the same name.
    // In particular, this system allows for circular and forward definitions.
    // The Def object d returned from Type.def may be used to configure the
    // type d.type by calling methods such as d.bases, d.build, and d.field.
    Type.def = function (typeName) {
        isString.assert(typeName);
        return hasOwn.call(defCache, typeName)
          ? defCache[typeName]
          : defCache[typeName] = new Def(typeName);
    };

    // In order to return the same Def instance every time Type.def is called
    // with a particular name, those instances need to be stored in a cache.
    var defCache = Object.create(null);

    function Def(typeName) {
        var self = this;
        if (!(self instanceof Def)) {
            throw new Error("Def constructor cannot be invoked without 'new'");
        }

        Object.defineProperties(self, {
            typeName: {value: typeName},
            baseNames: {value: []},
            ownFields: {value: Object.create(null)},

            // These two are populated during finalization.
            allSupertypes: {value: Object.create(null)}, // Includes own typeName.
            supertypeList: {value: []}, // Linear inheritance hierarchy.
            allFields: {value: Object.create(null)}, // Includes inherited fields.
            fieldNames: {value: []}, // Non-hidden keys of allFields.

            type: {
                value: new Type(function (value, deep) {
                    return self.check(value, deep);
                }, typeName)
            }
        });
    }

    Def.fromValue = function (value) {
        if (value && typeof value === "object") {
            var type = value.type;
            if (typeof type === "string" &&
              hasOwn.call(defCache, type)) {
                var d = defCache[type];
                if (d.finalized) {
                    return d;
                }
            }
        }

        return null;
    };

    var Dp = Def.prototype;

    Dp.isSupertypeOf = function (that) {
        if (that instanceof Def) {
            if (this.finalized !== true ||
              that.finalized !== true) {
                throw new Error("");
            }
            return hasOwn.call(that.allSupertypes, this.typeName);
        } else {
            throw new Error(that + " is not a Def");
        }
    };

    // Note that the list returned by this function is a copy of the internal
    // supertypeList, *without* the typeName itself as the first element.
    exports.getSupertypeNames = function (typeName) {
        if (!hasOwn.call(defCache, typeName)) {
            throw new Error("");
        }
        var d = defCache[typeName];
        if (d.finalized !== true) {
            throw new Error("");
        }
        return d.supertypeList.slice(1);
    };

    // Returns an object mapping from every known type in the defCache to the
    // most specific supertype whose name is an own property of the candidates
    // object.
    exports.computeSupertypeLookupTable = function (candidates) {
        var table = {};
        var typeNames = Object.keys(defCache);
        var typeNameCount = typeNames.length;

        for (var i = 0; i < typeNameCount; ++i) {
            var typeName = typeNames[i];
            var d = defCache[typeName];
            if (d.finalized !== true) {
                throw new Error("" + typeName);
            }
            for (var j = 0; j < d.supertypeList.length; ++j) {
                var superTypeName = d.supertypeList[j];
                if (hasOwn.call(candidates, superTypeName)) {
                    table[typeName] = superTypeName;
                    break;
                }
            }
        }

        return table;
    };

    Dp.checkAllFields = function (value, deep) {
        var allFields = this.allFields;
        if (this.finalized !== true) {
            throw new Error("" + this.typeName);
        }

        function checkFieldByName(name) {
            var field = allFields[name];
            var type = field.type;
            var child = field.getValue(value);
            return type.check(child, deep);
        }

        return isObject.check(value)
          && Object.keys(allFields).every(checkFieldByName);
    };

    Dp.check = function (value, deep) {
        if (this.finalized !== true) {
            throw new Error(
              "prematurely checking unfinalized type " + this.typeName
            );
        }

        // A Def type can only match an object value.
        if (!isObject.check(value))
            return false;

        var vDef = Def.fromValue(value);
        if (!vDef) {
            // If we couldn't infer the Def associated with the given value,
            // and we expected it to be a SourceLocation or a Position, it was
            // probably just missing a "type" field (because Esprima does not
            // assign a type property to such nodes). Be optimistic and let
            // this.checkAllFields make the final decision.
            if (this.typeName === "SourceLocation" ||
              this.typeName === "Position") {
                return this.checkAllFields(value, deep);
            }

            // Calling this.checkAllFields for any other type of node is both
            // bad for performance and way too forgiving.
            return false;
        }

        // If checking deeply and vDef === this, then we only need to call
        // checkAllFields once. Calling checkAllFields is too strict when deep
        // is false, because then we only care about this.isSupertypeOf(vDef).
        if (deep && vDef === this)
            return this.checkAllFields(value, deep);

        // In most cases we rely exclusively on isSupertypeOf to make O(1)
        // subtyping determinations. This suffices in most situations outside
        // of unit tests, since interface conformance is checked whenever new
        // instances are created using builder functions.
        if (!this.isSupertypeOf(vDef))
            return false;

        // The exception is when deep is true; then, we recursively check all
        // fields.
        if (!deep)
            return true;

        // Use the more specific Def (vDef) to perform the deep check, but
        // shallow-check fields defined by the less specific Def (this).
        return vDef.checkAllFields(value, deep)
          && this.checkAllFields(value, false);
    };

    Dp.bases = function () {
        var args = slice.call(arguments);
        var bases = this.baseNames;

        if (this.finalized) {
            if (args.length !== bases.length) {
                throw new Error("");
            }
            for (var i = 0; i < args.length; i++) {
                if (args[i] !== bases[i]) {
                    throw new Error("");
                }
            }
            return this;
        }

        args.forEach(function (baseName) {
            isString.assert(baseName);

            // This indexOf lookup may be O(n), but the typical number of base
            // names is very small, and indexOf is a native Array method.
            if (bases.indexOf(baseName) < 0)
                bases.push(baseName);
        });

        return this; // For chaining.
    };

    // False by default until .build(...) is called on an instance.
    Object.defineProperty(Dp, "buildable", {value: false});

    var builders = {};
    exports.builders = builders;

    // This object is used as prototype for any node created by a builder.
    var nodePrototype = {};

    // Call this function to define a new method to be shared by all AST
     // nodes. The replaced method (if any) is returned for easy wrapping.
    exports.defineMethod = function (name, func) {
        var old = nodePrototype[name];

        // Pass undefined as func to delete nodePrototype[name].
        if (isUndefined.check(func)) {
            delete nodePrototype[name];

        } else {
            isFunction.assert(func);

            Object.defineProperty(nodePrototype, name, {
                enumerable: true, // For discoverability.
                configurable: true, // For delete proto[name].
                value: func
            });
        }

        return old;
    };

    var isArrayOfString = isString.arrayOf();

    // Calling the .build method of a Def simultaneously marks the type as
    // buildable (by defining builders[getBuilderName(typeName)]) and
    // specifies the order of arguments that should be passed to the builder
    // function to create an instance of the type.
    Dp.build = function (/* param1, param2, ... */) {
        var self = this;

        var newBuildParams = slice.call(arguments);
        isArrayOfString.assert(newBuildParams);

        // Calling Def.prototype.build multiple times has the effect of merely
        // redefining this property.
        Object.defineProperty(self, "buildParams", {
            value: newBuildParams,
            writable: false,
            enumerable: false,
            configurable: true
        });

        if (self.buildable) {
            // If this Def is already buildable, update self.buildParams and
            // continue using the old builder function.
            return self;
        }

        // Every buildable type will have its "type" field filled in
        // automatically. This includes types that are not subtypes of Node,
        // like SourceLocation, but that seems harmless (TODO?).
        self.field("type", String, function () { return self.typeName });

        // Override Dp.buildable for this Def instance.
        Object.defineProperty(self, "buildable", {value: true});

        Object.defineProperty(builders, getBuilderName(self.typeName), {
            enumerable: true,

            value: function () {
                var args = arguments;
                var argc = args.length;
                var built = Object.create(nodePrototype);

                if (!self.finalized) {
                    throw new Error(
                      "attempting to instantiate unfinalized type " +
                      self.typeName
                    );
                }

                function add(param, i) {
                    if (hasOwn.call(built, param))
                        return;

                    var all = self.allFields;
                    if (!hasOwn.call(all, param)) {
                        throw new Error("" + param);
                    }

                    var field = all[param];
                    var type = field.type;
                    var value;

                    if (isNumber.check(i) && i < argc) {
                        value = args[i];
                    } else if (field.defaultFn) {
                        // Expose the partially-built object to the default
                        // function as its `this` object.
                        value = field.defaultFn.call(built);
                    } else {
                        var message = "no value or default function given for field " +
                          JSON.stringify(param) + " of " + self.typeName + "(" +
                          self.buildParams.map(function (name) {
                              return all[name];
                          }).join(", ") + ")";
                        throw new Error(message);
                    }

                    if (!type.check(value)) {
                        throw new Error(
                          shallowStringify(value) +
                          " does not match field " + field +
                          " of type " + self.typeName
                        );
                    }

                    // TODO Could attach getters and setters here to enforce
                    // dynamic type safety.
                    built[param] = value;
                }

                self.buildParams.forEach(function (param, i) {
                    add(param, i);
                });

                Object.keys(self.allFields).forEach(function (param) {
                    add(param); // Use the default value.
                });

                // Make sure that the "type" field was filled automatically.
                if (built.type !== self.typeName) {
                    throw new Error("");
                }

                return built;
            }
        });

        return self; // For chaining.
    };

    function getBuilderName(typeName) {
        return typeName.replace(/^[A-Z]+/, function (upperCasePrefix) {
            var len = upperCasePrefix.length;
            switch (len) {
                case 0: return "";
                // If there's only one initial capital letter, just lower-case it.
                case 1: return upperCasePrefix.toLowerCase();
                default:
                    // If there's more than one initial capital letter, lower-case
                    // all but the last one, so that XMLDefaultDeclaration (for
                    // example) becomes xmlDefaultDeclaration.
                    return upperCasePrefix.slice(
                        0, len - 1).toLowerCase() +
                      upperCasePrefix.charAt(len - 1);
            }
        });
    }
    exports.getBuilderName = getBuilderName;

    function getStatementBuilderName(typeName) {
        typeName = getBuilderName(typeName);
        return typeName.replace(/(Expression)?$/, "Statement");
    }
    exports.getStatementBuilderName = getStatementBuilderName;

    // The reason fields are specified using .field(...) instead of an object
    // literal syntax is somewhat subtle: the object literal syntax would
    // support only one key and one value, but with .field(...) we can pass
    // any number of arguments to specify the field.
    Dp.field = function (name, type, defaultFn, hidden) {
        if (this.finalized) {
            console.error("Ignoring attempt to redefine field " +
              JSON.stringify(name) + " of finalized type " +
              JSON.stringify(this.typeName));
            return this;
        }
        this.ownFields[name] = new Field(name, type, defaultFn, hidden);
        return this; // For chaining.
    };

    var namedTypes = {};
    exports.namedTypes = namedTypes;

    // Like Object.keys, but aware of what fields each AST type should have.
    function getFieldNames(object) {
        var d = Def.fromValue(object);
        if (d) {
            return d.fieldNames.slice(0);
        }

        if ("type" in object) {
            throw new Error(
              "did not recognize object of type " +
              JSON.stringify(object.type)
            );
        }

        return Object.keys(object);
    }
    exports.getFieldNames = getFieldNames;

    // Get the value of an object property, taking object.type and default
    // functions into account.
    function getFieldValue(object, fieldName) {
        var d = Def.fromValue(object);
        if (d) {
            var field = d.allFields[fieldName];
            if (field) {
                return field.getValue(object);
            }
        }

        return object && object[fieldName];
    }
    exports.getFieldValue = getFieldValue;

    // Iterate over all defined fields of an object, including those missing
    // or undefined, passing each field name and effective value (as returned
    // by getFieldValue) to the callback. If the object has no corresponding
    // Def, the callback will never be called.
    exports.eachField = function (object, callback, context) {
        getFieldNames(object).forEach(function (name) {
            callback.call(this, name, getFieldValue(object, name));
        }, context);
    };

    // Similar to eachField, except that iteration stops as soon as the
    // callback returns a truthy value. Like Array.prototype.some, the final
    // result is either true or false to indicates whether the callback
    // returned true for any element or not.
    exports.someField = function (object, callback, context) {
        return getFieldNames(object).some(function (name) {
            return callback.call(this, name, getFieldValue(object, name));
        }, context);
    };

    // This property will be overridden as true by individual Def instances
    // when they are finalized.
    Object.defineProperty(Dp, "finalized", {value: false});

    Dp.finalize = function () {
        var self = this;

        // It's not an error to finalize a type more than once, but only the
        // first call to .finalize does anything.
        if (!self.finalized) {
            var allFields = self.allFields;
            var allSupertypes = self.allSupertypes;

            self.baseNames.forEach(function (name) {
                var def = defCache[name];
                if (def instanceof Def) {
                    def.finalize();
                    extend(allFields, def.allFields);
                    extend(allSupertypes, def.allSupertypes);
                } else {
                    var message = "unknown supertype name " +
                      JSON.stringify(name) +
                      " for subtype " +
                      JSON.stringify(self.typeName);
                    throw new Error(message);
                }
            });

            // TODO Warn if fields are overridden with incompatible types.
            extend(allFields, self.ownFields);
            allSupertypes[self.typeName] = self;

            self.fieldNames.length = 0;
            for (var fieldName in allFields) {
                if (hasOwn.call(allFields, fieldName) &&
                    !allFields[fieldName].hidden) {
                        self.fieldNames.push(fieldName);
                }
            }

            // Types are exported only once they have been finalized.
            Object.defineProperty(namedTypes, self.typeName, {
                enumerable: true,
                value: self.type
            });

            Object.defineProperty(self, "finalized", {value: true});

            // A linearization of the inheritance hierarchy.
            populateSupertypeList(self.typeName, self.supertypeList);

            if (self.buildable && self.supertypeList.lastIndexOf("Expression") >= 0) {
                wrapExpressionBuilderWithStatement(self.typeName);
            }
        }
    };

    // Adds an additional builder for Expression subtypes
    // that wraps the built Expression in an ExpressionStatements.
    function wrapExpressionBuilderWithStatement(typeName) {
        var wrapperName = getStatementBuilderName(typeName);

        // skip if the builder already exists
        if (builders[wrapperName]) return;

        // the builder function to wrap with builders.ExpressionStatement
        var wrapped = builders[getBuilderName(typeName)];

        // skip if there is nothing to wrap
        if (!wrapped) return;

        builders[wrapperName] = function () {
            return builders.expressionStatement(wrapped.apply(builders, arguments));
        };
    }

    function populateSupertypeList(typeName, list) {
        list.length = 0;
        list.push(typeName);

        var lastSeen = Object.create(null);

        for (var pos = 0; pos < list.length; ++pos) {
            typeName = list[pos];
            var d = defCache[typeName];
            if (d.finalized !== true) {
                throw new Error("");
            }

            // If we saw typeName earlier in the breadth-first traversal,
            // delete the last-seen occurrence.
            if (hasOwn.call(lastSeen, typeName)) {
                delete list[lastSeen[typeName]];
            }

            // Record the new index of the last-seen occurrence of typeName.
            lastSeen[typeName] = pos;

            // Enqueue the base names of this type.
            list.push.apply(list, d.baseNames);
        }

        // Compaction loop to remove array holes.
        for (var to = 0, from = to, len = list.length; from < len; ++from) {
            if (hasOwn.call(list, from)) {
                list[to++] = list[from];
            }
        }

        list.length = to;
    }

    function extend(into, from) {
        Object.keys(from).forEach(function (name) {
            into[name] = from[name];
        });

        return into;
    };

    exports.finalize = function () {
        Object.keys(defCache).forEach(function (name) {
            defCache[name].finalize();
        });
    };

    return exports;
};
