/**
 * @module jsdoc/src/parser
 */
'use strict';

var events = require('events');
var fs = require('jsdoc/fs');
var jsdoc = {
    doclet: require('jsdoc/doclet'),
    name: require('jsdoc/name'),
    src: {
        astnode: require('jsdoc/src/astnode'),
        syntax: require('jsdoc/src/syntax')
    },
    util: {
        doop: require('jsdoc/util/doop'),
        runtime: require('jsdoc/util/runtime')
    }
};
var logger = require('jsdoc/util/logger');
var path = require('jsdoc/path');
var util = require('util');

var hasOwnProp = Object.prototype.hasOwnProperty;
var Syntax = jsdoc.src.syntax.Syntax;

// TODO: docs
var PARSERS = exports.PARSERS = {
    esprima: 'jsdoc/src/parser',
    rhino: 'rhino/jsdoc/src/parser'
};
/*eslint-disable no-script-url */
// Prefix for JavaScript strings that were provided in lieu of a filename.
var SCHEMA = 'javascript:';
/*eslint-enable no-script-url */

// TODO: docs
exports.createParser = function(type) {
    var modulePath;

    if (!type) {
        type = jsdoc.util.runtime.isRhino() ? 'rhino' : 'esprima';
    }

    if (PARSERS[type]) {
        modulePath = PARSERS[type];
    }
    else {
        modulePath = path.join( path.getResourcePath(path.dirname(type)), path.basename(type) );
    }

    try {
        return new ( require(modulePath) ).Parser();
    }
    catch (e) {
        logger.fatal('Unable to create the parser type "' + type + '": ' + e);
    }
};

// TODO: docs
/**
 * @class
 * @alias module:jsdoc/src/parser.Parser
 * @mixes module:events.EventEmitter
 *
 * @example <caption>Create a new parser.</caption>
 * var jsdocParser = new (require('jsdoc/src/parser').Parser)();
 */
var Parser = exports.Parser = function(builderInstance, visitorInstance, walkerInstance) {
    this.clear();

    this._astBuilder = builderInstance || new (require('jsdoc/src/astbuilder')).AstBuilder();
    this._visitor = visitorInstance || new (require('jsdoc/src/visitor')).Visitor(this);
    this._walker = walkerInstance || new (require('jsdoc/src/walker')).Walker();

    Object.defineProperties(this, {
        astBuilder: {
            get: function() {
                return this._astBuilder;
            }
        },
        visitor: {
            get: function() {
                return this._visitor;
            }
        },
        walker: {
            get: function() {
                return this._walker;
            }
        }
    });
};
util.inherits(Parser, events.EventEmitter);

// TODO: docs
Parser.prototype.clear = function() {
    this._resultBuffer = [];
    this.refs = {};
    this.refs[jsdoc.name.LONGNAMES.GLOBAL] = {};
    this.refs[jsdoc.name.LONGNAMES.GLOBAL].meta = {};
};

// TODO: update docs
/**
 * Parse the given source files for JSDoc comments.
 * @param {Array.<string>} sourceFiles An array of filepaths to the JavaScript sources.
 * @param {string} [encoding=utf8]
 *
 * @fires module:jsdoc/src/parser.Parser.parseBegin
 * @fires module:jsdoc/src/parser.Parser.fileBegin
 * @fires module:jsdoc/src/parser.Parser.jsdocCommentFound
 * @fires module:jsdoc/src/parser.Parser.symbolFound
 * @fires module:jsdoc/src/parser.Parser.newDoclet
 * @fires module:jsdoc/src/parser.Parser.fileComplete
 * @fires module:jsdoc/src/parser.Parser.parseComplete
 *
 * @example <caption>Parse two source files.</caption>
 * var myFiles = ['file1.js', 'file2.js'];
 * var docs = jsdocParser.parse(myFiles);
 */
