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

using gfx

**
** Dialog is a transient window, typically used to notify or
** input information from the user.  Dialog also contains
** convenience routines for opening message boxes.
**
@Js
@Serializable
class Dialog : Window
{

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

  **
  ** Image to the left of the body when building content.
  ** See `buildContent`.
  **
  Image? image

  **
  ** Main body of the content:
  **   - Str: displays string as label
  **   - Widget: used as main content
  ** See `buildContent`.
  **
  Obj? body

  **
  ** The details parameter is hidden by default, but may be displayed by
  ** the user via the "Details" button.  The details button is implicitly
  ** added to the command set if details is non-null.  Details may be any
  ** of the following
  **   - Str: displays string as label
  **   - Err: displays error trace as string
  **   - Widget: mounted as main content of details box
  ** See `buildContent`.
  **
  Obj? details

  **
  ** The commands are mapped to buttons along the bottom of the dialog.
  ** If a predefined command such as `ok` is passed, then it closes
  ** the dialog and is returned as the result.  If a custom command
  ** is passed, then it should close the dialog as appropiate with
  ** the result object.
  **
  Command[]? commands
  {
    set
    {
      &commands = it
      defCommand = it.first
    }
  }

  **
  ** Optional command to specify as default action. This field must
  ** be configured *after* `commands` is set.  Use 'null' for no default
  ** command.
  **
  Command? defCommand

//////////////////////////////////////////////////////////////////////////
// Predefined Commands
//////////////////////////////////////////////////////////////////////////

  ** Predefined dialog command for OK.
  static Command ok() { return DialogCommand(DialogCommandId.ok) }

  ** Predefined dialog command for Cancel.
  static Command cancel() { return DialogCommand(DialogCommandId.cancel) }

  ** Predefined dialog command for Yes.
  static Command yes() { return DialogCommand(DialogCommandId.yes) }

  ** Predefined dialog command for No.
  static Command no() { return DialogCommand(DialogCommandId.no) }

  ** Convenience for '[ok, cancel]'.
  static Command[] okCancel() { return [ok, cancel] }

  ** Convenience for '[yes, no]'.
  static Command[] yesNo() { return [yes, no] }

//////////////////////////////////////////////////////////////////////////
// Message Boxes
//////////////////////////////////////////////////////////////////////////

