// printf-like functions: format strings with conversions (`%d`, `%s`,
// ...) and pretty-printing directives (`@[`, `@]`, `@ `, ...), a port of
// OCaml's `Format.fprintf` & co and of the format string parser of
// `camlinternalFormat.ml` (in the default, legacy mode used by the OCaml
// compiler).
//
// Since MoonBit has no typed format strings, the format is parsed at run
// time and the arguments are given as an array of `Arg`. Invalid formats
// and arguments that don't match the format are programming errors, like
// type errors in OCaml: they abort.
//
// Copyright 1996 Institut National de Recherche en Informatique et en
// Automatique (OCaml), distributed under the terms of the GNU Lesser
// General Public License version 2.1, with the special exception on
// linking described in the file LICENSE.

///|
/// An argument of a printf-like function.
pub(all) enum Arg {
  /// `%d`, `%i`, `%u`, `%x`, `%X`, `%o` (with OCaml's 63-bit `int`
  /// semantics for unsigned conversions of negative numbers), widths and
  /// precisions given by `*`, and `%N`, `%l`, `%n`, `%L` used alone
  Int(Int)
  /// `%ld`, `%li`, ... (32-bit integers)
  Int32(Int)
  /// `%Ld`, `%nd`, ... (64-bit integers)
  Int64(Int64)
  /// `%f`, `%e`, `%E`, `%g`, `%G`, `%F`, `%h`, `%H`
  Float(Double)
  /// `%s`, `%S`
  String(String)
  /// `%c`, `%C`
  Char(Char)
  /// `%b`, `%B`
  Bool(Bool)
  /// `%a` and `%t`: a function printing on the formatter (OCaml's `%a`
  /// takes a printer and a value; here, the printer is already applied to
  /// its value)
  Print((Formatter) -> Unit)
  /// `%{ fmt %}` and `%( fmt %)`: a format string
  Format(String)
}

///|
priv enum Pad {
  NoPad
  LitPad(PadTy, Int)
  ArgPad(PadTy)
}

///|
priv enum Prec {
  NoPrec
  LitPrec(Int)
  ArgPrec
}

///|
priv enum FloatKind {
  /// `%f`, `%e`, `%E`, `%g`, `%G`
  FC(Char)
  /// `%F`
  FF
  /// `%#F`
  FCF
  /// `%h`
  FH
  /// `%H`
  FHU
}

///|
priv enum Conv {
  CChar
  CCamlChar
  CString
  CCamlString
  CInt(IntSize, Char, Bool, Bool, Bool, Bool) // size, conv, plus, space, alt, group
  CFloat(Char, FloatKind) // sign flag, kind
  CBool
  CAlpha
  CTheta
}

///|
priv enum FmtLit {
  CloseBox
  CloseTag
  BreakHint(Int, Int)
  FFlush
  ForceNewline
  FlushNewline
  MagicSize(Int)
  EscapedAt
  EscapedPercent
  ScanIndic(Char)
}

///|
priv enum Item {
  Lit(String)
  Conversion(Conv, Pad, Prec)
  Flush
  Formatting(FmtLit)
  OpenBoxGen(Array[Item])
  OpenTagGen(Array[Item])
  /// `%{ fmt %}`: consumes a format, prints the signature of `fmt`
  FormatArg(String)
  /// `%( fmt %)`: consumes a format and prints it with the next arguments
  /// (the signature of `fmt`)
  FormatSubst(String)
  /// `%N`, `%l`, `%n`, `%L` alone: consumes an int, prints it as `%u`
  ScanCounter
  /// `%_d`, `%[...]`, `%r`...: an error when output (the message, and the
  /// signature of the item)
  Invalid(String, String)
}

///|
/// OCaml's `%S`: a string in OCaml syntax.
fn string_to_caml_string(s : String) -> String {
  "\"" + string_escaped(s) + "\""
}

///|
/// OCaml's `%C`: a character in OCaml syntax.
fn caml_char(c : Char) -> String {
  "'" + char_escaped(c) + "'"
}

