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

trait Shape{
  //def draw(implicit c:DrawContext):Unit
  /**Attach a style to a shape*/
  def ~(style:Style):Shape.Styled = Shape.Styled(this,style)
  /**initialize shape with a style */
  def ~(property:Style.Property):Shape.Styled = Shape.Styled(this,Style()~property)
  /** provide a quick automatic way to set a fill or stroke
   * all shapes have a stroke even if they can be filled (but the ClosedShape overrides this to provide a fill color)
   */
  def ~(color:Color):Shape.Styled =  this ~ Style.Stroke(color) //default is stroke color...

  /** generic draw method*/
  def draw(implicit g:DrawContext):Unit
}

object Shape{
  /**these are shapes that have an area and default to coloring by area fill rather than stroke*/
  trait Open extends Shape {
     override def ~(color:Color):Styled =  this ~ Style.Stroke(color)
  }
  trait Closed extends Shape {
     override def ~(color:Color):Styled =  this ~ Style.Fill(color)
  }
  case class Styled(shape:Shape,style:Style) extends Shape{
    override def ~(mergeStyle:Style):Styled           = Styled(shape, style ~ mergeStyle)
    override def ~(addProperty:Style.Property):Styled = Styled(shape, style ~ addProperty)

    def draw(implicit g:DrawContext):Unit = g ! this
  }
  case class Grouped(shapes:Iterable[Shape]) extends Shape{
    def draw(implicit g:DrawContext):Unit = g ! this
  }
  lazy val empty = new Empty
  class Empty extends Shape{
    def draw(implicit g:DrawContext):Unit = ()
  }
}
//TODO possibly remove this in favor of a speciallized unit draw function

abstract class DrawContext{
  import Style._
  //---mutable state variables
  private var horzAlign:AlignHorizontal = Center
  private var vertAlign:AlignVertical   = Midline
  private def align() = this ! Align(horzAlign,vertAlign)


  //---Required
  //--Query
  def size:Vec
  def screen:Rect = Rect(Vec(0,0), size)

  //== events to values
  //-- mutable event values
  //-- raw events
  private var _lastClick:Option[Vec] = None
  private var _lastPress:Option[Vec] = None
  // TODO private var _lastPressButton:Option[Style.HorizontalAlign] = None  //Left or Right
  private var _lastRelease:Option[Vec] = None
  private var _lastDrag:Option[Line] = None
  private var _lastTouchPressed:List[Vec] = Nil
  private var _lastTouchMoved:List[Vec] = Nil
  private var _lastTouchReleased:List[Vec] = Nil
  private var _lastScrollDelta:Option[Vec] = None
  //--drx derived
  private var _mouse:Option[Vec] = None
  private var _press:Option[Vec] = None
  private var _drag:Option[Line] = None
  private var _touch:List[Line] = Nil
  private var _onRelease:Option[Vec => Unit] = None
  private var _scrollSum:Vec = Vec.zero
  private var _focus:Boolean = false
  //-- functional access to event values
  def lastClick:Option[Vec] = _lastClick
  def lastPress:Option[Vec] = _lastPress
  def lastRelease:Option[Vec] = _lastRelease
  def lastDrag:Option[Line] = _lastDrag

  def mouseOption:Option[Vec] = _mouse
  def mouse:Vec   = _mouse getOrElse Vec(-1,-1) //a default offscreen location if not specified
  def press:Option[Vec] = _press
  def drag:Option[Line] = _drag
  def touch:List[Line] = _touch
  def scrollSum:Vec = _scrollSum
  // def scroll:Option[Vec] = _lastScrollDelta
  def focus:Boolean = _focus

  //def wheel = TODO 
  //-- call the following to implement event value states
  private[drx] def mouseMoved(v:Vec):Unit = {_mouse = Some(v)}
  private[drx] def mouseClicked(v:Vec):Unit = _lastClick = Some(v)
  private[drx] def mousePressed(v:Vec):Unit = _press = Some(v)
  def onRelease(f: Vec => Unit):Unit = _onRelease = Some(f) //TODO make this a onRelease stack so more than one click is allowed
  def onInit(f: => Unit):Unit = if(frame == 0) f
  private[drx] def mouseReleased(v:Vec):Unit = {
    _lastRelease = Some(v)
    _drag = None
    _press = None
    for(f <- _onRelease) f(v)
  }
  private[drx] def mouseDragged(v:Vec):Unit = {
    _drag match {
      case Some(Line(a,_)) => _drag = Some(Line(a,v)) //update new drag point v from starting point a
      case _ => _drag = Some(Line(_lastPress getOrElse v,v))
    }
    _lastDrag = _drag
  }
  private[drx] def touchPressed(vs:List[Vec]):Unit = {
    _lastTouchPressed = vs
    _touch = vs zip vs map {case (a,b) => a lineTo b}
  }
  private[drx] def touchMoved(vs:List[Vec]):Unit = {
    _lastTouchMoved = vs
    _touch = _lastTouchPressed zip vs map {case (a,b) => a lineTo b}
  }
  private[drx] def touchReleased(vs:List[Vec]):Unit = {
    _lastTouchReleased = vs
    _touch = Nil
  }

