/*
   Copyright 2010 Aaron J. Radke

   Licensed under the Apache License, Version 2.0 (the "License");
   you may not use this file except in compliance with the License.
   You may obtain a copy of the License at

       http://www.apache.org/licenses/LICENSE-2.0

   Unless required by applicable law or agreed to in writing, software
   distributed under the License is distributed on an "AS IS" BASIS,
   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   See the License for the specific language governing permissions and
   limitations under the License.
*/
package cc.drx

object Keyboard{
   // private val CODED = 65535;  //Char.MaxValue.toInt = 2^16 - 1
   lazy val shift:Map[Char,Char] = {
       ('a' to 'z').zip('A' to 'Z') ++
       """`~ 1! 2@ 3# 4$ 5% 6^ 7& 8* 9( 0) -_ =+ [{ ]} \| ;: '" ,< .> /?""".split(" ").map(s => s(0)->s(1))
     }.toMap
   lazy val unShift:Map[Char,Char] = shift.map{_.swap}

   def mkLookup(prefix:String,spec:String):Map[String,KeyCode] =
     mkLookup(spec).map{case (k,v) => (prefix + k) -> v}
   def mkLookup(prefix:String,postfix:Iterable[Int],codes:Iterable[Int]):Map[String,KeyCode] =
     (postfix zip codes).map{case (a,b) => (s"$prefix$a") -> KeyCode(b)}.toMap
   def mkLookup(spec:String):Map[String,KeyCode] = spec.trim.split("""\s+""").grouped(2).map{g =>
     g(0) -> KeyCode(g(1).toInt)
   }.toMap

   val lookupAscii:Map[String,KeyCode] = shift.keys.map{char =>
      val name = char.toString //uppercase version for the ascii
      name.toLowerCase -> KeyCode(name.toUpperCase.head.toInt)
   }.toMap
   /**key codes for P2D, P3D, JOGL, OPENGL*/
   lazy val lookupJogl:Map[String,KeyCode] = lookupAscii ++
      // "caps"     -> ???,//TODO not caps
      //"cmd" -> ???,//TODO add meta and apple cmd key...
      //TODO fix overlapping shift-pageup keycodes one is coded the other is not...
      mkLookup("cmd 0 space 32 backspace 8 tab 9 enter 10 esc 27 shift -16 ctrl -17 alt -18 win -157 context 153") ++
      mkLookup("up -38 down -40 left -37 right -39") ++
      mkLookup("pad", 0 to 9, 128 to 142) ++
      mkLookup("Dot 138 Plus 139 Minus 140 Times 141 Div 142") ++
      mkLookup("ins 26 del 147 home 2 end 3 pageup 16 pagedown 11 print 154 scoll 23 pause 148 numlock 148") ++
      mkLookup("f", 1 to 12, 97 to 108)
   /**key codes for Java2d*/
   lazy val lookupJava2d = lookupJogl ++
      //TODO Add capability to directly include the shift keys as key codes as well ie. tilde and quote are broken here
      mkLookup("win -524 context -525") ++
      mkLookup("ins -155 del 127 home -36 end -35 pageup -33 pagedown -34 pause -19 numlock -144") ++
      mkLookup("pad", 0 to 9, 96 to 105) ++
      mkLookup("Dot 110 Plus 107 Minus 109 Times 106 Div 111") ++
      mkLookup("f", 1 to 12, (112 to 123) map {-_})

   lazy val lookupJavaFX = lookupJava2d ++
      mkLookup("space 32 back_space 8 tab 9 enter 10 escape 27 shift -16 control -17 alt -18 windows -157 context_menu 153") ++
      mkLookup("insert -155 delete 127 home -36 end -35 page_up -33 page_down -34 printscreen 154 scroll_lock 23 pause -19 num_lock -144") ++
      mkLookup("digit", 0 to 9, '0'.toInt to '9'.toInt) ++
      mkLookup("numpad", 0 to 9, 96 to 105) ++
      mkLookup("f", 1 to 12, (112 to 123) map {-_}) ++
      mkLookup("comma 44 colon 58 semicolon 59 slash 47 underscore 95 ampersand 38 asterisk 42 at 64 back_quote 96 back_slash 92") ++
      mkLookup("plus 43 minus 45 pound 35 quote 39 quotedbl 34") ++
      mkLookup("less 60 greater 62 braceleft 123 braceright 125 open_bracket 91 close_bracket 92 left_parenthesis 40 right_parenthesis 41") ++
      mkLookup("decimal 46 dollar 36 equals 61 exclamation_mark 33") ++
      mkLookup("period 110 add 107 subtract 109 multiply 106 divide 111")
      // mkLookup("command context_menu copy cut cancel circumflex eject_toggle help 
      // mkLookup("kp_down kp_left kp_right kp_up minus num_lock number_sign") ++ 
      // mkLookup("paste power"
      // mkLookup("start stop undefined underscore undo windows") ++
   def Jogl   = new Keyboard(lookupJogl)
   def Java2d = new Keyboard(lookupJava2d)
   def JavaFX = new Keyboard(lookupJavaFX)

   case class KeyCode(code:Int) extends AnyVal

