"use strict";

Object.defineProperty(exports, "__esModule", {
  value: true
});
exports.default = void 0;
var _commentParser = require("comment-parser");
/**
 * Transform based on https://github.com/syavorsky/comment-parser/blob/master/src/transforms/align.ts
 *
 * It contains some customizations to align based on the tags, and some custom options.
 */

/**
 * Detects if a line starts with a markdown list marker
 * Supports: -, *, numbered lists (1., 2., etc.)
 * This explicitly excludes hyphens that are part of JSDoc tag syntax
 * @param {string} text - The text to check
 * @param {boolean} isFirstLineOfTag - True if this is the first line (tag line)
 * @returns {boolean} - True if the text starts with a list marker
 */
const startsWithListMarker = (text, isFirstLineOfTag = false) => {
  // On the first line of a tag, the hyphen is typically the JSDoc separator,
  // not a list marker
  if (isFirstLineOfTag) {
    return false;
  }

  // Match lines that start with optional whitespace, then a list marker
  // - or * followed by a space
  // or a number followed by . or ) and a space
  return /^\s*(?:[\-*]|\d+(?:\.|\)))\s+/v.test(text);
};

/**
 * @typedef {{
 *   hasNoTypes: boolean,
 *   maxNamedTagLength: import('./iterateJsdoc.js').Integer,
 *   maxUnnamedTagLength: import('./iterateJsdoc.js').Integer
 * }} TypelessInfo
 */

const {
  rewireSource
} = _commentParser.util;

/**
 * @typedef {{
 *   name: import('./iterateJsdoc.js').Integer,
 *   start: import('./iterateJsdoc.js').Integer,
 *   tag: import('./iterateJsdoc.js').Integer,
 *   type: import('./iterateJsdoc.js').Integer
 * }} Width
 */

/** @type {Width} */
const zeroWidth = {
  name: 0,
  start: 0,
  tag: 0,
  type: 0
};

/**
 * @param {string[]} tags
 * @param {import('./iterateJsdoc.js').Integer} index
 * @param {import('comment-parser').Line[]} source
 * @returns {boolean}
 */
const shouldAlign = (tags, index, source) => {
  const tag = source[index].tokens.tag.replace('@', '');
  const includesTag = tags.includes(tag);
  if (includesTag) {
    return true;
  }
  if (tag !== '') {
    return false;
  }
  for (let iterator = index; iterator >= 0; iterator--) {
    const previousTag = source[iterator].tokens.tag.replace('@', '');
    if (previousTag !== '') {
      if (tags.includes(previousTag)) {
        return true;
      }
      return false;
    }
  }
  return true;
};

/**
 * @param {string[]} tags
 * @returns {(
 *   width: Width,
 *   line: {
 *     tokens: import('comment-parser').Tokens
 *   },
 *   index: import('./iterateJsdoc.js').Integer,
 *   source: import('comment-parser').Line[]
 * ) => Width}
 */
const getWidth = tags => {
  return (width, {
    tokens
  }, index, source) => {
    if (!shouldAlign(tags, index, source)) {
      return width;
    }
    return {
      name: Math.max(width.name, tokens.name.length),
      start: tokens.delimiter === '/**' ? tokens.start.length : width.start,
      tag: Math.max(width.tag, tokens.tag.length),
      type: Math.max(width.type, tokens.type.length)
    };
  };
};

/**
 * @param {{
 *   description: string;
 *   tags: import('comment-parser').Spec[];
 *   problems: import('comment-parser').Problem[];
 * }} fields
 * @returns {TypelessInfo}
 */
const getTypelessInfo = fields => {
  const hasNoTypes = fields.tags.every(({
    type
  }) => {
    return !type;
  });
  const maxNamedTagLength = Math.max(...fields.tags.map(({
    name,
    tag
  }) => {
    return name.length === 0 ? -1 : tag.length;
  }).filter(length => {
    return length !== -1;
  })) + 1;
  const maxUnnamedTagLength = Math.max(...fields.tags.map(({
    name,
    tag
  }) => {
    return name.length === 0 ? tag.length : -1;
  }).filter(length => {
    return length !== -1;
  })) + 1;
  return {
    hasNoTypes,
    maxNamedTagLength,
    maxUnnamedTagLength
  };
};

