/**
 * In-house syntax tokenizer for the Code Highlight widget (no Prism / highlight.js).
 *
 * `tokenize(code, lang)` is lossless: concatenating every token's `text` gives the input back. The live site
 * prints the escaped code server-side (one `.zx-code-highlight__line` per line) and `frontend.ts` re-renders
 * each line with `highlightLines()`, so this file is the single tokenizer for the editor and the site.
 */

export type TokenType =
  | 'keyword'
  | 'string'
  | 'comment'
  | 'number'
  | 'function'
  | 'tag'
  | 'attr-name'
  | 'attr-value'
  | 'property'
  | 'selector'
  | 'punctuation'
  | 'operator'
  | 'variable'
  | 'builtin'
  | 'boolean'
  | 'plain';

export interface Token {
  type: TokenType;
  text: string;
}

export type Language = 'html' | 'css' | 'js' | 'php' | 'json' | 'bash' | 'python' | 'text';

export const LANGUAGES: Language[] = ['html', 'css', 'js', 'php', 'json', 'bash', 'python', 'text'];

const ALIASES: Record<string, Language> = {
  javascript: 'js',
  ts: 'js',
  typescript: 'js',
  jsx: 'js',
  tsx: 'js',
  mjs: 'js',
  xml: 'html',
  svg: 'html',
  vue: 'html',
  sh: 'bash',
  shell: 'bash',
  zsh: 'bash',
  py: 'python',
  plain: 'text',
  plaintext: 'text',
  txt: 'text',
  '': 'text',
};

export function normalizeLanguage(lang: unknown): Language {
  const l = String(lang ?? '').trim().toLowerCase();
  if ((LANGUAGES as string[]).includes(l)) return l as Language;
  return ALIASES[l] ?? 'text';
}

/** Line endings normalised to \n and trailing newlines dropped (render.php does the same). */
export function normalizeCode(code: unknown): string {
  return String(code ?? '').replace(/\r\n?/g, '\n').replace(/\n+$/, '');
}

/* ------------------------------------------------------------------------------------------------ */
/* Output buffer                                                                                    */
/* ------------------------------------------------------------------------------------------------ */

class Out {
  tokens: Token[] = [];
  /** Type and last non-space character of the last token that was not only whitespace. */
  lastType: TokenType | null = null;
  lastChar = '';
  lastText = '';

  push(type: TokenType, text: string): void {
    if (!text) return;
    const last = this.tokens[this.tokens.length - 1];
    if (last && last.type === type) last.text += text;
    else this.tokens.push({ type, text });
    const trimmed = text.trimEnd();
    if (trimmed) {
      this.lastType = type;
      this.lastChar = trimmed[trimmed.length - 1];
      this.lastText = type === 'plain' ? trimmed : text;
    }
  }
}

interface Ctx {
  code: string;
  start: number;
  end: number;
  out: Out;
}
type Classifier = (text: string, ctx: Ctx) => TokenType | null;
interface Rule {
  re: RegExp;
  type: TokenType | Classifier;
}

/** Runs sticky regex rules; the first rule that matches (and whose classifier agrees) wins. */
function lex(code: string, rules: Rule[], out: Out): void {
  let pos = 0;
  outer: while (pos < code.length) {
    for (const rule of rules) {
      rule.re.lastIndex = pos;
      const m = rule.re.exec(code);
      if (!m || m[0] === '') continue;
      const end = pos + m[0].length;
      const type = typeof rule.type === 'function' ? rule.type(m[0], { code, start: pos, end, out }) : rule.type;
      if (type === null) continue;
      out.push(type, m[0]);
      pos = end;
      continue outer;
    }
    out.push('plain', code[pos]);
    pos++;
  }
}

