var assert = require(“assert”); var types = require(“./types”); var NodePath = require(“./node-path”); var Node = types.namedTypes.Node; var isArray = types.builtInTypes.array; var isObject = types.builtInTypes.object; var isFunction = types.builtInTypes.function; var hasOwn = Object.prototype.hasOwnProperty; var undefined;

function PathVisitor() {

assert.ok(this instanceof PathVisitor);
this._reusableContextStack = [];
this._methodNameTable = computeMethodNameTable(this);
this.Context = makeContextConstructor(this);

}

function computeMethodNameTable(visitor) {

var typeNames = Object.create(null);

for (var methodName in visitor) {
    if (/^visit[A-Z]/.test(methodName)) {
        typeNames[methodName.slice("visit".length)] = true;
    }
}

var supertypeTable = types.computeSupertypeLookupTable(typeNames);
var methodNameTable = Object.create(null);

for (var typeName in supertypeTable) {
    if (hasOwn.call(supertypeTable, typeName)) {
        methodName = "visit" + supertypeTable[typeName];
        if (isFunction.check(visitor[methodName])) {
            methodNameTable[typeName] = methodName;
        }
    }
}

return methodNameTable;

}

PathVisitor.fromMethodsObject = function fromMethodsObject(methods) {

if (methods instanceof PathVisitor) {
    return methods;
}

if (!isObject.check(methods)) {
    // An empty visitor?
    return new PathVisitor;
}

function Visitor() {
    assert.ok(this instanceof Visitor);
    PathVisitor.call(this);
}

var Vp = Visitor.prototype = Object.create(PVp);
Vp.constructor = Visitor;

extend(Vp, methods);
extend(Visitor, PathVisitor);

isFunction.assert(Visitor.fromMethodsObject);
isFunction.assert(Visitor.visit);

return new Visitor;

};

function extend(target, source) {

for (var property in source) {
    if (hasOwn.call(source, property)) {
        target[property] = source[property];
    }
}

return target;

}

PathVisitor.visit = function visit(node, methods) {

var visitor = PathVisitor.fromMethodsObject(methods);

if (node instanceof NodePath) {
    visitor.visit(node);
    return node.value;
}

var rootPath = new NodePath({ root: node });
visitor.visit(rootPath.get("root"));
return rootPath.value.root;

};

var PVp = PathVisitor.prototype;

PVp.visit = function(path) {

if (this instanceof this.Context) {
    // If we somehow end up calling context.visit, then we need to
    // re-invoke the .visit method against context.visitor.
    return this.visitor.visit(path);
}

assert.ok(path instanceof NodePath);
var value = path.value;

var methodName = Node.check(value) && this._methodNameTable[value.type];
if (methodName) {
    var context = this.acquireContext(path);
    try {
        context.invokeVisitorMethod(methodName);
    } finally {
        this.releaseContext(context);
    }

} else {
    // If there was no visitor method to call, visit the children of
    // this node generically.
    visitChildren(path, this);
}

};

function visitChildren(path, visitor) {

assert.ok(path instanceof NodePath);
assert.ok(visitor instanceof PathVisitor);

var value = path.value;

if (isArray.check(value)) {
    path.each(visitor.visit, visitor);
} else if (!isObject.check(value)) {
    // No children to visit.
} else {
    var childNames = types.getFieldNames(value);
    var childCount = childNames.length;
    var childPaths = [];

    for (var i = 0; i < childCount; ++i) {
        var childName = childNames[i];
        if (!hasOwn.call(value, childName)) {
            value[childName] = types.getFieldValue(value, childName);
        }
        childPaths.push(path.get(childName));
    }

    for (var i = 0; i < childCount; ++i) {
        visitor.visit(childPaths[i]);
    }
}

}

PVp.acquireContext = function(path) {

if (this._reusableContextStack.length === 0) {
    return new this.Context(path);
}
return this._reusableContextStack.pop().reset(path);

};

PVp.releaseContext = function(context) {

assert.ok(context instanceof this.Context);
this._reusableContextStack.push(context);
context.currentPath = null;

};

function makeContextConstructor(visitor) {

function Context(path) {
    assert.ok(this instanceof Context);
    assert.ok(this instanceof PathVisitor);
    assert.ok(path instanceof NodePath);

    Object.defineProperty(this, "visitor", {
        value: visitor,
        writable: false,
        enumerable: true,
        configurable: false
    });

    this.currentPath = path;
    this.needToCallTraverse = true;

    Object.seal(this);
}

assert.ok(visitor instanceof PathVisitor);

// Note that the visitor object is the prototype of Context.prototype,
// so all visitor methods are inherited by context objects.
var Cp = Context.prototype = Object.create(visitor);

Cp.constructor = Context;
extend(Cp, sharedContextProtoMethods);

return Context;

}

// Every PathVisitor has a different this.Context constructor and // this.Context.prototype object, but those prototypes can all use the // same reset, invokeVisitorMethod, and traverse function objects. var sharedContextProtoMethods = Object.create(null);

sharedContextProtoMethods.reset = function reset(path) {

assert.ok(this instanceof this.Context);
assert.ok(path instanceof NodePath);

this.currentPath = path;
this.needToCallTraverse = true;

return this;

};

sharedContextProtoMethods.invokeVisitorMethod = function invokeVisitorMethod(methodName) {

assert.ok(this instanceof this.Context);
assert.ok(this.currentPath instanceof NodePath);

var result = this.visitor[methodName].call(this, this.currentPath);

if (result === false) {
    // Visitor methods return false to indicate that they have handled
    // their own traversal needs, and we should not complain if
    // this.needToCallTraverse is still true.
    this.needToCallTraverse = false;

} else if (result !== undefined) {
    // Any other non-undefined value returned from the visitor method
    // is interpreted as a replacement value.
    this.currentPath = this.currentPath.replace(result)[0];

    if (this.needToCallTraverse) {
        // If this.traverse still hasn't been called, visit the
        // children of the replacement node.
        this.traverse(this.currentPath);
    }
}

assert.strictEqual(
    this.needToCallTraverse, false,
    "Must either call this.traverse or return false in " + methodName
);

};

sharedContextProtoMethods.traverse = function traverse(path, newVisitor) {

assert.ok(this instanceof this.Context);
assert.ok(path instanceof NodePath);
assert.ok(this.currentPath instanceof NodePath);

this.needToCallTraverse = false;

visitChildren(path, PathVisitor.fromMethodsObject(
    newVisitor || this.visitor
));

};

module.exports = PathVisitor;