/*
   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.p5

//import java.util.Date

//import scala.collection.JavaConverters._

import scala.language.implicitConversions
import scala.collection.concurrent.TrieMap

//import scala.util.{Either,Left,Right}

import processing.core._
import processing.core.PConstants._
import processing.core.PApplet._

import cc.drx._

import java.awt.event._
//import java.awt.event.WindowStateListener
//import java.awt.event.WindowEvent
import java.awt.Frame

/*
   TODO: describe p5(data driven drawing) and the similarities but difference between p5(data driven documents)

   d3: scales , data merge animations
   underscoreio/doodle: shape composition: beside, above, below
   node2: data driven, cross platform, realtime, data adjust
   processing: low barrier to entry (single file app, no boilerplate code), draw loop, mouse interaction
   contextFree, structureSynth: simple method modifications rotate translate color : rx t c
*/

case class RenderMode(className:String) extends AnyVal
object RenderMode{
  //def launch = TODO make a launcher??
  def find(name:String):Option[RenderMode] = list.find(_.className endsWith name)
  lazy val list:List[RenderMode] = List(JAVA2D, FX2D, P2D, P3D, PDF)
  lazy val JAVA2D =  RenderMode(PConstants.JAVA2D) //canvas
  lazy val FX2D   =  RenderMode(PConstants.FX2D) //javaFX
  lazy val P2D    =  RenderMode(PConstants.P2D) //2d opengl
  lazy val P3D    =  RenderMode(PConstants.P3D) //3d opengl
  lazy val PDF    =  RenderMode(PConstants.PDF) //requires itext??
}

trait P5App{
   private def p5className:String = this.getClass.getName.dropRight(1)
   def launch:Unit = P5.launch(Array(p5className))
   def launch(args:Array[String]):Unit = P5.launch(p5className +: args)
   def main(args:Array[String]) = launch(args)
}
object P5{
   def launch[A <: PApplet](klass:Class[A]):Unit = PApplet.main(Array(klass.getName)) //TODO add a feature so the classOf is not required
   def launch(args:Array[String]):Unit = PApplet.main(args)
   @deprecated("use RenderMode instead","v0.2.14")
   val renderClasses:Set[String] = Set(JAVA2D, P2D, P3D, FX2D)
   @deprecated("use RenderMode instead","v0.2.14")
   def renderClass(k:Symbol):String = renderClasses.find(_ contains k.name.toUpperCase) getOrElse JAVA2D
}


@deprecated("use some version of mouseDrag and scale","v0.1.15")
class CanvasZoom{
   var poffset = Vec(0,0)
   var offset = Vec(0,0)
   var scale = 1.0

   def zoom(fraction:Double,center:Vec=Vec(0,0)) = {
      val tmp = scale - scale*fraction
      val new_scale = if(tmp <= 0.0) 0.01 else tmp

      val pc = (center - offset)/scale
      offset = center - (pc*new_scale)
      scale = new_scale
   }

   def initMove = poffset = offset
   def move(relative:Vec) = offset = poffset + relative

   def draw(drawFunc:PGraphics=>Unit)(implicit g:PGraphics):Unit = {
      g.pushMatrix
      g.scale(scale)
      g.translate(offset.x/scale, offset.y/scale)
      drawFunc(g)
      g.popMatrix
   }
   def draw(drawFunc: =>Unit)(implicit ctx:PApplet):Unit = {
      ctx.pushMatrix
      ctx.scale(scale)
      ctx.translate(offset.x/scale, offset.y/scale)
      drawFunc
      ctx.popMatrix
   }
}
trait FullScreen extends P5{
   override def settings = {
      fullScreen(renderMode.className)
   }
}

class P5 extends PApplet{
   import P5._
   import Color._

   //--wip external launcher frame's
   /**Interfacing with java (this will make a jframe) */
   lazy val getFrame:javax.swing.JFrame = {
     import javax.swing.JFrame
     val frame = new JFrame(this.getClass.getSimpleName)
     frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

     //TODO get size from the jframe instead
     // val h = frame.getSize.getHeight
     // val w = frame.getSize.getWidgth

     //--add the native surface to the frame
     frame.add(getCanvasAWT)

     //--adjust frame size
     frame.setSize(sketchSize.x.toInt, sketchSize.y.toInt)
     frame.setResizable(true)

     frame.setVisible(true)  //TODO add boolean argument to prevent thread startup
     //--TODO add resize listeners...
     frame
   }

   private lazy val getStartedSurface:processing.core.PSurface = {
     val ps:processing.core.PSurface = if(surface == null) this.initSurface() else surface //TODO make sure this null check is really the right way to test this, it is required if something else like the applet already made an initSurface
     ps.setResizable(true)
     ps.setSize(sketchSize.x.toInt, sketchSize.y.toInt)
     ps.startThread()//the animation thread  //TODO add boolean argument to prevent thread startup
     ps //return the that has already been started surface
   }