const words = (s: string): Set<string> => new Set(s.split(/\s+/).filter(Boolean));
const FOLLOWED_BY_PAREN = /\s*\(/y;
function followedByParen(ctx: Ctx): boolean {
  FOLLOWED_BY_PAREN.lastIndex = ctx.end;
  return FOLLOWED_BY_PAREN.test(ctx.code);
}
/** True when only spaces/tabs sit between the previous newline (or the start) and `start`. */
function atLineStart(code: string, start: number): boolean {
  let i = start - 1;
  while (i >= 0 && (code[i] === ' ' || code[i] === '\t')) i--;
  return i < 0 || code[i] === '\n';
}

const WS: Rule = { re: /\s+/y, type: 'plain' };
const BLOCK_COMMENT: Rule = { re: /\/\*[\s\S]*?(?:\*\/|$)/y, type: 'comment' };
const LINE_COMMENT: Rule = { re: /\/\/[^\n]*/y, type: 'comment' };
const DQ_STRING: Rule = { re: /"(?:\\[\s\S]|[^"\\\n])*"?/y, type: 'string' };
const SQ_STRING: Rule = { re: /'(?:\\[\s\S]|[^'\\\n])*'?/y, type: 'string' };
const C_NUMBER: Rule = { re: /(?:0[xX][\da-fA-F_]+|0[bB][01_]+|0[oO][0-7_]+|(?:\d[\d_]*\.?[\d_]*|\.\d[\d_]*)(?:[eE][+-]?\d+)?)n?/y, type: 'number' };
const PUNCT: Rule = { re: /[{}()[\];,.]/y, type: 'punctuation' };

/* ------------------------------------------------------------------------------------------------ */
/* JavaScript / TypeScript                                                                          */
/* ------------------------------------------------------------------------------------------------ */

const JS_KEYWORDS = words(`await break case catch class const continue debugger default delete do else export extends finally for from
  function if import in instanceof let new of return static super switch this throw try typeof var void while with yield async as
  interface enum implements private protected public readonly declare namespace abstract keyof`);
const JS_BOOLEANS = words('true false null undefined NaN Infinity');
const JS_BUILTINS = words(`Array Boolean Date Error JSON Map Math Number Object Promise Proxy Reflect RegExp Set String Symbol WeakMap
  WeakSet BigInt Intl console document window globalThis navigator location process require module exports fetch
  setTimeout setInterval clearTimeout clearInterval parseInt parseFloat`);
/** Characters after which a `/` starts a regex literal rather than a division. */
const REGEX_PRECEDERS = '(,=:[!&|?{};+-*%<>~^';

