import { extend, isArray, isStaticExpression } from './utils'; const scopeStack: any[] = []; function enterScope(newScope: any) { scopeStack.push(newScope); } function exitScope() { scopeStack.pop(); } export function parse(context: any, ancestors: any) { const parent: any = last(ancestors); const nodes: any = []; while (!isEnd(context, ancestors)) { const s = context.source; let node: any; if (startsWith(s, context.options.delimiters[0])) { node = parseInterpolation(context); } else if (s[0] === '<') { if (s.length === 1) { // 什么都不做 } else if (s[1] === '/') { if (s[2] === '>') { advanceBy(context, 3); continue; } else if (/[a-z]/i.test(s[2])) { parseTag(context, TagType.End, parent); continue; } else { node = parseComment(context); } } else if (/[a-z]/i.test(s[1])) { node = parseElement(context, ancestors); } else if (s[1] === '!') { node = parseComment(context); } } if (!node) { node = parseText(context); } if (isArray(node)) { for (let i = 0; i < node.length; i++) { pushNode(nodes, node[i]); } } else { pushNode(nodes, node); } } let removedWhitespace = false; const shouldCondense = context.options.whitespace !== 'preserve'; for (let i = 0; i < nodes.length; i++) { const node = nodes[i]; if (node.type === NodeTypes.TEXT) { if (!context.inPre) { if (!/[^\t\r\n\f ]/.test(node.content)) { const prev = nodes[i - 1]; const next = nodes[i + 1]; if ( !prev || !next || (shouldCondense && ((prev.type === NodeTypes.COMMENT && next.type === NodeTypes.COMMENT) || (prev.type === NodeTypes.COMMENT && next.type === NodeTypes.ELEMENT) || (prev.type === NodeTypes.ELEMENT && next.type === NodeTypes.COMMENT) || (prev.type === NodeTypes.ELEMENT && next.type === NodeTypes.ELEMENT && /[\r\n]/.test(node.content)))) ) { removedWhitespace = true; nodes[i] = null as any; } else { // Otherwise, the whitespace is condensed into a single space node.content = ' '; } } else if (shouldCondense) { // in condense mode, consecutive whitespaces in text are condensed // down to a single space. node.content = node.content.replace(/[\t\r\n\f ]+/g, ' '); } } else { // #6410 normalize windows newlines in
:
        // in the DOM
        node.content = node.content.replace(/\r\n/g, '\n');
      }
    } else if (node.type === NodeTypes.COMMENT && !context.options.comments) {
      removedWhitespace = true;
      nodes[i] = null as any;
    }
  }

  if (context.inPre && parent && context.options.isPreTag(parent.tag)) {
    // remove leading newline per html spec
    // https://html.spec.whatwg.org/multipage/grouping-content.html#the-pre-element
    const first = nodes[0];
    if (first && first.type === NodeTypes.TEXT) {
      first.content = first.content.replace(/^\r?\n/, '');
    }
  }

  return removedWhitespace ? nodes.filter(Boolean) : nodes;
}

function parseAttributes(context: any, type: any) {
  const props = [];
  const attributeNames = new Set();
  // ✅ 提前检查当前标签是否含有 s-for="(item,index) in xxx"
  const firstTagMatch = context.source
    .slice(0, context.source.indexOf('>'))
    .match(/s-for\s*=\s*["']\s*\(([^)]+)\)\s+in\s+[^"']+["']/);
  if (firstTagMatch) {
    const aliases = firstTagMatch[1].split(',').map((s: string) => s.trim());
    enterScope(aliases);
  } else if (type === 0) {
    enterScope([]);
  }

  while (
    context.source.length > 0 &&
    !startsWith(context.source, '>') &&
    !startsWith(context.source, '/>')
  ) {
    if (startsWith(context.source, '/')) {
      advanceBy(context, 1);
      advanceSpaces(context);
      continue;
    }

    const attr: any = parseAttribute(context, attributeNames);
    if (['s-if', 's-html'].includes(attr.name)) {
      attr.value.content = bindCtx(attr.value.content);
    }

    if (attr.name === 's-for') {
      const tar = attr.value.content.split(' in ');
      attr.value.content = tar[0] + ' in ' + bindCtx(tar[1]);
    }

    if (attr.type === NodeTypes.ATTRIBUTE && attr.value && attr.name === 'class') {
      attr.value.content = attr.value.content.replace(/\s+/g, ' ').trim();
    }

    if (type === TagType.Start) {
      props.push(attr);
    }
    advanceSpaces(context);
  }
  return props;
}

