"use strict";

Object.defineProperty(exports, "__esModule", {
  value: true
});
exports.default = void 0;
var _react = require("react");
var _interface = require("../interface");
/* ------------ Type ------------ */

/* ------------ Constants ------------ */
// Validates whether a token is still incomplete in the streaming context.
// Returns true if the token is syntactically incomplete; false if it is complete or invalid.
const STREAM_INCOMPLETE_REGEX = {
  image: [/^!\[[^\]\r\n]{0,1000}$/, /^!\[[^\r\n]{0,1000}\]\(*[^)\r\n]{0,1000}$/],
  link: [/^\[[^\]\r\n]{0,1000}$/, /^\[[^\r\n]{0,1000}\]\(*[^)\r\n]{0,1000}$/],
  html: [/^<\/$/, /^<\/?[a-zA-Z][a-zA-Z0-9-]{0,100}[^>\r\n]{0,1000}$/],
  commonEmphasis: [/^(\*{1,3}|_{1,3})(?!\s)(?!.*\1$)[^\r\n]{0,1000}$/],
  // regex2 matches cases like "- **"
  list: [/^[-+*]\s{0,3}$/, /^[-+*]\s{1,3}(\*{1,3}|_{1,3})(?!\s)(?!.*\1$)[^\r\n]{0,1000}$/],
  'inline-code': [/^`[^`\r\n]{0,300}$/]
};
const isTableInComplete = markdown => {
  if (markdown.includes('\n\n')) return false;
  const lines = markdown.split('\n');
  if (lines.length <= 1) return true;
  const [header, separator] = lines;
  const trimmedHeader = header.trim();
  if (!/^\|.*\|$/.test(trimmedHeader)) return false;
  const trimmedSeparator = separator.trim();
  const columns = trimmedSeparator.split('|').map(col => col.trim()).filter(Boolean);
  const separatorRegex = /^:?-+:?$/;
  return columns.every((col, index) => index === columns.length - 1 ? col === ':' || separatorRegex.test(col) : separatorRegex.test(col));
};
const tokenRecognizerMap = {
  [_interface.StreamCacheTokenType.Link]: {
    tokenType: _interface.StreamCacheTokenType.Link,
    isStartOfToken: markdown => markdown.startsWith('['),
    isStreamingValid: markdown => STREAM_INCOMPLETE_REGEX.link.some(re => re.test(markdown))
  },
  [_interface.StreamCacheTokenType.Image]: {
    tokenType: _interface.StreamCacheTokenType.Image,
    isStartOfToken: markdown => markdown.startsWith('!'),
    isStreamingValid: markdown => STREAM_INCOMPLETE_REGEX.image.some(re => re.test(markdown))
  },
  [_interface.StreamCacheTokenType.Html]: {
    tokenType: _interface.StreamCacheTokenType.Html,
    isStartOfToken: markdown => markdown.startsWith('<'),
    isStreamingValid: markdown => STREAM_INCOMPLETE_REGEX.html.some(re => re.test(markdown))
  },
  [_interface.StreamCacheTokenType.Emphasis]: {
    tokenType: _interface.StreamCacheTokenType.Emphasis,
    isStartOfToken: markdown => markdown.startsWith('*') || markdown.startsWith('_'),
    isStreamingValid: markdown => STREAM_INCOMPLETE_REGEX.commonEmphasis.some(re => re.test(markdown))
  },
  [_interface.StreamCacheTokenType.List]: {
    tokenType: _interface.StreamCacheTokenType.List,
    isStartOfToken: markdown => /^[-+*]/.test(markdown),
    isStreamingValid: markdown => STREAM_INCOMPLETE_REGEX.list.some(re => re.test(markdown))
  },
  [_interface.StreamCacheTokenType.Table]: {
    tokenType: _interface.StreamCacheTokenType.Table,
    isStartOfToken: markdown => markdown.startsWith('|'),
    isStreamingValid: isTableInComplete
  },
  [_interface.StreamCacheTokenType.InlineCode]: {
    tokenType: _interface.StreamCacheTokenType.InlineCode,
    isStartOfToken: markdown => markdown.startsWith('`'),
    isStreamingValid: markdown => STREAM_INCOMPLETE_REGEX['inline-code'].some(re => re.test(markdown))
  }
};
const recognize = (cache, tokenType) => {
  const recognizer = tokenRecognizerMap[tokenType];
  if (!recognizer) return;
  const {
    token,
    pending
  } = cache;
  if (token === _interface.StreamCacheTokenType.Text && recognizer.isStartOfToken(pending)) {
    cache.token = tokenType;
    return;
  }
  if (token === tokenType && !recognizer.isStreamingValid(pending)) {
    commitCache(cache);
  }
};
const recognizeHandlers = Object.values(tokenRecognizerMap).map(rec => ({
  tokenType: rec.tokenType,
  recognize: cache => recognize(cache, rec.tokenType)
}));