///|
/// Parse a box description such as ``, like OCaml's
/// `open_box_of_string` (the angle brackets are already removed).
fn open_box_of_string(str : String) -> (Int, BoxType) raise FormatError {
  if str == "" {
    return (0, Box)
  }
  let len = str.length()
  let invalid_box = () => {
    FormatError("invalid box description \{string_to_caml_string(str)}")
  }
  let mut i = 0
  while i < len && (str[i] == ' ' || str[i] == '\t') {
    i += 1
  }
  let wstart = i
  while i < len && str[i] >= 'a' && str[i] <= 'z' {
    i += 1
  }
  let box_name = str.view(start_offset=wstart, end_offset=i).to_owned()
  while i < len && (str[i] == ' ' || str[i] == '\t') {
    i += 1
  }
  let nstart = i
  while i < len && ((str[i] >= '0' && str[i] <= '9') || str[i] == '-') {
    i += 1
  }
  let indent = if nstart == i {
    0
  } else {
    // OCaml's integers have 63 bits
    let n = @string.parse_int64(str.view(start_offset=nstart, end_offset=i)) catch {
      _ => raise invalid_box()
    }
    if n > 0x7FFFFFFFL {
      0x7FFFFFFF
    } else if n < -0x80000000L {
      -0x80000000
    } else {
      n.to_int()
    }
  }
  while i < len && (str[i] == ' ' || str[i] == '\t') {
    i += 1
  }
  if i != len {
    raise invalid_box()
  }
  let box_type = match box_name {
    "" | "b" => Box
    "h" => HBox
    "v" => VBox
    "hv" => HVBox
    "hov" => HOVBox
    _ => raise invalid_box()
  }
  (indent, box_type)
}

// Execution

///|
priv struct Printer {
  args : ArrayView[Arg]
  mut next : Int
  /// size given by `@` for the next string or character
  mut magic_size : Int?
}

///|
fn arg_name(a : Arg) -> String {
  match a {
    Int(_) => "Int"
    Int32(_) => "Int32"
    Int64(_) => "Int64"
    Float(_) => "Float"
    String(_) => "String"
    Char(_) => "Char"
    Bool(_) => "Bool"
    Print(_) => "Print"
    Format(_) => "Format"
  }
}

///|
fn Printer::take(self : Printer, expected : String) -> Arg raise FormatError {
  if self.next >= self.args.length() {
    raise FormatError(
      "Format: missing argument \{self.next + 1}, expected \{expected}",
    )
  }
  let a = self.args[self.next]
  self.next += 1
  a
}

///|
fn Printer::mismatch(self : Printer, a : Arg, expected : String) -> FormatError {
  FormatError(
    "Format: argument \{self.next} is \{arg_name(a)}, expected \{expected}",
  )
}

///|
fn Printer::take_int(self : Printer) -> Int raise FormatError {
  match self.take("Int") {
    Int(n) => n
    a => raise self.mismatch(a, "Int")
  }
}

///|
fn Printer::pad_width(
  self : Printer,
  pad : Pad,
) -> (PadTy, Int)? raise FormatError {
  match pad {
    NoPad => None
    LitPad(t, w) => Some((t, w))
    ArgPad(t) => Some((t, self.take_int()))
  }
}

///|
fn Printer::precision(self : Printer, prec : Prec) -> Int? raise FormatError {
  match prec {
    NoPrec => None
    LitPrec(p) => Some(p)
    ArgPrec => Some(self.take_int())
  }
}

///|
fn apply_pad(p : (PadTy, Int)?, s : String) -> String {
  match p {
    None => s
    Some((t, w)) => fix_padding(t, w, s)
  }
}

///|
/// Output a string, with the size given by `@` if any.
fn Printer::output_string(self : Printer, ppf : Formatter, s : String) -> Unit {
  match self.magic_size {
    Some(size) => {
      self.magic_size = None
      ppf.print_as(size, s)
    }
    None => ppf.print_string(s)
  }
}

///|
fn Printer::output_char(self : Printer, ppf : Formatter, c : Char) -> Unit {
  match self.magic_size {
    Some(size) => {
      self.magic_size = None
      ppf.print_as(size, c.to_string())
    }
    None => ppf.print_char(c)
  }
}

