'use strict';

Object.defineProperty(exports, "__esModule", {
  value: true
});
exports.default = parse;

var _doctrine;

function _load_doctrine() {
  return _doctrine = _interopRequireDefault(require('doctrine'));
}

var _exampleTagParser;

function _load_exampleTagParser() {
  return _exampleTagParser = _interopRequireDefault(require('./example-tag-parser'));
}

var _utils;

function _load_utils() {
  return _utils = require('../utils');
}

function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }

var docgen = require('react-docgen');


function formatTag(tag) {
  delete tag.title;
  if (!tag.type) return tag;
  var formattedType = (_doctrine || _load_doctrine()).default.type.stringify(tag.type);
  if (tag.type.type === (_doctrine || _load_doctrine()).default.type.Syntax.TypeApplication) {
    // Doctrine adds a dot after the generic type for historical reasons.
    // see here for more info: https://github.com/eslint/doctrine/issues/185
    formattedType = formattedType.replace('.<', '<');
  }
  if (tag.type.type === (_doctrine || _load_doctrine()).default.type.Syntax.OptionalType) {
    // Doctrine shows an optional type with a suffix `=` (e.g. `string=`), we prefer the more
    // common syntax `?` (e.g. `string?`)
    formattedType = formattedType.replace('=', '?');
  }
  tag.type = formattedType;

  return tag;
}

function extractDataRegex(doc, doclets, filePath) {
  var commentsAst = (_doctrine || _load_doctrine()).default.parse(doc.trim(), { unwrap: true, recoverable: true, sloppy: true });
  if (!commentsAst) return;

  var args = [];
  var description = commentsAst.description;
  var returns = {};
  var isStatic = false;
  var access = 'public';
  var examples = [];
  var properties = [];
  var name = '';
  var render = '';

  commentsAst.tags.forEach(function (tag) {
    switch (tag.title) {
      case 'desc':
      case 'description':
        description = tag.description;
        break;
      case 'name':
        name = tag.name;
        break;
      case 'param':
      case 'arg':
      case 'argument':
        args.push(formatTag(tag));
        break;
      case 'returns':
      case 'return':
        returns = formatTag(tag);
        break;
      case 'static':
        isStatic = true;
        break;
      case 'private':
      case 'protected':
        access = tag.title;
        break;
      case 'access':
        access = tag.access;
        break;
      case 'example':
        examples.push((0, (_exampleTagParser || _load_exampleTagParser()).default)(tag.description));
        break;
      case 'property':
        properties.push(formatTag(tag));
        break;
      case 'render':
        render = tag.description;
        break;
      default:
        break;
    }
  });

  var doclet = {
    name, // todo: find the function/method name by regex
    description,
    args,
    returns,
    access,
    examples,
    render,
    properties,
    static: isStatic,
    filePath: (0, (_utils || _load_utils()).pathNormalizeToLinux)(filePath)
  };
  doclets.push(doclet);
}

function formatProperties(props) {
  var parseDescription = function parseDescription(description) {
    // an extra step is needed to parse the properties description correctly. without this step
    // it'd show the entire tag, e.g. `@property {propTypes.string} text - Button text.`
    // instead of just `text - Button text.`.
    try {
      var descriptionAST = (_doctrine || _load_doctrine()).default.parse(description, { unwrap: true, recoverable: true, sloppy: true });
      if (descriptionAST && descriptionAST.tags[0]) return descriptionAST.tags[0].description;
    } catch (err) {
      // failed to parse the react property, that's fine, it'll return the original description
    }
    return description;
  };
  return Object.keys(props).map(function (name) {
    var _props$name = props[name],
        type = _props$name.type,
        description = _props$name.description,
        required = _props$name.required,
        defaultValue = _props$name.defaultValue,
        flowType = _props$name.flowType;


    return {
      name,
      description: parseDescription(description),
      required,
      type: stringifyType(type || flowType),
      defaultValue
    };
  });
}

function formatMethods(methods) {
  return Object.keys(methods).map(function (key) {
    var _methods$key = methods[key],
        returns = _methods$key.returns,
        modifiers = _methods$key.modifiers,
        params = _methods$key.params,
        docblock = _methods$key.docblock,
        name = _methods$key.name;

    return {
      name,
      description: docblock,
      returns,
      modifiers,
      params
    };
  });
}

function fromReactDocs(_ref, filePath) {
  var description = _ref.description,
      displayName = _ref.displayName,
      props = _ref.props,
      methods = _ref.methods;

  return {
    filePath: (0, (_utils || _load_utils()).pathNormalizeToLinux)(filePath),
    name: displayName,
    description,
    properties: formatProperties(props),
    access: 'public',
    methods: formatMethods(methods)
  };
}

function stringifyType(prop) {
  var name = prop.name;

  var transformed = void 0;

  switch (name) {
    default:
      transformed = name;
      break;
    case 'func':
      transformed = 'function';
      break;
    case 'shape':
      transformed = JSON.stringify(Object.keys(prop.value).reduce(function () {
        var acc = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
        var current = arguments[1];

        acc[current] = stringifyType(prop.value[current]);
        return acc;
      }, {}));
      break;
    case 'enum':
      transformed = prop.value.map(function (enumProp) {
        return enumProp.value;
      }).join(' | ');
      break;
    case 'instanceOf':
      transformed = prop.value;
      break;
    case 'union':
      transformed = prop.value.map(function (prop) {
        return stringifyType(prop);
      }).join(' | ');
      break;
    case 'arrayOf':
      transformed = `${stringifyType(prop.value)}[]`;
      break;
  }

  return transformed;
}

function parse(data, filePath) {
  var doclets = [];
  try {
    var reactDocs = docgen.parse(data);
    if (reactDocs) {
      var formatted = fromReactDocs(reactDocs, filePath);
      formatted.args = [];
      // this is a workaround to get the 'example' tag parsed when using react-docs
      // because as of now Docgen doesn't parse @example tag, instead, it shows it inside
      // the @description tag.
      extractDataRegex(formatted.description, doclets, filePath);
      formatted.description = doclets[0].description;
      formatted.examples = doclets[0].examples;
      return formatted;
    }
  } catch (err) {}

  try {
    /**
     * [ \t]*  => can start with any number of tabs
     * \/\*\*  => must start with exact `/**`
     * \s*     => may follow with any number of white spaces
     * [^*]*   => anything except star may repeat
     * ([^\*]|(\*(?!\/)))* => may follow with stars as long as there is no "/" after the star
     * \*\/ => must finish with a star and then \.
     * This was taken as a combination of:
     * https://stackoverflow.com/questions/35905181/regex-for-jsdoc-comments
     * https://github.com/neogeek/jsdoc-regex/blob/master/index.js
     */
    var jsdocRegex = /[ \t]*\/\*\*\s*\n([^*]|(\*(?!\/)))*\*\//g;
    var docs = data.match(jsdocRegex);
    // populate doclets array
    docs.forEach(function (doc) {
      return extractDataRegex(doc, doclets, filePath);
    });
  } catch (e) {
    // never mind, ignore the doc of this source
  }

  return doclets.filter(function (doclet) {
    return doclet.access === 'public';
  });
}