///|
pub struct LicenseAtom {
  id : String
  exception : String?
} derive(Eq, @debug.Debug)

///|
pub fn LicenseAtom::new(
  id : String,
  exception? : String? = None,
) -> LicenseAtom {
  { id, exception }
}

///|
pub fn LicenseAtom::id(self : LicenseAtom) -> String {
  self.id
}

///|
pub fn LicenseAtom::exception(self : LicenseAtom) -> String? {
  self.exception
}

///|
pub fn LicenseAtom::canonical(self : LicenseAtom) -> String {
  match self.exception {
    Some(value) => self.id + " WITH " + value
    None => self.id
  }
}

///|
pub enum Expression {
  Atom(LicenseAtom)
  And(Expression, Expression)
  Or(Expression, Expression)
} derive(Eq, @debug.Debug)

///|
fn expression_precedence(expression : Expression) -> Int {
  match expression {
    Atom(_) => 3
    And(_, _) => 2
    Or(_, _) => 1
  }
}

///|
fn render_expression(
  output : StringBuilder,
  expression : Expression,
  parent_precedence : Int,
) -> Unit {
  let precedence = expression_precedence(expression)
  let parentheses = precedence < parent_precedence
  if parentheses {
    output.write_char('(')
  }
  match expression {
    Atom(atom) => {
      output.write_string(atom.id)
      match atom.exception {
        Some(value) => output.write_string(" WITH " + value)
        None => ()
      }
    }
    And(left, right) => {
      render_expression(output, left, precedence)
      output.write_string(" AND ")
      render_expression(output, right, precedence)
    }
    Or(left, right) => {
      render_expression(output, left, precedence)
      output.write_string(" OR ")
      render_expression(output, right, precedence)
    }
  }
  if parentheses {
    output.write_char(')')
  }
}

///|
/// Render the expression with stable spacing and only semantic parentheses.
pub fn Expression::canonical(self : Expression) -> String {
  let output = StringBuilder()
  render_expression(output, self, 0)
  output.to_string()
}

///|
pub fn Expression::atoms(self : Expression) -> Array[LicenseAtom] {
  match self {
    Atom(value) => [value]
    And(left, right) | Or(left, right) => left.atoms() + right.atoms()
  }
}