///|
fn has_blank(text : String) -> Bool {
  text.is_empty() ||
  text.iter().any(c => c == ' ' || c == '\t' || c == '\n' || c == '\r')
}

///|
/// The name of a symbol. It is not empty and has no whitespace.
pub struct SymbolName(String) derive(Eq, Hash, Debug)

///|
/// Lexical order of the text.
pub impl Compare for SymbolName with fn compare(self, other) {
  lexical(self.0, other.0)
}

///|
pub fn SymbolName::parse(text : String) -> SymbolName? {
  if has_blank(text) {
    None
  } else {
    Some(SymbolName(text))
  }
}

///|
pub fn SymbolName::to_string(self : SymbolName) -> String {
  self.0
}

///|
/// A namespace of a symbol, with the separator that joins it to the name.
/// The root namespace has empty text and an empty separator.
pub struct Namespace {
  text : String
  separator : String
} derive(Eq, Hash, Debug)

///|
/// Lexical order of the text, then of the separator.
pub impl Compare for Namespace with fn compare(self, other) {
  match lexical(self.text, other.text) {
    0 => lexical(self.separator, other.separator)
    order => order
  }
}

///|
/// Parse a namespace. Empty text is not valid: use `Namespace::root`.
pub fn Namespace::parse(text : String, separator~ : String) -> Namespace? {
  if text.is_empty() {
    None
  } else {
    Some({ text, separator, })
  }
}

///|
pub fn Namespace::root() -> Namespace {
  { text: "", separator: "", }
}

///|
pub fn Namespace::is_root(self : Namespace) -> Bool {
  self.text.is_empty()
}

///|
/// A relative output path with `/` as the only separator.
pub struct OutputPath(String) derive(Eq, Hash, Debug)

///|
/// Lexical order of the text.
pub impl Compare for OutputPath with fn compare(self, other) {
  lexical(self.0, other.0)
}

///|
/// Parse an output path. I reject empty text, a leading `/`, empty segments,
/// `.` and `..` segments, `\` and a drive prefix such as `C:`.
pub fn OutputPath::parse(text : String) -> OutputPath? {
  if text.is_empty() || text.contains("\\") {
    return None
  }
  for segment in text.split("/") {
    let segment = segment.to_owned()
    if segment.is_empty() ||
      segment == "." ||
      segment == ".." ||
      segment.contains(":") {
      return None
    }
  }
  Some(OutputPath(text))
}

///|
pub fn OutputPath::to_string(self : OutputPath) -> String {
  self.0
}

///|
/// The name of a package that a generated file depends on.
pub struct PackageName(String) derive(Eq, Hash, Debug)

///|
/// Lexical order of the text.
pub impl Compare for PackageName with fn compare(self, other) {
  lexical(self.0, other.0)
}

///|
pub fn PackageName::parse(text : String) -> PackageName? {
  if has_blank(text) {
    None
  } else {
    Some(PackageName(text))
  }
}

///|
pub fn PackageName::to_string(self : PackageName) -> String {
  self.0
}

///|
pub(all) enum DependencyKind {
  Runtime
  Test
  Build
} derive(Eq, Compare, Hash, Debug)

///|
pub(all) struct SymbolDependency {
  package_name : PackageName
  version : String
  kind : DependencyKind
} derive(Eq, Hash, Debug)

///|
/// Order by package, then version in lexical order, then kind in declaration order.
pub impl Compare for SymbolDependency with fn compare(self, other) {
  match Compare::compare(self.package_name, other.package_name) {
    0 =>
      match lexical(self.version, other.version) {
        0 => Compare::compare(self.kind, other.kind)
        order => order
      }
    order => order
  }
}

///|
/// A symbol: the target-language name of a shape, where it is defined and what it needs.
pub struct Symbol {
  name : SymbolName
  ns : Namespace
  definition : OutputPath?
  references : Array[SymbolReference]
  dependencies : Array[SymbolDependency]
  properties : Map[String, Json]
} derive(Eq, Debug)

///|
/// A reference from one symbol to another, with an optional local name.
pub struct SymbolReference {
  symbol : Symbol
  local_name : SymbolName?
} derive(Eq, Debug)

///|
pub fn Symbol::new(
  name : SymbolName,
  ns~ : Namespace = Namespace::root(),
  definition~ : OutputPath? = None,
) -> Symbol {
  { name, ns, definition, references: [], dependencies: [], properties: {}, }
}

///|
/// A copy of the symbol with another name.
pub fn Symbol::with_name(self : Symbol, name : SymbolName) -> Symbol {
  { ..self, name, }
}

///|
/// A copy of the symbol with one more reference.
pub fn Symbol::with_reference(
  self : Symbol,
  reference : SymbolReference,
) -> Symbol {
  { ..self, references: [..self.references, reference], }
}

///|
/// A copy of the symbol with one more dependency.
pub fn Symbol::with_dependency(
  self : Symbol,
  dependency : SymbolDependency,
) -> Symbol {
  { ..self, dependencies: [..self.dependencies, dependency], }
}

///|
/// A copy of the symbol with a property set.
pub fn Symbol::with_property(
  self : Symbol,
  key : String,
  value : Json,
) -> Symbol {
  let properties = self.properties.copy()
  properties[key] = value
  { ..self, properties, }
}

///|
pub fn Symbol::property(self : Symbol, key : String) -> Json? {
  self.properties.get(key)
}

///|
/// The namespace, the separator and the name. Only the name when the namespace is root.
pub fn Symbol::full_name(self : Symbol) -> String {
  if self.ns.is_root() {
    self.name.0
  } else {
    "\{self.ns.text}\{self.ns.separator}\{self.name.0}"
  }
}

///|
/// The dependencies of the symbol and of its references, sorted and without duplicates.
/// The walk keeps its own stack, so deep reference chains do not overflow, and a set of
/// the full names that it has seen, so a cycle or a diamond of references ends.
pub fn Symbol::all_dependencies(self : Symbol) -> Array[SymbolDependency] {
  let found = []
  let seen : Map[String, Unit] = Map([])
  let stack = [self]
  while stack.pop() is Some(symbol) {
    let key = symbol.full_name()
    if seen.contains(key) {
      continue
    }
    seen[key] = ()
    found.append(symbol.dependencies)
    for reference in symbol.references {
      stack.push(reference.symbol)
    }
  }
  found.sort()
  let result = []
  for dependency in found {
    if result.last() is Some(last) && last == dependency {
      continue
    }
    result.push(dependency)
  }
  result
}

///|
pub fn SymbolReference::new(
  symbol : Symbol,
  local_name~ : SymbolName? = None,
) -> SymbolReference {
  { symbol, local_name, }
}