import { ParseError } from "./errors.js"

export class Tokenizer {
  pos: number
  tokens: Token[]
  input: string

  constructor(input: string) {
    this.pos = 0
    this.tokens = []
    this.input = input
  }

  static tokens(input: string): Token[] {
    const tokenizer = new Tokenizer(input)
    return tokenizer.tokenize()
  }

  tokenize(): Token[] {
    while (this.pos < this.input.length) {
      if (this.#skipWhitespace(this.input)) continue

      const rawTokenStart = this.pos
      const rawToken = this.#readRawToken()
      const token = this.#analyzeToken(rawToken, rawTokenStart, this.pos)
      this.tokens.push(token)
    }

    return this.tokens
  }

  #analyzeToken(value: string, start: number, end: number): Token {
    if (!value) throw new ParseError("Unexpected empty argument", start, end)

    if (this.tokens.length === 0) {
      return { type: "command", value, start, end }
    }

    const namedMatch = value.match(/^([\w-]+)=(.*)$/)
    if (namedMatch) {
      const [, name, argValue] = namedMatch
      if (!name) {
        throw new ParseError("Named argument missing name", start, end)
      }

      return { type: "named", name, value: argValue, start, end }
    } else {
      return { type: "positional", value, start, end }
    }
  }

  #readRawToken(): string {
    let token = ""
    let inQuotes = false
    let escapeNext = false

    while (!this.#eol()) {
      const char = this.input[this.pos]

      if (escapeNext) {
        if (char === '"') {
          token += char // unescape the quote
        } else {
          token += "\\" + char // keep the backslash for non-quotes
        }
        escapeNext = false
      } else if (char === "\\") {
        escapeNext = true
      } else if (char === '"') {
        inQuotes = !inQuotes
      } else if (char === " " && !inQuotes) {
        break
      } else {
        token += char
      }

      this.pos++
    }

    if (inQuotes) {
      throw new ParseError("Unclosed quote", this.pos - token.length - 1, this.pos)
    } else if (escapeNext) {
      throw new ParseError("Trailing backslash", this.pos - 1, this.pos)
    }

    return token
  }

  #skipWhitespace(input: string): boolean {
    const match = input.slice(this.pos).match(/^\s+/)
    if (!match) return false

    const offset = match[0].length
    this.pos += offset
    return true
  }

  #eol(): boolean {
    return this.pos >= this.input.length
  }
}

export type Token =
  | {
      type: "command"
      value: string
      start: number
      end: number
    }
  | {
      type: "positional"
      value: string
      start: number
      end: number
    }
  | {
      type: "named"
      name: string
      value: string
      start: number
      end: number
    }
