///|
/// Shrubbery notation to a CSS syntax tree.
///
/// The surface syntax has one rule -- write the thing's name, not a symbol for
/// it -- and this is where that rule is read back. A call is a thing, a `~name`
/// keyword is a relation, and everything else is what shrubbery already made of
/// it.
///
/// The lowering is a pure function of the shrubbery tree. It reads `Node.span`,
/// for diagnostics, and never `Node.meta`. That matters more than it looks:
/// because nothing depends on how the source was laid out, the lowering is
/// total over hand-built trees as well as parsed ones, which is what lets the
/// round-trip properties generate trees rather than text.
///
/// It never fails in the tolerant mode. Anything unreadable becomes a `Bogus`
/// carrying the reason, so a file with one mistake still lowers to a stylesheet
/// with everything else intact.

///|
/// A lowering: the tree, and everything noticed on the way.
pub struct Lowered {
  sheet : @ast.Stylesheet
  diagnostics : Array[@err.Diagnostic]
}

///|
pub fn Lowered::sheet(self : Lowered) -> @ast.Stylesheet {
  self.sheet
}

///|
pub fn Lowered::diagnostics(self : Lowered) -> Array[@err.Diagnostic] {
  self.diagnostics
}

///|
pub fn Lowered::has_errors(self : Lowered) -> Bool {
  for d in self.diagnostics {
    if d.is_error() {
      return true
    }
  }
  false
}

///|
priv struct Lowerer {
  diags : Array[@err.Diagnostic]
  strict : Bool
}

///|
/// Where a group sits, which decides what it may be.
///
/// The distinction is load-bearing and is the first level of the rule/
/// declaration cascade: at the top of a file a `Group` with a block is always a
/// rule, because CSS has no declarations outside rules; inside `font_face()` it
/// is always a declaration, because that at-rule has no nested rules. Most of a
/// real file is settled here, before any head-shape guessing happens at all.
priv enum Ctx {
  /// The top of the file.
  Top
  /// The body of a style rule: declarations and nested rules interleave.
  Nested
  /// `font_face()`, `page()`, `property()`, a keyframe: declarations only.
  DeclOnly
}

///|
/// Lower a parsed shrubbery tree.
pub fn lower(
  root : @sast.Node,
  strict? : Bool = false,
) -> Lowered raise @err.ShrubCssError {
  let l = { diags: [], strict, }
  let items : Array[@ast.TopItem] = []
  let groups = match root.it {
    Multi(gs) => gs
    // A bare group, which is what a fragment parses to.
    Group(_) => [root]
    _ => []
  }
  for g in groups {
    match as_comment(g) {
      Some(c) => {
        items.push(Comment(c))
        continue
      }
      None => ()
    }
    match l.statement(g, Top) {
      Some(r) => items.push(Rule(r))
      None => ()
    }
  }
  { sheet: { items, span: cspan(root.span), }, diagnostics: l.diags, }
}

///|
/// Parse shrubbery source and lower it in one step.
///
/// Shrubbery's own diagnostics come first and are converted, so a caller sees
/// one list rather than having to consult two. A source that does not parse
/// lowers to an empty stylesheet: there is no tree to lower, and inventing one
/// would put the bridge's guesses where the notation's answer should be.
pub fn lower_source(
  src : String,
  strict? : Bool = false,
) -> Lowered raise @err.ShrubCssError {
  let parsed = @shrub.parse(src, recover=true) catch {
    e => {
      let d = shrub_diagnostic(e.diagnostic(), src)
      if strict {
        d.raise_()
      }
      return {
        sheet: { items: [], span: @cspan.Span::new(0, src.length()), },
        diagnostics: [d],
      }
    }
  }
  let pre : Array[@err.Diagnostic] = []
  for d in parsed.diagnostics() {
    pre.push(shrub_diagnostic(d, src))
  }
  if strict && pre.length() > 0 {
    pre[0].raise_()
  }
  let out = lower(parsed.root(), strict~)
  let all : Array[@err.Diagnostic] = []
  for d in pre {
    all.push(d)
  }
  for d in out.diagnostics {
    all.push(d)
  }
  { sheet: out.sheet, diagnostics: all, }
}

// -------------------------------------------------------------- statements