function parseAttribute(context: any, nameSet: any) {
  const start = getPos(context);
  const match = /^[^\t\r\n\f />][^\t\r\n\f />=]*/.exec(context.source)!;

  const name = match[0];
  nameSet.add(name);
  advanceBy(context, name.length);
  let value;
  if (/^[\t\r\n\f ]*=/.test(context.source)) {
    advanceSpaces(context);
    advanceBy(context, 1);
    advanceSpaces(context);
    value = parseAttributeValue(context);
  }

  const loc = getSelection(context, start);
  if (!context.inVPre && /^(v-[A-Za-z0-9-]|:|\.|@|#)/.test(name)) {
    const match = /(?:^v-([a-z0-9-]+))?(?:(?::|^\.|^@|^#)(\[[^\]]+\]|[^\.]+))?(.+)?$/i.exec(name)!;
    const isPropShorthand = startsWith(name, '.');
    const dirName =
      match[1] ||
      (isPropShorthand || startsWith(name, ':')
        ? 'bind'
        : startsWith(name, '@') || startsWith(name, 's-on:')
          ? 'on'
          : 'slot');
    let arg: { type: NodeTypes; content: string; loc: { start: any; end: any; source: any } };
    if (match[2]) {
      const isSlot = dirName === 'slot';
      const startOffset = name.lastIndexOf(match[2], name.length - (match[3]?.length || 0));

      const loc = getSelection(
        context,
        getNewPosition(context, start, startOffset),
        getNewPosition(
          context,
          start,
          startOffset + match[2].length + ((isSlot && match[3]) || '').length,
        ),
      );

      let content = match[2];

      if (content.startsWith('[')) {
        if (!content.endsWith(']')) {
          content = content.slice(1);
        } else {
          content = content.slice(1, content.length - 1);
        }
      } else if (isSlot) {
        content += match[3] || '';
      }

      arg = {
        type: NodeTypes.SIMPLE_EXPRESSION,
        content,
        loc,
      };
    }

    if (value?.isQuoted) {
      const valueLoc = value.loc;
      valueLoc.start.offset++;
      valueLoc.start.column++;
      valueLoc.end = advancePosWithClone(valueLoc.start, value.content);
      valueLoc.source = valueLoc.source.slice(1, -1);
    }

    const modifiers = match[3] ? match[3].slice(1).split('.') : [];
    if (isPropShorthand) modifiers.push('prop');

    return {
      type: NodeTypes.DIRECTIVE,
      name: dirName,
      exp: value && {
        type: NodeTypes.SIMPLE_EXPRESSION,
        content: bindCtx(value.content),
        loc: value.loc,
        isStatic: !isStaticExpression(value.content),
      },
      arg,
      modifiers,
      loc,
    };
  }

  return {
    type: NodeTypes.ATTRIBUTE,
    name,
    value: value && {
      type: NodeTypes.TEXT,
      content: value.content,
      loc: value.loc,
    },
    loc,
  };
}

function getNewPosition(context: any, start: any, numberOfCharacters: number) {
  return advancePosWithClone(
    start,
    context.originalSource.slice(start.offset, numberOfCharacters),
    numberOfCharacters,
  );
}

function parseAttributeValue(context: any) {
  const start = getPos(context);
  let content: string;

  const quote = context.source[0];
  const isQuoted = quote === '"' || quote === "'";

  if (isQuoted) {
    // Quoted value.
    advanceBy(context, 1);

    const endIndex = context.source.indexOf(quote);
    if (endIndex === -1) {
      content = parseTextData(context, context.source.length);
    } else {
      content = parseTextData(context, endIndex);
      advanceBy(context, 1);
    }
  } else {
    // Unquoted
    const match = /^[^\t\r\n\f >]+/.exec(context.source);
    if (!match) {
      return undefined;
    }
    content = parseTextData(context, match[0].length);
  }

  return {
    content,
    isQuoted,
    loc: getSelection(context, start),
  };
}

function pushNode(nodes: any, node: any): void {
  if (node.type === NodeTypes.TEXT) {
    const prev: any = last(nodes);
    // Merge if both this and the previous node are text and those are
    // consecutive. This happens for cases like "a < b".
    if (prev && prev.type === NodeTypes.TEXT && prev.loc.end.offset === node.loc.start.offset) {
      prev.content += node.content;
      prev.loc.end = node.loc.end;
      prev.loc.source += node.loc.source;
      return;
    }
  }

  nodes.push(node);
}

function parseElement(context: any, ancestors: any) {
  const parent = last(ancestors);
  const element: any = parseTag(context, TagType.Start, parent);
  if (element.isSelfClosing || context.options.isVoidTag(element.tag)) {
    return element;
  }
  ancestors.push(element);
  const children = parse(context, ancestors);
  ancestors.pop();
  element.children = children;
  if (startsWithEndTagOpen(context.source, element.tag)) {
    parseTag(context, TagType.End, parent);
  }
  element.loc = getSelection(context, element.loc.start);
  return element;
}

function parseTag(context, type, parent) {
  const start = getPos(context);
  const match = /^<\/?([a-z][^\t\r\n\f />]*)/i.exec(context.source)!;
  const tag = match[1];

  advanceBy(context, match[0].length);
  advanceSpaces(context);

  const props = parseAttributes(context, type);

  let isSelfClosing = false;

  if (context.source.length !== 0) {
    isSelfClosing = startsWith(context.source, '/>');
    advanceBy(context, isSelfClosing ? 2 : 1);
  }

  if (type === TagType.End) {
    exitScope();
    return;
  }

  const tagType = ElementTypes.ELEMENT;

  return {
    type: NodeTypes.ELEMENT,
    tag,
    tagType,
    children: [],
    props,
    isSelfClosing,
    loc: getSelection(context, start),
  };
}

const enum TagType {
  Start,
  End,
}

export const enum ElementTypes {
  ELEMENT,
}

export const enum Namespaces {
  HTML,
}

function parseComment(context: any) {
  const start = getPos(context);
  const contentStart = context.source[1] === '?' ? 1 : 2;
  let content: string;

  const closeIndex = context.source.indexOf('-->') + 2;
  if (closeIndex === -1) {
    content = context.source.slice(contentStart);
    advanceBy(context, context.source.length);
  } else {
    content = context.source.slice(contentStart, closeIndex);
    advanceBy(context, closeIndex + 1);
  }

  return {
    type: NodeTypes.COMMENT,
    content,
    loc: getSelection(context, start),
  };
}

function isEnd(context, ancestors) {
  const s = context.source;
  if (startsWith(s, '= 0; --i) {
      if (startsWithEndTagOpen(s, ancestors[i].tag)) {
        return true;
      }
    }
  }
  return !s;
}

function parseInterpolation(context) {
  const [open, close] = context.options.delimiters;
  const closeIndex = context.source.indexOf(close, open.length);
  const start = getPos(context);
  advanceBy(context, open.length);
  const innerStart = getPos(context);
  const innerEnd = getPos(context);
  const rawContentLength = closeIndex - open.length;
  const rawContent = context.source.slice(0, rawContentLength);
  const preTrimContent = matchText(context, rawContentLength);
  const content = preTrimContent.trim();
  const startOffset = preTrimContent.indexOf(content);
  if (startOffset > 0) {
    advancePos(innerStart, rawContent, startOffset);
  }
  const endOffset = rawContentLength - (preTrimContent.length - content.length - startOffset);
  advancePos(innerEnd, rawContent, endOffset);
  advanceBy(context, close.length);
  return {
    type: NodeTypes.INTERPOLATION,
    content: {
      type: NodeTypes.SIMPLE_EXPRESSION,
      content: bindCtx(content),
      loc: getSelection(context, innerStart, innerEnd),
    },
    loc: getSelection(context, start),
  };
}

function parseText(context) {
  const endTokens = ['<', context.options.delimiters[0]];
  let endIndex = context.source.length;
  for (let i = 0; i < endTokens.length; i++) {
    const index = context.source.indexOf(endTokens[i], 1);
    if (index !== -1 && endIndex > index) {
      endIndex = index;
    }
  }

  const start = getPos(context);
  const content = parseTextData(context, endIndex);

  return {
    type: NodeTypes.TEXT,
    content,
    loc: getSelection(context, start),
  };
}

function parseTextData(context: any, length: any) {
  const rawText = context.source.slice(0, length);
  advanceBy(context, length);
  return rawText;
}

function getSelection(context: any, start: any, end?: any) {
  end = end || getPos(context);
  return {
    start,
    end,
    source: context.originalSource.slice(start.offset, end.offset),
  };
}

function matchText(context: any, length: any) {
  const rawText = context.source.slice(0, length);
  advanceBy(context, length);
  return rawText;
}

function advanceBy(context: any, numberOfCharacters: number): void {
  const { source } = context;
  advancePos(context, source, numberOfCharacters);
  context.source = source.slice(numberOfCharacters);
}

export function advancePos(pos, source, numberOfCharacters = source.length) {
  let linesCount = 0;
  let lastNewLinePos = -1;
  for (let i = 0; i < numberOfCharacters; i++) {
    if (source.charCodeAt(i) === 10 /* newline char code */) {
      linesCount++;
      lastNewLinePos = i;
    }
  }
  pos.offset += numberOfCharacters;
  pos.line += linesCount;
  pos.column =
    lastNewLinePos === -1 ? pos.column + numberOfCharacters : numberOfCharacters - lastNewLinePos;

  return pos;
}

function advancePosWithClone(pos: any, source: string, numberOfCharacters: number = source.length) {
  return advancePos(extend({}, pos), source, numberOfCharacters);
}

function advanceSpaces(context: any): void {
  const match = /^[\t\r\n\f ]+/.exec(context.source);
  if (match) {
    advanceBy(context, match[0].length);
  }
}

function startsWith(source: string, searchString: string): boolean {
  return source.startsWith(searchString);
}

function startsWithEndTagOpen(source: string, tag: string): boolean {
  return (
    startsWith(source, ']/.test(source[2 + tag.length] || '>')
  );
}

function last(xs: T[]): T | undefined {
  return xs[xs.length - 1];
}

function getPos(context) {
  const { column, line, offset } = context;
  return {
    column,
    line,
    offset,
  };
}

export const enum NodeTypes {
  ROOT,
  ELEMENT,
  TEXT,
  COMMENT,
  SIMPLE_EXPRESSION,
  INTERPOLATION,
  ATTRIBUTE,
  DIRECTIVE,
  COMPOUND_EXPRESSION,
  COMPONENT,
  SLOT,
}

// JavaScript 内置全局变量和关键字白名单
const jsGlobals = new Set([
  // 原始值
  'true',
  'false',
  'null',
  'undefined',
  'NaN',
  'Infinity',

  // 基本对象
  'Object',
  'Function',
  'Boolean',
  'Symbol',
  'Error',
  'EvalError',
  'InternalError',
  'RangeError',
  'ReferenceError',
  'SyntaxError',
  'TypeError',
  'URIError',

  // 数字与数学
  'Number',
  'BigInt',
  'Math',
  'Date',

  // 文本处理
  'String',
  'RegExp',

  // Indexed collections
  'Array',
  'Int8Array',
  'Uint8Array',
  'Uint8ClampedArray',
  'Int16Array',
  'Uint16Array',
  'Int32Array',
  'Uint32Array',
  'Float32Array',
  'Float64Array',

  // Keyed collections
  'Map',
  'Set',
  'WeakMap',
  'WeakSet',

  // Structured data
  'ArrayBuffer',
  'SharedArrayBuffer',
  'Atomics',
  'DataView',
  'JSON',

  // Control abstraction objects
  'Promise',
  'Generator',
  'GeneratorFunction',
  'AsyncFunction',

  // Reflection
  'Reflect',
  'Proxy',

  // Web APIs & runtime globals
  'window',
  'globalThis',
  'console',
  'alert',
  'setTimeout',
  'setInterval',
  'clearTimeout',
  'clearInterval',
  'requestAnimationFrame',
  'cancelAnimationFrame',

  // DOM
  'document',
  'location',
  'history',
  'navigator',

  // 新语言关键字等
  'await',
  'async',
  'arguments',
  'this',
]);

function bindCtx(code: string): string {
  return code.replace(/\b([a-zA-Z_$][a-zA-Z0-9_$]*)\b/g, (match, varName, offset, fullText) => {
    // 1. 跳过字符串内的变量
    const before = fullText.slice(0, offset);
    const inSingleQuotes = /'[^']*$/.test(before) && /[^']*'/.test(fullText.slice(offset));
    const inDoubleQuotes = /"[^"]*$/.test(before) && /[^"]*"/.test(fullText.slice(offset));
    const inBacktick = /`[^`]*$/.test(before) && /[^`]*`/.test(fullText.slice(offset));
    if (inSingleQuotes || inDoubleQuotes || inBacktick) return match;

    // 2. 排除属性访问,例如 obj.key 中的 key
    const prevChar = fullText[offset - 1];
    if (prevChar === '.' || prevChar === ':') return match;

    // 3. 排除全局作用域变量、JS 内置变量
    if (hasScope(varName) || jsGlobals.has(varName)) return match;

    // 4. 默认加 _ctx_
    return `_ctx_.${varName}`;
  });
}

function flattenArray(arr: any[]): any[] {
  const result: any[] = [];

  for (const item of arr) {
    if (Array.isArray(item)) {
      result.push(...flattenArray(item));
    } else {
      result.push(item);
    }
  }

  return result;
}

function hasScope(name: string): boolean {
  return flattenArray(scopeStack).includes(name);
}