/**
 * @param {import('./iterateJsdoc.js').Integer} len
 * @returns {string}
 */
const space = len => {
  return ''.padStart(len, ' ');
};

/**
 * Check if a tag or any of its lines contain list markers
 * @param {import('./iterateJsdoc.js').Integer} index - Current line index
 * @param {import('comment-parser').Line[]} source - All source lines
 * @returns {{hasListMarker: boolean, tagStartIndex: import('./iterateJsdoc.js').Integer}}
 */
const checkForListMarkers = (index, source) => {
  let hasListMarker = false;
  let tagStartIndex = index;
  while (tagStartIndex > 0 && source[tagStartIndex].tokens.tag === '') {
    tagStartIndex--;
  }
  for (let idx = tagStartIndex; idx <= index; idx++) {
    const isFirstLine = idx === tagStartIndex;
    if (source[idx]?.tokens?.description && startsWithListMarker(source[idx].tokens.description, isFirstLine)) {
      hasListMarker = true;
      break;
    }
  }
  return {
    hasListMarker,
    tagStartIndex
  };
};

/**
 * Calculate extra indentation for list items relative to the first continuation line
 * @param {import('./iterateJsdoc.js').Integer} index - Current line index
 * @param {import('./iterateJsdoc.js').Integer} tagStartIndex - Index of the tag line
 * @param {import('comment-parser').Line[]} source - All source lines
 * @returns {string} - Extra indentation spaces
 */
const calculateListExtraIndent = (index, tagStartIndex, source) => {
  // Find the first continuation line to use as baseline
  let firstContinuationIndent = null;
  for (let idx = tagStartIndex + 1; idx < source.length; idx++) {
    if (source[idx].tokens.description && !source[idx].tokens.tag) {
      firstContinuationIndent = source[idx].tokens.postDelimiter.length;
      break;
    }
  }

  // Calculate the extra indentation of current line relative to the first continuation line
  const currentOriginalIndent = source[index].tokens.postDelimiter.length;
  const extraIndent = firstContinuationIndent !== null && currentOriginalIndent > firstContinuationIndent ? ' '.repeat(currentOriginalIndent - firstContinuationIndent) : '';
  return extraIndent;
};

/**
 * @param {{
 *   customSpacings: import('../src/rules/checkLineAlignment.js').CustomSpacings,
 *   tags: string[],
 *   indent: string,
 *   preserveMainDescriptionPostDelimiter: boolean,
 *   wrapIndent: string,
 *   disableWrapIndent: boolean,
 * }} cfg
 * @returns {(
 *   block: import('comment-parser').Block
 * ) => import('comment-parser').Block}
 */
