///|
/// The compiler reads only this caller-supplied virtual filesystem.
pub(all) struct Compilation {
  css : String
  loaded_files : Array[String]
  diagnostics : Array[String]
}

///|
fn clean_path(path : String) -> String raise ParseError {
  if path.is_empty() ||
    path.has_prefix("/") ||
    path.contains("\\") ||
    path.contains(":") ||
    path.contains("\u0000") {
    raise Invalid("expected relative virtual file path")
  }
  let parts : Array[String] = []
  for item in path.split("/") {
    let part = item.to_owned()
    if part.is_empty() || part == "." {
      continue
    }
    if part == ".." {
      if parts.is_empty() {
        raise Invalid("module escapes virtual root")
      }
      ignore(parts.pop())
    } else {
      parts.push(part)
    }
  }
  if parts.is_empty() {
    raise Invalid("empty file path")
  }
  parts.join("/")
}

///|
fn directory(path : String) -> String {
  let parts = path.split("/").to_array()
  if parts.length() < 2 {
    ""
  } else {
    parts[:parts.length() - 1].to_owned().join("/") + "/"
  }
}

///|
fn Evaluation::resolve(
  self : Evaluation,
  from : String,
  url : String,
) -> String raise ParseError {
  if url.has_prefix("/") || url.contains("\\") || url.contains(":") {
    raise Invalid("unsupported module URL")
  }
  let path = clean_path(directory(from) + url)
  let dir = directory(path)
  let base = path[dir.length():].to_owned()
  let suffix = if base.has_suffix(".scss") { "" } else { ".scss" }
  let candidates = [path + suffix, dir + "_" + base + suffix]
  let found = candidates.filter(p => self.files.contains(p))
  if found.length() > 1 {
    raise Invalid("ambiguous module " + url)
  }
  if found.length() == 1 {
    return found[0]
  }
  if !base.has_suffix(".scss") {
    let found = [path + "/index.scss", path + "/_index.scss"].filter(p => {
      self.files.contains(p)
    })
    if found.length() > 1 {
      raise Invalid("ambiguous index module " + url)
    }
    if found.length() == 1 {
      return found[0]
    }
  }
  raise Invalid("module not found " + url)
}

///|
fn Evaluation::load_file(
  self : Evaluation,
  path : String,
  configuration : Map[String, SassValue],
  strict? : Bool = true,
) -> Scope raise ParseError {
  if self.loading.contains(path) {
    raise Invalid("module load cycle " + path)
  }
  if self.loaded.get(path) is Some(frame) {
    if !configuration.is_empty() {
      raise Invalid("module already loaded; cannot configure " + path)
    }
    return frame
  }
  if self.loading.length() >= 32 {
    raise Invalid("module nesting limit")
  }
  let source = match self.files.get(path) {
    Some(value) => value
    None => raise Invalid("entry file not found")
  }
  let nodes = Source::{ chars: source.to_array(), pos: 0, }.statements(false, 0)
  validate_content(nodes, false)
  validate_semantics(nodes, true, false, false, false)
  let frame = { ..Scope::new(None), evaluation: self, path, }
  for key, value in configuration {
    frame.configuration[key] = value
    frame.configured[key] = true
  }
  self.loading.push(path)
  let emitter = Emitter::{
    output: StringBuilder(),
    size: 0,
    budget: 10000,
    pending: [],
    pending_size: 0,
    parents: [],
  }
  render(nodes, [], frame, emitter, 0, None, false, "")
  emitter.flush()
  if strict && !frame.configured.is_empty() {
    raise Invalid("configured variable was not declared with !default")
  }
  ignore(self.loading.pop())
  self.loaded[path] = frame
  self.order.push(path)
  self.css.push(emitter.output.to_string())
  frame
}

///|
/// Compile an entry and its SCSS modules without accessing the host filesystem.
pub fn compile_files(
  entry : String,
  files : Map[String, String],
) -> Compilation raise ParseError {
  if files.is_empty() || files.length() > 256 {
    raise Invalid("virtual file count limit")
  }
  let evaluation = Evaluation::new()
  let mut size = 0
  for path, source in files {
    let path = clean_path(path)
    if !path.has_suffix(".scss") {
      raise Invalid("virtual sources must use .scss")
    }
    if source.length() > 100000 {
      raise Invalid("source limit")
    }
    size += source.length()
    if size > 2000000 {
      raise Invalid("project source limit")
    }
    if evaluation.files.contains(path) {
      raise Invalid("duplicate canonical file path")
    }
    evaluation.files[path] = source
  }
  ignore(evaluation.load_file(clean_path(entry), Map([])))
  let mut length = 0
  for css in evaluation.css {
    length += css.length()
    if length > 1000000 {
      raise Invalid("CSS output limit")
    }
  }
  {
    css: evaluation.css.join(""),
    loaded_files: evaluation.order,
    diagnostics: evaluation.diagnostics,
  }
}