///|
fn convert_float(
  kind : FloatKind,
  flag : Char,
  prec : Int,
  x : Double,
) -> String {
  let hex = () => hexstring_of_float(x, prec, flag)
  let caml_special_val = (s : String) => {
    if x.is_nan() {
      "nan"
    } else if x.is_inf() {
      if x < 0.0 {
        "neg_infinity"
      } else {
        "infinity"
      }
    } else {
      s
    }
  }
  match kind {
    FH => hex()
    FHU => hex().to_upper()
    FCF => caml_special_val(hex())
    FF => {
      let s = format_float_c(x, 'g', prec.abs(), sign=flag)
      let s = if s.iter().any(c => c == '.' || c == 'e' || c == 'E') {
        s
      } else {
        s + "."
      }
      caml_special_val(s)
    }
    FC(c) => format_float_c(x, c, prec.abs(), sign=flag)
  }
}

///|
fn Printer::run(
  self : Printer,
  ppf : Formatter,
  items : Array[Item],
) -> Unit raise FormatError {
  for item in items {
    match item {
      Lit(s) => self.output_string(ppf, s)
      Conversion(conv, pad, prec) => {
        let p = self.pad_width(pad)
        let pr = self.precision(prec)
        match conv {
          CChar =>
            match self.take("Char") {
              Char(c) => self.output_char(ppf, c)
              a => raise self.mismatch(a, "Char")
            }
          CCamlChar =>
            match self.take("Char") {
              Char(c) => self.output_string(ppf, caml_char(c))
              a => raise self.mismatch(a, "Char")
            }
          CString =>
            match self.take("String") {
              String(s) => self.output_string(ppf, apply_pad(p, s))
              a => raise self.mismatch(a, "String")
            }
          CCamlString =>
            match self.take("String") {
              String(s) =>
                self.output_string(ppf, apply_pad(p, string_to_caml_string(s)))
              a => raise self.mismatch(a, "String")
            }
          CBool =>
            match self.take("Bool") {
              Bool(b) =>
                self.output_string(
                  ppf,
                  apply_pad(p, if b { "true" } else { "false" }),
                )
              a => raise self.mismatch(a, "Bool")
            }
          CInt(size, c, plus, space, alt, group) => {
            let expected = match size {
              OInt => "Int"
              I32 => "Int32"
              I64 | Native => "Int64"
            }
            let n = match (size, self.take(expected)) {
              (OInt, Int(n)) => n.to_int64()
              (I32, Int32(n)) => n.to_int64()
              (I64 | Native, Int64(n)) => n
              (_, a) => raise self.mismatch(a, expected)
            }
            let s = format_int_c(n, size, c, plus~, space~, alt~)
            let s = if group { group_digits(s) } else { s }
            let s = match pr {
              Some(prec) => fix_int_precision(prec, s)
              None => s
            }
            self.output_string(ppf, apply_pad(p, s))
          }
          CFloat(flag, kind) => {
            let x = match self.take("Float") {
              Float(x) => x
              a => raise self.mismatch(a, "Float")
            }
            let prec = match pr {
              Some(prec) => prec
              None => if kind is FF { 12 } else { -6 }
            }
            self.output_string(
              ppf,
              apply_pad(p, convert_float(kind, flag, prec, x)),
            )
          }
          CAlpha | CTheta => {
            self.magic_size = None
            match self.take("Print") {
              Print(f) => f(ppf)
              a => raise self.mismatch(a, "Print")
            }
          }
        }
      }
      Flush => {
        self.magic_size = None
        ppf.print_flush()
      }
      Formatting(lit) => {
        self.magic_size = None
        match lit {
          CloseBox => ppf.close_box()
          CloseTag => ppf.close_tag()
          BreakHint(width, offset) => ppf.print_break(width, offset)
          FFlush => ppf.print_flush()
          ForceNewline => ppf.force_newline()
          FlushNewline => ppf.print_newline()
          MagicSize(size) => self.magic_size = Some(size)
          EscapedAt => ppf.print_char('@')
          EscapedPercent => ppf.print_char('%')
          ScanIndic(c) => {
            ppf.print_char('@')
            ppf.print_char(c)
          }
        }
      }
      OpenTagGen(sub) => {
        self.magic_size = None
        let tag = self.compute_tag(sub)
        ppf.open_stag(StringTag(tag))
      }
      OpenBoxGen(sub) => {
        self.magic_size = None
        let (indent, bty) = open_box_of_string(self.compute_tag(sub))
        ppf.open_box_gen(indent, bty)
      }
      FormatArg(sig) =>
        match self.take("Format") {
          Format(_) => self.output_string(ppf, sig)
          a => raise self.mismatch(a, "Format")
        }
      FormatSubst(sub_fmtty) =>
        match self.take("Format") {
          Format(s) => {
            let p : FormatParser = { str: s, }
            let fmt = p.parse(0, s.length())
            // the format must have the type of the sub-format
            if signature(fmt) != sub_fmtty {
              raise FormatError("CamlinternalFormat.Type_mismatch")
            }
            self.run(ppf, fmt.to_array())
          }
          a => raise self.mismatch(a, "Format")
        }
      ScanCounter => {
        let n = self.take_int()
        self.output_string(
          ppf,
          format_int_c(
            n.to_int64(),
            OInt,
            'u',
            plus=false,
            space=false,
            alt=false,
          ),
        )
      }
      Invalid(msg, _) => raise FormatError(msg)
    }
  }
}