Parser.prototype.parse = function(sourceFiles, encoding) {
    encoding = encoding || global.env.conf.encoding || 'utf8';

    var filename = '';
    var sourceCode = '';
    var parsedFiles = [];
    var e = {};

    if (typeof sourceFiles === 'string') {
        sourceFiles = [sourceFiles];
    }

    e.sourcefiles = sourceFiles;
    logger.debug('Parsing source files: %j', sourceFiles);

    this.emit('parseBegin', e);

    for (var i = 0, l = sourceFiles.length; i < l; i++) {
        sourceCode = '';

        if (sourceFiles[i].indexOf(SCHEMA) === 0) {
            sourceCode = sourceFiles[i].substr(SCHEMA.length);
            filename = '[[string' + i + ']]';
        }
        else {
            filename = sourceFiles[i];
            try {
                sourceCode = fs.readFileSync(filename, encoding);
            }
            catch(e) {
                logger.error('Unable to read and parse the source file %s: %s', filename, e);
            }
        }

        if (sourceCode.length) {
            this._parseSourceCode(sourceCode, filename);
            parsedFiles.push(filename);
        }
    }

    this.emit('parseComplete', {
        sourcefiles: parsedFiles,
        doclets: this._resultBuffer
    });
    logger.debug('Finished parsing source files.');

    return this._resultBuffer;
};

// TODO: docs
Parser.prototype.fireProcessingComplete = function(doclets) {
    this.emit('processingComplete', { doclets: doclets });
};

// TODO: docs
Parser.prototype.results = function() {
    return this._resultBuffer;
};

// TODO: update docs
/**
 * @param {Object} o The parse result to add to the result buffer.
 */
Parser.prototype.addResult = function(o) {
    this._resultBuffer.push(o);
};

// TODO: docs
Parser.prototype.addAstNodeVisitor = function(visitor) {
    this._visitor.addAstNodeVisitor(visitor);
};

// TODO: docs
Parser.prototype.getAstNodeVisitors = function() {
    return this._visitor.getAstNodeVisitors();
};