///|
fn public_name(name : String) -> Bool {
  !identifier(name).has_prefix("-")
}

///|
fn Scope::public_variable(self : Scope, name : String) -> (Scope, String)? {
  if !public_name(name) {
    return None
  }
  if self.vars.contains(name) {
    Some((self, name))
  } else {
    self.forwarded_vars.get(name)
  }
}

///|
fn Scope::public_callable(
  self : Scope,
  name : String,
  function : Bool,
) -> Mixin? {
  if !public_name(name) {
    return None
  }
  if function {
    match self.functions.get(name) {
      Some(value) => Some(value)
      None => self.forwarded_functions.get(name)
    }
  } else {
    match self.mixins.get(name) {
      Some(value) => Some(value)
      None => self.forwarded_mixins.get(name)
    }
  }
}

///|
fn Scope::find_module(self : Scope, prefix : String) -> Scope? {
  match self.module_scopes.get(prefix) {
    Some(value) => Some(value)
    None =>
      match self.parent {
        Some(parent) => parent.find_module(prefix)
        None => None
      }
  }
}

///|
fn Scope::qualified_owner(
  self : Scope,
  name : String,
) -> (Scope, String) raise ParseError {
  let parts = name.split(".").to_array()
  if parts.length() != 2 {
    raise Invalid("invalid module member")
  }
  let prefix = parts[0].to_owned()
  if self.standard(prefix) is Some(_) {
    raise Invalid("standard module variables are read-only")
  }
  let library = match self.find_module(prefix) {
    Some(frame) => frame
    None => raise Invalid("undefined module " + prefix)
  }
  match library.public_variable(parts[1].to_owned()) {
    Some(origin) => origin
    None => raise Invalid("undefined or private module variable")
  }
}

///|
fn Scope::qualified_variable(
  self : Scope,
  name : String,
) -> SassValue? raise ParseError {
  let parts = name.split(".").to_array()
  if parts.length() == 2 && self.standard(parts[0].to_owned()) == Some("math") {
    match parts[1].to_owned() {
      "pi" => return Some(numeric(3.141592653589793))
      "e" => return Some(numeric(2.718281828459045))
      "epsilon" => return Some(numeric(0.0000000000000002220446049250313))
      "max-safe-integer" => return Some(numeric(9007199254740991.0))
      "min-safe-integer" => return Some(numeric(-9007199254740991.0))
      _ => raise Invalid("undefined standard module variable")
    }
  }
  let (owner, key) = self.qualified_owner(name)
  owner.vars.get(key)
}

///|
fn Scope::qualified_callable(
  self : Scope,
  name : String,
  function : Bool,
) -> Mixin? raise ParseError {
  let parts = name.split(".").to_array()
  if parts.length() != 2 {
    raise Invalid("invalid module callable")
  }
  let prefix = parts[0].to_owned()
  if self.standard(prefix) is Some(_) {
    return None
  }
  let library = match self.find_module(prefix) {
    Some(frame) => frame
    None => raise Invalid("undefined module " + prefix)
  }
  library.public_callable(parts[1].to_owned(), function)
}

///|
fn same_origin(a : (Scope, String), b : (Scope, String)) -> Bool {
  a.0.path == b.0.path && a.1 == b.1
}

///|
fn Scope::star_variable(
  self : Scope,
  name : String,
) -> (Scope, String)? raise ParseError {
  let mut result = None
  for library in self.star_scopes {
    if library.public_variable(name) is Some(origin) {
      if result is Some(previous) && !same_origin(previous, origin) {
        raise Invalid("ambiguous global module variable")
      }
      result = Some(origin)
    }
  }
  result
}

///|
fn Scope::star_callable(
  self : Scope,
  name : String,
  function : Bool,
) -> Mixin? raise ParseError {
  let mut result : Mixin? = None
  for library in self.star_scopes {
    if library.public_callable(name, function) is Some(value) {
      if result is Some(previous) &&
        (previous.scope.path != value.scope.path || previous.name != value.name) {
        raise Invalid("ambiguous global module callable")
      }
      result = Some(value)
    }
  }
  result
}

