// Inlines.
// NOTE: Document data in inline nodes is always stored unescaped and with entity and character references resolved.

///|
pub(all) enum Inline {
  Autolink(Node[InlineAutolink])
  Break(Node[InlineBreak])
  CodeSpan(Node[InlineCodeSpan])
  Emphasis(Node[InlineEmphasis])
  Image(Node[InlineLink])
  Inlines(Node[Seq[Inline]])
  Link(Node[InlineLink])
  RawHtml(Node[InlineRawHtml])
  StrongEmphasis(Node[InlineEmphasis])
  Text(Node[InlineText])
  ExtStrikethrough(Node[InlineStrikethrough])
  ExtMathSpan(Node[InlineMathSpan])
} derive(Eq, Debug)

///|
#deprecated
#doc(hidden)
pub extend Inline with Eq::{not_equal, equal}

///|
#deprecated
#doc(hidden)
pub extend Inline with @debug.Debug::{to_repr}

///|
pub fn Inline::empty() -> Inline {
  Inlines({ v: [], meta: Meta::none(), })
}

///|
pub fn Inline::is_empty(self : Inline) -> Bool {
  match self {
    Inlines({ v: raw, .. }) => raw.is_empty()
    Text({ v: raw, .. }) => raw.is_empty()
    _ => false
  }
}

///|
/// `meta(i)` is the metadata of `i`.
pub fn Inline::meta(
  self : Inline,
  // ext~ : (Inline) -> Meta!InvalidArgumentError = fn(_i) {
  //   raise InvalidArgumentError("Inline.meta")
  // }
) -> Meta {
  match self {
    Autolink({ meta, .. })
    | Break({ meta, .. })
    | CodeSpan({ meta, .. })
    | Emphasis({ meta, .. })
    | Image({ meta, .. })
    | Inlines({ meta, .. })
    | Link({ meta, .. })
    | RawHtml({ meta, .. })
    | StrongEmphasis({ meta, .. })
    | Text({ meta, .. })
    | ExtStrikethrough({ meta, .. })
    | ExtMathSpan({ meta, .. }) => meta
  }
}

///|
/// `normalize(i)` has the same content as `i` but is such that for any
/// occurence of `Inlines(is, _)` in `i` the list of inlines `is`:
/// - `is` is not a singleton list.
/// - Has no two consecutive `Text _` cases. If that occurs the texts are
///   concatenated, the meta of the first one is kept and its text
///   location extended to include the second one.
/// - Has no `Inlines _` case. The meta is dropped and the nested
///   inlines are spliced in `is` where the case occurs.
pub fn Inline::normalize(self : Inline) -> Inline {
  match self {
    Autolink(_)
    | Break(_)
    | CodeSpan(_)
    | RawHtml(_)
    | Text(_)
    | Inlines({ v: Seq([]), .. })
    | ExtMathSpan(_) as i => i
    Image({ v: raw, meta, }) =>
      Image({ v: { ..raw, text: raw.text.normalize(), }, meta, })
    Link({ v: raw, meta, }) =>
      Link({ v: { ..raw, text: raw.text.normalize(), }, meta, })
    Emphasis({ v: raw, meta, }) =>
      Emphasis({ v: { ..raw, inline: raw.inline.normalize(), }, meta, })
    StrongEmphasis({ v: raw, meta, }) =>
      StrongEmphasis({ v: { ..raw, inline: raw.inline.normalize(), }, meta, })
    ExtStrikethrough({ v: raw, meta, }) =>
      ExtStrikethrough({ v: raw.0.normalize(), meta, })
    Inlines({ v: Seq([i]), .. }) => i
    Inlines({ v: Seq([i, .. is_]), meta, }) => {
      let acc = [i.normalize()]
      let is_ = Array::from_iter(is_.iter())
      for ;; {
        guard is_.pop() is Some(curr) else { break }
        match curr {
          Inlines({ v: is1, .. }) =>
            for i1 in is1.0.rev() {
              is_.push(i1)
            }
          Text({ v: t1, meta: meta1, }) as i1 =>
            match acc {
              [.., Text({ v: t, meta, })] =>
                acc[acc.length() - 1] = Text({
                  v: t + t1,
                  meta: { ..meta, loc: meta.loc.span(meta1.loc), },
                })
              _ => acc.push(i1.normalize())
            }
          i => acc.push(i.normalize())
        }
      }
      match acc {
        [i] => i
        is_ => Inlines({ v: is_, meta, })
      }
    }
  }
}