///|
/// One group, in a context that says what it may be.
fn Lowerer::statement(
  self : Lowerer,
  g : @sast.Node,
  ctx : Ctx,
) -> @ast.CssRule? raise @err.ShrubCssError {
  let items = match g.it {
    Group(xs) => xs
    _ => return Some(self.bogus_rule(BadSelector, g))
  }
  let (head, block, has_alts) = split_block(items[:])
  if has_alts {
    return Some(self.bogus_rule(AltsUnsupported, g))
  }
  if head.length() == 0 {
    return Some(self.bogus_rule(HeadlessBlock, g))
  }
  // An at-rule is recognised only in statement position, only as `name(...)`,
  // and only for a name in the table -- so a type selector called `media` is
  // merely unspellable rather than silently reinterpreted. `tag(media)` is the
  // way out.
  match as_call(head) {
    Some((name, args, rest)) =>
      if rest.length() == 0 && @names.is_at_rule(@names.unkebab(name)) {
        return self.at_rule(g, @names.unkebab(name), args, block, ctx)
      }
    None => ()
  }
  match block {
    None => Some(self.bogus_rule(ExpectedBlock, g))
    Some(b) =>
      if is_declaration(head, b, ctx) {
        // A declaration is not a rule; the caller that wanted one gets nothing
        // and a diagnostic saying why.
        match ctx {
          Top => {
            self.error(DeclarationAtTopLevel, g.span)
            Some(self.bogus_rule(DeclarationAtTopLevel, g))
          }
          _ => None
        }
      } else {
        match ctx {
          DeclOnly => {
            self.error(RuleInDeclarationContext, g.span)
            Some(self.bogus_rule(RuleInDeclarationContext, g))
          }
          _ => {
            let selectors = self.selector_list(head, g.span)
            let body = self.block_items(b, Nested)
            Some(Style({ selectors, body, span: cspan(g.span), }))
          }
        }
      }
  }
}

///|
/// The rule-versus-declaration cascade, level two and three.
///
/// Level one was the context, already applied by the caller. What is left:
///
///   * head shape -- a declaration head is a lone identifier, a `--custom`
///     property, or the `ident("literal")` escape, and nothing else;
///   * block shape -- for a lone identifier only, a value is one group with no
///     block of its own, while a rule body always contains a group that has
///     one.
///
/// The residue is exactly `a: hover`: a lone identifier over a block of one
/// bare word. It is read as a declaration, which is what CSS would say, and
/// `colon_pseudo` fires so the reader is told rather than surprised.
fn is_declaration(
  head : ArrayView[@sast.Node],
  block : @sast.Node,
  ctx : Ctx,
) -> Bool {
  match ctx {
    Top => return false
    DeclOnly => return true
    Nested => ()
  }
  if as_dashed(head) is Some(_) {
    return true
  }
  match as_call(head) {
    Some((name, args, rest)) =>
      if name == "ident" &&
        rest.length() == 0 &&
        literal_string(args) is Some(_) {
        return true
      }
    None => ()
  }
  match head {
    [{ it: Id(_), .. }] => block_is_value(block)
    _ => false
  }
}

///|
/// `comment("...")`: a CSS comment, carried as data.
///
/// It has to be data because shrubbery comments are trivia, and trivia does not
/// reach the tree. See the emitter for the whole argument.
fn as_comment(g : @sast.Node) -> @ast.Comment? {
  let items = match g.it {
    Group(xs) => xs[:]
    _ => return None
  }
  match as_call(items) {
    Some((name, args, rest)) =>
      if name == "comment" && rest.length() == 0 {
        match literal_string(args) {
          Some(text) => Some({ text, span: cspan(g.span), })
          None => None
        }
      } else {
        None
      }
    None => None
  }
}

///|
/// Whether a block holds one value rather than a rule body.
fn block_is_value(block : @sast.Node) -> Bool {
  match block.it {
    Block(gs) => gs.length() == 1 && !group_has_block(gs[0])
    _ => false
  }
}

///|
fn group_has_block(g : @sast.Node) -> Bool {
  match g.it {
    Group(xs) => {
      for x in xs {
        match x.it {
          Block(_) | Alts(_) => return true
          _ => ()
        }
      }
      false
    }
    _ => false
  }
}