// TODO: docs
function pretreat(code) {
    return code
        // comment out hashbang at the top of the file, like: #!/usr/bin/env node
        .replace(/^(\#\![\S \t]+\r?\n)/, '// $1')

        // to support code minifiers that preserve /*! comments, treat /*!* as equivalent to /**
        .replace(/\/\*\!\*/g, '/**')
        // merge adjacent doclets
        .replace(/\*\/\/\*\*+/g, '@also');
}

/** @private */
Parser.prototype._parseSourceCode = function(sourceCode, sourceName) {
    var ast;
    var globalScope;

    var e = {
        filename: sourceName
    };

    this.emit('fileBegin', e);
    logger.printInfo('Parsing %s ...', sourceName);

    if (!e.defaultPrevented) {
        e = {
            filename: sourceName,
            source: sourceCode
        };
        this.emit('beforeParse', e);
        sourceCode = e.source;
        sourceName = e.filename;

        sourceCode = pretreat(e.source);

        ast = this._astBuilder.build(sourceCode, sourceName);
        if (ast) {
            this._walkAst(ast, this._visitor, sourceName);
        }
    }

    this.emit('fileComplete', e);
    logger.info('complete.');
};

/** @private */
Parser.prototype._walkAst = function(ast, visitor, sourceName) {
    this._walker.recurse(ast, visitor, sourceName);
};

// TODO: docs
Parser.prototype.addDocletRef = function(e) {
    var node;

    if (e && e.code && e.code.node) {
        node = e.code.node;
        // allow lookup from value => doclet
        if (e.doclet) {
            this.refs[node.nodeId] = e.doclet;
        }
        // keep references to undocumented anonymous functions, too, as they might have scoped vars
        else if (
            (node.type === Syntax.FunctionDeclaration || node.type === Syntax.FunctionExpression) &&
            !this.refs[node.nodeId] ) {
            this.refs[node.nodeId] = {
                longname: jsdoc.name.LONGNAMES.ANONYMOUS,
                meta: {
                    code: e.code
                }
            };
        }
    }
};

// TODO: docs
Parser.prototype._getDoclet = function(id) {
    if ( hasOwnProp.call(this.refs, id) ) {
        return this.refs[id];
    }

    return null;
};

// TODO: docs
/**
 * @param {string} name - The symbol's longname.
 * @return {string} The symbol's basename.
 */
Parser.prototype.getBasename = function(name) {
    if (name !== undefined) {
        return name.replace(/^([$a-z_][$a-z_0-9]*).*?$/i, '$1');
    }
};

// TODO: docs
function definedInScope(doclet, basename) {
    return !!doclet && !!doclet.meta && !!doclet.meta.vars && !!basename &&
        hasOwnProp.call(doclet.meta.vars, basename);
}

// TODO: docs
/**
 * Given a node, determine what the node is a member of.
 * @param {node} node
 * @returns {string} The long name of the node that this is a member of.
 */
Parser.prototype.astnodeToMemberof = function(node) {
    var basename;
    var doclet;
    var scope;

    var result = '';
    var type = node.type;

    if ( (type === Syntax.FunctionDeclaration || type === Syntax.FunctionExpression ||
        type === Syntax.VariableDeclarator) && node.enclosingScope ) {
        doclet = this._getDoclet(node.enclosingScope.nodeId);

        if (!doclet) {
            result = jsdoc.name.LONGNAMES.ANONYMOUS + jsdoc.name.SCOPE.PUNC.INNER;
       }
        else {
            result = (doclet.longname || doclet.name) + jsdoc.name.SCOPE.PUNC.INNER;
        }
    }
    else {
        // check local references for aliases
        scope = node;
        basename = this.getBasename( jsdoc.src.astnode.nodeToString(node) );

        // walk up the scope chain until we find the scope in which the node is defined
        while (scope.enclosingScope) {
            doclet = this._getDoclet(scope.enclosingScope.nodeId);
            if ( doclet && definedInScope(doclet, basename) ) {
                result = [doclet.meta.vars[basename], basename];
                break;
            }
            else {
                // move up
                scope = scope.enclosingScope;
            }
        }

        // do we know that it's a global?
        doclet = this.refs[jsdoc.name.LONGNAMES.GLOBAL];
        if ( doclet && definedInScope(doclet, basename) ) {
            result = [doclet.meta.vars[basename], basename];
        }

        // have we seen the node's parent? if so, use that
        else if (node.parent) {
            doclet = this._getDoclet(node.parent.nodeId);

            // set the result if we found a doclet. (if we didn't, the AST node may describe a
            // global symbol.)
            if (doclet) {
                result = doclet.longname || doclet.name;
            }
        }
    }

    return result;
};

// TODO: docs
/**
 * Resolve what "this" refers to relative to a node.
 * @param {node} node - The "this" node
 * @returns {string} The longname of the enclosing node.
 */
Parser.prototype.resolveThis = function(node) {
    var doclet;
    var result;

    // In general, if there's an enclosing scope, we use the enclosing scope to resolve `this`.
    // For object properties, we use the node's parent (the object) instead.
    if (node.type !== Syntax.Property && node.enclosingScope) {
        doclet = this._getDoclet(node.enclosingScope.nodeId);

        if (!doclet) {
            result = jsdoc.name.LONGNAMES.ANONYMOUS; // TODO handle global this?
        }
        else if (doclet.this) {
            result = doclet.this;
        }
        // like: Foo.constructor = function(n) { /** blah */ this.name = n; }
        else if (doclet.kind === 'function' && doclet.memberof) {
            result = doclet.memberof;
        }
        // like: var foo = function(n) { /** blah */ this.bar = n; }
        else if ( doclet.kind === 'member' && jsdoc.src.astnode.isAssignment(node) ) {
            result = doclet.longname || doclet.name;
        }
        // walk up to the closest class we can find
        else if (doclet.kind === 'class' || doclet.kind === 'module') {
            result = doclet.longname || doclet.name;
        }
        else if (node.enclosingScope) {
            result = this.resolveThis(node.enclosingScope);
        }
    }
    else if (node.parent) {
        doclet = this.refs[node.parent.nodeId];

        // TODO: is this behavior correct? when do we get here?
        if (!doclet) {
            result = ''; // global?
        }
        else {
            result = doclet.longname || doclet.name;
        }
    }
    // TODO: is this behavior correct? when do we get here?
    else {
        result = ''; // global?
    }

    return result;
};

/**
 * Given an AST node representing an object property, find the doclets for the parent object or
 * objects.
 *
 * If the object is part of a simple assignment (for example, `var foo = { x: 1 }`), this method
 * returns a single doclet (in this case, the doclet for `foo`).
 *
 * If the object is part of a chained assignment (for example, `var foo = exports.FOO = { x: 1 }`,
 * this method returns multiple doclets (in this case, the doclets for `foo` and `exports.FOO`).
 *
 * @param {Object} node - An AST node representing an object property.
 * @return {Array.<jsdoc/doclet.Doclet>} An array of doclets for the parent object or objects, or
 * an empty array if no doclets are found.
 */
Parser.prototype.resolvePropertyParents = function(node) {
    var currentAncestor = node.parent;
    var nextAncestor = currentAncestor ? currentAncestor.parent : null;
    var doclet;
    var doclets = [];

    while (currentAncestor) {
        doclet = this._getDoclet(currentAncestor.nodeId);
        if (doclet) {
            doclets.push(doclet);
        }

        // if the next ancestor is an assignment expression (for example, `exports.FOO` in
        // `var foo = exports.FOO = { x: 1 }`, keep walking upwards
        if (nextAncestor && nextAncestor.type === Syntax.AssignmentExpression) {
            nextAncestor = nextAncestor.parent;
            currentAncestor = currentAncestor.parent;
        }
        // otherwise, we're done
        else {
            currentAncestor = null;
        }
    }

    return doclets;
};

// TODO: docs
/**
 * Resolve what function a var is limited to.
 * @param {astnode} node
 * @param {string} basename The leftmost name in the long name: in foo.bar.zip the basename is foo.
 */
Parser.prototype.resolveVar = function(node, basename) {
    var doclet;
    var result;
    var scope = node.enclosingScope;

    // HACK: return an empty string for function declarations so they don't end up in anonymous
    // scope (see #685 and #693)
    if (node.type === Syntax.FunctionDeclaration) {
        result = '';
    }
    else if (!scope) {
        result = ''; // global
    }
    else {
        doclet = this._getDoclet(scope.nodeId);
        if ( definedInScope(doclet, basename) ) {
            result = doclet.longname;
        }
        else {
            result = this.resolveVar(scope, basename);
        }
    }

    return result;
};

// TODO: docs
Parser.prototype.resolveEnum = function(e) {
    var doclets = this.resolvePropertyParents(e.code.node.parent);

    doclets.forEach(function(doclet) {
        if (doclet && doclet.isEnum) {
            doclet.properties = doclet.properties || [];

            // members of an enum inherit the enum's type
            if (doclet.type && !e.doclet.type) {
                // clone the type to prevent circular refs
                e.doclet.type = jsdoc.util.doop(doclet.type);
            }

            delete e.doclet.undocumented;
            e.doclet.defaultvalue = e.doclet.meta.code.value;

            // add the doclet to the parent's properties
            doclet.properties.push(e.doclet);
        }
    });
};

// TODO: document other events
/**
 * Fired once for each JSDoc comment in the current source code.
 * @event jsdocCommentFound
 * @memberof module:jsdoc/src/parser.Parser
 * @type {Object}
 * @property {string} comment The text content of the JSDoc comment
 * @property {number} lineno The line number associated with the found comment.
 * @property {string} filename The file name associated with the found comment.
 */
