//
// Copyright (c) 2024, Brian Frank and Andy Frank
// Licensed under the Academic Free License version 3.0
//
// History:
//   05 Nov 2024  Matthew Giannini  Creation
//

**
** Writer for Markdown (CommonMark) text.
**
@Js
class MarkdownWriter
{
  new make(OutStream out)
  {
    this.out = out
  }

  private OutStream out
  private Int blockSep := 0

  ** The last character that was written
  Int lastChar := 0 { private set }

  ** Wheter we're at the line start (not counting any prefixes),
  ** i.e. after a `line` or `block`.
  Bool atLineStart := true { private set }

  // stackso of settings that affect various rendering behaviors. The common pattern
  // here is that callers use "push" to change a setting, render some nodes, and then
  // "pop" the setting off the stack again to restore previous state
  private Str[] prefixes := [,]
  private Bool[] tight := [,]
  private |Int->Bool|[] rawEscapes := [,]

  ** Write the supplied string or character (raw/unescaped except if `pushRawEscape`
  ** was used).
  Void raw(Obj obj)
  {
    if (obj is Str) rawStr(obj)
    else rawCh(obj)
  }

  private Void rawStr(Str s)
  {
    flushBlockSeparator
    writeStr(s)
  }

  private Void rawCh(Int c)
  {
    flushBlockSeparator
    write(c)
  }

  ** Write the supplied string with escaping
  Void text(Str s, |Int->Bool|? escape := null)
  {
    if (s.isEmpty) return
    flushBlockSeparator
    writeStr(s, escape)

    lastChar = s[-1]
    atLineStart = false
  }

  ** Write a newline (line terminator).
  Void line()
  {
    write('\n')
    writePrefixes
    atLineStart = true
  }

  ** Enqueue a block separator to be written before the next text is written.
  ** Block separators are not written straight away because if there are no more blocks
  ** to write, we don't want a separator (at the end of the document)
  Void block()
  {
    // remember whether this should be a tight or loose separator now because tight
    // could get changed in between this and the next flush
    blockSep = isTight ? 1 : 2
    atLineStart = true
  }

  ** Push a prefix onto the top of the stack. All prefixes are written at the
  ** beginning of each line, until the prefix is popped again.
  Void pushPrefix(Str prefix) { prefixes.add(prefix) }

  ** Write a prefix
  Void writePrefix(Str prefix)
  {
    tmp := atLineStart
    rawStr(prefix)
    atLineStart = tmp
  }

  ** Remove the last prefix from the top of the stack
  Void popPrefix() { prefixes.pop }

  ** Change whether blocks are tight or loose. Loose is the default where blocks are
  ** separated by a blank line. Tight is where blocks are not separated by a blank line.
  ** Tight blocks are used in lists, if there are no blank lines within the list.
  **
  ** Note that changing this does not affect block separators that have already been
  ** enqueued with `block`, only future ones.
  Void pushTight(Bool tight) { this.tight.add(tight) }

  ** Remove the last "tight" setting from the top of the stack
  Void popTight() { this.tight.pop }

  ** Escape the characters matching the supplied matcher, in all text (text and raw).
  ** This might be usefult to extensions that add another layer of syntax, e.g. the
  ** tables extension that uses '|' to separate cells and needs all '|' characters to be
  ** escaped (even in code spans)
  Void pushRawEscape(|Int->Bool| rawEscape) { rawEscapes.add(rawEscape) }

  ** Remove the last raw escape from the top of the stack
  Void popRawEscape() { rawEscapes.pop }

  private Void write(Int c)
  {
    append(c)
    lastChar = c
    atLineStart = false
  }

  private Void writeStr(Str s, |Int->Bool|? escape := null)
  {
    if (rawEscapes.isEmpty && escape == null)
    {
      // normal fast path
      out.writeChars(s)
    }
    else
    {
      s.each |c| { append(c, escape) }
    }

    if (!s.isEmpty) lastChar = s[-1]
    atLineStart = false
  }

  private Void writePrefixes()
  {
    prefixes.each |prefix| { writeStr(prefix) }
  }

  ** If a block separator has been enqueued with `block` but not yet written, write it now
  private Void flushBlockSeparator()
  {
    if (blockSep != 0)
    {
      write('\n')
      writePrefixes
      if (blockSep > 1)
      {
        write('\n')
        writePrefixes
      }
      blockSep = 0
    }
  }

  private Void append(Int c, |Int->Bool|? escape := null)
  {
    if (needsEscaping(c, escape))
    {
      if (c == '\n')
      {
        // can't escape this with \, use numeric character reference
        out.writeChars("
")
      }
      else
      {
        out.writeChar('\\')
        out.writeChar(c)
      }
    }
    else out.writeChar(c)
  }

  private Bool isTight() { !tight.isEmpty && tight.last }

  private Bool needsEscaping(Int c, |Int->Bool|? escape)
  {
    (escape != null && escape(c)) || rawNeedsEscaping(c)
  }

  private Bool rawNeedsEscaping(Int c)
  {
   rawEscapes.any |esc| { esc(c) }
  }
}