const alignTransform = ({
  customSpacings,
  disableWrapIndent,
  indent,
  preserveMainDescriptionPostDelimiter,
  tags,
  wrapIndent
}) => {
  let intoTags = false;
  /** @type {Width} */
  let width;

  /**
   * @param {import('comment-parser').Tokens} tokens
   * @param {TypelessInfo} typelessInfo
   * @returns {import('comment-parser').Tokens}
   */
  const alignTokens = (tokens, typelessInfo) => {
    const nothingAfter = {
      delim: false,
      name: false,
      tag: false,
      type: false
    };
    if (tokens.description === '') {
      nothingAfter.name = true;
      tokens.postName = '';
      if (tokens.name === '') {
        nothingAfter.type = true;
        tokens.postType = '';
        if (tokens.type === '') {
          nothingAfter.tag = true;
          tokens.postTag = '';

          /* c8 ignore next: Never happens because the !intoTags return. But it's here for consistency with the original align transform */
          if (tokens.tag === '') {
            nothingAfter.delim = true;
          }
        }
      }
    }
    let untypedNameAdjustment = 0;
    let untypedTypeAdjustment = 0;
    if (typelessInfo.hasNoTypes) {
      nothingAfter.tag = true;
      tokens.postTag = '';
      if (tokens.name === '') {
        untypedNameAdjustment = typelessInfo.maxNamedTagLength - tokens.tag.length;
      } else {
        untypedNameAdjustment = typelessInfo.maxNamedTagLength > typelessInfo.maxUnnamedTagLength ? 0 : Math.max(0, typelessInfo.maxUnnamedTagLength - (tokens.tag.length + tokens.name.length + 1));
        untypedTypeAdjustment = typelessInfo.maxNamedTagLength - tokens.tag.length;
      }
    }

    // Todo: Avoid fixing alignment of blocks with multiline wrapping of type
    if (tokens.tag === '' && tokens.type) {
      return tokens;
    }
    const spacings = {
      postDelimiter: customSpacings?.postDelimiter || 1,
      postName: customSpacings?.postName || 1,
      postTag: customSpacings?.postTag || 1,
      postType: customSpacings?.postType || 1
    };
    tokens.postDelimiter = nothingAfter.delim ? '' : space(spacings.postDelimiter);
    if (!nothingAfter.tag) {
      tokens.postTag = space(width.tag - tokens.tag.length + spacings.postTag);
    }
    if (!nothingAfter.type) {
      tokens.postType = space(width.type - tokens.type.length + spacings.postType + untypedTypeAdjustment);
    }
    if (!nothingAfter.name) {
      // If post name is empty for all lines (name width 0), don't add post name spacing.
      tokens.postName = width.name === 0 ? '' : space(width.name - tokens.name.length + spacings.postName + untypedNameAdjustment);
    }
    return tokens;
  };

  /**
   * @param {import('comment-parser').Line} line
   * @param {import('./iterateJsdoc.js').Integer} index
   * @param {import('comment-parser').Line[]} source
   * @param {TypelessInfo} typelessInfo
   * @param {string|false} indentTag
   * @returns {import('comment-parser').Line}
   */
  const update = (line, index, source, typelessInfo, indentTag) => {
    /** @type {import('comment-parser').Tokens} */
    const tokens = {
      ...line.tokens
    };
    if (tokens.tag !== '') {
      intoTags = true;
    }
    const isEmpty = tokens.tag === '' && tokens.name === '' && tokens.type === '' && tokens.description === '';

    // dangling '*/'
    if (tokens.end === '*/' && isEmpty) {
      tokens.start = indent + ' ';
      return {
        ...line,
        tokens
      };
    }
    switch (tokens.delimiter) {
      case '*':
        tokens.start = indent + ' ';
        break;
      case '/**':
        tokens.start = indent;
        break;
      default:
        tokens.delimiter = '';

        // compensate delimiter
        tokens.start = indent + '  ';
    }
    if (!intoTags) {
      if (tokens.description === '') {
        tokens.postDelimiter = '';
      } else if (!preserveMainDescriptionPostDelimiter) {
        tokens.postDelimiter = ' ';
      }
      return {
        ...line,
        tokens
      };
    }
    const postHyphenSpacing = customSpacings?.postHyphen ?? 1;
    const hyphenSpacing = /^\s*-\s+/v;
    tokens.description = tokens.description.replace(hyphenSpacing, '-' + ''.padStart(postHyphenSpacing, ' '));

    // Not align.
    if (shouldAlign(tags, index, source)) {
      alignTokens(tokens, typelessInfo);
      if (!disableWrapIndent && indentTag) {
        const {
          hasListMarker,
          tagStartIndex
        } = checkForListMarkers(index, source);
        if (hasListMarker && index > tagStartIndex) {
          const extraIndent = calculateListExtraIndent(index, tagStartIndex, source);
          tokens.postDelimiter += wrapIndent + extraIndent;
        } else {
          // Normal case: add wrapIndent after the aligned delimiter
          tokens.postDelimiter += wrapIndent;
        }
      }
    }
    return {
      ...line,
      tokens
    };
  };
  return ({
    source,
    ...fields
  }) => {
    width = source.reduce(getWidth(tags), {
      ...zeroWidth
    });
    const typelessInfo = getTypelessInfo(fields);
    let tagIndentMode = false;
    return rewireSource({
      ...fields,
      source: source.map((line, index) => {
        const indentTag = !disableWrapIndent && tagIndentMode && !line.tokens.tag && line.tokens.description;
        const ret = update(line, index, source, typelessInfo, indentTag);
        if (!disableWrapIndent && line.tokens.tag) {
          tagIndentMode = true;
        }
        return ret;
      })
    });
  };
};
var _default = exports.default = alignTransform;
module.exports = exports.default;
//# sourceMappingURL=alignTransform.cjs.map