var assert = require("assert");
var types = require("./types");
var n = types.namedTypes;
var b = types.builders;
var isObject = types.builtInTypes.object;
var isArray = types.builtInTypes.array;
var isFunction = types.builtInTypes.function;
var Patcher = require("./patcher").Patcher;
var normalizeOptions = require("./options").normalize;
var fromString = require("./lines").fromString;
var attachComments = require("./comments").attach;

exports.parse = function parse(source, options) {
    options = normalizeOptions(options);

    var lines = fromString(source, options);

    var sourceWithoutTabs = lines.toString({
        tabWidth: options.tabWidth,
        reuseWhitespace: false,
        useTabs: false
    });

    var program = options.esprima.parse(sourceWithoutTabs, {
        loc: true,
        range: options.range,
        comment: true,
        tolerant: options.tolerant,
        sourceType: 'module'
    });

    var comments = program.comments;
    delete program.comments;

    // In order to ensure we reprint leading and trailing program
    // comments, wrap the original Program node with a File node.
    var file = b.file(program);
    file.loc = {
        lines: lines,
        indent: 0,
        start: lines.firstPos(),
        end: lines.lastPos()
    };

    // Return a copy of the original AST so that any changes made may be
    // compared to the original.
    var copy = new TreeCopier(lines).copy(file);

    // Attach comments to the copy rather than the original.
    attachComments(comments, copy, lines);

    return copy;
};

function TreeCopier(lines) {
    assert.ok(this instanceof TreeCopier);
    this.lines = lines;
    this.indent = 0;
}

var TCp = TreeCopier.prototype;

TCp.copy = function(node) {
    if (isArray.check(node)) {
        return node.map(this.copy, this);
    }

    if (!isObject.check(node)) {
        return node;
    }

    if ((n.MethodDefinition && n.MethodDefinition.check(node)) ||
        (n.Property.check(node) && (node.method || node.shorthand))) {
        // If the node is a MethodDefinition or a .method or .shorthand
        // Property, then the location information stored in
        // node.value.loc is very likely untrustworthy (just the {body}
        // part of a method, or nothing in the case of shorthand
        // properties), so we null out that information to prevent
        // accidental reuse of bogus source code during reprinting.
        node.value.loc = null;

        if (n.FunctionExpression.check(node.value)) {
            // FunctionExpression method values should be anonymous,
            // because their .id fields are ignored anyway.
            node.value.id = null;
        }
    }

    var copy = Object.create(Object.getPrototypeOf(node), {
        original: { // Provide a link from the copy to the original.
            value: node,
            configurable: false,
            enumerable: false,
            writable: true
        }
    });

    var loc = node.loc;
    var oldIndent = this.indent;
    var newIndent = oldIndent;

    if (loc) {
        if (loc.start.line < 1) {
            loc.start.line = 1;
        }

        if (loc.end.line < 1) {
            loc.end.line = 1;
        }

        if (this.lines.isPrecededOnlyByWhitespace(loc.start)) {
            newIndent = this.indent = loc.start.column;
        }

        loc.lines = this.lines;
        loc.indent = newIndent;
    }

    var keys = Object.keys(node);
    var keyCount = keys.length;
    for (var i = 0; i < keyCount; ++i) {
        var key = keys[i];
        if (key === "loc") {
            copy[key] = node[key];
        } else if (key === "comments") {
            // Handled below.
        } else {
            copy[key] = this.copy(node[key]);
        }
    }

    this.indent = oldIndent;

    if (node.comments) {
        Object.defineProperty(copy, "comments", {
            value: node.comments,
            enumerable: false
        });
    }

    return copy;
};