  private[drx] def scrollMoved(delta:Vec):Unit = {
    _lastScrollDelta = Some(delta)
    _scrollSum = _scrollSum + delta
  }
  private[drx] def focusChanged(v:Boolean):Unit = {
    _focus = v
    if(!focus) keyboard.clearHeld() //since alt-tab loses focus this is needed to make sure alt is not held (similar for meta-windows and other switch mechanisms)
  }

  //TODO FIXME test these drag features
  private var _drop:Option[String] = None
  def drop:Option[String] = _drop
  private[drx] def dragOver(str:String):Unit = _drop = Some(str)
  private[drx] def dragDropped(str:String):Unit = _drop = Some(str)

  private var _frame:Long = 0L
  private var _frameNanos:Long = 1L
  private var _lastFrameNanos = System.nanoTime()
  final private[drx] def frameFinished():Unit = {
    val nowNanos = System.nanoTime()
    _frameNanos = nowNanos - _lastFrameNanos //dt
    _frame += 1
    // Log(nowNanos, _lastFrameNanos, _frame, _frameNanos, fps, 1E9 / _frameNanos)
    _lastFrameNanos = nowNanos
  }
  final def frame:Long = _frame
  final def fps:Int = (1E9 / _frameNanos).round.toInt // (ns/frame).inv => frame/ns * n/1E-9 * 1count/frame => 1E9/ns
  final def frameRate:Frequency = Frequency(1E9 / _frameNanos)

  implicit val keyboard:Keyboard = new Keyboard(Keyboard.lookupJavaFX)
  //--add default key bindings
  keyboard.on("print"){  //print screen should export to a screen
    save match {
      case Success(f) => println(s"Saved screenshot to $f")
      case Failure(e) => Console.err.println(s"Failed to save screenshot ($e)")
    }
  }
  keyboard.on("ctrl-print"){  //cntrl-print screen should launch the export directory
    OS.open(exportDir)
  }
  private[drx] def keyPressed(k:Keyboard.KeyCode):Unit  = keyboard += k
  private[drx] def keyReleased(k:Keyboard.KeyCode):Unit = keyboard -= k

  //--required
  def save(img:Img, file:File):Try[File] = ??? //TODO remove default implementation
  def save(file:File):Try[File] = ???
  def snapshot(size:Vec):Img = ???

  //default export methods
  private lazy val exportDir = File("export").canon
  def save:Try[File] = save( exportDir / s"snapshot-${Date.now.krypton.take(6)}.png" )
  def snapshot:Img = snapshot(size)

  //--Style
  def !(p:Default.type):Unit = {
      horzAlign = Center
      vertAlign = Midline
      align()
  }
  //  color
  def !(p:Background):Unit
  def !(p:Fill):Unit
  def !(p:Stroke):Unit
  def !(p:FillNone.type):Unit
  def !(p:StrokeNone.type):Unit
  def !(p:Weight):Unit
  //--get active color styles (support infered styles like arrowheads)
  def fill:Option[Color]
  def stroke:Option[Color]
  def weight:Double
  //  font
  def !(p:Font):Unit
  def !(p:Align):Unit
  // def !(p:AlignVertical):Unit
  //  transform
  def !(p:Translate):Unit
  def !(p:ScaleProperty):Unit
  def !(p:Rotate):Unit
  //--Shapes
  //--required
  def !(p:Path):Unit
  // deried from path
  def !(s:Circ):Unit
  def !(s:Ellipse):Unit
  def !(s:Rect):Unit
  def !(s:Line):Unit
  def !(s:Poly):Unit
  def !(s:Tri):Unit
  def !(s:Arc):Unit
  def !(s:Text):Unit
  def textSize(text:Text, font:Font):Vec
  // def textBox(text:Text, font:Font):Rect //to really get the box we need to know justfication styles aswell

  def !(s:Bezier):Unit = ???
  def !(s:Polys):Unit = ???
  def !(s:SvgPath):Unit = ???