const JS_RULES: Rule[] = [
  WS,
  LINE_COMMENT,
  BLOCK_COMMENT,
  { re: /`(?:\\[\s\S]|[^\\`])*`?/y, type: 'string' },
  DQ_STRING,
  SQ_STRING,
  {
    re: /\/(?![*/])(?:\\.|\[(?:\\.|[^\]\\\n])*\]|[^/\\\n[])+\/[dgimsuvy]*/y,
    type: (_t, ctx) => (ctx.out.lastType === null || REGEX_PRECEDERS.includes(ctx.out.lastChar) || ctx.out.lastType === 'keyword' ? 'string' : null),
  },
  C_NUMBER,
  {
    re: /[A-Za-z_$\u00a0-\uffff][\w$\u00a0-\uffff]*/y,
    type: (text, ctx) => {
      const member = ctx.out.lastChar === '.' && ctx.out.lastType === 'punctuation';
      if (!member) {
        if (JS_BOOLEANS.has(text)) return 'boolean';
        if (JS_KEYWORDS.has(text)) return 'keyword';
        if (JS_BUILTINS.has(text)) return 'builtin';
      }
      return followedByParen(ctx) ? 'function' : 'plain';
    },
  },
  { re: /=>|\.\.\.|[+\-*/%=&|^!<>?~:]+/y, type: 'operator' },
  PUNCT,
];

/* ------------------------------------------------------------------------------------------------ */
/* PHP                                                                                              */
/* ------------------------------------------------------------------------------------------------ */

const PHP_KEYWORDS = words(`abstract and array as break callable case catch class clone const continue declare default do echo else
  elseif empty enddeclare endfor endforeach endif endswitch endwhile enum extends final finally fn for foreach function global goto
  if implements include include_once instanceof insteadof interface isset list match namespace new or print private protected
  public readonly require require_once return static switch throw trait try unset use var while xor yield`);
const PHP_BOOLEANS = words('true false null');
const PHP_BUILTINS = words('int float string bool void mixed iterable object self parent never');

const PHP_RULES: Rule[] = [
  WS,
  { re: /<\?(?:php\b|=)?|\?>/iy, type: 'tag' },
  LINE_COMMENT,
  { re: /#(?!\[)[^\n]*/y, type: 'comment' },
  BLOCK_COMMENT,
  { re: /"(?:\\[\s\S]|[^"\\])*"?/y, type: 'string' },
  { re: /'(?:\\[\s\S]|[^'\\])*'?/y, type: 'string' },
  { re: /\$+[A-Za-z_\u0080-\uffff][\w\u0080-\uffff]*/y, type: 'variable' },
  C_NUMBER,
  {
    re: /[A-Za-z_\u0080-\uffff][\w\u0080-\uffff]*/y,
    type: (text, ctx) => {
      const lower = text.toLowerCase();
      const member = ctx.out.lastType === 'operator' && (ctx.out.lastText.endsWith('->') || ctx.out.lastText.endsWith('::'));
      if (!member) {
        if (PHP_BOOLEANS.has(lower)) return 'boolean';
        if (PHP_KEYWORDS.has(lower)) return 'keyword';
        if (PHP_BUILTINS.has(lower)) return 'builtin';
      }
      return followedByParen(ctx) ? 'function' : 'plain';
    },
  },
  { re: /->|=>|::|[+\-*/%=&|^!<>?~:.@]+/y, type: 'operator' },
  PUNCT,
];

function lexPhp(code: string, out: Out): void {
  if (!code.includes('<?')) {
    lex(code, PHP_RULES, out);
    return;
  }
  // Mixed file: HTML outside <?php ... ?>, PHP inside.
  let pos = 0;
  while (pos < code.length) {
    const open = code.indexOf('<?', pos);
    if (open < 0) {
      lexHtml(code.slice(pos), out);
      break;
    }
    if (open > pos) lexHtml(code.slice(pos, open), out);
    const close = code.indexOf('?>', open + 2);
    const end = close < 0 ? code.length : close + 2;
    lex(code.slice(open, end), PHP_RULES, out);
    pos = end;
  }
}

/* ------------------------------------------------------------------------------------------------ */
/* JSON                                                                                             */
/* ------------------------------------------------------------------------------------------------ */

const FOLLOWED_BY_COLON = /\s*:/y;
const JSON_RULES: Rule[] = [
  WS,
  LINE_COMMENT,
  BLOCK_COMMENT,
  {
    re: /"(?:\\[\s\S]|[^"\\\n])*"?/y,
    type: (_t, ctx) => {
      FOLLOWED_BY_COLON.lastIndex = ctx.end;
      return FOLLOWED_BY_COLON.test(ctx.code) ? 'property' : 'string';
    },
  },
  { re: /-?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?/y, type: 'number' },
  { re: /\b(?:true|false|null)\b/y, type: 'boolean' },
  { re: /[{}[\],:]/y, type: 'punctuation' },
];

/* ------------------------------------------------------------------------------------------------ */
/* Bash / shell                                                                                     */
/* ------------------------------------------------------------------------------------------------ */

const BASH_KEYWORDS = words(`if then else elif fi for while until do done case esac in function select return exit export local
  readonly declare unset shift break continue source alias`);
const BASH_BUILTINS = words(`echo printf cd pwd ls cat grep egrep sed awk find xargs sort uniq head tail cut tr wc mkdir rmdir rm cp mv
  ln chmod chown touch tar zip unzip gzip curl wget ssh scp rsync git npm npx yarn pnpm node php python python3 pip pip3 composer
  docker kubectl sudo apt apt-get brew systemctl service kill ps test read eval exec set trap wp make`);

const BASH_RULES: Rule[] = [
  WS,
  {
    re: /#[^\n]*/y,
    type: (_t, ctx) => {
      const prev = ctx.start > 0 ? ctx.code[ctx.start - 1] : '';
      return prev === '' || /[\s;|&(]/.test(prev) ? 'comment' : null;
    },
  },
  { re: /"(?:\\[\s\S]|[^"\\])*"?/y, type: 'string' },
  { re: /\$?'[^']*'?/y, type: 'string' },
  { re: /\$(?:\{[^}\n]*\}?|[A-Za-z_]\w*|[0-9#?@*!$-])/y, type: 'variable' },
  {
    re: /--?[A-Za-z0-9][\w-]*/y,
    type: (_t, ctx) => (ctx.start === 0 || /\s/.test(ctx.code[ctx.start - 1]) ? 'attr-name' : null),
  },
  { re: /\d+(?![\w.])/y, type: 'number' },
  {
    re: /[A-Za-z_][\w-]*/y,
    type: (text, ctx) => {
      if (ctx.code[ctx.end] === '=') return 'variable';
      if (text === 'true' || text === 'false') return 'boolean';
      if (BASH_KEYWORDS.has(text)) return 'keyword';
      if (BASH_BUILTINS.has(text)) return 'builtin';
      return 'plain';
    },
  },
  { re: /&&|\|\||;;|[|&;<>!=]+/y, type: 'operator' },
  { re: /[(){}[\]]/y, type: 'punctuation' },
];

/* ------------------------------------------------------------------------------------------------ */
/* Python                                                                                           */
/* ------------------------------------------------------------------------------------------------ */

const PY_KEYWORDS = words(`and as assert async await break class continue def del elif else except finally for from global if import
  in is lambda nonlocal not or pass raise return try while with yield match case`);
const PY_BOOLEANS = words('True False None');
const PY_BUILTINS = words(`print len range str int float list dict set tuple bool type isinstance issubclass open enumerate zip map
  filter sorted reversed sum min max abs any all input super object self cls repr hasattr getattr setattr iter next round format
  Exception ValueError TypeError KeyError IndexError RuntimeError`);

const PY_RULES: Rule[] = [
  WS,
  { re: /#[^\n]*/y, type: 'comment' },
  { re: /(?:[rRbBuUfF]{1,2})?(?:"""[\s\S]*?(?:"""|$)|'''[\s\S]*?(?:'''|$))/y, type: 'string' },
  { re: /(?:[rRbBuUfF]{1,2})?(?:"(?:\\[\s\S]|[^"\\\n])*"?|'(?:\\[\s\S]|[^'\\\n])*'?)/y, type: 'string' },
  { re: /@[A-Za-z_][\w.]*/y, type: (_t, ctx) => (atLineStart(ctx.code, ctx.start) ? 'function' : null) },
  { re: /(?:0[xX][\da-fA-F_]+|0[bB][01_]+|0[oO][0-7_]+|(?:\d[\d_]*\.?[\d_]*|\.\d[\d_]*)(?:[eE][+-]?\d+)?)[jJ]?/y, type: 'number' },
  {
    re: /[A-Za-z_\u00a0-\uffff][\w\u00a0-\uffff]*/y,
    type: (text, ctx) => {
      const member = ctx.out.lastChar === '.' && ctx.out.lastType === 'punctuation';
      if (!member) {
        if (PY_BOOLEANS.has(text)) return 'boolean';
        if (PY_KEYWORDS.has(text)) return 'keyword';
      }
      if (ctx.out.lastType === 'keyword' && (ctx.out.lastText === 'def' || ctx.out.lastText === 'class')) return 'function';
      if (!member && PY_BUILTINS.has(text)) return 'builtin';
      return followedByParen(ctx) ? 'function' : 'plain';
    },
  },
  { re: /->|[+\-*/%=&|^!<>~]+/y, type: 'operator' },
  { re: /[{}()[\];,.:]/y, type: 'punctuation' },
];

/* ------------------------------------------------------------------------------------------------ */
/* CSS                                                                                              */
/* ------------------------------------------------------------------------------------------------ */

const CSS_SELECTOR = /[^\s{};][^{};]*?(?=\s*\{)/y;
const CSS_PROPERTY = /-{0,2}[A-Za-z_][\w-]*(?=\s*:)/y;
const CSS_IDENT = /-{0,2}[A-Za-z_][\w-]*/y;
const CSS_FUNCTION = /-{0,2}[A-Za-z_][\w-]*(?=\()/y;
const CSS_NUMBER = /[+-]?(?:\d+\.?\d*|\.\d+)(?:%|[A-Za-z]+)?/y;
const CSS_HEX = /#[\da-fA-F]{3,8}\b/y;
const CSS_IMPORTANT = /!\s*important\b/iy;
const CSS_AT = /@[\w-]+/y;
const CSS_AT_WORDS = words('and or not only screen print all');

function sticky(re: RegExp, code: string, pos: number): string | null {
  re.lastIndex = pos;
  const m = re.exec(code);
  return m && m[0] !== '' ? m[0] : null;
}

function lexCss(code: string, out: Out): void {
  let pos = 0;
  /** rule: expecting a selector or a property; value: after `prop:`; at: at-rule prelude. */
  let mode: 'rule' | 'value' | 'at' = 'rule';
  const take = (type: TokenType, text: string): void => {
    out.push(type, text);
    pos += text.length;
  };
  while (pos < code.length) {
    const ch = code[pos];
    let m: string | null;
    if ((m = sticky(WS.re, code, pos))) take('plain', m);
    else if ((m = sticky(BLOCK_COMMENT.re, code, pos))) take('comment', m);
    else if (ch === '"' || ch === "'") take('string', sticky(ch === '"' ? DQ_STRING.re : SQ_STRING.re, code, pos) ?? ch);
    else if (ch === '{' || ch === '}' || ch === ';') {
      take('punctuation', ch);
      mode = 'rule';
    } else if (ch === '@' && (m = sticky(CSS_AT, code, pos))) {
      take('keyword', m);
      mode = 'at';
    } else if (mode === 'rule') {
      if ((m = sticky(CSS_SELECTOR, code, pos))) take('selector', m);
      else if ((m = sticky(CSS_PROPERTY, code, pos))) take('property', m);
      else if (ch === ':') {
        take('punctuation', ch);
        mode = 'value';
      } else if ((m = sticky(CSS_IDENT, code, pos))) take('plain', m);
      else take('plain', ch);
    } else {
      if (mode === 'at' && (m = sticky(CSS_PROPERTY, code, pos))) take('property', m);
      else if ((m = sticky(CSS_IMPORTANT, code, pos))) take('keyword', m);
      else if ((m = sticky(CSS_HEX, code, pos))) take('number', m);
      else if ((m = sticky(CSS_NUMBER, code, pos))) take('number', m);
      else if ((m = sticky(CSS_FUNCTION, code, pos))) take('function', m);
      else if ((m = sticky(CSS_IDENT, code, pos))) {
        if (m.startsWith('--')) take('variable', m);
        else take(mode === 'at' && CSS_AT_WORDS.has(m.toLowerCase()) ? 'keyword' : 'plain', m);
      } else if ('(),:'.includes(ch)) take('punctuation', ch);
      else if ('+*/>~='.includes(ch)) take('operator', ch);
      else take('plain', ch);
    }
  }
}

/* ------------------------------------------------------------------------------------------------ */
/* HTML (with embedded <script> / <style>)                                                          */
/* ------------------------------------------------------------------------------------------------ */

const HTML_COMMENT = /<!--[\s\S]*?(?:-->|$)/y;
const HTML_DOCTYPE = /<![A-Za-z][^>]*>?/y;
const HTML_TAG_OPEN = /<\/?(?=[A-Za-z])/y;
const HTML_TAG_NAME = /[A-Za-z][\w:.-]*/y;
const HTML_ATTR_NAME = /[^\s"'>/=]+/y;
const HTML_ATTR_VALUE = /"[^"]*"?|'[^']*'?|[^\s>"']+/y;
const HTML_ENTITY = /&(?:#\d+|#[xX][\da-fA-F]+|[A-Za-z][\w]*);/y;
const HTML_TEXT = /[^<&]+/y;

function lexHtml(code: string, out: Out): void {
  let pos = 0;
  const take = (type: TokenType, text: string): void => {
    out.push(type, text);
    pos += text.length;
  };
  while (pos < code.length) {
    let m: string | null;
    if ((m = sticky(HTML_COMMENT, code, pos))) take('comment', m);
    else if ((m = sticky(HTML_DOCTYPE, code, pos))) take('keyword', m);
    else if ((m = sticky(HTML_TAG_OPEN, code, pos))) {
      const closing = m === '</';
      take('punctuation', m);
      const name = sticky(HTML_TAG_NAME, code, pos) ?? '';
      take('tag', name);
      let attrs = '';
      let expectValue = false;
      let ended = false;
      while (pos < code.length) {
        const ch = code[pos];
        if ((m = sticky(WS.re, code, pos))) {
          take('plain', m);
          continue;
        }
        if (ch === '>' || (ch === '/' && code[pos + 1] === '>')) {
          take('punctuation', ch === '>' ? '>' : '/>');
          ended = true;
          break;
        } else if (ch === '=') {
          take('punctuation', '=');
          expectValue = true;
          continue;
        } else if (expectValue && (m = sticky(HTML_ATTR_VALUE, code, pos))) {
          attrs += m;
          take('attr-value', m);
        } else if ((m = sticky(HTML_ATTR_NAME, code, pos))) {
          attrs += ` ${m}=`;
          take('attr-name', m);
        } else take('plain', ch);
        expectValue = false;
      }
      const lower = name.toLowerCase();
      if (ended && !closing && (lower === 'script' || lower === 'style')) {
        const rest = code.slice(pos);
        const close = rest.search(new RegExp(`</${lower}\\b`, 'i'));
        const body = close < 0 ? rest : rest.slice(0, close);
        if (body) {
          if (lower === 'style') lexCss(body, out);
          else if (/type\s*=\s*["']?[^"'\s>]*json/i.test(attrs)) lex(body, JSON_RULES, out);
          else if (/type\s*=\s*["']?text\/(?:template|html|x-)/i.test(attrs)) out.push('plain', body);
          else lex(body, JS_RULES, out);
          pos += body.length;
        }
      }
    } else if ((m = sticky(HTML_ENTITY, code, pos))) take('builtin', m);
    else if ((m = sticky(HTML_TEXT, code, pos))) take('plain', m);
    else take('plain', code[pos]);
  }
}

/* ------------------------------------------------------------------------------------------------ */
/* Public API                                                                                       */
/* ------------------------------------------------------------------------------------------------ */

export function tokenize(code: string, lang: Language | string): Token[] {
  const out = new Out();
  const src = String(code ?? '');
  switch (normalizeLanguage(lang)) {
    case 'js':
      lex(src, JS_RULES, out);
      break;
    case 'php':
      lexPhp(src, out);
      break;
    case 'json':
      lex(src, JSON_RULES, out);
      break;
    case 'bash':
      lex(src, BASH_RULES, out);
      break;
    case 'python':
      lex(src, PY_RULES, out);
      break;
    case 'css':
      lexCss(src, out);
      break;
    case 'html':
      lexHtml(src, out);
      break;
    default:
      out.push('plain', src);
  }
  return out.tokens;
}

/** Same characters as PHP's htmlspecialchars(ENT_QUOTES) so server and client markup agree. */
export function escapeHtml(s: string): string {
  return s.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;').replace(/'/g, '&#039;');
}

/** Splits tokens at newlines so every line's markup is balanced (a block comment spanning lines is cut per line). */
export function splitLines(tokens: Token[]): Token[][] {
  const lines: Token[][] = [[]];
  for (const t of tokens) {
    const parts = t.text.split('\n');
    parts.forEach((p, i) => {
      if (i > 0) lines.push([]);
      if (p) lines[lines.length - 1].push({ type: t.type, text: p });
    });
  }
  return lines;
}

export function tokensToHtml(tokens: Token[]): string {
  return tokens.map((t) => (t.type === 'plain' ? escapeHtml(t.text) : `<span class="zx-tok zx-tok--${t.type}">${escapeHtml(t.text)}</span>`)).join('');
}

/** Highlighted inner HTML of every line (code normalised first). */
export function highlightLines(code: string, lang: Language | string): string[] {
  return splitLines(tokenize(normalizeCode(code), lang)).map(tokensToHtml);
}

/** Escaped, highlighted HTML with lines joined by \n. */
export function highlightToHtml(code: string, lang: Language | string): string {
  return highlightLines(code, lang).join('\n');
}

/**
 * Parses "1, 3-5" into the set of line numbers inside [first, last]. Mirrors zadex_code_highlight_lines() in
 * render.php. Reversed ranges are swapped; anything else is ignored.
 */
export function parseLineRanges(spec: unknown, first: number, last: number): Set<number> {
  const out = new Set<number>();
  for (const part of String(spec ?? '').split(',')) {
    const m = /^\s*(\d+)\s*(?:-\s*(\d+)\s*)?$/.exec(part);
    if (!m) continue;
    let a = Number(m[1]);
    let b = m[2] !== undefined ? Number(m[2]) : a;
    if (a > b) [a, b] = [b, a];
    for (let n = Math.max(a, first); n <= Math.min(b, last); n++) out.add(n);
  }
  return out;
}
