//
// Copyright (c) 2008, Brian Frank and Andy Frank
// Licensed under the Academic Free License version 3.0
//
// History:
//   14 Sep 08  Brian Frank  Creation
//

using concurrent
using gfx
using fwt

**
** Console is used to run external programs and capture output.
**
class Console : SideBar
{

//////////////////////////////////////////////////////////////////////////
// Construction
//////////////////////////////////////////////////////////////////////////

  **
  ** Use `Frame.console` to get the console.
  **
  override Void onLoad()
  {
    model = ConsoleModel()
    model.clear
    richText = RichText
    {
      it.model = this.model
      it.editable = false
      it.border = false
      it.font = Desktop.sysFontMonospace
      it.onMouseUp.add |e| { onRichTextMouseDown(e) }
    }
    content = EdgePane
    {
      top = EdgePane
      {
        top = BorderPane
        {
          border = Border("1,0,1,0 $Desktop.sysNormShadow,#000,$Desktop.sysHighlightShadow")
        }
        bottom = InsetPane(4,4,4,4)
        {
          EdgePane
          {
            left = ToolBar
            {
              addCommand(copyCmd)
              addCommand(frame.command(CommandId.jumpPrev))
              addCommand(frame.command(CommandId.jumpNext))
            }
            right = ToolBar
            {
              addCommand(hideCmd)
            }
          },
        }
      }
      center = BorderPane
      {
        it.content = richText
        it.border = Border("1,0,0,1 $Desktop.sysNormShadow")
      }
    }
  }

//////////////////////////////////////////////////////////////////////////
// SideBar
//////////////////////////////////////////////////////////////////////////

  **
  ** Console is aligned at the bottom of the frame.
  **
  override Obj prefAlign() { return Valign.bottom }

//////////////////////////////////////////////////////////////////////////
// Write
//////////////////////////////////////////////////////////////////////////

  **
  ** Write the string to the end of the console
  **
  This clear()
  {
    model.clear
    richText.repaint
    return this
  }

  **
  ** Write the string to the end of the console
  **
  This append(Str s)
  {
    model.append(s)
    richText.repaint
    richText.select(model.size, 0)
    return this
  }

//////////////////////////////////////////////////////////////////////////
// Exec
//////////////////////////////////////////////////////////////////////////

  **
  ** Return true if the console is busy executing a job.
  **
  private Bool busy := false

  **
  ** Execute an external process and capture its output
  ** in the console.  See `sys::Process` for a description
  ** of the command and dir parameters.
  **
  This exec(Str[] command, File? dir := null)
  {
    if (busy) throw Err("Console is busy")
    frame.marks = Mark[,]
    model.clear.append(command.join(" ") + "\n")
    richText.repaint
    busy = true
    params := ExecParams
    {
      it.frameId = frame.id
      it.command = command
      it.dir = dir
    }
    Actor(ActorPool(), |->| { execRun(params) }).send(null)
    return this
  }

  **
  ** This is the method which executes the process
  ** on a background thread.
  **
  internal static Void execRun(ExecParams params)
  {
    try
    {
      proc := Process(params.command, params.dir)
      proc.out = ConsoleOutStream(params.frameId)
      proc.run.join
    }
    catch (Err e)
    {
      e.trace
    }
    finally
    {
      Desktop.callAsync |->| { execDone(params.frameId) }
    }
  }

  **
  ** Called on UI thread by ConsoleOutStream when the
  ** process writes to stdout.
  **
  internal static Void execWrite(Str frameId, Str str)
  {
    try
    {
      Frame.findById(frameId).console.append(str)
    }
    catch (Err e)
    {
      e.trace
    }
  }

  **
  ** Called on UI thread by execRun when process completes.
  **
  internal static Void execDone(Str frameId)
  {
    try
    {
      frame := Frame.findById(frameId)
      console := frame.console
      console.busy = false
      frame.marks = console.model.toMarks
    }
    catch (Err e)
    {
      e.trace
    }
  }

//////////////////////////////////////////////////////////////////////////
// Run
//////////////////////////////////////////////////////////////////////////