  //--derived
  def !(arrow:Arrow):Unit = {
    val color:Color = stroke orElse fill getOrElse Black
    val headSize:Double=weight*8

    val a = arrow.a
    val b = arrow.b
    val vel = b - a
    if(vel.range != 0){
      val head = b - vel.mag(headSize)
      this ! Stroke(color)
      this ! Line(a, head)
      //--triangle vec boid
      this ! StrokeNone
      this ! Fill(color)
      val ortho = (vel x Vec.z) mag (headSize/2)
      this ! Poly(List(head-ortho, b, head+ortho))
      ////--reset color TODO let the style wrapper do this
      if(fill.isDefined) this ! Fill(fill.get) else this ! FillNone
      if(stroke.isDefined) this ! Stroke(stroke.get) else this ! StrokeNone
    }
  }

  def !(s:Star):Unit

  //--video context methods
  def remote(video:Video):MediaRemote

  def !(s:Video):Unit
  def !(s:Img):Unit
  def !(s:Svg):Unit
  def !(s:Html):Unit
  //TODO add more required interfaces...
  //--Derived= 
  final def !(p:AlignHorizontal):Unit = {horzAlign = p; align()}
  final def !(p:AlignVertical):Unit = {vertAlign = p; align()}
  final def !(s:Shape.Styled):Unit =  style{
    //--clear the current styles
    // this ! Default
    //--push the new style
    this ! s.style
    //--hack to make the runtime figure out the shape type
    drawShape(s.shape)
    //FIXME pop back to last style
  }
  def style(f: =>Unit):Unit //require a function that will push a style on the stack and then and pop it back (save; restore)


  final def drawShape(shape:Shape) = shape match {  //this is code smell that polymoric calles are not autmatic here a runtime instance off case list is required...
    //--basic shapes
    case p:Circ => this ! p
    case p:Rect => this ! p
    case p:Ellipse => this ! p
    case p:Text => this ! p
    case p:Arrow => this ! p
    case p:Tri  => this ! p
    case p:Line => this ! p
    case p:Path => this ! p
    case p:Poly => this ! p
    case p:Html => this ! p
    case p:Video => this ! p
    case p:Img => this ! p
    // case p:Html => this ! p
    //TODO add basic shapes
    //--compound shapes
    case p:Shape.Grouped => this ! p
    case p:Shape.Styled => this ! p
    case p:Axes[_,_] => this ! p
    case _ => {println(Red ansi "Error: drawShape match not implemented for $p"); ??? } //NOte should never be reached
  }

  final def !(p:Shape.Grouped):Unit = p.shapes foreach drawShape
  final def !(ps:Iterable[Shape]):Unit = ps foreach drawShape
  final def !(ps:Shape.Empty):Unit = ()
  final def !(p:Sketch):Unit = for(f <- p.drawFunctions) this ! Shape.empty //TODO //use the actual f(g)

  //  derived
  final def !(p:Transform):Unit = {
    this ! Translate(p.translate)
    this ! Rotate(p.rotate)
    this ! ScaleProperty(p.scale)
  }

  //------derived *final*
  //-- a style is a collection of properties, and this does a smart selection by (what hasn't been said) the negative space i.e. not specifying a stroke value implies setting noStroke

  final def !(style:Style):Unit = {
    var hasFill = false
    var hasStroke = false
    var hasAlign = true

    for(p <- style){
      //have to use matching since the Property parent doesn't know which specific property method to use //TODO use a typeclass
      p match {
        //--wait later
        case x:Fill            => this ! x; hasFill   = true
        case x:Stroke          => this ! x; hasStroke = true
        case FillNone          => hasFill   = false
        case StrokeNone        => hasStroke = false
        case h:AlignHorizontal => hasAlign = true; horzAlign = h
        case v:AlignVertical   => hasAlign = true; vertAlign = v
        case a:Align           => hasAlign = true; horzAlign = a.horz; vertAlign = a.vert
        //--do now
        case Default           => this ! Default; hasStroke = false; hasFill = false
        case x:Background      => this ! x
        case x:Weight          => this ! x
        case x:Font            => this ! x
        case x:Translate       => this ! x
        case x:Rotate          => this ! x
        case x:ScaleProperty   => this ! x
        case x:Transform       => this ! x
        //--do nothing??
        // case _ => //do nothing
      }
    }
    if(hasStroke && !hasFill)   this ! FillNone
    if(hasFill   && !hasStroke) this ! StrokeNone
    if(hasAlign) align()
  }
  def ![A,B](a:Axes[A,B]):Unit     = {
    //TODO add draw ticks and detect font size and spacings (and get tick stroke color from the set fill color
    a.xTicks foreach {t => this ! (t + 3.x) ~ Style.Top}   //auto center
    a.yTicks foreach {t => this ! (t - 3.x) ~ Style.Right} //auto midline
  }

  /**this is called before exiting in case resources are still being used*/
  def close():Unit
}