"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.parseComment = exports.getRawComment = exports.getJsDocCommentText = void 0;
const ts = require("typescript");
const index_1 = require("../../models/comments/index");
/**
 * Check whether the given module declaration is the topmost.
 *
 * This function returns TRUE if there is no trailing module defined, in
 * the following example this would be the case only for module <code>C</code>.
 *
 * ```
 * module A.B.C { }
 * ```
 *
 * @param node  The module definition that should be tested.
 * @return TRUE if the given node is the topmost module declaration, FALSE otherwise.
 */
function isTopmostModuleDeclaration(node) {
    return node.getChildren().some(ts.isModuleBlock);
}
/**
 * Return the root module declaration of the given module declaration.
 *
 * In the following example this function would always return module
 * <code>A</code> no matter which of the modules was passed in.
 *
 * ```
 * module A.B.C { }
 * ```
 */
function getRootModuleDeclaration(node) {
    while (node.parent &&
        node.parent.kind === ts.SyntaxKind.ModuleDeclaration) {
        const parent = node.parent;
        if (node.name.pos === parent.name.end + 1) {
            node = parent;
        }
        else {
            break;
        }
    }
    return node;
}
/**
 * Derived from the internal ts utility
 * https://github.com/Microsoft/TypeScript/blob/v3.2.2/src/compiler/utilities.ts#L954
 * @param node
 * @param text
 */
function getJSDocCommentRanges(node, text) {
    var _a, _b;
    const hasTrailingCommentRanges = [
        ts.SyntaxKind.Parameter,
        ts.SyntaxKind.FunctionExpression,
        ts.SyntaxKind.ArrowFunction,
        ts.SyntaxKind.ParenthesizedExpression,
    ].includes(node.kind);
    let commentRanges = (_a = ts.getLeadingCommentRanges(text, node.pos)) !== null && _a !== void 0 ? _a : [];
    if (hasTrailingCommentRanges) {
        commentRanges = ((_b = ts.getTrailingCommentRanges(text, node.pos)) !== null && _b !== void 0 ? _b : []).concat(commentRanges);
    }
    // True if the comment starts with '/**' but not if it is '/**/'
    return commentRanges.filter(({ pos }) => text.substr(pos, 3) === "/**" && text[pos + 4] !== "/");
}
function getJsDocCommentText(comment) {
    if (typeof comment === "string") {
        return comment;
    }
    return comment === null || comment === void 0 ? void 0 : comment.map((val) => val.text).join("");
}
exports.getJsDocCommentText = getJsDocCommentText;
/**
 * Return the raw comment string for the given node.
 *
 * @param node  The node whose comment should be resolved.
 * @returns     The raw comment string or undefined if no comment could be found.
 */
function getRawComment(node, logger) {
    var _a, _b;
    // This happens if we are converting a JS project that has @typedef "interfaces"
    // with an @property tag, a @typedef type alias, a callback with parameters, etc.
    if (ts.isJSDocTypedefTag(node) ||
        ts.isJSDocPropertyTag(node) ||
        ts.isJSDocParameterTag(node) ||
        ts.isJSDocCallbackTag(node)) {
        // Also strip off leading dashes:
        // @property {string} name - docs
        return (_a = getJsDocCommentText(node.comment)) === null || _a === void 0 ? void 0 : _a.replace(/^\s*-\s*/, "");
    }
    if (node.parent &&
        node.parent.kind === ts.SyntaxKind.VariableDeclarationList) {
        node = node.parent.parent;
    }
    else if (node.kind === ts.SyntaxKind.ModuleDeclaration) {
        if (!isTopmostModuleDeclaration(node)) {
            return;
        }
        else {
            node = getRootModuleDeclaration(node);
        }
    }
    else if (node.kind === ts.SyntaxKind.NamespaceExport) {
        node = node.parent;
    }
    else if (node.kind === ts.SyntaxKind.ExportSpecifier) {
        node = node.parent.parent;
    }
    else if (node.kind === ts.SyntaxKind.FunctionType) {
        node = node.parent;
    }
    const sourceFile = node.getSourceFile();
    const comments = getJSDocCommentRanges(node, sourceFile.text);
    if (comments.length) {
        let comment;
        if (node.kind === ts.SyntaxKind.SourceFile) {
            const explicitPackageComment = (_b = comments.find((comment) => sourceFile.text
                .substring(comment.pos, comment.end)
                .includes("@module"))) !== null && _b !== void 0 ? _b : comments.find((comment) => sourceFile.text
                .substring(comment.pos, comment.end)
                .includes("@packageDocumentation"));
            if (explicitPackageComment) {
                comment = explicitPackageComment;
            }
            else if (comments.length > 1) {
                // Legacy behavior, require more than one comment and use the first comment.
                comment = comments[0];
                logger.deprecated(`Specifying multiple comments at the start of a file to use the first comment as the comment for the module has been deprecated. Use @module or @packageDocumentation instead.\n\t${sourceFile.fileName}`, false);
            }
            else {
                // Single comment that may be a license comment, or no comments, bail.
                return;
            }
        }
        else {
            comment = comments[comments.length - 1];
            // If a non-SourceFile node comment has this tag, it should not be attached to the node
            // as it documents the module.
            if (sourceFile.text
                .substring(comment.pos, comment.end)
                .includes("@module") ||
                sourceFile.text
                    .substring(comment.pos, comment.end)
                    .includes("@packageDocumentation")) {
                return;
            }
        }
        return sourceFile.text.substring(comment.pos, comment.end);
    }
    else {
        return;
    }
}
exports.getRawComment = getRawComment;
/**
 * Parse the given doc comment string.
 *
 * @param text     The doc comment string that should be parsed.
 * @param comment  The {@link Comment} instance the parsed results should be stored into.
 * @returns        A populated {@link Comment} instance.
 */
