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

using gfx

**
** Command packages up the diplay name, icon, execution,
** and undo support for a user command.  You can create a
** command two ways:
**   1. use a closure (or any function) for `onInvoke`
**   2. subclass Command and override `invoked`
**
** If the command supports undo, then you must create a
** a subclass and override `undo`.
**
** Commands are often used to centralize control of multiple
** widgets.  For example if a Command is associated with
** both a menu item and a toolbar button, then disabling the
** command will disable both the menu item and toolbar button.
**
** See [pod doc]`pod-doc#commands` for details.
**
@Js
class Command
{

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

  **
  ** Name of the command.
  **
  Str name

  **
  ** Icon of the command or null.  Typically a 16x16.
  **
  Image? icon

  **
  ** Accelerator of the command or null.
  **
  Key? accelerator

  **
  ** The function to invoke when the command is executed.  If
  ** empty, then `invoke` must be overridden.
  **
  once EventListeners onInvoke() { EventListeners() }

  **
  ** The command mode determines who associated widgets are
  ** visualized.  The CommandMode maps to the `ButtonMode`
  ** and `MenuItemMode`.  The default is 'push'.
  **
  CommandMode mode := CommandMode.push

  **
  ** If this command is using toggle mode, then set the
  ** selected state and update all the registered widgets.
  **
  Bool selected := false
  {
    set
    {
      newVal := it
      if (mode != CommandMode.toggle) return
      this.&selected = newVal
      widgets.each |Widget w|
      {
        try { w->selected = newVal } catch {}
      }
    }
  }

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

  **
  ** Construct a command with the specified onInvoke function.
  ** If onInvoke is not specified, then the `invoke` method
  ** must be overridden to execute the command.
  **
  new make(Str name := "", Image? icon := null, |Event event|? onInvoke := null)
  {
    this.name = name
    this.icon = icon
    if (onInvoke != null) this.onInvoke.add(onInvoke)
  }

  **
  ** Construct a localized command using the specified pod name
  ** and keyBase.  The command is initialized from the following
  ** [localized]`sys::Env.locale` properties:
  **   - "{keyBase}.name.{plat}": text string for the command
  **   - "{keyBase}.icon.{plat}": uri for the icon image
  **   - "{keyBase}.accelerator.{plat}": string representation of Key
  **
  ** The '{plat}' string comes from `Desktop.platform`.  If the
  ** paltform specific key is not found, then we attempt to fallback
  ** to a generic key.  For example:
  **
  **    back.name=Back
  **    back.accelerator=Alt+Left
  **    back.accelerator.mac=Command+[
  **
  ** On all platforms the command name would be "Back".  On Macs
  ** the accelerator would be 'Command+[', and all others it would
  ** be 'Alt+Left'.  If running on a Mac and an explicit ".mac"
  ** property was not specified, then we automatically swizzle Ctrl
  ** to Command.
  **
  new makeLocale(Pod pod, Str keyBase, |Event event|? onInvoke := null)
  {
    plat := Desktop.platform

    // name
    name := pod.locale("${keyBase}.name.${plat}", null)
    if (name == null)
      name = pod.locale("${keyBase}.name")
    this.name = name

    // icon
    locIcon := pod.locale("${keyBase}.icon.${plat}", null)
    if (locIcon == null)
      locIcon = pod.locale("${keyBase}.icon", null)
    try
    {
      if (locIcon != null)
        this.icon = Image.make(locIcon.toUri)
    }
    catch Command#.pod.log.err("Command: cannot load '${keyBase}.icon' => $locIcon")

    // accelerator
    locAcc := pod.locale("${keyBase}.accelerator.${plat}", null)
    locAccPlat := locAcc != null
    if (locAcc == null)
      locAcc = pod.locale("${keyBase}.accelerator", null)
    try
    {
      if (locAcc != null)
      {
        this.accelerator = Key.fromStr(locAcc)

        // if on a Mac and an explicit .mac prop was not defined,
        // then automatically swizzle Ctrl to Command
        if (!locAccPlat && Desktop.isMac)
          this.accelerator = this.accelerator.replace(Key.ctrl, Key.command)
      }
    }
    catch Command#.pod.log.err("Command: cannot load '${keyBase}.accelerator ' => $locAcc")

    // onInvoke
    if (onInvoke != null) this.onInvoke.add(onInvoke)
  }

//////////////////////////////////////////////////////////////////////////
// Methods
//////////////////////////////////////////////////////////////////////////

  **
  ** Get the window associated with this command.  If this
  ** command is being used as the action of a dialog, then
  ** return the dialog.  Otherwise try to map to a window
  ** via one of the widgets bound to this command.  Return
  ** null if no associated window can be found.
  **
  Window? window()
  {
    if (assocDialog != null) return assocDialog
    return widgets.eachWhile |Widget w->Window| { w.window }
  }
  internal Dialog? assocDialog

  **
  ** The enable state of the command automatically controls
  ** the enabled state of all the registered widgets.
  **
  Bool enabled := true
  {
    set
    {
      newVal := it
      if (this.&enabled == newVal) return
      this.&enabled = newVal
      registry.each |Widget w| { w.enabled = newVal }
    }
  }

  **
  ** Get the associated widgets with this command.  Widgets are
  ** automatically associated with their command field is set.
  **
  Widget[] widgets() { registry.ro }

  **
  ** Register a widget with this command.  This is done
  ** automatically by the widget.  You only need to call
  ** this method if you are developing a custom widget.
  **
  Void register(Widget w) { registry.add(w) }

  **
  ** Unregister a widget with this command.  This is done
  ** automatically by the widget.  You only need to call
  ** this method if you are developing a custom widget.
  **
  Void unregister(Widget w) { registry.removeSame(w) }

  **
  ** Return name.
  **
  override Str toStr() { name }

  private Widget[] registry := Widget[,]

//////////////////////////////////////////////////////////////////////////
// Invoke
//////////////////////////////////////////////////////////////////////////

  **
  ** Invoke the command.  If the user event is known
  ** then is passed, otherwise it might be null.
  **
  Void invoke(Event? event)
  {
    try
      invoked(event)
    catch (Err e)
      onInvokeErr(event, e)
  }

  **
  ** Subclass hook to handle invoke event.
  **
  protected virtual Void invoked(Event? event)
  {
    if (onInvoke.isEmpty) throw UnsupportedErr("Must set onInvoke or override invoke: $name")
    onInvoke.fire(event)
  }

  **
  ** Subclass hook to handle when an exception is raised
  ** by invoke.  Default implementation raises an error dialog.
  **
  protected virtual Void onInvokeErr(Event? event, Err err)
  {
    window := event?.window ?: registry.first?.window
    Dialog.openErr(window, "$name: $err", err)
  }

//////////////////////////////////////////////////////////////////////////
// Undo
//////////////////////////////////////////////////////////////////////////

  **
  ** Return if the command can be undone.  Default implementation
  ** returns true if the `undo` method has been overridden.
  **
  virtual Bool undoable()
  {
    Type.of(this).method("undo").parent != Command#
  }

  **
  ** This method is invoked when the command invoked as
  ** a redo.  It is not called on the first invocation.
  ** Default calls `invoke` with a null event.
  **
  virtual Void redo()
  {
    invoke(null)
  }

  **
  ** This method is invoked to undo the command.  This
  ** method is only used if `undoable` returns true.
  **
  virtual Void undo()
  {
    throw UnsupportedErr("Command not undoable $name")
  }

}

**************************************************************************
** CommandStack
**************************************************************************

**
** Manages a stack of commands for undo/redo.
**
@Js
final class CommandStack
{
  **
  ** Max number of undo commands stored in the stack.
  **
  Int limit := 1000