  **
  ** Run the given function in another thread.
  ** TODO - this function is experimental and will change!
  **
  internal This run(Method method, Str[] params)
  {
    if (busy) throw Err("Console is busy")
    frame.marks = Mark[,]
    model.clear
    richText.repaint
    busy = true
    execParams := ExecParams
    {
      frameId = frame.id
      command = params
    }
    Actor(ActorPool(), |->| { doRun(method, execParams) }).send(null)
    return this
  }

  internal static Void doRun(Method method, ExecParams params)
  {
    try
    {
      results := (Str[])method.call(params)
      results.each |Str s| { Desktop.callAsync |->| { execWrite(params.frameId, s) } }
    }
    catch (Err e)
    {
      e.trace
    }
    finally
    {
      Desktop.callAsync |->| { execDone(params.frameId) }
    }
  }

//////////////////////////////////////////////////////////////////////////
// Eventing
//////////////////////////////////////////////////////////////////////////

  override Void onGotoMark(Mark mark)
  {
    model.curMark = mark
    line := model.lineForMark(mark)
    if (line != null) richText.showLine(line.index)
    richText.repaint
  }

  internal Void onRichTextMouseDown(Event event)
  {
    // clear current mark
    model.curMark = null

    // map event to line and check if line has mark
    offset := richText.offsetAtPos(event.pos.x, event.pos.y)
    if (offset != null)
    {
      line := model.lines[model.lineAtOffset(offset)]
      if (line.mark != null)
        model.curMark = line.mark
    }

    // update highlight
    richText.repaint

    // hyperlink to view
    if (model.curMark != null)
      frame.loadMark(model.curMark, LoadMode(event))
  }

  internal Void onCopy()
  {
    richText.selectAll
    richText.copy
  }

  internal Void onClose()
  {
    hide
  }

//////////////////////////////////////////////////////////////////////////
// Fields
//////////////////////////////////////////////////////////////////////////

  internal ConsoleModel? model
  internal RichText? richText

