//
// Copyright (c) 2024, Brian Frank and Andy Frank
// Licensed under the Academic Free License version 3.0
//
// History:
//   17 Jun 24  Brian Frank  Creation
//

**
** Console provides utilities to interact with the terminal console.
** For Java this API is designed to use [jline]`docTools::Setup#jline`
** if installed.  In browser JavaScript environments this APIs uses
** the JS debugging window.
**
@Js
native const final class Console
{
  ** Get the default console for the virtual machine
  static Console cur()

  ** Number of chars that fit horizontally in console or null if unknown
  Int? width()

  ** Number of lines that fit vertically in console or null if unknown
  Int? height()

  ** Print a message to the console at the debug level
  This debug(Obj? msg)

  ** Print a message to the console at the informational level
  This info(Obj? msg)

  ** Print a message to the console at the warning level
  This warn(Obj? msg)

  ** Print a message to the console at the error level
  This err(Obj? msg)

  ** Print tabular data to the console:
  **  - List of list is two dimensional data where first row is header names
  **  - List of items with an each method will create column per key
  **  - List of items without each will map to a column of "val"
  **  - Anything else will be table of one cell table
  This table(Obj? obj)

  ** Clear the console of all text if supported
  This clear()

  ** Enter an indented group level in the console.  The JS debug
  ** window can specify the group to default in a collapsed state (this
  ** flag is ignored in a standard terminal).
  This group(Obj? msg, Bool collapsed := false)

  ** Exit an indented, collapsable group level
  This groupEnd()

  ** Prompt the user to enter a string from standard input.
  ** Return null if end of stream has been reached.
  Str? prompt(Str msg := "")

  ** Prompt the user to enter a password string from standard input
  ** with echo disabled.  Return null if end of stream has been reached.
  Str? promptPassword(Str msg := "")
}

**************************************************************************
** ConsoleTable
**************************************************************************

**
** ConsoleTable is helper class to coerce objects to tables
**
@NoDoc @Js
class ConsoleTable
{
  new make(Obj? x)
  {
    list := x as List

    // list of lists
    if (list != null && list.first is List)
    {
      headers = list[0]
      if (list.size > 1) rows = list[1..-1]
      return
    }

    // list of something
    if (list != null)
    {
      // turn each item in list to a Str:Str map
      maps := Str:Str[,]
      list.each |item| { maps.add(map(item)) }

      // create list of columns union
      cols := Str:Str[:] { ordered = true }
      maps.each |map|
      {
        map.each |v, k| { cols[k] = k }
      }
      headers = cols.vals

      // now turn each row Str:Str into a Str[] of cells
      maps.each |map|
      {
        row := Str[,]
        row.capacity = headers.size
        cols.each |k| { row.add(map[k] ?: "") }
        rows.add(row)
      }
      return
    }

    // scalar value
    headers = ["val"]
    rows = [[str(x)]]
  }

  Str[] headers := [,]
  Str[][] rows := [,]

  once Int[] widths()
  {
    widths := Int[,]
    widths.capacity = headers.size
    headers.each |h, c|
    {
      w := h.size
      rows.each |row|
      {
        w = w.max(row[c].size)
      }
      widths.add(w)
    }
    return widths
  }

  Void dump(Console c)
  {
    c.info(row(headers))
    c.info(underlines)
    rows.each |x| { c.info(row(x)) }
  }

  private Str row(Str[] cells)
  {
    s := StrBuf()
    cells.each |cell, i|
    {
      if (i > 0) s.add("  ")
      s.add(cell)
      s.add(Str.spaces(widths[i] - cell.size))
    }
    return s.toStr
  }

  private Str underlines()
  {
    s := StrBuf()
    headers.each |h, i|
    {
      if (i > 0) s.add("  ")
      widths[i].times { s.addChar('-') }
    }
    return s.toStr
  }

  static Str:Str map(Obj? x)
  {
    if (x == null) return ["val":"null"]

    m := x.typeof.method("each", false)
    if (m == null || x is Str) return ["val":str(x)]

    acc := Str:Str[:] { ordered = true }
    f := |v, k| { acc[str(k)] = str(v)}
    m.callOn(x, [f])
    return acc
  }

  private static Str str(Obj? x)
  {
    if (x == null) return "null"
    s := x.toStr
    if (s.contains("\n")) s = s.splitLines.first
    if (s.size > 80) s = s[0..80] + ".."
    return s
  }
}