///|
priv struct MacroVariable {
  text : String
  immediate : Bool
}

///|
/// A per-parse macro context. Host effects are explicit callbacks; environment
/// is a snapshot, never the process-global environment. Return stdout from shell.
pub struct Preprocessor {
  priv variables : Map[String, MacroVariable]
  priv environment : Map[String, String]
  priv shell : (String) -> String raise ConfigError
  priv message : (String, String) -> Unit
}

///|
pub fn Preprocessor::new(
  environment~ : Map[String, String] = Map([]),
  shell~ : (String) -> String raise ConfigError = fn(_) raise ConfigError {
    raise Invalid("shell macro requires a host executor")
  },
  message~ : (String, String) -> Unit = fn(_, _) { () },
) -> Preprocessor {
  let snapshot : Map[String, String] = Map([])
  for key, value in environment {
    snapshot[key] = value
  }
  { variables: Map([]), environment: snapshot, shell, message, }
}

///|
fn macro_comment(text : String) -> String {
  let mut quote : UInt16 = 0
  let mut escaped = false
  for i in 0.. Array[String] raise ConfigError {
  let out = []
  let mut depth = 0
  let mut start = 0
  for i in 0.. Unit raise ConfigError {
  if args.length() != size {
    raise Invalid("macro " + name + " expects \{size} arguments")
  }
}

///|
fn Preprocessor::expand_at(
  self : Preprocessor,
  text : String,
  file : String,
  line : String,
  parameters : Array[String],
  stack : Array[String],
  depth : Int32,
) -> String raise ConfigError {
  if depth > 64 {
    raise Invalid("macro expansion depth exceeds 64")
  }
  let mut result = ""
  let mut i = 0
  while i < text.length() {
    if text[i] != 36 || i + 1 >= text.length() || text[i + 1] != 40 {
      result += text[i:i + 1].to_owned()
      i += 1
    } else {
      let start = i + 2
      i = start
      let mut nested = 1
      while i < text.length() && nested > 0 {
        if text[i] == 40 {
          nested += 1
        }
        if text[i] == 41 {
          nested -= 1
        }
        if nested != 0 {
          i += 1
        }
      }
      if nested != 0 {
        raise Invalid(file + ":" + line + ": unterminated macro")
      }
      let raw = macro_arguments(text[start:i].to_owned())
      i += 1
      let expanded = raw.map(fn(s) raise ConfigError {
        self.expand_at(s, file, line, parameters, stack, depth + 1)
      })
      let name = expanded[0]
      let args = suffix_strings(expanded, 1)
      let mut positional : String? = None
      for j in 0.. value
        None =>
          match name {
            "filename" => {
              macro_arity(name, args, 0)
              file
            }
            "lineno" => {
              macro_arity(name, args, 0)
              line
            }
            "shell" => {
              macro_arity(name, args, 1)
              let stdout = (self.shell)(args[0])
              stdout
              .trim_end(chars="\n")
              .to_owned()
              .split("\n")
              .map(fn(s) { s.to_owned() })
              .join(" ")
            }
            "info" => {
              macro_arity(name, args, 1)
              (self.message)("info", args[0])
              ""
            }
            "warning-if" | "error-if" => {
              macro_arity(name, args, 2)
              if args[0] == "y" {
                let msg = file + ":" + line + ": " + args[1]
                if name == "error-if" {
                  raise Invalid(msg)
                }
                (self.message)("warning", msg)
              }
              ""
            }
            _ =>
              match self.variables.get(name) {
                Some(variable) =>
                  if variable.immediate {
                    variable.text
                  } else {
                    if stack.contains(name) {
                      raise Invalid("recursive macro " + name)
                    }
                    self.expand_at(
                      variable.text,
                      file,
                      line,
                      args,
                      stack + [name],
                      depth + 1,
                    )
                  }
                None => self.environment.get(name).unwrap_or("")
              }
          }
      }
      result += replacement
    }
    if result.length() > 65536 {
      raise Invalid("expanded macro line too long")
    }
  }
  result
}

///|
fn suffix_strings(items : Array[String], start : Int32) -> Array[String] {
  let out = []
  for i in start.. String? raise ConfigError {
  let text = macro_comment(line.text)
  let (file, number) = match line.location.rev_split_once(":") {
    Some((f, n)) => (f.to_owned(), n.to_owned())
    None => (line.location, "1")
  }
  let mut equal = -1
  let mut nesting = 0
  let mut quoted : UInt16 = 0
  for i in 0..= 0 {
    let before = text[:equal].trim().to_owned()
    let immediate = before.has_suffix(":")
    let append = before.has_suffix("+")
    let lhs = if immediate || append {
      before[:before.length() - 1].trim().to_owned()
    } else {
      before
    }
    let name = self.expand_at(lhs, file, number, [], [], 0)
    if !name.is_empty() &&
      name
      .to_array()
      .all(fn(c) {
        c == '$' ||
        c == '-' ||
        c == '_' ||
        (c >= 'a' && c <= 'z') ||
        (c >= 'A' && c <= 'Z') ||
        (c >= '0' && c <= '9')
      }) {
      let old = self.variables.get(name)
      let simple = if append {
        match old {
          Some(v) => v.immediate
          None => false
        }
      } else {
        immediate
      }
      let raw = text[equal + 1:].trim_start().to_owned()
      let value = if simple {
        self.expand_at(raw, file, number, [], [], 0)
      } else {
        raw
      }
      let joined = if append {
        match old {
          Some(v) => v.text + " " + value
          None => value
        }
      } else {
        value
      }
      self.variables[name] = { text: joined, immediate: simple, }
      return None
    }
  }
  Some(self.expand_tokens(text, file, number))
}

///|
// Expand within the original lexical token. A replacement inside a quote is
// data, not new Kconfig syntax; quote/backslash bytes must remain literal.
fn Preprocessor::expand_tokens(
  self : Preprocessor,
  text : String,
  file : String,
  number : String,
) -> String raise ConfigError {
  let mut out = ""
  let mut quote : UInt16 = 0
  let mut i = 0
  while i < text.length() {
    if text[i] == 92 && i + 1 < text.length() {
      out += text[i:i + 2].to_owned()
      i += 2
      continue
    }
    if text[i] == 36 && i + 1 < text.length() && text[i + 1] == 40 {
      let start = i
      i += 2
      let mut depth = 1
      while i < text.length() && depth > 0 {
        if text[i] == 40 {
          depth += 1
        }
        if text[i] == 41 {
          depth -= 1
        }
        i += 1
      }
      if depth != 0 {
        raise Invalid(file + ":" + number + ": unterminated macro")
      }
      let replacement = self.expand_at(
        text[start:i].to_owned(),
        file,
        number,
        [],
        [],
        0,
      )
      if quote != 0 {
        for j in 0.. 65536 {
      raise Invalid("expanded macro line too long")
    }
  }
  out
}