  **
  ** Return is both the undo and redo lists are empty.
  **
  Bool isEmpty() { return undoStack.isEmpty && redoStack.isEmpty }

  **
  ** Are any commands available for undo.
  **
  Bool hasUndo() { return undoStack.size > 0 }

  **
  ** Are any commands available for redo.
  **
  Bool hasRedo() { return redoStack.size > 0 }

  **
  ** List all the undo commands in the stack.
  **
  Command[] listUndo() { return undoStack.ro }

  **
  ** List all the redo commands in the stack.
  **
  Command[] listRedo() { return redoStack.ro }

  **
  ** Clear the undo/redo stacks.  Return this.
  **
  CommandStack clear() { undoStack.clear; redoStack.clear; fireModified; return this }

  **
  ** Callback when command stack is modified.
  **
  once EventListeners onModify() { EventListeners() }

  private Void fireModified() { onModify.fire(Event { id = EventId.modified }) }

  **
  ** Push a command onto the undo stack.  This clears
  ** the redo stack.  If c is null or returns false
  ** for `Command.undoable` then ignore this call.
  ** Return this.
  **
  CommandStack push(Command? c)
  {
    if (c == null || !c.undoable) return this
    undoStack.push(c)
    if (undoStack.size > limit) undoStack.removeAt(0)
    redoStack.clear
    fireModified
    return this
  }

  **
  ** Call `Command.undo` on the last undo command and
  ** then push it onto the redo stack.  If the undo stack
  ** is empty, then ignore this call.  Return command undone.
  **
  Command? undo()
  {
    c := undoStack.pop
    if (c == null) return null
    c.undo
    redoStack.push(c)
    fireModified
    return c
  }

  **
  ** Call `Command.redo` on the last redo command and
  ** then push it onto the undo stack.  If the redo stack
  ** is empty, then ignore this call.  Return command redone.
  **
  Command? redo()
  {
    c := redoStack.pop
    if (c == null) return null
    c.redo
    undoStack.push(c)
    fireModified
    return c
  }

  **
  ** Return debug string representation.
  **
  override Str toStr()
  {
    return "CommandStack undo=${listUndo.size} redo=${listRedo.size}"
  }

  **
  ** Create a shallow copy of the undo and redo stacks.  The
  ** copy maintains references to the original command instances.
  **
  This dup()
  {
    return CommandStack
    {
      it.undoStack = this.undoStack.dup
      it.redoStack = this.redoStack.dup
    }
  }

  private Command[] undoStack := Command[,]
  private Command[] redoStack := Command[,]
}