export interface HighlightToken {
  text: string;
  cls: string | null;
}

function escapeHtml(text: string): string {
  return text.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
}

/**
 * Single-pass CSS tokenizer. Tracks brace depth + a selector/property/value
 * mode so the same bare-word token (e.g. `color`) is classified differently
 * depending on where it sits, without a full CSS parser.
 */
export function tokenizeCss(css: string): HighlightToken[] {
  const tokens: HighlightToken[] = [];
  let i = 0;
  let braceDepth = 0;
  let mode: 'selector' | 'property' | 'value' = 'selector';

  while (i < css.length) {
    if (css.startsWith('/*', i)) {
      const end = css.indexOf('*/', i + 2);
      const stop = end === -1 ? css.length : end + 2;
      tokens.push({ text: css.slice(i, stop), cls: 'cm-comment' });
      i = stop;
      continue;
    }
    const ch = css[i];
    if (ch === '"' || ch === "'") {
      let j = i + 1;
      while (j < css.length && css[j] !== ch) {
        if (css[j] === '\\') j++;
        j++;
      }
      j = Math.min(j + 1, css.length);
      tokens.push({ text: css.slice(i, j), cls: 'cm-string' });
      i = j;
      continue;
    }
    if (ch === '{') {
      tokens.push({ text: '{', cls: 'cm-punct' });
      braceDepth++;
      mode = 'property';
      i++;
      continue;
    }
    if (ch === '}') {
      tokens.push({ text: '}', cls: 'cm-punct' });
      braceDepth = Math.max(0, braceDepth - 1);
      mode = braceDepth > 0 ? 'property' : 'selector';
      i++;
      continue;
    }
    if (ch === ':' && braceDepth > 0 && mode === 'property') {
      tokens.push({ text: ':', cls: 'cm-punct' });
      mode = 'value';
      i++;
      continue;
    }
    if (ch === ';') {
      tokens.push({ text: ';', cls: 'cm-punct' });
      mode = braceDepth > 0 ? 'property' : mode;
      i++;
      continue;
    }
    if (ch === ',') {
      tokens.push({ text: ',', cls: 'cm-punct' });
      i++;
      continue;
    }
    let j = i;
    while (j < css.length && !'{}:;,"\''.includes(css[j]) && !css.startsWith('/*', j)) {
      j++;
    }
    if (j === i) {
      j = i + 1;
    }
    const text = css.slice(i, j);
    const cls = mode === 'selector' ? 'cm-selector' : mode === 'property' ? (/^\s*@/.test(text) ? 'cm-keyword' : 'cm-property') : 'cm-value';
    tokens.push({ text, cls });
    i = j;
  }
  return tokens;
}

/** Colors a whole `<tag ...>` as one token, except quoted attribute values (strings). */
function tokenizeTag(tagText: string): HighlightToken[] {
  const tokens: HighlightToken[] = [];
  const stringRe = /"[^"]*"|'[^']*'/g;
  let last = 0;
  let match: RegExpExecArray | null;
  while ((match = stringRe.exec(tagText))) {
    if (match.index > last) {
      tokens.push({ text: tagText.slice(last, match.index), cls: 'cm-tag' });
    }
    tokens.push({ text: match[0], cls: 'cm-string' });
    last = stringRe.lastIndex;
  }
  if (last < tagText.length) {
    tokens.push({ text: tagText.slice(last), cls: 'cm-tag' });
  }
  return tokens;
}

/**
 * HTML tokenizer: comments + tags get coarse coloring (fine-grained for
 * quoted attribute values), plain text stays uncolored. Content inside
 * `<style>...</style>` is delegated to the CSS tokenizer, since that is
 * where marketplace static pages actually author CSS.
 */
export function tokenizeHtml(html: string): HighlightToken[] {
  const tokens: HighlightToken[] = [];
  let i = 0;

  while (i < html.length) {
    if (html.startsWith('<!--', i)) {
      const end = html.indexOf('-->', i + 4);
      const stop = end === -1 ? html.length : end + 3;
      tokens.push({ text: html.slice(i, stop), cls: 'cm-comment' });
      i = stop;
      continue;
    }
    if (html[i] === '<') {
      let j = i + 1;
      let inStr: string | null = null;
      while (j < html.length) {
        const ch = html[j];
        if (inStr) {
          if (ch === inStr) inStr = null;
          j++;
          continue;
        }
        if (ch === '"' || ch === "'") {
          inStr = ch;
          j++;
          continue;
        }
        if (ch === '>') {
          j++;
          break;
        }
        j++;
      }
      const tagText = html.slice(i, j);
      tokens.push(...tokenizeTag(tagText));
      const isStyleOpenTag = /^<style(\s[^>]*)?>$/i.test(tagText);
      i = j;
      if (isStyleOpenTag) {
        const closeIdx = html.toLowerCase().indexOf('</style', i);
        const contentEnd = closeIdx === -1 ? html.length : closeIdx;
        tokens.push(...tokenizeCss(html.slice(i, contentEnd)));
        i = contentEnd;
      }
      continue;
    }
    let j = html.indexOf('<', i);
    if (j === -1) j = html.length;
    tokens.push({ text: html.slice(i, j), cls: null });
    i = j;
  }
  return tokens;
}

export function renderHighlighted(tokens: HighlightToken[]): string {
  return tokens.map(token => (token.cls ? `<span class="${token.cls}">${escapeHtml(token.text)}</span>` : escapeHtml(token.text))).join('');
}