  private Command copyCmd := Command.makeLocale(Flux#.pod, "copy") { onCopy }
  private Command hideCmd := Command.makeLocale(Flux#.pod, "navBar.close") { onClose }

}

**************************************************************************
** ConsoleModel
**************************************************************************

internal class ConsoleModel : RichTextModel
{
  override Str text
  {
    get { return lines.join(delimiter) |ConsoleLine line->Str| { return line.text } }
    set { modify(0, size, it) }
  }

  override Int charCount() { return size }

  override Int lineCount() { return lines.size }

  override Str line(Int lineIndex) { return lines[lineIndex].text }

  override Int offsetAtLine(Int lineIndex) { return lines[lineIndex].offset }

  override Int lineAtOffset(Int offset)
  {
    // binary search by offset, returns '-insertationPoint-1'
    key := ConsoleLine { it.offset = offset }
    line := lines.binarySearch(key) |ConsoleLine a, ConsoleLine b->Int| { return a.offset <=> b.offset }
    if (line < 0) line = -(line + 2)
    if (line >= lines.size) line = lines.size-1
    return line
  }

  override Void modify(Int startOffset, Int len, Str newText)
  {
    // we only allow appending to end of console text since we
    // are actually modifying the text displayed to show short
    // filenames versus full file paths
    throw UnsupportedErr("Cannot only call ConsoleModel.append")
  }

  This clear()
  {
    size = 0
    lines = [ConsoleLine { it.offset=0; it.text=""; it.fullText="" }]
    curMark = null
    return this
  }

  This append(Str s)
  {
    // save initial state for modification event
    startOffset := size
    startLineIndex := lines.last.index

    // normalize newlines
    newLines := s.splitLines
    numNewLines := newLines.size - 1

    // figure out if this we are starting a new line or need to append
    // to the last line; if appending to the last line we have to use
    // the original fullText to ensure we parse filenames correctly
    if (newLines.first == "")
    {
      newLines.removeAt(0)
      startLineIndex++
    }
    else
    {
      newLines[0] = lines.last.fullText + newLines.first
      lines.removeAt(-1)
    }

    // parse and append new lines
    newLines.each |Str line|
    {
      lines.add(parseLine(line))
    }

    // update total size, line offsets
    updateLines(lines)

    // fire modification event
    tc := TextChange
    {
      it.startOffset    = startOffset
      it.startLine      = startLineIndex
      it.oldText        = ""
      it.newText        = s
      it.oldNumNewlines = 0
      it.newNumNewlines = numNewLines
    }
    onModify.fire(Event { id =EventId.modified; data = tc })

    return this
  }

  private Void updateLines(ConsoleLine[] lines)
  {
    n := 0
    lastIndex := lines.size-1
    delimiterSize := delimiter.size

    // walk the lines
    lines.each |ConsoleLine line, Int i|
    {
      // update offset and total running size
      line.index  = i;
      line.offset = n
      n += line.text.size
      if (i != lastIndex) n += delimiterSize
    }

    // update total size
    size = n
  }

  ConsoleLine parseLine(Str t)
  {
    Obj[]? s := null
    full := t

    // attempt to parse mark (filename) in the line
    mp := MarkParser(t)
    m := mp.parse

    // don't show paths that are likely executables (bin)
    if (m != null && m.uri.path.contains("bin")) m = null

    // update the text to only show the filename (not the full path);
    // compute the styling to make filename appear as a hyperlink
    if (m != null)
    {
      start := mp.fileStart
      name  := m.uri.name
      t = t[0..<start] + name + t[mp.fileEnd+1..-1]
      if (start == 0)
        s = [0, link, name.size, norm]
      else
        s = [0, norm, start, link, start+name.size, norm]
    }

    return ConsoleLine { it.text = t; it.fullText = full; it.mark = m; it.styling = s }
  }

  override Obj[]? lineStyling(Int lineIndex)
  {
    return lines[lineIndex].styling
  }

  override Color? lineBackground(Int lineIndex)
  {
    if (curMark != null && lines[lineIndex].mark === curMark)
      return Color.yellow
    else
      return null
  }

  ConsoleLine? lineForMark(Mark m)
  {
    return lines.find |ConsoleLine line->Bool| { return line.mark === m }
  }

  Mark[] toMarks()
  {
    marks := Mark[,]
    lines.each |ConsoleLine line, Int i|
    {
      if (line.mark != null && i != 0) marks.add(line.mark)
    }
    return marks
  }

  Int size
  ConsoleLine[] lines := ConsoleLine[,]
  Str delimiter := "\n"
  RichTextStyle norm := RichTextStyle {}
  RichTextStyle link := RichTextStyle { fg=Color.blue; underline = RichTextUnderline.single; }
  Mark? curMark
}

**************************************************************************
** ConsoleLine
**************************************************************************

internal class ConsoleLine
{
  new make(|This| f) { f(this) }

  ** Return 'text'.
  override Str toStr() { return text }

  ** Zero based line index
  Int index

  ** Zero based offset from start of document (this
  ** field is managed by the Doc).
  Int offset { internal set; }

  ** Text we show (short uri filename)
  const Str text := ""

  ** Full text we show (long uri)
  const Str fullText := ""

  ** If we matched a file location from text
  Mark? mark

  ** Styling
  Obj[]? styling
}

**************************************************************************
** ConsoleOutStream
**************************************************************************

internal class ConsoleOutStream : OutStream
{
  new make(Str frameId) : super(null) { this.frameId = frameId }

  override This write(Int b)
  {
    frameId := this.frameId
    str := Buf().write(b).flip.readAllStr
    Desktop.callAsync |->| { Console.execWrite(frameId, str) }
    return this
  }

  override This writeBuf(Buf b, Int n := b.remaining)
  {
    frameId := this.frameId
    str := Buf().writeBuf(b, n).flip.readAllStr
    Desktop.callAsync |->| { Console.execWrite(frameId, str) }
    return this
  }

  Str frameId
}

**************************************************************************
** ExecParams
**************************************************************************

internal const class ExecParams
{
  new make(|This| f) { f(this) }
  const Str frameId := ""
  const Str[] command := Str#.emptyList
  const File? dir
}