   object KeyState{
      // def apply(codes:Set[KeyCode]):KeyState = new KeyState(codes)
      def apply(str:String)(implicit kb:Keyboard):KeyState = kb keyState str
      // def unapply(ks:KeyState)(implicit kb:Keyboard):Boolean = kb.keyState == ks
      // def unapply(str:String):Boolean = this == str //TODO make some matcher that could work in use with case statements
   }
   case class KeyState(codes:Set[KeyCode]){
     override def toString = "KeyState(" + codes.map{_.code}.mkString(",") +")"

     def ==(str:String)(implicit kb:Keyboard):Boolean = this == kb.keyState(str)  //code sets are equivalent

      //surprisingly complex because the numerics shift for nums and alpha are backwards from each other
     private def containsShiftable = codes.exists{c =>
        val char = c.code.toChar
        !char.isLower && (char.isUpper || Keyboard.shift.contains(char))
     }
     def nice(implicit kb:Keyboard) = {
       val symbols =
         if(codes.contains(kb.SHIFT) && containsShiftable)
           for(c <- (codes - kb.SHIFT).toList) yield {
             val char = c.code.toChar
             if(char.isUpper) char.toString
             else if(char.isLower) kb.inverseKeyLookup(c)
             else Keyboard.shift.get(char).map{_.toString} getOrElse kb.inverseKeyLookup(c)
           }
         else
           for(c <- codes.toList) yield kb.inverseKeyLookup(c)
       symbols.sortBy{n => (-n.size,n)}.mkString("-")
     }

     def ++(that:KeyState):KeyState = KeyState(this.codes ++ that.codes)
     def +(keyCode:KeyCode):KeyState = KeyState(this.codes + keyCode)
   }
}
import Keyboard.{KeyCode,KeyState}
class Keyboard(keyLookup:Map[String,KeyCode]){
   //--mutable state
   private val _keyHeld = TrieMap.empty[KeyCode,Boolean].withDefaultValue(false)
   private val _keyCount = TrieMap.empty[KeyState,Int].withDefaultValue(0)
   // private val _symbolCache = TrieMap.empty[KeyCode,String]
   private val _stateCache = TrieMap.empty[String,KeyState]
   /**clear all internal mutable state*/
   def clear() = {
      _keyCount.clear()
      _keyHeld.clear()
      // _symbolCache.clear()
      _stateCache.clear()
      callbacks.clear()
   }
   /**clear only the currently held variables*/
   def clearHeld() = _keyHeld.clear()

   //--reference vals
   implicit val keyboard:Keyboard = this
   val SHIFT = keyLookup("shift")
   // val ALT   = keyLookup("alt")
   // val CTRL  = keyLookup("ctrl")

   // private val _string = StringBuilder() //TODO add this like TEXT
       //TODO roll in features of p5.Text that gives nice readline like features support

   //--mutable interaction
   /**keyPressed*/
   def +=(keyCode:KeyCode):Unit = {
      _keyHeld(keyCode) = true
      val ks = keyState
      _keyCount(ks) += 1
      runCallbacksFor(ks)
   }
   /**keyReleased*/
   def -=(keyCode:KeyCode):Unit = {
      _keyHeld -= keyCode;
      {}//unit return type
   }

   // TODO add the keyboard event callback loops
   private type Callback = () => Unit
   private def runCallbacksFor(keyState:KeyState):Unit = for(cs <- callbacks.get(keyState); c <- cs) c()
   private val callbacks = TrieMap.empty[KeyState, List[Callback]].withDefaultValue(List.empty[Callback])
   //--register a callback on a key state command
   def on[A](keyStateString:String)(f: => Unit) = {
     val state = KeyState(keyStateString)
     val theseCallbacks = callbacks.getOrElse(state,  List.empty[Callback])
     val func:Callback = () => f
     callbacks(state) = func :: theseCallbacks
   }

   //TODO add a onPressed{} onReleased{} call backs
   //TODO add a onPressed{} onReleased{} call backs
   //TODO add a gui.Text or readline like state of the buffer 
   //TODO add vim like command events  register(List[keystate]){<callback>} with modes??

   //Note: a last function is not required if the super.keyReleased call is done at the end to 
   //      give a chance for the keyboard to be read
   // def last():Option[KeyState] //TODO add some last state mechanism here...

   //--cache lookups
   /*privateTODO*/ lazy val inverseKeyLookup:Map[KeyCode,String] = keyLookup.map{_.swap}.withDefault(kc => "Undefined:"+kc.code.toChar.toString)

   def keyState:KeyState = KeyState(_keyHeld.keys.toSet) //get all the keys that are currently held;  Note: the boxed wrapper is not that expensive considering the set is already required*/

   /**current key state as a nice string*/
   override def toString = keyState.nice

   private def keyStateString(k:String):KeyState = {
       def mkShift(c:Char) = KeyState(Set(SHIFT,KeyCode(c.toString.toUpperCase.head)))
       def default = KeyState(Set(keyLookup(k.toLowerCase)))

       if(k.size == 1) Keyboard.unShift.get(k.head).map(mkShift) getOrElse default
       else            default
   }

   private def keyStateSplit(string:String) = {
     if(string.size == 1) keyStateString(string)
     else{
       //--detect sep char
       val sepList = " -," //priority
       val sep = sepList.find{string contains _} getOrElse '-'
       string.split(sep).map{keyStateString}.reduce(_ ++ _)
     }
   }

   /**this is the primary interface for the lookup*/
   def keyState(str:String):KeyState = _stateCache.getOrElseUpdate(str, keyStateSplit(str))

   /**contains like test*/
   def apply(str:String):Boolean = contains(str)

   /**contains like test*/
   def contains(str:String):Boolean = keyState(str).codes forall _keyHeld.apply

   /**an exact test where the composed string is the exact state of the key codes pressed*/
   def ==(str:String):Boolean = _keyHeld.keys == keyState(str).codes  //comparison of a muttable and an imutable set wokrs by value

   /**immutable version of the internal count state*/
   def histogram:Map[KeyState,Int] = _keyCount.toSeq.map{case (k,n) => k -> n}.toMap

   /**count*/
   def count(key:String):Int = _keyCount(keyState(key))
   /**count up and down*/
   def count(up:String,down:String):Int = count(up)-count(down)
}