///|
/// `to_plain_text(i, break_on_soft~, acc~)` has the plain text of `i`
/// as a sequence of lines represented by a list of strings to be
/// concatenated. If `break_on_soft` is `true` soft line breaks
/// are turned into hard line breaks.
pub fn Inline::to_plain_text(
  self : Inline,
  break_on_soft~ : Bool,
) -> Seq[Seq[String]] {
  let acc = Seq::from_array([Seq::empty()])
  let push = fn(s) { acc[acc.length() - 1].0.push(s) }
  let newline = fn() { acc.0.push([]) }
  let st = [self]
  for ;; {
    guard st.pop() is Some(curr) else { break }
    match curr {
      Autolink(a) => push("<\{a.v.link.v}>")
      Break({ v: { ty: Hard, .. }, .. }) => newline()
      Break({ v: { ty: Soft, .. }, .. }) =>
        if break_on_soft {
          newline()
        } else {
          push(" ")
        }
      CodeSpan(cs) => push(cs.v.code())
      Emphasis({ v: { inline, .. }, .. })
      | StrongEmphasis({ v: { inline, .. }, .. }) => st.push(inline)
      Inlines({ v: is1, .. }) =>
        for i in is1.0.rev() {
          st.push(i)
        }
      Link(l) | Image(l) => st.push(l.v.text)
      RawHtml(_) => ()
      Text(t) => push(t.v)
      ExtStrikethrough(i) => st.push(i.v.0)
      ExtMathSpan(m) => push(m.v.tex())
    }
  }
  acc
}

///|
pub fn Inline::id(
  self : Inline,
  buf~ : StringBuilder = StringBuilder(size_hint=256),
) -> String {
  let text = self.to_plain_text(break_on_soft=false)
  let s = text.0.map(fn(l) { l.0.join("") }).join("\n")
  // Collapses non initial white 
  let collapse_blanks = fn(prev) {
    if @char.is_ascii_blank(prev) && !buf.is_empty() {
      buf.write_char('-')
    }
  }
  let max = s.length() - 1
  for prev = '\u{0}', k = 0 {
    if k > max {
      break buf.to_string()
    }
    match s.get_char(k).unwrap() {
      ' ' | '\t' as prev_ccode => continue prev_ccode, k + 1
      '_' | '-' as c => {
        collapse_blanks(prev)
        buf.write_char(c)
        continue c, k + 1
      }
      c => {
        collapse_blanks(prev)
        let mut u = s.get_char(k).unwrap()
        if u == '\u{0000}' {
          u = @char.rep
        }
        let k = k + @char.length_utf16(u.to_int())
        if @char.is_ascii_punctuation(c) || is_punctuation(c) {
          continue '\u{0}', k
        }
        match @casefold.casefold_char(u) {
          One(c) => buf.write_char(c)
          Two(c1, c2) => {
            buf.write_char(c1)
            buf.write_char(c2)
          }
          Three(c1, c2, c3) => {
            buf.write_char(c1)
            buf.write_char(c2)
            buf.write_char(c3)
          }
        }
        let prev = c
        continue prev, k
      }
    }
  }
}

///|
pub(all) struct InlineAutolink {
  /// https://spec.commonmark.org/0.30/#email-address
  is_email : Bool
  /// https://spec.commonmark.org/0.30/#absolute-uri
  /// https://spec.commonmark.org/0.30/#email-address
  link : StringNode
} derive(Eq, Debug)

///|
#deprecated
#doc(hidden)
pub extend InlineAutolink with Eq::{not_equal, equal}

///|
#deprecated
#doc(hidden)
pub extend InlineAutolink with @debug.Debug::{to_repr}

///|
pub fn InlineAutolink::new(link : StringNode) -> InlineAutolink {
  let raw_link = "<{$link.raw}>"
  { is_email: !(@cmark_base.autolink_email(raw_link) is None), link, }
}

///|
pub(all) struct InlineBreak {
  layout_before : BlanksNode
  ty : InlineBreakType
  layout_after : BlanksNode
} derive(Eq, Debug)

///|
#deprecated
#doc(hidden)
pub extend InlineBreak with Eq::{not_equal, equal}

///|
#deprecated
#doc(hidden)
pub extend InlineBreak with @debug.Debug::{to_repr}

///|
pub fn InlineBreak::new(
  layout_before~ : BlanksNode = layout_empty,
  layout_after~ : BlanksNode = layout_empty,
  ty : InlineBreakType,
) -> InlineBreak {
  { layout_before, ty, layout_after, }
}

///|
pub(all) enum InlineBreakType {
  Hard
  Soft
} derive(Eq, Compare, Debug, FromJson, ToJson)

///|
#deprecated
#doc(hidden)
pub extend InlineBreakType with Eq::{not_equal, equal}

///|
#deprecated
#doc(hidden)
pub extend InlineBreakType with Compare::{op_lt, op_le, op_ge, compare, op_gt}

///|
#deprecated
#doc(hidden)
pub extend InlineBreakType with @debug.Debug::{to_repr}

///|
#deprecated
#doc(hidden)
pub extend InlineBreakType with @json.FromJson::{from_json}

