text: 'use strict';
  
  Object.defineProperty(exports, "__esModule", {
    value: true
  });
  
  var _colorLogger = require('color-logger');
  
  var _colorLogger2 = _interopRequireDefault(_colorLogger);
  
  var _CommentParser = require('../Parser/CommentParser.js');
  
  var _CommentParser2 = _interopRequireDefault(_CommentParser);
  
  var _FileDoc = require('../Doc/FileDoc.js');
  
  var _FileDoc2 = _interopRequireDefault(_FileDoc);
  
  var _ClassDoc = require('../Doc/ClassDoc.js');
  
  var _ClassDoc2 = _interopRequireDefault(_ClassDoc);
  
  var _MethodDoc = require('../Doc/MethodDoc.js');
  
  var _MethodDoc2 = _interopRequireDefault(_MethodDoc);
  
  var _ClassPropertyDoc = require('../Doc/ClassPropertyDoc');
  
  var _ClassPropertyDoc2 = _interopRequireDefault(_ClassPropertyDoc);
  
  var _MemberDoc = require('../Doc/MemberDoc.js');
  
  var _MemberDoc2 = _interopRequireDefault(_MemberDoc);
  
  var _FunctionDoc = require('../Doc/FunctionDoc.js');
  
  var _FunctionDoc2 = _interopRequireDefault(_FunctionDoc);
  
  var _VariableDoc = require('../Doc/VariableDoc.js');
  
  var _VariableDoc2 = _interopRequireDefault(_VariableDoc);
  
  var _AssignmentDoc = require('../Doc/AssignmentDoc.js');
  
  var _AssignmentDoc2 = _interopRequireDefault(_AssignmentDoc);
  
  var _TypedefDoc = require('../Doc/TypedefDoc.js');
  
  var _TypedefDoc2 = _interopRequireDefault(_TypedefDoc);
  
  var _ExternalDoc = require('../Doc/ExternalDoc.js');
  
  var _ExternalDoc2 = _interopRequireDefault(_ExternalDoc);
  
  var _ASTUtil = require('../Util/ASTUtil.js');
  
  var _ASTUtil2 = _interopRequireDefault(_ASTUtil);
  
  function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
  
  const already = Symbol('already');
  const logger = new _colorLogger2.default('DocFactory');
  
  /**
   * Doc factory class.
   *
   * @example
   * let factory = new DocFactory(ast, pathResolver);
   * factory.push(node, parentNode);
   * let results = factory.results;
   */
  class DocFactory {
    /**
     * @type {DocObject[]}
     */
    get results() {
      return [...this._results];
    }
  
    /**
     * create instance.
     * @param {AST} ast - AST of source code.
     * @param {PathResolver} pathResolver - path resolver of source code.
     */
    constructor(ast, pathResolver) {
      this._ast = ast;
      this._pathResolver = pathResolver;
      this._results = [];
      this._processedClassNodes = [];
  
      this._inspectExportDefaultDeclaration();
      this._inspectExportNamedDeclaration();
  
      // file doc
      const doc = new _FileDoc2.default(ast, ast, pathResolver, []);
      this._results.push(doc.value);
  
      // ast does not child, so only comment.
      if (ast.program.body.length === 0 && ast.program.innerComments) {
        const results = this._traverseComments(ast, null, ast.program.innerComments);
        this._results.push(...results);
      }
    }
  
    /**
     * inspect ExportDefaultDeclaration.
     *
     * case1: separated export
     *
     * ```javascript
     * class Foo {}
     * export default Foo;
     * ```
     *
     * case2: export instance(directly).
     *
     * ```javascript
     * class Foo {}
     * export default new Foo();
     * ```
     *
     * case3: export instance(indirectly).
     *
     * ```javascript
     * class Foo {}
     * let foo = new Foo();
     * export default foo;
     * ```
     *
     * @private
     * @todo support function export.
     */
    _inspectExportDefaultDeclaration() {
      const pseudoExportNodes = [];
  
      for (const exportNode of this._ast.program.body) {
        if (exportNode.type !== 'ExportDefaultDeclaration') continue;
  
        let targetClassName = null;
        let targetVariableName = null;
        let pseudoClassExport;
  
        switch (exportNode.declaration.type) {
          case 'NewExpression':
            if (exportNode.declaration.callee.type === 'Identifier') {
              targetClassName = exportNode.declaration.callee.name;
            } else if (exportNode.declaration.callee.type === 'MemberExpression') {
              targetClassName = exportNode.declaration.callee.property.name;
            } else {
              targetClassName = '';
            }
            targetVariableName = targetClassName.replace(/^./, c => c.toLowerCase());
            pseudoClassExport = true;
            break;
          case 'Identifier':
            {
              const varNode = _ASTUtil2.default.findVariableDeclarationAndNewExpressionNode(exportNode.declaration.name, this._ast);
              if (varNode) {
                targetClassName = varNode.declarations[0].init.callee.name;
                targetVariableName = exportNode.declaration.name;
                pseudoClassExport = true;
                _ASTUtil2.default.sanitize(varNode);
              } else {
                targetClassName = exportNode.declaration.name;
                pseudoClassExport = false;
              }
              break;
            }
          default:
            logger.w(`unknown export declaration type. type = "${ exportNode.declaration.type }"`);
            break;
        }
  
        const { classNode, exported } = _ASTUtil2.default.findClassDeclarationNode(targetClassName, this._ast);
        if (classNode) {
          if (!exported) {
            const pseudoExportNode1 = this._copy(exportNode);
            pseudoExportNode1.declaration = this._copy(classNode);
            pseudoExportNode1.leadingComments = null;
            pseudoExportNode1.declaration.__PseudoExport__ = pseudoClassExport;
            pseudoExportNodes.push(pseudoExportNode1);
            _ASTUtil2.default.sanitize(classNode);
          }
  
          if (targetVariableName) {
            const pseudoExportNode2 = this._copy(exportNode);
            pseudoExportNode2.declaration = _ASTUtil2.default.createVariableDeclarationAndNewExpressionNode(targetVariableName, targetClassName, exportNode.loc);
            pseudoExportNodes.push(pseudoExportNode2);
          }
  
          _ASTUtil2.default.sanitize(exportNode);
        }
  
        const functionNode = _ASTUtil2.default.findFunctionDeclarationNode(exportNode.declaration.name, this._ast);
        if (functionNode) {
          const pseudoExportNode = this._copy(exportNode);
          pseudoExportNode.declaration = this._copy(functionNode);
          _ASTUtil2.default.sanitize(exportNode);
          _ASTUtil2.default.sanitize(functionNode);
          pseudoExportNodes.push(pseudoExportNode);
        }
  
        const variableNode = _ASTUtil2.default.findVariableDeclarationNode(exportNode.declaration.name, this._ast);
        if (variableNode) {
          const pseudoExportNode = this._copy(exportNode);
          pseudoExportNode.declaration = this._copy(variableNode);
          _ASTUtil2.default.sanitize(exportNode);
          _ASTUtil2.default.sanitize(variableNode);
          pseudoExportNodes.push(pseudoExportNode);
        }
      }
  
      this._ast.program.body.push(...pseudoExportNodes);
    }
  
    /* eslint-disable max-statements */
    /**
     * inspect ExportNamedDeclaration.
     *
     * case1: separated export
     *
     * ```javascript
     * class Foo {}
     * export {Foo};
     * ```
     *
     * case2: export instance(indirectly).
     *
     * ```javascript
     * class Foo {}
     * let foo = new Foo();
     * export {foo};
     * ```
     *
     * @private
     * @todo support function export.
     */
    _inspectExportNamedDeclaration() {
      const pseudoExportNodes = [];
  
      for (const exportNode of this._ast.program.body) {
        if (exportNode.type !== 'ExportNamedDeclaration') continue;
  
        if (exportNode.declaration && exportNode.declaration.type === 'VariableDeclaration') {
          for (const declaration of exportNode.declaration.declarations) {
            if (!declaration.init || declaration.init.type !== 'NewExpression') continue;
  
            const { classNode, exported } = _ASTUtil2.default.findClassDeclarationNode(declaration.init.callee.name, this._ast);
            if (classNode && !exported) {
              const pseudoExportNode = this._copy(exportNode);
              pseudoExportNode.declaration = this._copy(classNode);
              pseudoExportNode.leadingComments = null;
              pseudoExportNodes.push(pseudoExportNode);
              pseudoExportNode.declaration.__PseudoExport__ = true;
              _ASTUtil2.default.sanitize(classNode);
            }
          }
          continue;
        }
  
        for (const specifier of exportNode.specifiers) {
          if (specifier.type !== 'ExportSpecifier') continue;
  
          let targetClassName = null;
          let pseudoClassExport;
  
          const varNode = _ASTUtil2.default.findVariableDeclarationAndNewExpressionNode(specifier.exported.name, this._ast);
          if (varNode) {
            targetClassName = varNode.declarations[0].init.callee.name;
            pseudoClassExport = true;
  
            const pseudoExportNode = this._copy(exportNode);
            pseudoExportNode.declaration = this._copy(varNode);
            pseudoExportNode.specifiers = null;
            pseudoExportNodes.push(pseudoExportNode);
  
            _ASTUtil2.default.sanitize(varNode);
          } else {
            targetClassName = specifier.exported.name;
            pseudoClassExport = false;
          }
  
          const { classNode, exported } = _ASTUtil2.default.findClassDeclarationNode(targetClassName, this._ast);
          if (classNode && !exported) {
            const pseudoExportNode = this._copy(exportNode);
            pseudoExportNode.declaration = this._copy(classNode);
            pseudoExportNode.leadingComments = null;
            pseudoExportNode.specifiers = null;
            pseudoExportNode.declaration.__PseudoExport__ = pseudoClassExport;
            pseudoExportNodes.push(pseudoExportNode);
            _ASTUtil2.default.sanitize(classNode);
          }
  
          const functionNode = _ASTUtil2.default.findFunctionDeclarationNode(specifier.exported.name, this._ast);
          if (functionNode) {
            const pseudoExportNode = this._copy(exportNode);
            pseudoExportNode.declaration = this._copy(functionNode);
            pseudoExportNode.leadingComments = null;
            pseudoExportNode.specifiers = null;
            _ASTUtil2.default.sanitize(functionNode);
            pseudoExportNodes.push(pseudoExportNode);
          }
  
          const variableNode = _ASTUtil2.default.findVariableDeclarationNode(specifier.exported.name, this._ast);
          if (variableNode) {
            const pseudoExportNode = this._copy(exportNode);
            pseudoExportNode.declaration = this._copy(variableNode);
            pseudoExportNode.leadingComments = null;
            pseudoExportNode.specifiers = null;
            _ASTUtil2.default.sanitize(variableNode);
            pseudoExportNodes.push(pseudoExportNode);
          }
        }
      }
  
      this._ast.program.body.push(...pseudoExportNodes);
    }
  
    /**
     * push node, and factory processes node.
     * @param {ASTNode} node - target node.
     * @param {ASTNode} parentNode - parent node of target node.
     */
    push(node, parentNode) {
      if (node === this._ast) return;
  
      if (node[already]) return;
  
      const isLastNodeInParent = this._isLastNodeInParent(node, parentNode);
  
      node[already] = true;
      Reflect.defineProperty(node, 'parent', { value: parentNode });
  
      // unwrap export declaration
      if (['ExportDefaultDeclaration', 'ExportNamedDeclaration'].includes(node.type)) {
        parentNode = node;
        node = this._unwrapExportDeclaration(node);
        if (!node) return;
        node[already] = true;
        Reflect.defineProperty(node, 'parent', { value: parentNode });
      }
  
      // if node has decorators, leading comments is attached to decorators.
      if (node.decorators && node.decorators[0].leadingComments) {
        if (!node.leadingComments || !node.leadingComments.length) {
          node.leadingComments = node.decorators[0].leadingComments;
        }
      }
  
      let results;
      results = this._traverseComments(parentNode, node, node.leadingComments);
      this._results.push(...results);
  
      // for trailing comments.
      // traverse with only last node, because prevent duplication of trailing comments.
      if (node.trailingComments && isLastNodeInParent) {
        results = this._traverseComments(parentNode, null, node.trailingComments);
        this._results.push(...results);
      }
    }
  
    /**
     * traverse comments of node, and create doc object.
     * @param {ASTNode|AST} parentNode - parent of target node.
     * @param {?ASTNode} node - target node.
     * @param {ASTNode[]} comments - comment nodes.
     * @returns {DocObject[]} created doc objects.
     * @private
     */
    _traverseComments(parentNode, node, comments) {
      if (!node) {
        const virtualNode = {};
        Reflect.defineProperty(virtualNode, 'parent', { value: parentNode });
        node = virtualNode;
      }
  
      if (comments && comments.length) {
        const temp = [];
        for (const comment of comments) {
          if (_CommentParser2.default.isESDoc(comment)) temp.push(comment);
        }
        comments = temp;
      } else {
        comments = [];
      }
  
      if (comments.length === 0) {
        comments = [{ type: 'CommentBlock', value: '* @undocument' }];
      }
  
      const results = [];
      const lastComment = comments[comments.length - 1];
      for (const comment of comments) {
        const tags = _CommentParser2.default.parse(comment);
  
        let doc;
        if (comment === lastComment) {
          doc = this._createDoc(node, tags);
        } else {
          const virtualNode = {};
          Reflect.defineProperty(virtualNode, 'parent', { value: parentNode });
          doc = this._createDoc(virtualNode, tags);
        }
  
        if (doc) results.push(doc.value);
      }
  
      return results;
    }
  
    /**
     * create Doc.
     * @param {ASTNode} node - target node.
     * @param {Tag[]} tags - tags of target node.
     * @returns {AbstractDoc} created Doc.
     * @private
     */
    _createDoc(node, tags) {
      const result = this._decideType(tags, node);
      const type = result.type;
      node = result.node;
  
      if (!type) return null;
  
      if (type === 'Class') {
        this._processedClassNodes.push(node);
      }
  
      let Clazz;
      /* eslint-disable max-statements-per-line */
      switch (type) {
        case 'Class':
          Clazz = _ClassDoc2.default;break;
        case 'Method':
          Clazz = _MethodDoc2.default;break;
        case 'ClassProperty':
          Clazz = _ClassPropertyDoc2.default;break;
        case 'Member':
          Clazz = _MemberDoc2.default;break;
        case 'Function':
          Clazz = _FunctionDoc2.default;break;
        case 'Variable':
          Clazz = _VariableDoc2.default;break;
        case 'Assignment':
          Clazz = _AssignmentDoc2.default;break;
        case 'Typedef':
          Clazz = _TypedefDoc2.default;break;
        case 'External':
          Clazz = _ExternalDoc2.default;break;
        default:
          throw new Error(`unexpected type: ${ type }`);
      }
  
      if (!Clazz) return null;
      if (!node.type) node.type = type;
  
      return new Clazz(this._ast, node, this._pathResolver, tags);
    }
  
    /**
     * decide Doc type by using tags and node.
     * @param {Tag[]} tags - tags of node.
     * @param {ASTNode} node - target node.
     * @returns {{type: ?string, node: ?ASTNode}} decided type.
     * @private
     */
    _decideType(tags, node) {
      let type = null;
      for (const tag of tags) {
        const tagName = tag.tagName;
        /* eslint-disable default-case */
        switch (tagName) {
          case '@typedef':
            type = 'Typedef';break;
          case '@external':
            type = 'External';break;
        }
      }
  
      if (type) return { type, node };
  
      if (!node) return { type, node };
  
      /* eslint-disable default-case */
      switch (node.type) {
        case 'ClassDeclaration':
          return this._decideClassDeclarationType(node);
        case 'ClassMethod':
          return this._decideMethodDefinitionType(node);
        case 'ClassProperty':
          return this._decideClassPropertyType(node);
        case 'ExpressionStatement':
          return this._decideExpressionStatementType(node);
        case 'FunctionDeclaration':
          return this._decideFunctionDeclarationType(node);
        case 'FunctionExpression':
          return this._decideFunctionExpressionType(node);
        case 'VariableDeclaration':
          return this._decideVariableType(node);
        case 'AssignmentExpression':
          return this._decideAssignmentType(node);
        case 'ArrowFunctionExpression':
          return this._decideArrowFunctionExpressionType(node);
      }
  
      return { type: null, node: null };
    }
  
    /**
     * decide Doc type from class declaration node.
     * @param {ASTNode} node - target node that is class declaration node.
     * @returns {{type: string, node: ASTNode}} decided type.
     * @private
     */
    _decideClassDeclarationType(node) {
      if (!this._isTopDepthInBody(node, this._ast.program.body)) return { type: null, node: null };
  
      return { type: 'Class', node: node };
    }
  
    /**
     * decide Doc type from method definition node.
     * @param {ASTNode} node - target node that is method definition node.
     * @returns {{type: ?string, node: ?ASTNode}} decided type.
     * @private
     */
    _decideMethodDefinitionType(node) {
      const classNode = this._findUp(node, ['ClassDeclaration', 'ClassExpression']);
      if (this._processedClassNodes.includes(classNode)) {
        return { type: 'Method', node: node };
      } else {
        logger.w('this method is not in class', node);
        return { type: null, node: null };
      }
    }
  
    /**
     * decide Doc type from class property node.
     * @param {ASTNode} node - target node that is classs property node.
     * @returns {{type: ?string, node: ?ASTNode}} decided type.
     * @private
     */
    _decideClassPropertyType(node) {
      const classNode = this._findUp(node, ['ClassDeclaration', 'ClassExpression']);
      if (this._processedClassNodes.includes(classNode)) {
        return { type: 'ClassProperty', node: node };
      } else {
        logger.w('this class property is not in class', node);
        return { type: null, node: null };
      }
    }
  
    /**
     * decide Doc type from function declaration node.
     * @param {ASTNode} node - target node that is function declaration node.
     * @returns {{type: string, node: ASTNode}} decided type.
     * @private
     */
    _decideFunctionDeclarationType(node) {
      if (!this._isTopDepthInBody(node, this._ast.program.body)) return { type: null, node: null };
  
      return { type: 'Function', node: node };
    }
  
    /**
     * decide Doc type from function expression node.
     * babylon 6.11.2 judges`export default async function foo(){}` to be `FunctionExpression`.
     * I expect `FunctionDeclaration`. this behavior may be bug of babylon.
     * for now, workaround for it with this method.
     * @param {ASTNode} node - target node that is function expression node.
     * @returns {{type: string, node: ASTNode}} decided type.
     * @private
     * @todo inspect with newer babylon.
     */
    _decideFunctionExpressionType(node) {
      if (!node.async) return { type: null, node: null };
      if (!this._isTopDepthInBody(node, this._ast.program.body)) return { type: null, node: null };
  
      return { type: 'Function', node: node };
    }
  
    /**
     * decide Doc type from arrow function expression node.
     * @param {ASTNode} node - target node that is arrow function expression node.
     * @returns {{type: string, node: ASTNode}} decided type.
     * @private
     */
    _decideArrowFunctionExpressionType(node) {
      if (!this._isTopDepthInBody(node, this._ast.program.body)) return { type: null, node: null };
  
      return { type: 'Function', node: node };
    }
  
    /**
     * decide Doc type from expression statement node.
     * @param {ASTNode} node - target node that is expression statement node.
     * @returns {{type: ?string, node: ?ASTNode}} decided type.
     * @private
     */
    _decideExpressionStatementType(node) {
      const isTop = this._isTopDepthInBody(node, this._ast.program.body);
      Reflect.defineProperty(node.expression, 'parent', { value: node });
      node = node.expression;
      node[already] = true;
  
      let innerType;
      let innerNode;
  
      if (!node.right) return { type: null, node: null };
  
      switch (node.right.type) {
        case 'FunctionExpression':
          innerType = 'Function';
          break;
        case 'ClassExpression':
          innerType = 'Class';
          break;
        default:
          if (node.left.type === 'MemberExpression' && node.left.object.type === 'ThisExpression') {
            const classNode = this._findUp(node, ['ClassExpression', 'ClassDeclaration']);
            if (!this._processedClassNodes.includes(classNode)) {
              logger.w('this member is not in class.', this._pathResolver.filePath, node);
              return { type: null, node: null };
            }
  
            return { type: 'Member', node: node };
          } else {
            return { type: null, node: null };
          }
      }
  
      if (!isTop) return { type: null, node: null };
  
      /* eslint-disable prefer-const */
      innerNode = node.right;
      innerNode.id = this._copy(node.left.id || node.left.property);
      Reflect.defineProperty(innerNode, 'parent', { value: node });
      innerNode[already] = true;
  
      return { type: innerType, node: innerNode };
    }
  
    /**
     * decide Doc type from variable node.
     * @param {ASTNode} node - target node that is variable node.
     * @returns {{type: string, node: ASTNode}} decided type.
     * @private
     */
    _decideVariableType(node) {
      if (!this._isTopDepthInBody(node, this._ast.program.body)) return { type: null, node: null };
  
      let innerType = null;
      let innerNode = null;
  
      if (!node.declarations[0].init) return { type: innerType, node: innerNode };
  
      switch (node.declarations[0].init.type) {
        case 'FunctionExpression':
          innerType = 'Function';
          break;
        case 'ClassExpression':
          innerType = 'Class';
          break;
        case 'ArrowFunctionExpression':
          innerType = 'Function';
          break;
        default:
          return { type: 'Variable', node: node };
      }
  
      innerNode = node.declarations[0].init;
      innerNode.id = this._copy(node.declarations[0].id);
      Reflect.defineProperty(innerNode, 'parent', { value: node });
      innerNode[already] = true;
  
      return { type: innerType, node: innerNode };
    }
  
    /**
     * decide Doc type from assignment node.
     * @param {ASTNode} node - target node that is assignment node.
     * @returns {{type: string, node: ASTNode}} decided type.
     * @private
     */
    _decideAssignmentType(node) {
      if (!this._isTopDepthInBody(node, this._ast.program.body)) return { type: null, node: null };
  
      let innerType;
      let innerNode;
  
      switch (node.right.type) {
        case 'FunctionExpression':
          innerType = 'Function';
          break;
        case 'ClassExpression':
          innerType = 'Class';
          break;
        default:
          return { type: 'Assignment', node: node };
      }
  
      /* eslint-disable prefer-const */
      innerNode = node.right;
      innerNode.id = this._copy(node.left.id || node.left.property);
      Reflect.defineProperty(innerNode, 'parent', { value: node });
      innerNode[already] = true;
  
      return { type: innerType, node: innerNode };
    }
  
    /**
     * unwrap exported node.
     * @param {ASTNode} node - target node that is export declaration node.
     * @returns {ASTNode|null} unwrapped child node of exported node.
     * @private
     */
    _unwrapExportDeclaration(node) {
      // e.g. `export A from './A.js'` has not declaration
      if (!node.declaration) return null;
  
      const exportedASTNode = node.declaration;
      if (!exportedASTNode.leadingComments) exportedASTNode.leadingComments = [];
      exportedASTNode.leadingComments.push(...(node.leadingComments || []));
  
      if (!exportedASTNode.trailingComments) exportedASTNode.trailingComments = [];
      exportedASTNode.trailingComments.push(...(node.trailingComments || []));
  
      return exportedASTNode;
    }
  
    /**
     * judge node is last in parent.
     * @param {ASTNode} node - target node.
     * @param {ASTNode} parentNode - target parent node.
     * @returns {boolean} if true, the node is last in parent.
     * @private
     */
    _isLastNodeInParent(node, parentNode) {
      if (parentNode && parentNode.body) {
        const lastNode = parentNode.body[parentNode.body.length - 1];
        return node === lastNode;
      }
  
      return false;
    }
  
    /**
     * judge node is top in body.
     * @param {ASTNode} node - target node.
     * @param {ASTNode[]} body - target body node.
     * @returns {boolean} if true, the node is top in body.
     * @private
     */
    _isTopDepthInBody(node, body) {
      if (!body) return false;
      if (!Array.isArray(body)) return false;
  
      const parentNode = node.parent;
      if (['ExportDefaultDeclaration', 'ExportNamedDeclaration'].includes(parentNode.type)) {
        node = parentNode;
      }
  
      for (const _node of body) {
        if (node === _node) return true;
      }
      return false;
    }
  
    /**
     * deep copy object.
     * @param {Object} obj - target object.
     * @return {Object} copied object.
     * @private
     */
    _copy(obj) {
      return JSON.parse(JSON.stringify(obj));
    }
  
    /**
     * find node while goes up.
     * @param {ASTNode} node - start node.
     * @param {string[]} types - ASTNode types.
     * @returns {ASTNode|null} found first node.
     * @private
     */
    _findUp(node, types) {
      let parent = node.parent;
      while (parent) {
        if (types.includes(parent.type)) return parent;
        parent = parent.parent;
      }
  
      return null;
    }
  }
  exports.default = DocFactory;