function parseComment(text, comment = new index_1.Comment()) {
    let currentTag;
    let shortText = 0;
    function consumeTypeData(line) {
        line = line.replace(/^\{(?!@)[^}]*\}+/, "");
        line = line.replace(/^\[[^[][^\]]*\]+/, "");
        return line.trim();
    }
    function readBareLine(line) {
        if (currentTag) {
            currentTag.text += "\n" + line;
        }
        else if (line === "" && shortText === 0) {
            // Ignore
        }
        else if (line === "" && shortText === 1) {
            shortText = 2;
        }
        else {
            if (shortText === 2) {
                comment.text += (comment.text === "" ? "" : "\n") + line;
            }
            else {
                comment.shortText +=
                    (comment.shortText === "" ? "" : "\n") + line;
                shortText = 1;
            }
        }
    }
    function readTagLine(line, tag) {
        let tagName = tag[1].toLowerCase();
        let paramName;
        line = tag[2].trim();
        if (tagName === "return") {
            tagName = "returns";
        }
        if (tagName === "example") {
            line = line.replace(/<\/?caption>/g, "");
        }
        if (tagName === "param" ||
            tagName === "typeparam" ||
            tagName === "template" ||
            tagName === "inheritdoc") {
            line = consumeTypeData(line);
            const param = /[^\s]+/.exec(line);
            if (param) {
                paramName = param[0];
                line = line.substr(paramName.length + 1).trim();
            }
            line = consumeTypeData(line);
            line = line.replace(/^-\s+/, "");
        }
        else if (tagName === "returns") {
            line = consumeTypeData(line);
        }
        currentTag = new index_1.CommentTag(tagName, paramName, line);
        comment.tags.push(currentTag);
    }
    const CODE_FENCE = /^\s*```(?!.*```)/;
    let inFencedCode = false;
    function readLine(line) {
        line = line.replace(/^\s*\*? ?/, "");
        const rawLine = line;
        line = line.replace(/\s*$/, "");
        if (CODE_FENCE.test(line)) {
            inFencedCode = !inFencedCode;
        }
        // Four spaces can be used to make code blocks too.
        if (!inFencedCode && !line.startsWith("    ")) {
            const tag = /^\s*@(\S+)(.*)$/.exec(line);
            if (tag) {
                return readTagLine(line, tag);
            }
        }
        if (inFencedCode) {
            // This will not include code blocks declared with four spaces
            readBareLine(rawLine);
        }
        else {
            readBareLine(line);
        }
    }
    text = text.replace(/^\s*\/\*+/, "");
    text = text.replace(/\*+\/\s*$/, "");
    text.split(/\r\n?|\n/).forEach(readLine);
    return comment;
}
exports.parseComment = parseComment;