///|
#deprecated
#doc(hidden)
pub extend InlineBreakType with ToJson::{to_json}

///|
/// The type for [code spans](https://spec.commonmark.org/0.30/#code-spans).
pub(all) struct InlineCodeSpan {
  backticks : Count
  code_layout : Seq[Tight]
} derive(Eq, Debug)

///|
#deprecated
#doc(hidden)
pub extend InlineCodeSpan with Eq::{not_equal, equal}

///|
#deprecated
#doc(hidden)
pub extend InlineCodeSpan with @debug.Debug::{to_repr}

///|
pub fn InlineCodeSpan::new(
  backticks~ : Count,
  code_layout : Seq[Tight],
) -> InlineCodeSpan {
  { backticks, code_layout, }
}

///|
fn InlineCodeSpan::min_backticks(
  min~ : Count,
  counts : @sorted_set.SortedSet[Count],
) -> Count {
  let mut min = min
  for c in counts {
    guard min == c else { return min }
    min = c + 1
  }
  min
}

///|
/// `from_string(s)` is a code span for `s`. `s` can start with or
/// include backticks; the appropriate minimal backtick count and
/// possible needed leading and trailing space are computed
/// accordingly. If `s` contains newlines, blanks after newlines
/// are treated as layout like during parsing. `meta` is used for
/// the lines of the resulting code layout (see `code_layout`).
/// the resulting code layout (see `code_layout`).
pub fn InlineCodeSpan::from_string(
  meta~ : Meta = Meta::none(),
  s : String,
) -> InlineCodeSpan {
  if s.is_empty() {
    return { backticks: 1, code_layout: [Tight::empty(meta~)], }
  }
  // This finds out the needed backtick count, whether spaces are needed,
  // and treats blanks after newline as layout 
  let max = s.length() - 1
  let need_sp = s is (['`', ..] | [.., '`'])
  let s = if need_sp { " \{s} " } else { s }
  let bt_counts = @sorted_set.SortedSet([])
  let acc = []
  let code_layout = for btc = 0, start = 0, k = 0 {
    if k > max {
      // assert (btc = 0) because of [need_sp]
      let layout = if acc.is_empty() {
        [{ blanks: "", node: { v: s, meta, }, }]
      } else {
        BlockLine::flush_tight(meta~, s, start, max, acc)
        acc
      }
      break layout
    }
    let sk = s[k]
    if sk == '`' {
      continue btc + 1, start, k + 1
    }
    if btc > 0 {
      bt_counts.add(btc)
    }
    guard sk is ('\n' | '\r') else { continue 0, start, k + 1 }
    BlockLine::flush_tight(meta~, s, start, k - 1, acc)
    let start = if k + 1 <= max && sk == '\r' && s[k + 1] == '\n' {
      k + 2
    } else {
      k + 1
    }
    continue 0, start, start
  }
  { code_layout, backticks: InlineCodeSpan::min_backticks(min=1, bt_counts), }
}

///|
/// https://spec.commonmark.org/0.30/#code-spans
pub fn InlineCodeSpan::code(self : InlineCodeSpan) -> String {
  let s = self.code_layout.iter().map(Tight::to_string).to_array().join(" ")
  guard s != "" else { "" }
  if s is [' ', .. mid, ' '] && !mid.iter().all(fn(it) { it == ' ' }) {
    return s[1:s.length() - 1].to_owned()
  }
  s
}

///|
/// The type for emphasis and strong emphasis.
/// https://spec.commonmark.org/0.30/#emphasis-and-strong-emphasis
pub(all) struct InlineEmphasis {
  delim : Char
  inline : Inline
} derive(Eq, Debug)

///|
#deprecated
#doc(hidden)
pub extend InlineEmphasis with Eq::{not_equal, equal}

///|
#deprecated
#doc(hidden)
pub extend InlineEmphasis with @debug.Debug::{to_repr}

///|
pub fn InlineEmphasis::new(
  delim~ : Char = '*',
  inline : Inline,
) -> InlineEmphasis {
  { delim, inline, }
}

///|
pub(all) struct InlineLink {
  text : Inline
  reference : ReferenceKind
} derive(Eq, Debug)

///|
#deprecated
#doc(hidden)
pub extend InlineLink with Eq::{not_equal, equal}

///|
#deprecated
#doc(hidden)
pub extend InlineLink with @debug.Debug::{to_repr}

///|
pub(all) enum ReferenceKind {
  /// https://spec.commonmark.org/0.30/#inline-link
  Inline(Node[LinkDefinition])
  /// https://spec.commonmark.org/0.30/#reference-link
  /// First label is the label of the reference, second label is the label of the referenced definition.
  Ref(ReferenceLayout, Label, Label)
} derive(Eq, Debug)

///|
#deprecated
#doc(hidden)
pub extend ReferenceKind with Eq::{not_equal, equal}