   /**Interfacing with javafx (this will also spin up the animation threads)
    * https://docs.oracle.com/javase/8/javafx/api/javafx/scene/canvas/Canvas.html
    * https://github.com/processing/processing/blob/master/core/src/processing/javafx/PSurfaceFX.java
    * */
   private def requireJAVA2D = require(renderMode == RenderMode.JAVA2D,s"returning a canvas requires the `override renderMode == JAVA2D`, it is currently set to `$renderMode`")
   private def requireFX = require(renderMode == RenderMode.FX2D,s"returning a canvas requires the `override renderMode == FX2D`, it is currently set to `$renderMode`")
   lazy val getCanvasFX:javafx.scene.canvas.Canvas = {
      requireFX;
      getStartedSurface.getNative.asInstanceOf[javafx.scene.canvas.Canvas]
   }
   /**this stage will make sure the fx app will close on window events*/
   private lazy val getStageFXWithoutShow:javafx.stage.Stage = {
     requireFX
     val jbSurface = JailBreak(getStartedSurface, classOf[processing.javafx.PSurfaceFX])
     val stage = jbSurface[javafx.stage.Stage]("stage") //get the private stage value from the psurface object
     getCanvasFX.requestFocus()  //this is the necessary one to make the keyEvents start working in the embeded FX app, there are lots of places this works, but it seems all stages should have and if it works outside of a runLater then it shoudl be put there as it does

     //--stop the fx app on close
     stage.setOnCloseRequest(new javafx.event.EventHandler[javafx.stage.WindowEvent]{
       override def handle(e:javafx.stage.WindowEvent) = {
         javafx.application.Platform.exit()
         System.exit(0)
       }
     })
     stage
   }
   /**this stage will also show the stage in a Platform runLater thread, if you dont' want to auto show it with runLater use getSTageFXWithoutShow*/
   lazy val getStageFX:javafx.stage.Stage = {
     requireFX
     val stage = getStageFXWithoutShow

     //--show the stage //TODO maybe don't do this yet to let other apps make modifications first or add a boolean
     //TODO the mouse, wheel events work, but for some reason the keyboard events are not working ... the PApplet.launch seems to work though with JAVAFX so somethign with this getStageFX method
     javafx.application.Platform.runLater(new Runnable(){def run = {
       stage.show() //Note this crashes with a "Not on FX application thread" unless it lives inside the runLater runable
     }})

     stage //return the fx stage
   }

   @deprecated("use the more specific getCanvasAWT instead", "v0.2.14")
   lazy val getCanvas:java.awt.Canvas = getCanvasAWT
   /**Interfacing with java (this will also spin up the animation threads) */
   lazy val getCanvasAWT:java.awt.Canvas = {
     //requireJAVA2D //TODO require the check for JAVA2D if no other renders also use the java.awt.Canvas

     val canvas = getStartedSurface.getNative.asInstanceOf[processing.awt.PSurfaceAWT$SmoothCanvas]  //java2d AWT?

     canvas.addComponentListener(
       new java.awt.event.ComponentAdapter{//ComponentListener{
        override def componentResized(e:java.awt.event.ComponentEvent):Unit = {
          // println(s"event: $e")
          for(c <- Option(e.getComponent)) {
            val h = c.getSize.getHeight.toInt
            val w = c.getSize.getWidth.toInt
            // println(s"Resized native canvas to: $w x $h")
            surface.setSize(w,h)
            redraw()
          }
        }
       }
     )

     canvas
   }

   def sketchSize = Vec(1280,720)

   def renderMode:RenderMode = RenderMode.JAVA2D //TODO replace all renderClass settings with the renderMode

   /**note this is important that this renderclass is a def so that a val is not loaded as null in the JailBreak override of PApplet.renderer */
   @deprecated("use RenderMode instead","v0.2.14")
   def renderClass:String = JAVA2D //FX2D // P2D // P3D // OPENGL // JAVA2D // FX2D  

   //--WARNING the following is an initialization hack to change the default renderer 
   //this is used in an embedded app since non-launched frames may not use the PApplet.size(_,_,render) setting to set the renderer
   private val jailBreak = JailBreak(this,classOf[processing.core.PApplet])
   jailBreak("renderer") = renderMode.className
   // println("proccessing app found render:" + jailBreak[String]("renderer")  + " from renderClass:"+renderClass)

   override def settings = {
     size(sketchSize.x.toInt, sketchSize.y.toInt, renderMode.className)
     //fullScreen(renderMode.className)
     smooth(4)
     pixelDensity(displayDensity)
   }
   override def setup() = {
      surface.setResizable(true)
      frameRate(60)//limit the framerate, no need to over do things
      Draw.defaults(g) //background(White)
      hint(ENABLE_KEY_REPEAT)
      Option(java.awt.SplashScreen.getSplashScreen) foreach {_.close}
   }