///|
/// The contents of a block, as declarations and nested rules in source order.
fn Lowerer::block_items(
  self : Lowerer,
  block : @sast.Node,
  ctx : Ctx,
) -> Array[@ast.BlockItem] raise @err.ShrubCssError {
  let out : Array[@ast.BlockItem] = []
  let groups = match block.it {
    Block(gs) => gs
    _ => return out
  }
  for g in groups {
    match as_comment(g) {
      Some(c) => {
        out.push(Comment(c))
        continue
      }
      None => ()
    }
    let items = match g.it {
      Group(xs) => xs
      _ => {
        out.push(Bogus(self.bogus(BadSelector, g)))
        continue
      }
    }
    let (head, blk, has_alts) = split_block(items[:])
    if has_alts {
      out.push(Bogus(self.bogus(AltsUnsupported, g)))
      continue
    }
    match blk {
      None => {
        // A bare term: not a declaration, not a rule. When it names a
        // pseudo-class it is the `a: hover` mistake wearing its top-level
        // spelling, so it gets that diagnostic rather than a vaguer one.
        let what = describe_head(head)
        let css_what = @names.unkebab(what)
        let kind : @kind.ErrorKind = if @names.is_simple_pseudo(css_what) ||
          @names.is_pseudo_element(css_what) {
          ColonPseudo("", css_what)
        } else {
          BareTermInBlock(what)
        }
        self.error(kind, g.span)
        out.push(Bogus(bogus_node(kind, g)))
      }
      Some(b) =>
        if is_declaration(head, b, ctx) {
          match self.declaration(g, head, b) {
            Some(d) => out.push(Decl(d))
            None => ()
          }
        } else {
          match self.statement(g, ctx) {
            Some(r) => out.push(Rule(r))
            None => ()
          }
        }
    }
  }
  out
}

///|
/// A declaration block: the same, but a nested rule is an error rather than a
/// possibility.
fn Lowerer::decl_block(
  self : Lowerer,
  block : @sast.Node,
) -> @ast.DeclBlock raise @err.ShrubCssError {
  { decls: self.block_items(block, DeclOnly), span: cspan(block.span), }
}

// ------------------------------------------------------------ declarations

///|
fn Lowerer::declaration(
  self : Lowerer,
  g : @sast.Node,
  head : ArrayView[@sast.Node],
  block : @sast.Node,
) -> @ast.Declaration? raise @err.ShrubCssError {
  let property : @ast.PropertyName = match as_dashed(head) {
    Some(name) => Custom(name)
    None =>
      match as_call(head) {
        Some((name, args, _)) if name == "ident" =>
          match literal_string(args) {
            Some(s) => Ident(s)
            None => Ident("")
          }
        _ =>
          match head {
            [{ it: Id(n), .. }] => {
              // The one place a CSS habit produces a wrong tree rather than an
              // error: `a: hover` is structurally a declaration and reads as
              // one. Saying so is the whole point of the diagnostic.
              self.warn_colon_pseudo(n, block, g)
              Ident(@names.unkebab(n))
            }
            _ => Ident("")
          }
      }
  }
  let groups = match block.it {
    Block(gs) => gs
    _ => []
  }
  if groups.length() > 1 {
    // `;` after a value puts the next declaration INSIDE this one's block.
    self.error(SemicolonDeclarations, groups[1].span)
  }
  if groups.length() == 0 {
    return None
  }
  let (value, important) = self.value_of(groups[0])
  Some({ property, value, important, span: cspan(g.span), })
}

///|
/// `a: hover`, guarded so that `cursor: default` stays quiet.
///
/// The guard is what makes the check usable: `default`, `link`, `first` and
/// `left` are all pseudo-class names AND ordinary CSS values, so firing on the
/// value alone would cry wolf on every third stylesheet. Requiring the property
/// to be one CSS does not have is what narrows it to the real mistake.
fn Lowerer::warn_colon_pseudo(
  self : Lowerer,
  property : String,
  block : @sast.Node,
  g : @sast.Node,
) -> Unit {
  if is_known_property(@names.unkebab(property)) {
    return
  }
  match block.it {
    Block([{ it: Group([{ it: Id(v), .. }]), .. }]) => {
      let css_v = @names.unkebab(v)
      if @names.is_simple_pseudo(css_v) || @names.is_pseudo_element(css_v) {
        self.diags.push(
          @err.Diagnostic::new(ColonPseudo(property, css_v), g.span),
        )
      }
    }
    _ => ()
  }
}

// --------------------------------------------------------------- mechanics

///|
fn Lowerer::error(
  self : Lowerer,
  kind : @kind.ErrorKind,
  span : @basic.Span,
) -> Unit raise @err.ShrubCssError {
  let d = @err.Diagnostic::new(kind, span)
  self.diags.push(d)
  if self.strict {
    d.raise_()
  }
}