///|
#deprecated
#doc(hidden)
pub extend ReferenceKind with @debug.Debug::{to_repr}

///|
/// The type for reference link layouts.
pub(all) enum ReferenceLayout {
  /// https://spec.commonmark.org/0.30/#collapsed-reference-link
  Collapsed
  /// https://spec.commonmark.org/0.30/#full-reference-link
  Full
  /// https://spec.commonmark.org/0.30/#shortcut-reference-link
  Shortcut
} derive(Eq, Debug, FromJson, ToJson)

///|
#deprecated
#doc(hidden)
pub extend ReferenceLayout with Eq::{not_equal, equal}

///|
#deprecated
#doc(hidden)
pub extend ReferenceLayout with @debug.Debug::{to_repr}

///|
#deprecated
#doc(hidden)
pub extend ReferenceLayout with @json.FromJson::{from_json}

///|
#deprecated
#doc(hidden)
pub extend ReferenceLayout with ToJson::{to_json}

///|
pub fn InlineLink::new(text : Inline, reference : ReferenceKind) -> InlineLink {
  { text, reference, }
}

///|
/// `referenced_label(l)` is the label referenced by the label of `l`.
/// This is the second label of `Ref(_)` or `None` on inline references.
pub fn InlineLink::referenced_label(self : InlineLink) -> Label? {
  match self.reference {
    Inline(_) => None
    Ref(_, _, label) => Some(label)
  }
}

///|
/// `reference_definition(defs, l)` is the definition of `l`'s reference.
/// If `l` is an `Inline` reference this returns its link definition
/// wrapped in a `LinkDef`. If `l` is `Ref` this looks up the
/// `referenced_label` in `defs`.
pub fn InlineLink::reference_definition(
  self : InlineLink,
  defs : LabelDefs,
) -> LabelDef? {
  match self.reference {
    Inline(ld) => Some(LinkDef(ld))
    Ref(_, _, def) => defs.get(def.key)
  }
}

///|
/// `is_unsafe(l)` is `true` if `l` is deemed unsafe.
/// This is the case if `l` starts with a caseless match of
/// `javascript:`, `vbscript:`, `file:` or `data:` except if
/// `data:image/{gif,png,jpeg,webp}`.
/// These rules were taken from the C reference implementation of CommonMark
/// and are likely incomplete. If you are trying to prevent XSS you should
/// post-process rendering outputs with a dedicated HTML sanitizer.
pub fn InlineLink::is_unsafe(l : String) -> Bool {
  let allowed_data_url = fn(l : String) {
    // Extract mediatype from data:[][;base64],
    let start_offset = 4
    let j = match l.view(start_offset~).find(",") {
      Some(i) => i + start_offset
      None => return false
    }
    let k = match l.view(start_offset~).find(";") {
      Some(i) => i + start_offset
      None => j
    }
    let allowed = ["image/gif", "image/png", "image/jpeg", "image/webp"]
    allowed.contains(l[5:@cmp.minimum(j, k)].to_owned())
  }
  let lower = @casefold.casefold(l)
  lower.has_prefix("javascript:") ||
  lower.has_prefix("vbscript:") ||
  lower.has_prefix("file:") ||
  (lower.has_prefix("data:") && !allowed_data_url(l))
}

///|
/// The type for [inline raw HTML](https://spec.commonmark.org/0.30/#raw-html) (can span multiple lines).
pub(all) struct InlineRawHtml(Seq[Tight]) derive(Eq, Debug)

///|
#deprecated
#doc(hidden)
pub extend InlineRawHtml with Eq::{not_equal, equal}

///|
#deprecated
#doc(hidden)
pub extend InlineRawHtml with @debug.Debug::{to_repr}

///|
/// The type for [textual content](https://spec.commonmark.org/0.30/#textual-content).
///
/// Normally these strings should not contain newlines. This can
/// however happen if the source had newlines as
/// [character references](https://spec.commonmark.org/0.30/#entity-and-numeric-character-references).
pub type InlineText = String

// Extensions

///|
pub(all) struct InlineStrikethrough(Inline) derive(Eq, Debug)

///|
#deprecated
#doc(hidden)
pub extend InlineStrikethrough with Eq::{not_equal, equal}

///|
#deprecated
#doc(hidden)
pub extend InlineStrikethrough with @debug.Debug::{to_repr}

///|
pub(all) struct InlineMathSpan {
  display : Bool
  tex_layout : Seq[Tight]
} derive(Eq, Debug)

///|
#deprecated
#doc(hidden)
pub extend InlineMathSpan with Eq::{not_equal, equal}

///|
#deprecated
#doc(hidden)
pub extend InlineMathSpan with @debug.Debug::{to_repr}

///|
pub fn InlineMathSpan::tex(self : Self) -> String {
  self.tex_layout.0.map(Tight::to_string).join(" ")
}