///|
/// 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, }
}

///|
/// A lowering of a rule BODY: the items, and everything noticed on the way.
pub struct LoweredBlock {
  items : Array[@ast.BlockItem]
  diagnostics : Array[@err.Diagnostic]
}

///|
pub fn LoweredBlock::items(self : LoweredBlock) -> Array[@ast.BlockItem] {
  self.items
}

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

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

///|
/// Lower a parsed shrubbery tree as the BODY of a rule.
///
/// `lower` starts at the top of a file, where CSS has no declarations at all --
/// so a consumer holding a SCOPED fragment cannot use it. Everything that goes
/// inside one `[data-cid="3"] { ... }` is made of exactly the declarations that
/// are refused up there, and correctly refused: the context is the first level
/// of the rule-versus-declaration cascade, and at the top of a file the answer
/// really is "rule".
///
/// This is the same lowering in `Nested`, the context that admits both. A
/// declaration is a declaration, a nested rule is a nested rule, and a
/// top-level-only at-rule is refused the way it would be inside any other rule
/// body.
pub fn lower_block(
  root : @sast.Node,
  strict? : Bool = false,
) -> LoweredBlock raise @err.ShrubCssError {
  let l = { diags: [], strict, }
  let groups = match root.it {
    Multi(gs) => gs
    Block(gs) => gs
    // A bare group, which is what a one-line fragment parses to.
    Group(_) => [root]
    _ => []
  }
  { items: l.block_items_of(groups, Nested), diagnostics: l.diags, }
}

