import { marked } from 'marked';
import { isImageLine } from '../terminal-image.js';
import { applyBackgroundToLine, visibleWidth, wrapTextWithAnsi } from '../utils.js';
export class Markdown {
  text;
  paddingX; // Left/right padding
  paddingY; // Top/bottom padding
  defaultTextStyle;
  theme;
  defaultStylePrefix;
  // Cache for rendered output
  cachedText;
  cachedWidth;
  cachedLines;
  constructor(text, paddingX, paddingY, theme, defaultTextStyle) {
    this.text = text;
    this.paddingX = paddingX;
    this.paddingY = paddingY;
    this.theme = theme;
    this.defaultTextStyle = defaultTextStyle;
  }
  setText(text) {
    this.text = text;
    this.invalidate();
  }
  invalidate() {
    this.cachedText = undefined;
    this.cachedWidth = undefined;
    this.cachedLines = undefined;
  }
  render(width) {
    // Check cache
    if (this.cachedLines && this.cachedText === this.text && this.cachedWidth === width) {
      return this.cachedLines;
    }
    // Calculate available width for content (subtract horizontal padding)
    const contentWidth = Math.max(1, width - this.paddingX * 2);
    // Don't render anything if there's no actual text
    if (!this.text || this.text.trim() === '') {
      const result = [];
      // Update cache
      this.cachedText = this.text;
      this.cachedWidth = width;
      this.cachedLines = result;
      return result;
    }
    // Replace tabs with 3 spaces for consistent rendering
    const normalizedText = this.text.replace(/\t/g, '   ');
    // Parse markdown to HTML-like tokens
    const tokens = marked.lexer(normalizedText);
    // Convert tokens to styled terminal output
    const renderedLines = [];
    for (let i = 0; i < tokens.length; i++) {
      const token = tokens[i];
      const nextToken = tokens[i + 1];
      const tokenLines = this.renderToken(token, contentWidth, nextToken?.type);
      renderedLines.push(...tokenLines);
    }
    // Wrap lines (NO padding, NO background yet)
    const wrappedLines = [];
    for (const line of renderedLines) {
      if (isImageLine(line)) {
        wrappedLines.push(line);
      } else {
        wrappedLines.push(...wrapTextWithAnsi(line, contentWidth));
      }
    }
    // Add margins and background to each wrapped line
    const leftMargin = ' '.repeat(this.paddingX);
    const rightMargin = ' '.repeat(this.paddingX);
    const bgFn = this.defaultTextStyle?.bgColor;
    const contentLines = [];
    for (const line of wrappedLines) {
      if (isImageLine(line)) {
        contentLines.push(line);
        continue;
      }
      const lineWithMargins = leftMargin + line + rightMargin;
      if (bgFn) {
        contentLines.push(applyBackgroundToLine(lineWithMargins, width, bgFn));
      } else {
        // No background - just pad to width
        const visibleLen = visibleWidth(lineWithMargins);
        const paddingNeeded = Math.max(0, width - visibleLen);
        contentLines.push(lineWithMargins + ' '.repeat(paddingNeeded));
      }
    }
    // Add top/bottom padding (empty lines)
    const emptyLine = ' '.repeat(width);
    const emptyLines = [];
    for (let i = 0; i < this.paddingY; i++) {
      const line = bgFn ? applyBackgroundToLine(emptyLine, width, bgFn) : emptyLine;
      emptyLines.push(line);
    }
    // Combine top padding, content, and bottom padding
    const result = [...emptyLines, ...contentLines, ...emptyLines];
    // Update cache
    this.cachedText = this.text;
    this.cachedWidth = width;
    this.cachedLines = result;
    return result.length > 0 ? result : [''];
  }
  /**
   * Apply default text style to a string.
   * This is the base styling applied to all text content.
   * NOTE: Background color is NOT applied here - it's applied at the padding stage
   * to ensure it extends to the full line width.
   */
  applyDefaultStyle(text) {
    if (!this.defaultTextStyle) {
      return text;
    }
    let styled = text;
    // Apply foreground color (NOT background - that's applied at padding stage)
    if (this.defaultTextStyle.color) {
      styled = this.defaultTextStyle.color(styled);
    }
    // Apply text decorations using this.theme
    if (this.defaultTextStyle.bold) {
      styled = this.theme.bold(styled);
    }
    if (this.defaultTextStyle.italic) {
      styled = this.theme.italic(styled);
    }
    if (this.defaultTextStyle.strikethrough) {
      styled = this.theme.strikethrough(styled);
    }
    if (this.defaultTextStyle.underline) {
      styled = this.theme.underline(styled);
    }
    return styled;
  }
  getDefaultStylePrefix() {
    if (!this.defaultTextStyle) {
      return '';
    }
    if (this.defaultStylePrefix !== undefined) {
      return this.defaultStylePrefix;
    }
    const sentinel = '\u0000';
    let styled = sentinel;
    if (this.defaultTextStyle.color) {
      styled = this.defaultTextStyle.color(styled);
    }
    if (this.defaultTextStyle.bold) {
      styled = this.theme.bold(styled);
    }
    if (this.defaultTextStyle.italic) {
      styled = this.theme.italic(styled);
    }
    if (this.defaultTextStyle.strikethrough) {
      styled = this.theme.strikethrough(styled);
    }
    if (this.defaultTextStyle.underline) {
      styled = this.theme.underline(styled);
    }
    const sentinelIndex = styled.indexOf(sentinel);
    this.defaultStylePrefix = sentinelIndex >= 0 ? styled.slice(0, sentinelIndex) : '';
    return this.defaultStylePrefix;
  }
  getStylePrefix(styleFn) {
    const sentinel = '\u0000';
    const styled = styleFn(sentinel);
    const sentinelIndex = styled.indexOf(sentinel);
    return sentinelIndex >= 0 ? styled.slice(0, sentinelIndex) : '';
  }
  getDefaultInlineStyleContext() {
    return {
      applyText: (text) => this.applyDefaultStyle(text),
      stylePrefix: this.getDefaultStylePrefix()
    };
  }
  renderToken(token, width, nextTokenType) {
    const lines = [];
    switch (token.type) {
      case 'heading': {
        const headingLevel = token.depth;
        const headingPrefix = `${'#'.repeat(headingLevel)} `;
        const headingText = this.renderInlineTokens(token.tokens || []);
        let styledHeading;
        if (headingLevel === 1) {
          styledHeading = this.theme.heading(this.theme.bold(this.theme.underline(headingText)));
        } else if (headingLevel === 2) {
          styledHeading = this.theme.heading(this.theme.bold(headingText));
        } else {
          styledHeading = this.theme.heading(this.theme.bold(headingPrefix + headingText));
        }
        lines.push(styledHeading);
        if (nextTokenType !== 'space') {
          lines.push(''); // Add spacing after headings (unless space token follows)
        }
        break;
      }
      case 'paragraph': {
        const paragraphText = this.renderInlineTokens(token.tokens || []);
        lines.push(paragraphText);
        // Don't add spacing if next token is space or list
        if (nextTokenType && nextTokenType !== 'list' && nextTokenType !== 'space') {
          lines.push('');
        }
        break;
      }
      case 'code': {
        const indent = this.theme.codeBlockIndent ?? '  ';
        lines.push(this.theme.codeBlockBorder(`\`\`\`${token.lang || ''}`));
        if (this.theme.highlightCode) {
          const highlightedLines = this.theme.highlightCode(token.text, token.lang);
          for (const hlLine of highlightedLines) {
            lines.push(`${indent}${hlLine}`);
          }
        } else {
          // Split code by newlines and style each line
          const codeLines = token.text.split('\n');
          for (const codeLine of codeLines) {
            lines.push(`${indent}${this.theme.codeBlock(codeLine)}`);
          }
        }
        lines.push(this.theme.codeBlockBorder('```'));
        if (nextTokenType !== 'space') {
          lines.push(''); // Add spacing after code blocks (unless space token follows)
        }
        break;
      }
      case 'list': {
        const listLines = this.renderList(token, 0);
        lines.push(...listLines);
        // Don't add spacing after lists if a space token follows
        // (the space token will handle it)
        break;
      }
      case 'table': {
        const tableLines = this.renderTable(token, width);
        lines.push(...tableLines);
        break;
      }
      case 'blockquote': {
        const quoteStyle = (text) => this.theme.quote(this.theme.italic(text));
        const quoteStyleContext = {
          applyText: quoteStyle,
          stylePrefix: this.getStylePrefix(quoteStyle)
        };
        const quoteText = this.renderInlineTokens(token.tokens || [], quoteStyleContext);
        const quoteLines = quoteText.split('\n');
        // Calculate available width for quote content (subtract border "│ " = 2 chars)
        const quoteContentWidth = Math.max(1, width - 2);
        for (const quoteLine of quoteLines) {
          // Wrap the styled line, then add border to each wrapped line
          const wrappedLines = wrapTextWithAnsi(quoteLine, quoteContentWidth);
          for (const wrappedLine of wrappedLines) {
            lines.push(this.theme.quoteBorder('│ ') + wrappedLine);
          }
        }
        if (nextTokenType !== 'space') {
          lines.push(''); // Add spacing after blockquotes (unless space token follows)
        }
        break;
      }
      case 'hr':
        lines.push(this.theme.hr('─'.repeat(Math.min(width, 80))));
        if (nextTokenType !== 'space') {
          lines.push(''); // Add spacing after horizontal rules (unless space token follows)
        }
        break;
      case 'html':
        // Render HTML as plain text (escaped for terminal)
        if ('raw' in token && typeof token.raw === 'string') {
          lines.push(this.applyDefaultStyle(token.raw.trim()));
        }
        break;
      case 'space':
        // Space tokens represent blank lines in markdown
        lines.push('');
        break;
      default:
        // Handle any other token types as plain text
        if ('text' in token && typeof token.text === 'string') {
          lines.push(token.text);
        }
    }
    return lines;
  }
  renderInlineTokens(tokens, styleContext) {
    let result = '';
    const resolvedStyleContext = styleContext ?? this.getDefaultInlineStyleContext();
    const { applyText, stylePrefix } = resolvedStyleContext;
    const applyTextWithNewlines = (text) => {
      const segments = text.split('\n');
      return segments.map((segment) => applyText(segment)).join('\n');
    };
    for (const token of tokens) {
      switch (token.type) {
        case 'text':
          // Text tokens in list items can have nested tokens for inline formatting
          if (token.tokens && token.tokens.length > 0) {
            result += this.renderInlineTokens(token.tokens, resolvedStyleContext);
          } else {
            result += applyTextWithNewlines(token.text);
          }
          break;
        case 'paragraph':
          // Paragraph tokens contain nested inline tokens
          result += this.renderInlineTokens(token.tokens || [], resolvedStyleContext);
          break;
        case 'strong': {
          const boldContent = this.renderInlineTokens(token.tokens || [], resolvedStyleContext);
          result += this.theme.bold(boldContent) + stylePrefix;
          break;
        }
        case 'em': {
          const italicContent = this.renderInlineTokens(token.tokens || [], resolvedStyleContext);
          result += this.theme.italic(italicContent) + stylePrefix;
          break;
        }
        case 'codespan':
          result += this.theme.code(token.text) + stylePrefix;
          break;
        case 'link': {
          const linkText = this.renderInlineTokens(token.tokens || [], resolvedStyleContext);
          // If link text matches href, only show the link once
          // Compare raw text (token.text) not styled text (linkText) since linkText has ANSI codes
          // For mailto: links, strip the prefix before comparing (autolinked emails have
          // text="foo@bar.com" but href="mailto:foo@bar.com")
          const hrefForComparison = token.href.startsWith('mailto:')
            ? token.href.slice(7)
            : token.href;
          if (token.text === token.href || token.text === hrefForComparison) {
            result += this.theme.link(this.theme.underline(linkText)) + stylePrefix;
          } else {
            result +=
              this.theme.link(this.theme.underline(linkText)) +
              this.theme.linkUrl(` (${token.href})`) +
              stylePrefix;
          }
          break;
        }
        case 'br':
          result += '\n';
          break;
        case 'del': {
          const delContent = this.renderInlineTokens(token.tokens || [], resolvedStyleContext);
          result += this.theme.strikethrough(delContent) + stylePrefix;
          break;
        }
        case 'html':
          // Render inline HTML as plain text
          if ('raw' in token && typeof token.raw === 'string') {
            result += applyTextWithNewlines(token.raw);
          }
          break;
        default:
          // Handle any other inline token types as plain text
          if ('text' in token && typeof token.text === 'string') {
            result += applyTextWithNewlines(token.text);
          }
      }
    }
    return result;
  }
  /**
   * Render a list with proper nesting support
   */
  renderList(token, depth) {
    const lines = [];
    const indent = '  '.repeat(depth);
    // Use the list's start property (defaults to 1 for ordered lists)
    const startNumber = token.start ?? 1;
    for (let i = 0; i < token.items.length; i++) {
      const item = token.items[i];
      const bullet = token.ordered ? `${startNumber + i}. ` : '- ';
      // Process item tokens to handle nested lists
      const itemLines = this.renderListItem(item.tokens || [], depth);
      if (itemLines.length > 0) {
        // First line - check if it's a nested list
        // A nested list will start with indent (spaces) followed by cyan bullet
        const firstLine = itemLines[0];
        const isNestedList = /^\s+\x1b\[36m[-\d]/.test(firstLine); // starts with spaces + cyan + bullet char
        if (isNestedList) {
          // This is a nested list, just add it as-is (already has full indent)
          lines.push(firstLine);
        } else {
          // Regular text content - add indent and bullet
          lines.push(indent + this.theme.listBullet(bullet) + firstLine);
        }
        // Rest of the lines
        for (let j = 1; j < itemLines.length; j++) {
          const line = itemLines[j];
          const isNestedListLine = /^\s+\x1b\[36m[-\d]/.test(line); // starts with spaces + cyan + bullet char
          if (isNestedListLine) {
            // Nested list line - already has full indent
            lines.push(line);
          } else {
            // Regular content - add parent indent + 2 spaces for continuation
            lines.push(`${indent}  ${line}`);
          }
        }
      } else {
        lines.push(indent + this.theme.listBullet(bullet));
      }
    }
    return lines;
  }
  /**
   * Render list item tokens, handling nested lists
   * Returns lines WITHOUT the parent indent (renderList will add it)
   */
  renderListItem(tokens, parentDepth) {
    const lines = [];
    for (const token of tokens) {
      if (token.type === 'list') {
        // Nested list - render with one additional indent level
        // These lines will have their own indent, so we just add them as-is
        const nestedLines = this.renderList(token, parentDepth + 1);
        lines.push(...nestedLines);
      } else if (token.type === 'text') {
        // Text content (may have inline tokens)
        const text =
          token.tokens && token.tokens.length > 0
            ? this.renderInlineTokens(token.tokens)
            : token.text || '';
        lines.push(text);
      } else if (token.type === 'paragraph') {
        // Paragraph in list item
        const text = this.renderInlineTokens(token.tokens || []);
        lines.push(text);
      } else if (token.type === 'code') {
        // Code block in list item
        const indent = this.theme.codeBlockIndent ?? '  ';
        lines.push(this.theme.codeBlockBorder(`\`\`\`${token.lang || ''}`));
        if (this.theme.highlightCode) {
          const highlightedLines = this.theme.highlightCode(token.text, token.lang);
          for (const hlLine of highlightedLines) {
            lines.push(`${indent}${hlLine}`);
          }
        } else {
          const codeLines = token.text.split('\n');
          for (const codeLine of codeLines) {
            lines.push(`${indent}${this.theme.codeBlock(codeLine)}`);
          }
        }
        lines.push(this.theme.codeBlockBorder('```'));
      } else {
        // Other token types - try to render as inline
        const text = this.renderInlineTokens([token]);
        if (text) {
          lines.push(text);
        }
      }
    }
    return lines;
  }
  /**
   * Get the visible width of the longest word in a string.
   */
  getLongestWordWidth(text, maxWidth) {
    const words = text.split(/\s+/).filter((word) => word.length > 0);
    let longest = 0;
    for (const word of words) {
      longest = Math.max(longest, visibleWidth(word));
    }
    if (maxWidth === undefined) {
      return longest;
    }
    return Math.min(longest, maxWidth);
  }
  /**
   * Wrap a table cell to fit into a column.
   *
   * Delegates to wrapTextWithAnsi() so ANSI codes + long tokens are handled
   * consistently with the rest of the renderer.
   */
  wrapCellText(text, maxWidth) {
    return wrapTextWithAnsi(text, Math.max(1, maxWidth));
  }
  /**
   * Render a table with width-aware cell wrapping.
   * Cells that don't fit are wrapped to multiple lines.
   */
  renderTable(token, availableWidth) {
    const lines = [];
    const numCols = token.header.length;
    if (numCols === 0) {
      return lines;
    }
    // Calculate border overhead: "│ " + (n-1) * " │ " + " │"
    // = 2 + (n-1) * 3 + 2 = 3n + 1
    const borderOverhead = 3 * numCols + 1;
    const availableForCells = availableWidth - borderOverhead;
    if (availableForCells < numCols) {
      // Too narrow to render a stable table. Fall back to raw markdown.
      const fallbackLines = token.raw ? wrapTextWithAnsi(token.raw, availableWidth) : [];
      fallbackLines.push('');
      return fallbackLines;
    }
    const maxUnbrokenWordWidth = 30;
    // Calculate natural column widths (what each column needs without constraints)
    const naturalWidths = [];
    const minWordWidths = [];
    for (let i = 0; i < numCols; i++) {
      const headerText = this.renderInlineTokens(token.header[i].tokens || []);
      naturalWidths[i] = visibleWidth(headerText);
      minWordWidths[i] = Math.max(1, this.getLongestWordWidth(headerText, maxUnbrokenWordWidth));
    }
    for (const row of token.rows) {
      for (let i = 0; i < row.length; i++) {
        const cellText = this.renderInlineTokens(row[i].tokens || []);
        naturalWidths[i] = Math.max(naturalWidths[i] || 0, visibleWidth(cellText));
        minWordWidths[i] = Math.max(
          minWordWidths[i] || 1,
          this.getLongestWordWidth(cellText, maxUnbrokenWordWidth)
        );
      }
    }
    let minColumnWidths = minWordWidths;
    let minCellsWidth = minColumnWidths.reduce((a, b) => a + b, 0);
    if (minCellsWidth > availableForCells) {
      minColumnWidths = new Array(numCols).fill(1);
      const remaining = availableForCells - numCols;
      if (remaining > 0) {
        const totalWeight = minWordWidths.reduce(
          (total, width) => total + Math.max(0, width - 1),
          0
        );
        const growth = minWordWidths.map((width) => {
          const weight = Math.max(0, width - 1);
          return totalWeight > 0 ? Math.floor((weight / totalWeight) * remaining) : 0;
        });
        for (let i = 0; i < numCols; i++) {
          minColumnWidths[i] += growth[i] ?? 0;
        }
        const allocated = growth.reduce((total, width) => total + width, 0);
        let leftover = remaining - allocated;
        for (let i = 0; leftover > 0 && i < numCols; i++) {
          minColumnWidths[i]++;
          leftover--;
        }
      }
      minCellsWidth = minColumnWidths.reduce((a, b) => a + b, 0);
    }
    // Calculate column widths that fit within available width
    const totalNaturalWidth = naturalWidths.reduce((a, b) => a + b, 0) + borderOverhead;
    let columnWidths;
    if (totalNaturalWidth <= availableWidth) {
      // Everything fits naturally
      columnWidths = naturalWidths.map((width, index) => Math.max(width, minColumnWidths[index]));
    } else {
      // Need to shrink columns to fit
      const totalGrowPotential = naturalWidths.reduce((total, width, index) => {
        return total + Math.max(0, width - minColumnWidths[index]);
      }, 0);
      const extraWidth = Math.max(0, availableForCells - minCellsWidth);
      columnWidths = minColumnWidths.map((minWidth, index) => {
        const naturalWidth = naturalWidths[index];
        const minWidthDelta = Math.max(0, naturalWidth - minWidth);
        let grow = 0;
        if (totalGrowPotential > 0) {
          grow = Math.floor((minWidthDelta / totalGrowPotential) * extraWidth);
        }
        return minWidth + grow;
      });
      // Adjust for rounding errors - distribute remaining space
      const allocated = columnWidths.reduce((a, b) => a + b, 0);
      let remaining = availableForCells - allocated;
      while (remaining > 0) {
        let grew = false;
        for (let i = 0; i < numCols && remaining > 0; i++) {
          if (columnWidths[i] < naturalWidths[i]) {
            columnWidths[i]++;
            remaining--;
            grew = true;
          }
        }
        if (!grew) {
          break;
        }
      }
    }
    // Render top border
    const topBorderCells = columnWidths.map((w) => '─'.repeat(w));
    lines.push(`┌─${topBorderCells.join('─┬─')}─┐`);
    // Render header with wrapping
    const headerCellLines = token.header.map((cell, i) => {
      const text = this.renderInlineTokens(cell.tokens || []);
      return this.wrapCellText(text, columnWidths[i]);
    });
    const headerLineCount = Math.max(...headerCellLines.map((c) => c.length));
    for (let lineIdx = 0; lineIdx < headerLineCount; lineIdx++) {
      const rowParts = headerCellLines.map((cellLines, colIdx) => {
        const text = cellLines[lineIdx] || '';
        const padded = text + ' '.repeat(Math.max(0, columnWidths[colIdx] - visibleWidth(text)));
        return this.theme.bold(padded);
      });
      lines.push(`│ ${rowParts.join(' │ ')} │`);
    }
    // Render separator
    const separatorCells = columnWidths.map((w) => '─'.repeat(w));
    const separatorLine = `├─${separatorCells.join('─┼─')}─┤`;
    lines.push(separatorLine);
    // Render rows with wrapping
    for (let rowIndex = 0; rowIndex < token.rows.length; rowIndex++) {
      const row = token.rows[rowIndex];
      const rowCellLines = row.map((cell, i) => {
        const text = this.renderInlineTokens(cell.tokens || []);
        return this.wrapCellText(text, columnWidths[i]);
      });
      const rowLineCount = Math.max(...rowCellLines.map((c) => c.length));
      for (let lineIdx = 0; lineIdx < rowLineCount; lineIdx++) {
        const rowParts = rowCellLines.map((cellLines, colIdx) => {
          const text = cellLines[lineIdx] || '';
          return text + ' '.repeat(Math.max(0, columnWidths[colIdx] - visibleWidth(text)));
        });
        lines.push(`│ ${rowParts.join(' │ ')} │`);
      }
      if (rowIndex < token.rows.length - 1) {
        lines.push(separatorLine);
      }
    }
    // Render bottom border
    const bottomBorderCells = columnWidths.map((w) => '─'.repeat(w));
    lines.push(`└─${bottomBorderCells.join('─┴─')}─┘`);
    lines.push(''); // Add spacing after table
    return lines;
  }
}
//# sourceMappingURL=markdown.js.map