/* ------------ Utils ------------ */
const getInitialCache = () => ({
  pending: '',
  token: _interface.StreamCacheTokenType.Text,
  processedLength: 0,
  completeMarkdown: ''
});
const commitCache = cache => {
  if (cache.pending) {
    cache.completeMarkdown += cache.pending;
    cache.pending = '';
  }
  cache.token = _interface.StreamCacheTokenType.Text;
};
const isInCodeBlock = (text, isFinalChunk = false) => {
  const lines = text.split('\n');
  let inFenced = false;
  let fenceChar = '';
  let fenceLen = 0;
  for (let i = 0; i < lines.length; i++) {
    const rawLine = lines[i];
    const line = rawLine.endsWith('\r') ? rawLine.slice(0, -1) : rawLine;
    const match = line.match(/^(`{3,}|~{3,})(.*)$/);
    if (match) {
      const fence = match[1];
      const after = match[2];
      const char = fence[0];
      const len = fence.length;
      if (!inFenced) {
        inFenced = true;
        fenceChar = char;
        fenceLen = len;
      } else {
        // Check if this is a valid closing fence
        const isValidEnd = char === fenceChar && len >= fenceLen && /^\s*$/.test(after);
        if (isValidEnd) {
          // In streaming context, only close if this is the final chunk
          // or if there are more lines after this fence
          if (isFinalChunk || i < lines.length - 1) {
            inFenced = false;
            fenceChar = '';
            fenceLen = 0;
          }
          // Otherwise, keep the fence open for potential streaming continuation
        }
      }
    }
  }
  return inFenced;
};
const sanitizeForURIComponent = input => {
  let result = '';
  for (let i = 0; i < input.length; i++) {
    const charCode = input.charCodeAt(i);

    // 处理代理对：保留合法，跳过孤立
    if (charCode >= 0xd800 && charCode <= 0xdbff) {
      // High surrogate
      // Check for a following low surrogate to form a valid pair
      if (i + 1 < input.length && input.charCodeAt(i + 1) >= 0xdc00 && input.charCodeAt(i + 1) <= 0xdfff) {
        result += input[i] + input[i + 1];
        i++; // Skip the low surrogate as it's already processed
      }
      // Lone high surrogates are otherwise skipped
    } else if (charCode < 0xdc00 || charCode > 0xdfff) {
      // Append characters that are not lone low surrogates
      result += input[i];
    }
    // Lone low surrogates are otherwise skipped
  }
  return result;
};
const safeEncodeURIComponent = str => {
  try {
    return encodeURIComponent(str);
  } catch (e) {
    if (e instanceof URIError) {
      return encodeURIComponent(sanitizeForURIComponent(str));
    }
    return '';
  }
};

/* ------------ Main Hook ------------ */
const useStreaming = (input, config) => {
  const {
    streaming,
    components = {}
  } = config || {};
  const {
    hasNextChunk: enableCache = false,
    incompleteMarkdownComponentMap
  } = streaming || {};
  const [output, setOutput] = (0, _react.useState)('');
  const cacheRef = (0, _react.useRef)(getInitialCache());
  const handleIncompleteMarkdown = (0, _react.useCallback)(cache => {
    const {
      token,
      pending
    } = cache;
    if (token === _interface.StreamCacheTokenType.Text) return;
    /**
     * An image tag starts with '!', if it's the only character, it's incomplete and should be stripped.
     * ！
     * ^
     */
    if (token === _interface.StreamCacheTokenType.Image && pending === '!') return undefined;

    /**
     * If a table has more than two lines (header, separator, and at least one row),
     * it's considered complete enough to not be replaced by a placeholder.
     * | column1 | column2 |\n| -- | --|\n
     *                                   ^
     */
    if (token === _interface.StreamCacheTokenType.Table && pending.split('\n').length > 2) {
      return pending;
    }
    const componentMap = incompleteMarkdownComponentMap || {};
    const componentName = componentMap[token] || `incomplete-${token}`;
    const encodedPending = safeEncodeURIComponent(pending);
    return components?.[componentName] ? `<${componentName} data-raw="${encodedPending}" />` : undefined;
  }, [incompleteMarkdownComponentMap, components]);
  const processStreaming = (0, _react.useCallback)(text => {
    if (!text) {
      setOutput('');
      cacheRef.current = getInitialCache();
      return;
    }
    const expectedPrefix = cacheRef.current.completeMarkdown + cacheRef.current.pending;
    // Reset cache if input doesn't continue from previous state
    if (!text.startsWith(expectedPrefix)) {
      cacheRef.current = getInitialCache();
    }
    const cache = cacheRef.current;
    const chunk = text.slice(cache.processedLength);
    if (!chunk) return;
    cache.processedLength += chunk.length;
    for (const char of chunk) {
      cache.pending += char;
      const isContentInCodeBlock = isInCodeBlock(cache.completeMarkdown + cache.pending);
      if (isContentInCodeBlock) {
        commitCache(cache);
        continue;
      }
      if (cache.token === _interface.StreamCacheTokenType.Text) {
        for (const handler of recognizeHandlers) handler.recognize(cache);
      } else {
        const handler = recognizeHandlers.find(handler => handler.tokenType === cache.token);
        handler?.recognize(cache);
      }
      if (cache.token === _interface.StreamCacheTokenType.Text) {
        commitCache(cache);
      }
    }
    const incompletePlaceholder = handleIncompleteMarkdown(cache);
    setOutput(cache.completeMarkdown + (incompletePlaceholder || ''));
  }, [handleIncompleteMarkdown]);
  (0, _react.useEffect)(() => {
    if (typeof input !== 'string') {
      console.error(`X-Markdown: input must be string, not ${typeof input}.`);
      setOutput('');
      return;
    }
    enableCache ? processStreaming(input) : setOutput(input);
  }, [input, enableCache, processStreaming]);
  return output;
};
var _default = exports.default = useStreaming;