///|
fn Scope::load_directive(
  self : Scope,
  source : String,
  forward : Bool,
) -> Unit raise ParseError {
  if self.parent is Some(_) {
    raise Invalid("module directives must be at root")
  }
  let parser = ExpressionParser::{ chars: source.to_array(), pos: 0, depth: 0, }
  ignore(parser.space())
  if parser.peek() != '"' && parser.peek() != '\'' {
    raise Invalid("module URL must be quoted")
  }
  let expression = parser.atom()
  let url = require_text(self.eval_ast(expression, true, 0)).0
  let mut tail = String::from_array(parser.chars[parser.pos:]).trim().to_owned()
  if url.has_prefix("sass:") {
    if forward {
      raise Invalid("forwarding standard modules is not implemented")
    }
    self.use_standard(source)
    return
  }
  let configuration : Map[String, SassValue] = Map([])
  let defaults : Map[String, Bool] = Map([])
  let config_parts = tail.split("with ").to_array()
  if config_parts.length() > 2 {
    raise Invalid("invalid module configuration")
  }
  if config_parts.length() == 2 {
    tail = config_parts[0].trim().to_owned()
    let values = config_parts[1].trim().to_owned()
    if !values.has_prefix("(") || !values.has_suffix(")") {
      raise Invalid("configuration requires parentheses")
    }
    for part in split_top(values[1:values.length() - 1].to_owned(), ',') {
      if part.is_empty() {
        continue
      }
      let pieces = split_top(part, ':')
      if pieces.length() < 2 {
        raise Invalid("configuration requires variable")
      }
      let key = control_variable(pieces[0])
      if !public_name(key) {
        raise Invalid("cannot configure private variable")
      }
      if configuration.contains(key) {
        raise Invalid("duplicate configuration variable")
      }
      let mut expression = pieces[1:].to_owned().join(":")
      if expression.has_suffix("!default") {
        if !forward {
          raise Invalid("default flag is only allowed in forward configuration")
        }
        expression = expression[:expression.length() - 8].trim().to_owned()
        defaults[key] = true
      }
      configuration[key] = self.evaluate(expression)
    }
  }
  let path = self.evaluation.resolve(self.path, url)
  if forward {
    let (prefix, show, hide, filters) = forward_policy(tail)
    let incoming : Map[String, String] = Map([])
    let explicit = configuration.copy()
    for key, _ in self.configured {
      if !key.has_prefix(prefix) {
        continue
      }
      let original = key[prefix.length():].to_owned()
      if !forward_visible("$" + key, show, hide, filters) {
        continue
      }
      if configuration.contains(original) && !defaults.contains(original) {
        raise Invalid("forward configuration cannot be overridden")
      }
      if self.configuration.get(key) is Some(value) {
        configuration[original] = value
        incoming[original] = key
      }
    }
    let library = self.evaluation.load_file(path, configuration, strict=false)
    for key, _ in explicit {
      if library.configured.contains(key) {
        raise Invalid(
          "forward configured variable was not declared with !default",
        )
      }
    }
    for original, key in incoming {
      if !library.configured.contains(original) {
        self.configured.remove(key)
      }
    }
    self.forward_members(library, tail)
    return
  }
  let library = self.evaluation.load_file(path, configuration)
  let base = path[directory(path).length():path.length() - 5].to_owned()
  let default_prefix = if base.has_prefix("_") {
    base[1:].to_owned()
  } else {
    base
  }
  // An index module receives the URL's directory name.
  let default_prefix = if default_prefix == "index" {
    let parts = url.split("/").to_array()
    parts.last().unwrap_or("").to_owned()
  } else {
    default_prefix
  }
  let prefix = if tail.is_empty() {
    default_prefix
  } else {
    if !tail.has_prefix("as ") {
      raise Invalid("invalid use clause")
    }
    tail[3:].trim().to_owned()
  }
  if prefix == "*" {
    for name, _ in self.vars {
      if library.public_variable(name) is Some(_) {
        raise Invalid("global module variable conflicts with existing variable")
      }
    }
    self.star_scopes.push(library)
    return
  }
  if prefix.is_empty() || !prefix.to_array().iter().all(name_char) {
    raise Invalid("invalid module namespace")
  }
  if self.module_scopes.contains(prefix) ||
    self.standard_modules.contains(prefix) {
    raise Invalid("module namespace already used")
  }
  self.module_scopes[prefix] = library
}

