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

using gfx

**
** RichText is used to view and edit text styled with
** different fonts and colors.
**
@Js
@Serializable
class RichText : TextWidget
{

  **
  ** Default constructor.
  **
  new make(|This|? f := null)
  {
    if (f != null) f(this)
  }

  **
  ** Callback when the text is modified.  This event occurs
  ** after the modification.  See `onVerify` to trap changes
  ** before they occur.
  **
  ** Event id fired:
  **   - `EventId.modified`
  **
  ** Event fields:
  **   - `Event.data`: the `TextChange` instance.
  **
  once EventListeners onModify() { EventListeners() }

  **
  ** Callback before the text is modified.  This gives listeners
  ** a chance to intercept modifications and potentially modify
  ** the inserted text.  This event occurs before the modification.
  ** See `onModify` to trap changes after they occur.
  **
  ** Event id fired:
  **   - `EventId.verify`
  **
  ** Event fields:
  **   - `Event.data`: a `TextChange` instance where 'newText'
  **     specifies the proposed text being inserted.  The callback
  **     can update 'newText' with the actual text to be inserted
  **     or set to null to cancel the modification.
  **
  once EventListeners onVerify() { EventListeners() }

  **
  ** Callback before a key event is processed.  This gives listeners
  ** a chance to trap the key event and [consume]`Event.consume`
  ** it before it is processed by the editor.
  **
  ** Event id fired:
  **   - `EventId.verifyKey`
  **
  ** Event fields:
  **   - `Event.keyChar`: unicode character represented by key event
  **   - `Event.key`: key code including the modifiers
  **
  once EventListeners onVerifyKey() { EventListeners() }

  **
  ** Callback when the selection is modified.
  **
  ** Event id fired:
  **   - `EventId.select`
  **
  ** Event fields:
  **   - `Event.offset`: the starting offset
  **   - `Event.size`:   the number of chars selected
  **
  once EventListeners onSelect() { EventListeners() }

  **
  ** Callback when the caret position is modified.
  **
  ** Event id fired:
  **   - `EventId.caret`
  **
  ** Event fields:
  **   - `Event.offset`: the new caret offset
  **
  once EventListeners onCaret() { EventListeners() }

  **
  ** Horizontal scroll bar.
  **
  @Transient ScrollBar hbar := ScrollBar.makeNative(Orientation.horizontal) { private set }

  **
  ** Vertical scroll bar.
  **
  @Transient ScrollBar vbar := ScrollBar.makeNative(Orientation.vertical) { private set }

  **
  ** Backing data model of text document.
  ** The model cannot be changed once the widget has been
  ** been mounted into an open window.
  **
  RichTextModel? model
  {
    set
    {
      if (attached) throw Err("Cannot change model once widget is attached")
      old := this.&model
      if (old != null) old.onModify.remove(onModelModifyFunc)
      if (it != null) it.onModify.add(onModelModifyFunc)
      this.&model = it
    }
  }

  private |Event| onModelModifyFunc := |e| { onModelModify(e) }
  internal native Void onModelModify(Event event)

  **
  ** Tab width measured in space characters.  Default is 2.
  **
  native Int tabSpacing

  **
  ** The zero based line index which is currently at the
  ** top of the scrolling viewport.
  **
  virtual native Int topLine

  **
  ** Convenience for 'model.text' (model must be installed).
  **
  override Str text
  {
    get { return model.text }
    set { model.text = it }
  }

  **
  ** The current foreground color for text.
  ** Defaults to null (system default)
  **
  native Color? fg

  **
  ** The current background color of text field
  ** Defaults to null (system default)
  **
  native Color? bg

//////////////////////////////////////////////////////////////////////////
// Utils
//////////////////////////////////////////////////////////////////////////

  **
  ** Map a coordinate on the widget to an offset in the text,
  ** or return null if no mapping at specified point.
  **
  native Int? offsetAtPos(Int x, Int y)

  **
  ** Convenience for `RichTextModel.modify`.
  **
  override Void modify(Int start, Int replaceLen, Str newText)
  {
    model.modify(start, replaceLen, newText)
  }

//////////////////////////////////////////////////////////////////////////
// Painting
//////////////////////////////////////////////////////////////////////////

  **
  ** Repaint the line specified by the zero based line index.
  **
  Void repaintLine(Int lineIndex)
  {
    repaintRange(model.offsetAtLine(lineIndex), model.line(lineIndex).size)
  }

  **
  ** Repaint the specified text range.
  **
  native Void repaintRange(Int offset, Int len)

  **
  ** Ensure the editor is scrolled such that the specified line is visible.
  **
  native Void showLine(Int lineIndex)
}

**************************************************************************
** RichTextModel
**************************************************************************