///|
/// Print the description of a box or tag into a string and remove its
/// first and last characters (the angle brackets), like OCaml's
/// `compute_tag`.
fn Printer::compute_tag(
  self : Printer,
  sub : Array[Item],
) -> String raise FormatError {
  let buf = StringBuilder()
  let ppf = Formatter::of_buffer(buf)
  self.run(ppf, sub)
  ppf.print_flush()
  let s = buf.to_string()
  if s.length() < 2 {
    s
  } else {
    s.view(start_offset=1, end_offset=s.length() - 1).to_owned()
  }
}

///|
/// Check that a format string is valid, like the OCaml compiler does for
/// format literals: printing with an invalid format aborts the program.
/// The error messages are OCaml's. Some errors (e.g. an invalid box
/// description) are only detected when printing, like in OCaml.
pub fn check_format(fmt : String) -> Unit raise Failure {
  let p : FormatParser = { str: fmt, }
  ignore(p.parse(0, fmt.length())) catch {
    FormatError(msg) => raise Failure(msg)
  }
}

///|
fn parse_format_string(fmt : String) -> Array[Item] {
  let p : FormatParser = { str: fmt, }
  p.parse(0, fmt.length()).to_array() catch {
    FormatError(msg) => abort(msg)
  }
}

///|
/// Print on the formatter according to a format string, like OCaml's
/// `Format.fprintf`. Conversions take their values from `args`, in order.
///
/// Invalid formats and arguments that don't match the format abort the
/// program, since they are programming errors (type errors in OCaml).
pub fn Formatter::printf(
  self : Formatter,
  fmt : String,
  args : ArrayView[Arg],
) -> Unit {
  let items = parse_format_string(fmt)
  let p : Printer = { args, next: 0, magic_size: None, }
  p.run(self, items) catch {
    FormatError(msg) => abort(msg)
  }
  if p.next < args.length() {
    abort("Format: too many arguments for format \{string_to_caml_string(fmt)}")
  }
}

///|
/// Like `printf`, then call `k` with the formatter, like OCaml's
/// `Format.kfprintf`.
pub fn[T] Formatter::kprintf(
  self : Formatter,
  k : (Formatter) -> T,
  fmt : String,
  args : ArrayView[Arg],
) -> T {
  self.printf(fmt, args)
  k(self)
}

///|
/// Like `asprintf`, then call `k` with the result, like OCaml's
/// `Format.kasprintf`.
pub fn[T] kasprintf(
  k : (String) -> T,
  fmt : String,
  args : ArrayView[Arg],
) -> T {
  k(asprintf(fmt, args))
}

///|
/// Format into a string, like OCaml's `Format.asprintf`: the material is
/// printed with a fresh formatter (margin 78) that is flushed at the end.
pub fn asprintf(fmt : String, args : ArrayView[Arg]) -> String {
  let buf = StringBuilder()
  let ppf = Formatter::of_buffer(buf)
  ppf.printf(fmt, args)
  ppf.flush_queue(false)
  buf.to_string()
}

///|
/// Format into a string, like OCaml's `Format.sprintf`. The functions of
/// `%a` and `%t` print on the formatter, as with `asprintf`.
pub fn sprintf(fmt : String, args : ArrayView[Arg]) -> String {
  asprintf(fmt, args)
}

///|
/// Delayed printing, like OCaml's `Format.dprintf`: the result prints on
/// the formatter it is given.
pub fn dprintf(fmt : String, args : ArrayView[Arg]) -> (Formatter) -> Unit {
  let args = args.to_owned()
  ppf => ppf.printf(fmt, args)
}