///|
fn Scope::forward_members(
  self : Scope,
  library : Scope,
  clause : String,
) -> Unit raise ParseError {
  let (prefix, show, hide, filters) = forward_policy(clause)
  let visible = fn(name : String) -> Bool {
    forward_visible(name, show, hide, filters)
  }
  let vars = library.forwarded_vars.copy()
  for name, _ in library.vars {
    if public_name(name) {
      vars[name] = (library, name)
    }
  }
  for name, origin in vars {
    if !public_name(name) || !visible("$" + prefix + name) {
      continue
    }
    let key = prefix + name
    if self.forwarded_vars.get(key) is Some(previous) &&
      !same_origin(previous, origin) {
      raise Invalid("conflicting forwarded variable")
    }
    self.forwarded_vars[key] = origin
  }
  let functions = library.forwarded_functions.copy()
  for name, value in library.functions {
    if public_name(name) {
      functions[name] = value
    }
  }
  for name, value in functions {
    if !public_name(name) || !visible(prefix + name) {
      continue
    }
    let key = prefix + name
    if self.forwarded_functions.get(key) is Some(previous) &&
      (previous.scope.path != value.scope.path || previous.name != value.name) {
      raise Invalid("conflicting forwarded function")
    }
    self.forwarded_functions[key] = value
  }
  let mixins = library.forwarded_mixins.copy()
  for name, value in library.mixins {
    if public_name(name) {
      mixins[name] = value
    }
  }
  for name, value in mixins {
    if !public_name(name) || !visible(prefix + name) {
      continue
    }
    let key = prefix + name
    if self.forwarded_mixins.get(key) is Some(previous) &&
      (previous.scope.path != value.scope.path || previous.name != value.name) {
      raise Invalid("conflicting forwarded mixin")
    }
    self.forwarded_mixins[key] = value
  }
}

///|
fn forward_policy(
  clause : String,
) -> (String, Bool, Bool, Array[String]) raise ParseError {
  let mut tail = clause
  let mut prefix = ""
  if tail.has_prefix("as ") {
    let parts = tail[3:].split(" ").to_array()
    let value = parts[0].to_owned()
    if !value.has_suffix("*") {
      raise Invalid("forward prefix requires star")
    }
    prefix = identifier(value[:value.length() - 1].to_owned())
    tail = parts[1:].to_owned().join(" ").trim().to_owned()
  }
  let show = tail.has_prefix("show ")
  let hide = tail.has_prefix("hide ")
  if !tail.is_empty() && !show && !hide {
    raise Invalid("invalid forward clause")
  }
  let filters = if show || hide {
    split_top(tail[5:].to_owned(), ',').map(identifier)
  } else {
    []
  }
  (prefix, show, hide, filters)
}

///|
fn forward_visible(
  name : String,
  show : Bool,
  hide : Bool,
  filters : Array[String],
) -> Bool {
  (!show || filters.contains(name)) && (!hide || !filters.contains(name))
}

///|
fn validate_semantics(
  nodes : Array[Statement],
  root : Bool,
  function : Bool,
  inside_mixin : Bool,
  flow : Bool,
) -> Unit raise ParseError {
  let mut before_rules = true
  for node in nodes {
    match node {
      Leaf(text) => {
        if text.has_prefix("@return") && !function {
          raise Invalid("return outside function")
        }
        if text.has_prefix("@use ") || text.has_prefix("@forward ") {
          if !root || !before_rules {
            raise Invalid("module directives must precede rules")
          }
        } else if !text.has_prefix("$") {
          before_rules = false
        }
      }
      Block(header, body) => {
        before_rules = false
        let is_function = header.has_prefix("@function ")
        let is_mixin = header.has_prefix("@mixin ")
        let is_flow = header.has_prefix("@if ") ||
          header == "@else" ||
          header.has_prefix("@else if ") ||
          header.has_prefix("@while ") ||
          header.has_prefix("@each ") ||
          header.has_prefix("@for ")
        if (is_function || is_mixin) && (flow || function || inside_mixin) {
          raise Invalid("nested callable definition")
        }
        if is_function {
          validate_function(body)
        }
        validate_semantics(
          body,
          false,
          function || is_function,
          inside_mixin || is_mixin,
          flow || is_flow,
        )
      }
    }
  }
}