**
** RichTextModel models the document and styling of a `RichText` document.
**
@Js
abstract class RichTextModel
{

  **
  ** Callback model generated when the text is modified.
  **
  ** Event id fired:
  **   - `EventId.modified`
  **
  ** Event fields:
  **   - `Event.data`: the `TextChange`.
  **
  once EventListeners onModify() { EventListeners() }

  **
  ** Get or set the entire text document.
  **
  abstract Str text

  **
  ** Return the number of characters in the content.
  **
  abstract Int charCount()

  **
  ** Return the number of lines.
  **
  abstract Int lineCount()

  **
  ** Return the line at the given zero based line index without delimiters.
  **
  abstract Str line(Int lineIndex)

  **
  ** Return the zero based line index at the given character offset.
  **
  abstract Int lineAtOffset(Int offset)

  **
  ** Return the character offset of the first character of the
  ** given zero based line index.
  **
  abstract Int offsetAtLine(Int lineIndex)

  **
  ** Return the line delimiter that should be used when inserting
  ** new lines. The default is "\n".
  **
  virtual Str lineDelimiter() { return "\n" }

  **
  ** Returns a string representing the content at the given range.
  ** The default implementation of textRange is optimized to assume
  ** the backing store is based on lines.
  **
  virtual Str textRange(Int start, Int len)
  {
    // map offsets to line, if the offset is the line's
    // delimiter itself, then offsetInLine will be negative
    lineIndex := lineAtOffset(start)
    lineOffset := offsetAtLine(lineIndex)
    lineText := line(lineIndex)
    offsetInLine := start-lineOffset

    // if this is a range within a single line, then use normal Str slice
    if (offsetInLine+len <= lineText.size)
    {
      return lineText[offsetInLine..<offsetInLine+len]
    }

    // the range spans multiple lines
    buf := StrBuf(len)
    n := len

    // if the start offset is in the delimiter, then make sure
    // we start at next line, otherwise add the slice of the
    // first line to our buffer
    if (offsetInLine >= 0)
    {
      buf.add(lineText[offsetInLine..-1])
      n -= buf.size
    }

    // add delimiter of first line
    delimiter := lineDelimiter
    if (n > 0) { buf.add(delimiter);  n -= delimiter.size }

    // keep adding lines until we've gotten the full len
    while (n > 0)
    {
      lineText = line(++lineIndex)
      // full line (and maybe its delimiter)
      if (n >= lineText.size)
      {
        buf.add(lineText)
        n -= lineText.size
        if (n > 0) { buf.add(delimiter);  n -= delimiter.size }
      }
      // partial line
      else
      {
        buf.add(lineText[0..<n])
        break
      }
    }
    return buf.toStr
  }

  **
  ** Replace the text with 'newText' starting at position 'start'
  ** for a length of 'replaceLen'.  The model implementation must
  ** fire the `onModify` event.
  **
  abstract Void modify(Int start, Int replaceLen, Str newText)

  **
  ** Return the styled segments for the given zero based line index.
  ** The result is a list of Int/RichTextStyle pairs where the Int
  ** specifies a zero based char offset of the line using a pattern
  ** such as:
  **
  **   [Int, RichTextStyle, Int, RichTextStyle, ...]
  **
  virtual Obj[]? lineStyling(Int lineIndex) { return null }

  **
  ** Return the color to use for the specified line's background.
  ** Normal lineStyling backgrounds only cover the width of the text.
  ** However, the lineBackground covers the width of the entire
  ** edit area.  Return null for no special background.
  **
  virtual Color? lineBackground(Int lineIndex) { return null }
}

**************************************************************************
** RichTextStyle
**************************************************************************

**
** Defines the font and color styling of a text
** segment in a `RichTextModel`.
**
@Js
@Serializable
const class RichTextStyle
{
  **
  ** Default constructor.
  **
  new make(|This|? f := null)
  {
    if (f != null) f(this)
  }

  ** Foreground color
  const Color? fg

  ** Background color or null
  const Color? bg

  ** Font of text segment
  const Font? font

  ** Underline color, if null then use fg color.
  const Color? underlineColor

  ** Underline style or none for no underline.
  const RichTextUnderline underline := RichTextUnderline.none

  override Str toStr()
  {
    s := StrBuf()
    if (fg != null) s.add("fg=$fg")
    if (bg != null) s.add(" bg=$bg")
    if (font != null) s.add(" font=$font")
    if (underline != RichTextUnderline.none) s.add(" underline=$underline")
    if (underlineColor != null) s.add(" underlineColor=$underlineColor")
    return s.toStr.trim
  }
}

**************************************************************************
** RichTextUnderline
**************************************************************************

**
** Defines how to paint the underline of a RichText segment.
**
@Js
enum class RichTextUnderline
{
  none,
  single,
  squiggle
}