///|
/// A `Bogus` node, with its diagnostic recorded.
fn Lowerer::bogus(
  self : Lowerer,
  kind : @kind.ErrorKind,
  n : @sast.Node,
) -> @ast.Bogus raise @err.ShrubCssError {
  self.error(kind, n.span)
  bogus_node(kind, n)
}

///|
/// A `Bogus` node whose diagnostic the caller has already recorded.
fn bogus_node(kind : @kind.ErrorKind, n : @sast.Node) -> @ast.Bogus {
  // The CSS layer's kinds and this one's are different sets, so the reason is
  // carried across as text. What the printer echoes is the shrubbery source,
  // which is the useful half anyway.
  @ast.Bogus::new(
    @csskind.ErrorKind::Unexpected(kind.code()),
    cspan(n.span),
    text=n.to_source(),
  )
}

///|
fn Lowerer::bogus_rule(
  self : Lowerer,
  kind : @kind.ErrorKind,
  n : @sast.Node,
) -> @ast.CssRule raise @err.ShrubCssError {
  Bogus(self.bogus(kind, n))
}

///|
/// A shrubbery span as a CSS one. Both are UTF-16 code unit offsets, so this
/// reads a field rather than converting anything.
fn cspan(s : @basic.Span) -> @cspan.Span {
  @cspan.Span::new(s.start.idx, s.end.idx)
}

///|
/// A shrubbery diagnostic, carried across with CSS-specific help attached.
///
/// A CSS author's first mistakes are all lexical -- `10px`, `#fff`, `@media` --
/// and shrubbery rejects each of them before this layer sees a tree at all. So
/// the enrichment happens here, on the way past: the original kind and span are
/// kept, and what is added is the sentence naming the thing to type instead.
fn shrub_diagnostic(d : @shrub_err.Diagnostic, src : String) -> @err.Diagnostic {
  let text = slice(src, d.span.start.idx, d.span.end.idx)
  @err.Diagnostic::new(lexical_hint(text), d.span)
}

///|
/// What a rejected run of source most likely meant.
fn lexical_hint(text : String) -> @kind.ErrorKind {
  if text.has_prefix("#") {
    SigilSelector("#")
  } else if text.has_prefix("@") {
    SigilSelector("@")
  } else if text == "~" {
    OperatorCombinator("~")
  } else if looks_dimension(text) {
    Unsupported("`" + text + "`: a CSS dimension is a call, like `px(10)`")
  } else {
    Unsupported("`" + text + "`")
  }
}

///|
/// `10px`, `2n` -- a number run into letters, which shrubbery rejects.
fn looks_dimension(text : String) -> Bool {
  if text.length() == 0 {
    return false
  }
  let mut seen_digit = false
  let mut seen_alpha = false
  for c in text {
    if c >= '0' && c <= '9' {
      if seen_alpha {
        return false
      }
      seen_digit = true
    } else if (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || c == '%' {
      if !seen_digit {
        return false
      }
      seen_alpha = true
    } else if c != '.' {
      return false
    }
  }
  seen_digit && seen_alpha
}

///|
fn slice(src : String, from : Int, to : Int) -> String {
  let a = if from < 0 { 0 } else { from }
  let b = if to < a { a } else { to }
  src.clamped_view(start=a, end=b).to_owned()
}

///|
/// A short name for what a head looks like, for a diagnostic to quote.
fn describe_head(head : ArrayView[@sast.Node]) -> String {
  match head {
    [{ it: Id(n), .. }, ..] => n
    [{ it: Kw(k), .. }, ..] => "~" + k
    [{ it: Op(o), .. }, ..] => o
    _ => "this"
  }
}

///|
/// Whether CSS has a property by this name.
///
/// Advisory, and short on purpose: it exists only to keep `colon_pseudo` from
/// firing on `cursor: default`, so it needs the properties whose values collide
/// with pseudo-class names, not all of them. Missing a property makes a
/// diagnostic slightly more eager, never wrong.
fn is_known_property(name : String) -> Bool {
  match name {
    "cursor" | "display" | "position" | "float" | "clear" | "color" => true
    "content" | "visibility" | "overflow" | "direction" | "resize" => true
    "all" | "appearance" | "user-select" | "pointer-events" => true
    "text-align" | "vertical-align" | "white-space" | "word-break" => true
    _ => name.has_prefix("--")
  }
}