   implicit lazy val keyboard = renderMode match {
     case RenderMode.FX2D   => Keyboard.Java2d //TODO make sure this works right
     case RenderMode.JAVA2D => Keyboard.Java2d
     case _                 => Keyboard.Jogl
   }
   // @deprecated("use keyboard.count(up,down) instead", "v0.2.13") //TODO
   def keyCount(s:String) = keyboard.count(s(0).toString, s(1).toString)
   // @deprecated("use keyboard.count(up) instead", "v0.2.13") //TODO
   def keyCount(c:Char) = keyboard.count(c.toString)
   // @deprecated("use keyboard("alt-c") instead", "v0.2.13") //TODO
   // def isCoded(c:Char):Boolean = keyboard.keyState.codes contains Keyboard.KeyCode(c.toInt)

   private var _wheelCount:Int = 0
   def wheelCount:Int = _wheelCount
   override def mouseWheel(e:processing.event.MouseEvent) = _wheelCount += e.getCount()

   /**this should be called at the end of an overridden keyReleasedd*/
   override def keyReleased() = {
      if(keyboard("f12") || keyboard("print")) save(s"export/screen-${Date.now.krypton take 6}.png") // _savePng = true
      keyboard -= Keyboard.KeyCode(if (key==CODED) -keyCode else keyCode)
      if(keyboard("shift-f12") || keyboard("shift-print")) OS.browse(f"export") // open the export directory
      //if(key == CODED && keyCode == 154 /*PrtScn*/) 
      //if(key == 'q') {stop; dispose; System.exit(0)}
      //if(coded(ALT) && key == 'q'){ stop; dispose; System.exit(0)}
   }
   def logKeyEvent(root:String="KeyEvent") =  println(s"$root key:'$key' key.toInt:${key.toInt} keyCode:$keyCode coded:${key == CODED}")
   override def keyPressed() = {
      keyboard += Keyboard.KeyCode(if (key==CODED) -keyCode else keyCode)
      if(key == ESC) key = 0  //hack to stop processing from quiting the application on ESC
   }

   /**clear UI state like the keyHeld state, keyCount, WheelCount*/
   def clearState() = {
      keyboard.clear()
      _wheelCount = 0  //TODO roll in wheel count ot the Keyboard state
      mouseDrag = None
   }
   override def clear() = {
      super.clear()
      clearState()
   }

   /**defines the line that the current mouseDrag has drawn, returns None if no mouse drag*/
   var mouseDrag:Option[Line] = None  //TODO make the var private
   /**defines the press of an event, this value is setup at the begining of a draw loop and cleared at the end*/
   var mousePress:Option[Vec] = None  //TODO make the var private

   override def mouseDragged = {
      mouseDrag = mouseDrag match {
         case Some(Line(start,_)) => Some(Line(start,mouse))
         case None  => Some(Line(mouse,mouse))
      }
   }
   override def mouseReleased = {
     super.mouseReleased
      mouseDrag = None
      mousePress = None
   }
   override def mousePressed = {
     super.mousePressed()  //the () are required to differentiate between the callback and the variable name
     mousePress = Some(mouse)
   }

   //---mouse functions as vectors
   def mouse = Vec(mouseX,mouseY)
   def pmouse = Vec(pmouseX,pmouseY)
   def size = Vec(width,height)
   def screen = Rect(Vec(0,0),size)
   //def center = size/2
   //def gmouse = frame.loc + mouse

   @deprecated("just use surface.setTitle since procesing doesn't always use frames or overload the resource /icon/icon-{sz}.png files", "0.1.15")
   def setAppIcon(title:String,img:PImage,iconSize:Int=64) = {
      //val pg = draw(iconSize,iconSize){_.image(img,0,0,iconSize,iconSize)}
      //frame.setIconImage(pg.image)
      surface.setTitle(title)
   }

   //---new draw factories
   @deprecated("Use implicit RichPGraphics.draw{drawFunc} instead","v0.2.0")
   def draw(drawFunc: => Unit)(implicit g:PGraphics):PGraphics= {
      draw{g => drawFunc}
   }
   @deprecated("This makes it far to easy to make expensive graphics buffers use createGraphics(w,h) instead","v0.2.0")
   def draw(drawFunc:PGraphics => Unit)(implicit g:PGraphics):PGraphics= {
      g.beginDraw
         g.pushMatrix
            drawFunc(g)
         g.popMatrix
      g.endDraw
      g
   }

   override def createGraphics(w:Int,h:Int):PGraphics = Draw.defaults(super.createGraphics(w,h))

   def launch:Unit = PApplet.main(Array(this.getClass.getName))


}