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

**
** Key models a keyboard key or key combination.
**
@Js
@Serializable { simple = true }
const class Key
{

//////////////////////////////////////////////////////////////////////////
// Predefined
//////////////////////////////////////////////////////////////////////////

  static const Key a               := predefine('a', "A")
  static const Key b               := predefine('b', "B")
  static const Key c               := predefine('c', "C")
  static const Key d               := predefine('d', "D")
  static const Key e               := predefine('e', "E")
  static const Key f               := predefine('f', "F")
  static const Key g               := predefine('g', "G")
  static const Key h               := predefine('h', "H")
  static const Key i               := predefine('i', "I")
  static const Key j               := predefine('j', "J")
  static const Key k               := predefine('k', "K")
  static const Key l               := predefine('l', "L")
  static const Key m               := predefine('m', "M")
  static const Key n               := predefine('n', "N")
  static const Key o               := predefine('o', "O")
  static const Key p               := predefine('p', "P")
  static const Key q               := predefine('q', "Q")
  static const Key r               := predefine('r', "R")
  static const Key s               := predefine('s', "S")
  static const Key t               := predefine('t', "T")
  static const Key u               := predefine('u', "U")
  static const Key v               := predefine('v', "V")
  static const Key w               := predefine('w', "W")
  static const Key x               := predefine('x', "X")
  static const Key y               := predefine('y', "Y")
  static const Key z               := predefine('z', "Z")

  static const Key num0            := predefine('0', "0")
  static const Key num1            := predefine('1', "1")
  static const Key num2            := predefine('2', "2")
  static const Key num3            := predefine('3', "3")
  static const Key num4            := predefine('4', "4")
  static const Key num5            := predefine('5', "5")
  static const Key num6            := predefine('6', "6")
  static const Key num7            := predefine('7', "7")
  static const Key num8            := predefine('8', "8")
  static const Key num9            := predefine('9', "9")

  static const Key space           := predefine(' ',  "Space")
  static const Key backspace       := predefine('\b', "Backspace")
  static const Key enter           := predefine('\r', "Enter")
  static const Key delete          := predefine(0x7F, "Del")
  static const Key esc             := predefine(0x1B, "Esc")
  static const Key tab             := predefine('\t', "Tab")

  static const Key comma           := predefine(',',  "Comma")
  static const Key period          := predefine('.',  "Period")
  static const Key slash           := predefine('/',  "Slash")
  static const Key semicolon       := predefine(';',  "Semicolon")
  static const Key quote           := predefine('\'', "Quote")
  static const Key openBracket     := predefine('[',  "OpenBracket")
  static const Key closeBracket    := predefine(']',  "CloseBracket")
  static const Key backSlash       := predefine('\\', "BackSlash")
  static const Key backtick        := predefine('`',  "Backtick")

  static const Key up              := predefine(0x0100_0000 + 1,  "Up")
  static const Key down            := predefine(0x0100_0000 + 2,  "Down")
  static const Key left            := predefine(0x0100_0000 + 3,  "Left")
  static const Key right           := predefine(0x0100_0000 + 4,  "Right")
  static const Key pageUp          := predefine(0x0100_0000 + 5,  "PageUp")
  static const Key pageDown        := predefine(0x0100_0000 + 6,  "PageDown")
  static const Key home            := predefine(0x0100_0000 + 7,  "Home")
  static const Key end             := predefine(0x0100_0000 + 8,  "End")
  static const Key insert          := predefine(0x0100_0000 + 9,  "Insert")

  static const Key f1              := predefine(0x0100_0000 + 10, "F1")
  static const Key f2              := predefine(0x0100_0000 + 11, "F2")
  static const Key f3              := predefine(0x0100_0000 + 12, "F3")
  static const Key f4              := predefine(0x0100_0000 + 13, "F4")
  static const Key f5              := predefine(0x0100_0000 + 14, "F5")
  static const Key f6              := predefine(0x0100_0000 + 15, "F6")
  static const Key f7              := predefine(0x0100_0000 + 16, "F7")
  static const Key f8              := predefine(0x0100_0000 + 17, "F8")
  static const Key f9              := predefine(0x0100_0000 + 18, "F9")
  static const Key f10             := predefine(0x0100_0000 + 19, "F10")
  static const Key f11             := predefine(0x0100_0000 + 20, "F11")
  static const Key f12             := predefine(0x0100_0000 + 21, "F12")

  static const Key keypadMult      := predefine(0x0100_0000 + 42, "Keypad*")
  static const Key keypadPlus      := predefine(0x0100_0000 + 43, "Keypad+")
  static const Key keypadMinus     := predefine(0x0100_0000 + 45, "Keypad-")
  static const Key keypadDot       := predefine(0x0100_0000 + 46, "Keypad.")
  static const Key keypadDiv       := predefine(0x0100_0000 + 47, "Keypad/")
  static const Key keypad0         := predefine(0x0100_0000 + 48, "Keypad0")
  static const Key keypad1         := predefine(0x0100_0000 + 49, "Keypad1")
  static const Key keypad2         := predefine(0x0100_0000 + 50, "Keypad2")
  static const Key keypad3         := predefine(0x0100_0000 + 51, "Keypad3")
  static const Key keypad4         := predefine(0x0100_0000 + 52, "Keypad4")
  static const Key keypad5         := predefine(0x0100_0000 + 53, "Keypad5")
  static const Key keypad6         := predefine(0x0100_0000 + 54, "Keypad6")
  static const Key keypad7         := predefine(0x0100_0000 + 55, "Keypad7")
  static const Key keypad8         := predefine(0x0100_0000 + 56, "Keypad8")
  static const Key keypad9         := predefine(0x0100_0000 + 57, "Keypad9")
  static const Key keypadEqual     := predefine(0x0100_0000 + 61, "Keypad=")
  static const Key keypadEnter     := predefine(0x0100_0000 + 80, "KeypadEnter")

  static const Key capsLock        := predefine(0x0100_0000 + 82, "CapsLock")
  static const Key numLock         := predefine(0x0100_0000 + 83, "NumLock")
  static const Key scrollLock      := predefine(0x0100_0000 + 84, "ScrollLock")
  static const Key pause           := predefine(0x0100_0000 + 85, "Pause")
  static const Key printScreen     := predefine(0x0100_0000 + 87, "PrintScreen")

  static const Key alt             := predefine(0x0001_0000, "Alt")
  static const Key shift           := predefine(0x0002_0000, "Shift")
  static const Key ctrl            := predefine(0x0004_0000, "Ctrl")
  static const Key command         := predefine(0x0040_0000, "Command")

  private new predefine(Int mask, Str str, Bool mod := false)
  {
    this.mask = mask
    this.str  = str
  }

  private static const Int modifierMask := alt.mask.or(shift.mask).or(ctrl.mask).or(command.mask)
  private static const Int modifierUnmask := modifierMask.not
  private static const Key none := predefine(0, "")
  private static const Int:Key byMask
  private static const Str:Key byStr
  static
  {
    m := Int:Key[:]
    s := Str:Key[:]
    Key#.fields.each |Field f|
    {
      if (f.isStatic && f.type == Key#)
      {
        Key key := f.get(null)
        m[key.mask] = key
        if (!key.str.isEmpty) s[key.str]  = key
      }
    }
    byMask = m
    byStr  = s
  }

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

  **
  ** Parse font from string (see `toStr`).  If invalid
  ** and checked is true then throw ParseErr otherwise
  ** return null.
  **
  static new fromStr(Str s, Bool checked := true)
  {
    try
    {
      // check predefined name
      key := byStr[s]
      if (key != null) return key

      // split by +
      toks := s.split('+')

      // if one token, then single key not predefined
      if (toks.size == 1)
      {
        x := toks.first
        if (x.size == 1 && !x[0].isAlpha) return makeNew(x[0], x)
        throw Err()
      }

      // combine
      mask := 0
      gotBase := false
      toks.each |Str tok|
      {
        part := fromStr(tok)
        if (!part.isModifier)
        {
          if (gotBase) throw Err()
          gotBase = true
        }
        mask = mask.or(part.mask)
      }
      return makeNew(mask, null)
    }
    catch {}
    if (checked) throw ParseErr("Invalid Key: $s")
    return null
  }

  **
  ** Iternal lookup by mask: we either return a predefined
  ** instance or create a new one just in case we don't have
  ** a predefined instance defined.
  **
  internal static Key fromMask(Int mask)
  {
    return byMask[mask] ?: makeNew(mask, mask.toChar)
  }

  **
  ** Private constructor
  **
  private new makeNew(Int mask, Str? str)
  {
    this.mask = mask
    this.str  = str
  }

//////////////////////////////////////////////////////////////////////////
// Identity
//////////////////////////////////////////////////////////////////////////

  **
  ** Hash code is based on keycode.
  **
  override Int hash()
  {
    return mask
  }

  **
  ** Equality is based on keycode.
  **
  override Bool equals(Obj? that)
  {
    x := that as Key
    if (x == null) return false
    return mask == x.mask
  }

  **
  ** Format as key names combined with "+" symbol.
  **
  override Str toStr()
  {
    if (str != null) return str
    s := StrBuf()
    if (isShift)   s.join(shift.str,   "+")
    if (isAlt)     s.join(alt.str,     "+")
    if (isCtrl)    s.join(ctrl.str,    "+")
    if (isCommand) s.join(command.str, "+")
    baseMask := mask.and(modifierUnmask)
    if (baseMask != 0) s.join(fromMask(baseMask).str, "+")
    return s.toStr
  }

//////////////////////////////////////////////////////////////////////////
// Modifiers
//////////////////////////////////////////////////////////////////////////

  **
  ** Decompose a key combination into its individual keys.
  ** If instance isn't a combination then return a list with
  ** one item (this instance).
  **
  Key[] list()
  {
    toStr.split('+').map |Str tok->Key| { fromStr(tok) }
  }

  **
  ** Decompose the key into its primary key (without modifiers).
  **
  Key primary() { fromMask(mask.and(modifierUnmask)) }

  **
  ** Return a Key instance with only the modifiers.
  **
  Key modifiers()
  {
    key := none
    if (isAlt)     key += alt
    if (isShift)   key += shift
    if (isCtrl)    key += ctrl
    if (isCommand) key += command
    return key
  }

  **
  ** Is this instance is a modifier which may be combined
  ** with other keys: shift, alt, ctrl, command.
  **
  Bool isModifier() { mask.and(modifierUnmask) == 0 }

  **
  ** Return if any of the modifier keys are down.
  **
  Bool hasModifier() { mask.and(modifierMask) != 0 }

  **
  ** Return if the specified modifier is down.
  **
  Bool isDown(Key modifier) { mask.and(modifier.mask) != 0 }

  **
  ** Convenience for 'isDown(shift)'
  **
  Bool isShift() { isDown(shift) }

  **
  ** Convenience for 'isDown(alt)'
  **
  Bool isAlt() { isDown(alt) }

  **
  ** Convenience for 'isDown(ctrl)'
  **
  Bool isCtrl() { isDown(ctrl) }

  **
  ** Convenience for 'isDown(comand)'
  **
  Bool isCommand() { isDown(command) }

  **
  ** Add two keys to create a new key combination.
  ** Throws ArgErr if neither this nor x returns true
  ** true for `isModifier`.
  **
  @Operator Key plus(Key x)
  {
    if (!isModifier && !x.isModifier) throw ArgErr("Neither is modifier: $this + $x")
    if (mask == 0) return x
    if (x.mask == 0) return this
    return makeNew(mask.or(x.mask), null)
  }

  **
  ** Remove the key from this combination.
  ** Throws ArgErr if x is not defined in this combination
  ** or x is not a modifier.
  **
  @Operator Key minus(Key x)
  {
    if (mask.and(x.mask) == 0 || !x.isModifier)
      throw ArgErr("Not modifier: $this - $x")
    return makeNew(mask.and(x.mask.not), null)
  }

  **
  ** Replace one modifier with another modifer.  If
  ** modFrom is not defined in this key, then return this.
  **
  Key replace(Key modFrom, Key modTo)
  {
    if ((mask.and(modFrom.mask)) == 0) return this;
    return makeNew((mask.and(modFrom.mask.not)).or(modTo.mask), null);
  }

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

  ** Internal mask is based on SWT mask values
  internal const Int mask

  ** String encoding (if null we have to calcualte in toStr)
  internal const Str? str
}