  **
  ** Open an information message box.  See `openMsgBox`.
  **
  static Obj? openInfo(Window? parent, Str msg, Obj? details := null,
                      Command[] commands := [ok])
  {
    return openMsgBox(Dialog#.pod, "info", parent, msg, details, commands)
  }

  **
  ** Open a warning message box.  See `openMsgBox`.
  **
  static Obj? openWarn(Window? parent, Str msg, Obj? details := null,
                      Command[] commands := [ok])
  {
    return openMsgBox(Dialog#.pod, "warn", parent, msg, details, commands)
  }

  **
  ** Open an error message box.  See `openMsgBox`.
  **
  static Obj? openErr(Window? parent, Str msg, Obj? details := null,
                     Command[] commands := [ok])
  {
    return openMsgBox(Dialog#.pod, "err", parent, msg, details, commands)
  }

  **
  ** Open a question message box.  See `openMsgBox`.
  **
  static Obj? openQuestion(Window? parent, Str msg, Obj? details := null,
                          Command[] commands := [ok])
  {
    return openMsgBox(Dialog#.pod, "question", parent, msg, details, commands)
  }

  **
  ** Open a message box.  The pod's locale properties map as follows:
  **   - "{keyBase}.name": title of the message box
  **   - "{keyBase}.icon": icon for the message box
  **
  ** See `buildContent` for a description of the body, details, and
  ** commands.  You may pass commands as the details parameter if
  ** details are null.
  **
  ** The command invoked to close message box is returned.  If the
  ** dialog is canceled using the window manager then null is returned.
  **
  static Obj? openMsgBox(Pod pod, Str keyBase, Window? parent, Obj body,
                         Obj? details := null, Command[] commands := [ok])
  {
    // get localized props
    title := pod.locale("${keyBase}.name")
    locImage := pod.locale("${keyBase}.image")
    Image? image
    try { image = Image(locImage.toUri) } catch {}

    // swizzle details if passed commands
    if (details is Command[]) { commands = details; details = null }
    dialog := Dialog(parent)
    {
      it.title    = title
      it.image    = image
      it.body     = body
      it.details  = details
      it.commands = commands
    }
    return dialog.open
  }

  **
  ** Open a prompt for the user to enter a string with an ok and cancel
  ** button. Return the string value or null if the dialog is canceled.
  ** The text field is populated with the 'def' string which defaults
  ** to "".
  **
  static Str? openPromptStr(Window? parent, Str msg, Str def := "", Int prefCols := 20)
  {
    field := Text { it.text = def; it.prefCols = prefCols }
    pane := GridPane
    {
      numCols = 2
      expandCol = 1
      halignCells=Halign.fill
      Label { text=msg },
      field,
    }
    ok := Dialog.ok
    cancel := Dialog.cancel
    field.onAction.add |Event e| { e.widget.window.close(ok) }
    r := openMsgBox(Dialog#.pod, "question", parent, pane, [ok, cancel])
    if (r != ok) return null
    return field.text
  }

//////////////////////////////////////////////////////////////////////////
// Constructor
//////////////////////////////////////////////////////////////////////////

  **
  ** Construct dialog.
  **
  new make(Window? parent, |This|? f := null)
    : super(parent, f)
  {
    icon = parent?.icon
  }

  **
  ** If the content field is null, then construct is via `buildContent`.
  **
  override Obj? open()
  {
    if (content == null) buildContent
    return super.open
  }

  **
  ** Build the dialog content using the `image`, `body`,
  ** `details`, and `commands` fields.  Return this.
  ** This method is automatically called by `open` if
  ** the content field is null.
  **
  virtual This buildContent()
  {
    // build body widget if necessary
    body := this.body
    if (body == null) body = Label {}
    if (body is Str) body = Label { text = body.toStr }
    if (body isnot Widget) throw Err("body is not Str or Widget: ${Type.of(body)}")

    // combine body with image if specified
    bodyAndImage := body as Widget
    if (image != null)
    {
      bodyAndImage = GridPane
      {
        numCols = 2
        expandCol = 1
        halignCells = Halign.fill
        Label { it.image = this.image },
        body,
      }
    }

    // details
    if (commands == null) commands = Command[,]
    if (details != null)
    {
      if (details is Err) details = ((Err)details).traceToStr
      if (details is Str) details = Text
      {
        multiLine  =true
        editable = false
        prefRows = 20
        font = Desktop.sysFontMonospace
        text = details.toStr
      }
      if (details isnot Widget) throw ArgErr("details not Err, Str, or Widget: ${Type.of(details)}")
      commands = commands.dup.add(DialogCommand(DialogCommandId.details, details))
    }

    // build buttons from commands
    buttons := GridPane
    {
      numCols = commands.size
      halignCells = Halign.fill
      halignPane = Halign.right
      uniformRows = true
      uniformCols = true
      hgap = Env.cur.runtime == "js" ? 2 : 4
    }
    commands.each |Command c|
    {
      c.assocDialog = this
      buttons.add(ConstraintPane
      {
        minw = 70
        b := Button.makeCommand(c) { insets=Insets(0, 10, 0, 10) }
        if (c == defCommand) setDefButton(b)
        it.add(b)
      })
    }

    // build overall
    this.content = GridPane
    {
      expandCol = 0
      expandRow = 0
      valignCells = Valign.fill
      halignCells = Halign.fill
      InsetPane(16)
      {
        ConstraintPane
        {
          minw = (details == null) ? 200 : 350
          bodyAndImage,
        },
      },
      InsetPane
      {
        insets = Env.cur.runtime == "js" ? Insets(0,14,14,14) : Insets(0,16,16,16)
        buttons,
      },
    }

    return this
  }

  protected native Void setDefButton(Button b)
}

**************************************************************************
** DialogCommand
**************************************************************************

**
** Internal class used for predefined Dialog commands.
**
@Js
internal class DialogCommand : Command
{
  new make(DialogCommandId id, Obj? arg := null)
    : super.makeLocale(Dialog#.pod, id.name)
  {
    this.id = id
    this.arg = arg
    if (id == DialogCommandId.details)
      this.mode = CommandMode.toggle
  }

  override Void invoked(Event? e)
  {
    switch (id)
    {
      case DialogCommandId.details:
        toggleDetails
      default:
        window?.close(this)
    }
  }

  override Int hash() { return id.hash }

  override Bool equals(Obj? that)
  {
    if (that isnot DialogCommand) return false
    return ((DialogCommand)that).id == id
  }

  internal Void toggleDetails()
  {
    Dialog dialog := window
    Widget details := arg
    if (details.parent == null) dialog.content.add(details)
    details.visible = selected
    dialog.pack
  }

  const DialogCommandId id
  Obj? arg
}

**************************************************************************
** DialogCommandId
**************************************************************************

**
** Ids for internal predefined Dialog commands.
**
@Js
internal enum class DialogCommandId
{
  ok,
  cancel,
  yes,
  no,
  details
}