///|
/// Parse a scoped shrubbery-CSS fragment and lower it as a rule body.
pub fn lower_block_source(
  src : String,
  strict? : Bool = false,
) -> LoweredBlock raise @err.ShrubCssError {
  let parsed = @shrub.parse(src, recover=true) catch {
    e => {
      let d = shrub_diagnostic(e.diagnostic(), src)
      if strict {
        d.raise_()
      }
      return { items: [], 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_block(parsed.root(), strict~)
  let all : Array[@err.Diagnostic] = []
  for d in pre {
    all.push(d)
  }
  for d in out.diagnostics {
    all.push(d)
  }
  { items: out.items, diagnostics: all, }
}

///|
/// 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 reads it as a declaration itself. `Top` cannot arrive here --
        // `is_declaration` says so -- and the case it used to claim to handle
        // is the one below.
        None
      } else {
        match ctx {
          DeclOnly => Some(self.bogus_rule(RuleInDeclarationContext, g))
          Top =>
            // Level one settled what this IS: at the top of a file a group with
            // a block is a rule, because CSS has no declarations outside one.
            // It did not settle what to SAY about it. A group shaped like a
            // declaration here is a mistake with a name, and reading it as a
            // rule prints `color{ red}` -- which looks like valid CSS, is not,
            // and is worse than a refusal.
            if declaration_shape(head, b) && !colon_pseudo_shape(b) {
              Some(self.bogus_rule(DeclarationAtTopLevel, g))
            } else {
              let selectors = self.selector_list(head, g.span)
              let body = self.block_items(b, Nested)
              Some(Style({ selectors, body, span: cspan(g.span), }))
            }
          Nested => {
            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 => false
    // Not a bare `true`. `font_face()` holds declarations only, and saying so
    // by fiat made `rule_in_declaration_context` unreachable in exactly the way
    // `declaration_at_top_level` was: a nested rule in there was read as a
    // declaration and printed as `@font-face{a:color : red}`. The context still
    // decides -- a lone identifier over a value is a declaration here and would
    // be a rule at the top of a file -- it just no longer decides alone.
    DeclOnly => declaration_shape(head, block)
    Nested => declaration_shape(head, block)
  }
}

///|
/// Levels two and three alone: does this GROUP look like a declaration?
///
/// Split out from `is_declaration` because the two questions are different and
/// were once the same function. "Is it a declaration here" is settled by the
/// context first, and at the top of a file the answer is always no. "Does it
/// look like one" has no context in it, which is what lets the top-level case
/// report `declaration_at_top_level` instead of printing `color{ red}`.
fn declaration_shape(head : ArrayView[@sast.Node], block : @sast.Node) -> Bool {
  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
  }
}

///|
/// Is this block a single bare word naming a pseudo-class or pseudo-element?
///
/// The `a: hover` mistake, seen from the top of a file. It is declaration-
/// shaped, so `declaration_at_top_level` would fire and be right in the letter
/// and wrong in the spirit: the author meant `a:hover`, a selector, and
/// `colon_pseudo` is the diagnostic that says so. Letting it through to the
/// rule path is what puts it in front of the guard in `block_items_of`, which
/// is the one that already knows how to name it.
fn colon_pseudo_shape(block : @sast.Node) -> Bool {
  let groups = match block.it {
    Block(gs) => gs
    _ => return false
  }
  if groups.length() != 1 {
    return false
  }
  match groups[0].it {
    Group([{ it: Id(v), .. }]) => {
      let css_v = @names.unkebab(v)
      @names.is_simple_pseudo(css_v) || @names.is_pseudo_element(css_v)
    }
    _ => false
  }
}

///|
/// An at-rule written as a call, in a position where one is being looked for.
fn is_at_rule_call(head : ArrayView[@sast.Node]) -> Bool {
  match as_call(head) {
    Some((name, _, rest)) =>
      rest.length() == 0 && @names.is_at_rule(@names.unkebab(name))
    None => 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 {
  match block.it {
    Block(gs) => self.block_items_of(gs, ctx)
    _ => []
  }
}

///|
/// The same, over a group sequence that is not wrapped in a `Block`.
///
/// This is what `lower_block` needs: a scoped fragment parses to a `Multi`, and
/// its groups are a rule body without any rule around them.
fn Lowerer::block_items_of(
  self : Lowerer,
  groups : Array[@sast.Node],
  ctx : Ctx,
) -> Array[@ast.BlockItem] raise @err.ShrubCssError {
  let out : Array[@ast.BlockItem] = []
  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 blockless at-rule -- `charset("utf-8")`, `import("x.css")` -- has
        // no block, so without this it would be reported as a bare word. It is
        // not a bare word, it is an at-rule in a place that has none, and
        // `statement` is what knows how to say so.
        if is_at_rule_call(head) {
          match self.statement(g, ctx) {
            Some(r) => out.push(Rule(r))
            None => ()
          }
          continue
        }
        // 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 that was
  // written, PRINTED FLAT rather than reproduced from the node's raw metadata.
  //
  // Two reasons, and both are load-bearing. Reproducing the source carries its
  // newlines and its indentation, so a `Minified` stylesheet came back with a
  // literal line break inside it. And `to_source` reads `Node.meta`, which this
  // lowering promises never to do -- a hand-built tree has no metadata, so the
  // echo was empty for exactly the trees the property tests generate.
  @ast.Bogus::new(
    @csskind.ErrorKind::Unexpected(kind.code()),
    cspan(n.span),
    text=flat(n),
  )
}

///|
/// A shrubbery node on one line, for echoing inside a diagnostic or a `Bogus`.
///
/// `Style::Flat` is not it: flat writes a block as `« ... »`, which is legal
/// shrubbery and unreadable inside a CSS error. `Pretty` at a width nothing
/// reaches gives the same one line without the armouring, and the collapse
/// below makes that a guarantee rather than a hope -- a run of whitespace that
/// contains a newline becomes one space, and a run that does not is left alone,
/// so the spaces inside a string literal survive.
fn flat(n : @sast.Node) -> String {
  let s = @swrite.write(n, style=Pretty, width=Some(1_000_000))
  let out = StringBuilder()
  let run = StringBuilder()
  let mut broke = false
  let mut started = false
  fn flush() {
    let ws = run.to_string()
    if ws != "" {
      if broke {
        if started {
          out.write_char(' ')
        }
      } else {
        out.write_string(ws)
      }
    }
    run.reset()
    broke = false
  }

  for c in s {
    if c == '\n' || c == '\r' || c == '\t' || c == ' ' {
      if c == '\n' || c == '\r' {
        broke = true
      }
      run.write_char(c)
    } else {
      flush()
      out.write_char(c)
      started = true
    }
  }
  // A trailing run is dropped: it is the newline the printer ends on.
  out.to_string()
}

///|
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("--")
  }
}