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

/**fundamental generic Scale ops*/
trait ScaleOneWay[A,B]{
   def apply(a:A):B
   /**alias for apply which is a kind of evaluator*/
   //TODO 2017-07-30("op may not be clear","v0.2.15")
   final def %(a:A):B = apply(a)

   /** true if the argument can be applied to a domain*/
   def contains(a:A):Boolean

   /** first check if the argument can be applied to a domain then apply the scale*/
   def get(a:A):Option[B] = if(contains(a)) Some(apply(a)) else None

   //TODO think hard about the this method name for proper left right composition,  andThen, compose, apply, --> ??? to Scale and to Bounds
   // Scale's do not compose, The domain and range are not necessarily the same
   // def andThen[C](that:Scale[B,C]) = new Scale.Chained(this,that)
}

/**The most common Scale is invertible (a two way function)*/
trait Scale[A,B] extends ScaleOneWay[A,B]{
   def inv(b:B):A  //TODO check if this is needed here or can be provided by sub scales

   //--supporting types
   /**invOption uses inv with exception handling (divide by zero is ugly).  This method can be overridden for specific case speedup*/
   def invOption(b:B):Option[A] = Try{inv(b)}.toOption
}
object Scale{
   //-------------------
   //-----Scale Constructors
   //-------------------
   //TODO make the --> an operator object that is type aliased to Scale

   /** Convenience constructor to the most common Scale type of 0~1 to 0~1*/
   2017-07-30("too much default and why this default useful", "v0.2.15")
   def apply():Quantitative[Double,Double] = 0~1 --> 0~1 //Default 

   // def apply[A,B](domain:Bound[A], range:Bound[B]) = new QuantitativeInv(domain,range)
   //note: alias's of the following exists to define --> appropriately
   def apply[A,B](domain:Bound[A], range:Bound[B])     = new QuantitativeBound(domain,range)
   def apply[A,B](domain:LerpInv[A], range:LerpInv[B]) = new QuantitativeInv(domain,range)
   def apply[A,B](domain:LerpInv[A], range:Lerp[B])    = new Quantitative(domain,range)

   /** Construct scales with multiple regions 
     *
     * @param boundaries boundary point mappings of Boundable types
     *
     * Example: {{{
     *   val ageColor = Scale(0 -> Red, 2 -> Orange, 10 -> Green)
     *   ageColor(1) //Redish-orange)
     * }}}
     */
   def apply[A,B](boundaries:(A,B)*)
         (implicit bDomain:Bound.Boundable[A], bRange:Bound.Boundable[B]) =
      new QuantitativeRegions(boundaries:_*)

   //--- 2d Scales
   def apply[Ad,Ar,Bd,Br](aScale:Scale[Ad,Ar], bScale:Scale[Bd,Br]):ScaleTuple2[Ad,Ar,Bd,Br] = new ScaleTuple2(aScale, bScale)
   def apply[Ad,Bd](aScale:Scale[Ad,Double], bScale:Scale[Bd,Double]):ScaleVec[Ad,Bd] = new ScaleVec(aScale, bScale)

   //xy Plot vec range plot
   def apply[Ad,Bd](aDomain:Bound[Ad], bDomain:Bound[Bd], range:Rect):ScaleVec[Ad,Bd] = {
     val aScale = Scale( aDomain , range.xBound )
     val bScale = Scale( bDomain , range.yBound.swap )
     new ScaleVec(aScale, bScale)
   }

   /*
   //TODO try to abstract the 2d functions...
   trait Scale2[Ad,Bd,R](aDomain:LerpInv[Ad], bDomain:LerpInv[Ad]) extends Scale[(Ad,Bd),R]{
     def get(pair:(Ad,Bd)):Option[(Ar,Br)] = for(a <- aScale get pair._1; b <- bScale get pair._2) yield (a,b)
     def get(a:Ad,b:Bd):Option[(Ar,Br)] = get((a,b))
   }
   */
   class ScaleTuple2[Ad,Ar,Bd,Br]( aScale:Scale[Ad,Ar], bScale:Scale[Bd,Br]) extends Scale[(Ad,Bd),(Ar,Br)]{
     def apply(pair:(Ad,Bd)):(Ar,Br) = (aScale(pair._1), bScale(pair._2))
     def apply(a:Ad,b:Bd):(Ar,Br) = (aScale(a), bScale(b))
     def inv(rv:(Ar,Br)):(Ad,Bd) = (aScale.inv(rv._1), bScale.inv(rv._2))
     def contains(pair:(Ad,Bd)):Boolean = contains(pair._1, pair._2)
     def contains(a:Ad,b:Bd):Boolean = aScale.contains(a) && bScale.contains(b)
     def get(a:Ad,b:Bd):Option[(Ar,Br)] = get((a,b))
     override def toString = s"ScaleTuple2($aScale, $bScale)"
   }
   class ScaleVec[Ad,Bd](val xScale:Scale[Ad,Double], val yScale:Scale[Bd,Double]) extends Scale[(Ad,Bd),Vec]{
     def apply(pair:(Ad,Bd)):Vec = Vec(xScale(pair._1), yScale(pair._2))
     def apply(a:Ad,b:Bd):Vec = Vec(xScale(a), yScale(b))
     def get(a:Ad,b:Bd):Option[Vec] = for(x <- xScale get a; y <- yScale get b) yield Vec(x,y)
     def contains(pair:(Ad,Bd)):Boolean = contains(pair._1, pair._2)
     def contains(a:Ad,b:Bd):Boolean = xScale.contains(a) && yScale.contains(b)
     def inv(rv:Vec):(Ad,Bd) = (xScale.inv(rv.x), yScale.inv(rv.y)) //x => _1, y => _2
     override def toString = s"ScaleVec($xScale, $yScale)"
   }
   //---grid
   def apply[Ad,Bd,R](xDomain:Bound[Ad], yDomain:Bound[Bd], grid:Grid[R]):ScaleGrid[Ad,Bd,R] = new ScaleGrid(xDomain,yDomain,grid)

   /**scale x,y domains to to m-n matrix values */
   //TODO remove the iterable trait extension
   //class ScaleGrid[Ad,Bd,R](val xDomain:Bound[Ad], val yDomain:Bound[Bd], val grid:Grid[R]) extends Scale[(Ad,Bd),R] with Iterable[((Ad,Bd),R)]{
   class ScaleGrid[Ad,Bd,R](val xDomain:Bound[Ad], val yDomain:Bound[Bd], val grid:Grid[R]) extends ScaleOneWay[(Ad,Bd),R] {
     val xScale:Quantitative[Ad,Int] = Scale(xDomain,Bound(0,grid.n-1)) //j-scale
     val yScale:Quantitative[Bd,Int] = Scale(yDomain,Bound(0,grid.m-1)) //i-scale
     def apply(pair:(Ad,Bd)):R = apply(pair._1, pair._2)
     def apply(a:Ad,b:Bd):R = grid( yScale(b), xScale(a) ) //grid lookup is matrix indexed as i(down) then j(across)
     def contains(pair:(Ad,Bd)):Boolean = contains(pair._1, pair._2)
     def contains(a:Ad,b:Bd):Boolean = xDomain.contains(a) && yDomain.contains(b)
     def get(a:Ad,b:Bd):Option[R] = if(contains(a,b)) Some(apply(a,b)) else None
     //FIXME this iterator does not seem safe to add, it can be useful but adds many methods to this class that are not clearly related to scales and can cause preformacne issues iterator over every time
     // def iterator:Iterator[((Ad,Bd),R)] = grid.zipWithIndex.map{case (r,i,j) => ((xScale inv j,  yScale inv i),r)}.iterator
     // def inv(rv:R):(Ad,Bd) = ??? //Note can't really do an inverse from R (we could from i,j but that not the *range* here) so only extend Scale instead of ScaleInv
     override def toString = s"ScaleGrid($xDomain, $yDomain, $grid)"
   }

   //---scaled scale
   // This comment is left here to prevent use of this concept again
   ////Note a chained scale is an incorrect type of idea, the range of A and the domain of B are not nessesarily the same
   //class Chained[A,B,C](scaleA:Scale[A,B],scaleB:Scale[B,C]) extends Scale[A,C]{
   //  def apply(a:A):C = scaleB(scaleA(a))
   //  def inv(c:C):A = scaleA.inv(scaleB.inv(c))
   //}


   /** Construct an ordinal scales
     *
     * @param range the modulus output values
     *
     * Example: {{{
     *   val truth = Scale[Int,String](Seq["Black","White"])
     *   truth(0) //"Black"
     *   truth(1) //"White"
     *   truth(2) //"Black"
     *   truth(10)//"White"
     *   truth(1) //"White"
     *   truth.domain //IndexedSeq(0,1,2,10)
     * }}}
     */
   def apply[A,B](range:Iterable[B]):Ordinal[A,B] = new Ordinal(Seq[A](),range.toIndexedSeq)
   /** Construct an ordinal scales with predefined domain (better for deterministic results and thread safety)*/
   def apply[A,B](initDomain:Iterable[A],range:Iterable[B]):Ordinal[A,B] = new Ordinal(initDomain,range.toIndexedSeq)
   /** Construct an ordinal scale from a domain where range bands are automatically created from the size of the domain*/
   def apply[A,B](initDomain:Iterable[A],range:Bound[B]):OrdinalBoundedRange[A,B] =
      //new Ordinal(initDomain,range.steps(initDomain.size-1).toIndexedSeq)
      new OrdinalBoundedRange(initDomain,range)
   /**indexed map ex: Scale(0 to 10, Color.viridis) and Scale(1 to 10, Vector(1,2,3))*/
   def apply[A,B](domain:Bound[A],range:IndexedSeq[B]) = new OrdinalBoundedDomain(domain, range)
   //TODO is this used anymore???
   def apply[A](step:A)(implicit d:Discrete.Discretable[A]) = Discrete(step)

   // def colormap[A](domain:Bound[A]):OrdinalBoundedDomain[A,Color] = apply(domain,Color.viridis)

   //-------------------
   //-----Scale Classes
   //-------------------

   /** The most common 1d Scale with a continuous domain and range */
   // type ScaleLerp[A,B] = Quantitative[A,B]
   //case class Quantitative[A,B](domain:Bound[A], range:Bound[B]) extends ScaleOneWay[A,B]{ 
   //TODO try Lerp based quantitative
   class Quantitative[A,B](val domain:LerpInv[A], val range:Lerp[B]) extends ScaleOneWay[A,B]{

      def take(N:Int):Iterable[(A,B)] = domain.take(N) map {x => x -> apply(x)}
      2017-07-30("use take(N:Int) for a more scala semantic", "v0.2.4")
      def steps(N:Int):Seq[(A,B)] = domain.steps(N) map {x => x -> apply(x)}

      def apply(a:A):B = range.lerp(domain.ratioOf(a)) //range.min + (x-domain.min)*m //ymin + (x-min)*m
      def sat(a:A):B = range.lerp( domain.ratioOf(a).sat(0,1) )
      def contains(a:A):Boolean = domain contains a

      override def toString = s"Scale([${domain.min},${domain.max}] --> [${range.min},${range.max}])"

      //TODO add ticks or just base them off of the domain ??
      //def ticks(n:Int,gain:Double):Ticks = new TicksNew(this,n,gain)
   }
   class QuantitativeInv[A,B](override val domain:LerpInv[A], override val range:LerpInv[B]) extends Quantitative[A,B](domain,range) with Scale[A,B]{
      //lazy val invScale:Quantitative[B,A] = new Quantitative(range,domain)
      //def inv(b:B):A = range.ratioOf(b) = invScale(b)
      def inv(b:B):A = domain.lerp(range.ratioOf(b))
      def invSat(b:B):A = domain.lerp(range.ratioOf(b).sat(0,1))
   }
   class QuantitativeBound[A,B](override val domain:Bound[A], override val range:Bound[B]) extends QuantitativeInv[A,B](domain,range) with Scale[A,B]

   object Discrete{
      trait Discretable[A] {
         def apply(step:A, x:A):A
      }
      object Discretable{
         implicit object DiscreteInt extends Discretable[Int] {
            def apply(step:Int,x:Int) = x - x % step
         }
         /** map a Double to a discrete set of doubles separated by a step size*/
         implicit object DiscreteDouble extends Discretable[Double] {
            def apply(step:Double,x:Double) = x - x % step
         }
         /** map a Vec to a discrete set of vecs separated by a vec step size*/
         implicit object DiscreteVec extends Discretable[Vec] {
            def apply(step:Vec,v:Vec) = (v point step){(x,s) => x - x % s}
         }
      }
   }
   /** Discrete scale offers a simple way to map [A] to a discrete set of [A's] separated by a step size
     *  //TODO deprecate?? this has been used more with Bound.take or Bound.step??
     * @param step the step size of the discretization
     */
   case class Discrete[A](step:A)(implicit d:Discrete.Discretable[A]) extends Scale[A,A]{
      def apply(x:A):A = d(step,x)
      def contains(x:A):Boolean = true //always valid input
      def inv(x:A) = x
   }

   type ScaleBounds[A,B] = QuantitativeRegions[A,B]
   /** A Scale with multiple scales regions (a poly scale)
     *
     * @param boundaries boundary point mappings
     */
   class QuantitativeRegions[A,B](boundaries:(A,B)*)(implicit bDomain:Bound.Boundable[A], bRange:Bound.Boundable[B]) extends ScaleOneWay[A,B]{
      lazy val scales:Seq[Quantitative[A,B]] = {
         for( interval <- boundaries.sliding(2);
              (dMin,rMin) = interval(0);
              (dMax,rMax) = interval(1)
         ) yield Bound(dMin,dMax) --> Bound(rMin,rMax)
      }.toSeq

      private def findScale(x:A) = scales.find{scale => bDomain.lessThan(x , scale.domain.max)}
      def apply(x:A):B = findScale(x).getOrElse(scales.last).sat(x)
      override def get(x:A):Option[B] = findScale(x).map{_ apply x}
      override def contains(x:A):Boolean = findScale(x).isDefined
   }

   type ScaleIndexed[A,B] = Ordinal[A,B]
   /** An Ordinal scale
     *
     * @param initDomain initial domain set (this can be empty and will be dynamically non-deterministically from the calls)
     * @param range the modulus output values
     */
   class Ordinal[A,B](initDomain:Iterable[A],val range:IndexedSeq[B]) extends Scale[A,B]{
      private var _domain:IndexedSeq[A] = initDomain.toVector
      def domain:IndexedSeq[A] = _domain

      private var indices:Map[A,Int] = _domain.zipWithIndex.toMap
      private lazy val reverseIndex:Map[B,Int] = range.zipWithIndex.toMap

      def inv(y:B):A = _domain % reverseIndex(y) // Note: this will throw an error if the element does not exist in the range

      def contains(x:A):Boolean = indices contains x
      def apply(x:A):B =
         if(indices contains x) range % indices(x)
         else {
            val i = indices.size
            _domain :+= x
            indices += (x -> i)
            range % i
         }
   }

   /** An simple class to throw a more specific error for Bounds that can not be inverted*/
   class SingularException extends Exception("Not invertable")

   /** An Ordinal scale based on an outoutBound
     *
     * @param initDomain initial domain set (this can be empty and will be dynamically non-deterministically from the calls)
     * @param range the modulus output values
     */
   class OrdinalBoundedRange[A,B](initDomain:Iterable[A],val range:Bound[B]) extends Scale[A,B]{
      private var _domain:IndexedSeq[A] = initDomain.toVector
      def domain:IndexedSeq[A] = _domain
      private var indices:Map[A,Int] = _domain.zipWithIndex.toMap

      def inv(y:B):A = {
         val N = _domain.size
         if(N==0) throw new SingularException
         else if(N==1) _domain(0)
         else{
            val i = ((range ratioOf y)*(N-1)).round.toInt
            _domain(i.sat(0,indices.size-1))
         }
      }
      def contains(x:A):Boolean = indices.contains(x)
      def apply(x:A):B =
         if(indices contains x){
            val N = indices.size
            if(N == 1) range % 0.5
            else range % (indices(x)/(indices.size - 1.0)) //TODO add check for zero division (single item should go to 0.5) and not get stored in the index
         } else {
            val i = indices.size
            _domain :+= x
            indices += (x -> i)
            if(i == 0) range % 0.5
            else range.max
         }
   }
   class OrdinalBoundedDomain[A,B](val domain:Bound[A],val range:IndexedSeq[B]) extends Scale[A,B]{
      def apply(x:A):B = range lerp domain.ratioOf(x)
      def contains(x:A):Boolean  = domain contains x
      def inv(y:B):A = {
        val N = range.size
        if(N==0) throw new SingularException
        else if(N==1) domain.mid
        else domain.lerp( range.indexOf(y).toDouble/(N-1) )
      }
   }

   /** Convenience function for d3.scale.category10 ordinal scale */
   def cat10[A](domain:Iterable[A]=Seq[A]()):Ordinal[A,Color] =
      new Ordinal(domain,Color.cat10)
   /** Convenience function for d3.scale.category20 ordinal scale */
   def cat20[A](domain:Iterable[A]=Seq[A]()):Ordinal[A,Color] =
      new Ordinal(domain,Color.cat20)
   /** Convenience function for d3.scale.category20b ordinal scale */
   def cat20b[A](domain:Iterable[A]=Seq[A]()):Ordinal[A,Color] =
      new Ordinal(domain,Color.cat20b)
   /** Convenience function for d3.scale.category20c ordinal scale */
   def cat20c[A](domain:Iterable[A]=Seq[A]()):Ordinal[A,Color] =
      new Ordinal(domain,Color.cat20c)
}

object Axes{
  import Style._

  val xSpacing:Double = 60//60pt
  val ySpacing:Double = 30//60pt

  def apply(xs:Iterable[Double], ys:Iterable[Double], box:Rect):Axes[Double,Double]  =
    apply(Stat(xs).bound, Stat(ys).bound, box)

  //  TODO try to make a generic Tickable auto bound method
  // def apply[A,B](xs:Iterable[A], ys:Iterable[B], box:Rect)(implicit ta:Tickable[A],tb:Tickable[B]):Axes[A,B]  = {
  //   val xbound = Bound.find(xs).get //TODO  dangerous
  //   val ybound = Bound.find(ys).get //TODO 
  //   apply(xbound, ybound, box)
  // }

  def apply[A,B](xbound:Bound[A], ybound:Bound[B], box:Rect)(implicit ta:Tickable[A],tb:Tickable[B]):Axes[A,B] = new Axes(xbound,ybound,box).pad
  // TODO  make generic Bound based plotting
  /**create a nice wrapped axis for the data and box*/
  def plot(xs:Iterable[Double], ys:Iterable[Double], box:Rect, size:Double=2d, color:Color=Black alpha 200)(implicit g:DrawContext):Unit = {
    val axes = Axes(xs,ys,box)
    for( xy <- xs zip ys) g ! Circ(axes(xy), size) ~ color
    axes.draw
  }
  def plot(xs:Iterable[Double], ys:Iterable[Double])(implicit g:DrawContext):Unit = plot(xs,ys, g.screen)
}

// TODO add axis units and label values
/**primary means of construction is through rect.axes(xDomain,yDomain) */
class Axes[A,B](xDomain:Bound[A], yDomain:Bound[B], val box:Rect)(implicit ta:Tickable[A],tb:Tickable[B]) extends Scale[(A,B),Vec] with Shape.Closed{

   /**adjust explict bounds to include space for x and y axis with nice tick mark bounds*/
   def pad:Axes[A,B] = {
     import Style._ //need Left and Right
     val pbox = box.pad(-50,Left).pad(-25,Bottom).pad(-6,Top).pad(-10,Right)

     //--estimate number of ticks  (this gives nice options to the bound extent)
     val xN = pbox.bottom.tickCount(Axes.xSpacing)
     val yN = pbox.left.tickCount(Axes.ySpacing) //smallers spacing is needed for y text

     new Axes(xDomain.extend(xN), yDomain.extend(yN), pbox)
   }

   override def draw(implicit g:DrawContext):Unit = {
      import Style._
      val w = 5 //tick width
      //-- x-domain ticks
      g ! box.se ~> w.y ~ Black
      g ! Line(box.sw-w.x, box.se) ~ Black
      for(t <- xTicks ) {
        g ! (t.pos ~> w.y) ~ Black
        g ! (t + (w+1).y) ~ Black ~ Center ~ Top
      }
      //-- y-domain ticks
      g ! box.nw ~> -w.x ~ Black
      g ! Line(box.nw, box.sw+w.y) ~ Black
      for(t <- yTicks) {
        g ! (t.pos ~> -w.x) ~ Black
        g ! (t - (w+5).x) ~ Black ~ Right ~ Midline
      }
   }
   val x = Scale(xDomain, box.xBound)
   val y = Scale(yDomain, box.yBound.swap)//down to up for the yscale range
   def apply(pair:(A,B)):Vec = Vec(x(pair._1), y(pair._2))
   def apply(a:A,b:B):Vec = Vec(x(a), y(b))
   // def get(pair:(A,B)):Option[Vec] = get(pair._1, pair._2)
   def get(a:A,b:B):Option[Vec] = if(contains(a,b)) Some(apply(a,b)) else None
   def contains(pair:(A,B)):Boolean = contains(pair._1, pair._2)
   def contains(a:A,b:B):Boolean = xDomain.contains(a) && yDomain.contains(b)
   def inv(rv:Vec):(A,B) = (x.inv(rv.x), y.inv(rv.y)) //x => _1, y => _2

   def xTicks(count:Int):Iterable[Text] = x.domain.ticksOn(box.bottom, count)
   def yTicks(count:Int):Iterable[Text] = y.domain.ticksOn(box.left.swap, count)
   def xTicks:Iterable[Text] = xTicks( box.bottom.tickCount(Axes.xSpacing) ) //60pt spacing
   def yTicks:Iterable[Text] = yTicks( box.left.tickCount(Axes.ySpacing) ) //60pt spacing

   def yLine(v:B):Line = Vec(box.a.x, y(v)   ) ~> box.width.x
   def xLine(v:A):Line = Vec(x(v)   , box.a.y) ~> box.height.y
}


//ticks should somehow be included into Scale as a method that returns a list of ticks points or an iterator type
//TODO add 2017-07-30("Use Bound.ticks(n) instead", "v0.2.??")
class Ticks(scale:Scale.Quantitative[Double,Double], n:Int, gain:Double) extends Iterable[Double]{
   val min:Double = scale.domain.min*gain
   val max:Double = scale.domain.max*gain
   //TODO nice should be made a method of Bound which returns a new Bound ...
   private def nice(range:Double,round:Boolean):Double = {
      val exp  = math.floor(math.log10(range))
      val frac = range / math.pow(10,exp)
      val mag = math.pow(10,exp)
      val niceFrac = if(round){
         if      (frac < 1.5) 1.0
         else if (frac < 3.0) 2.0
         else if (frac < 7.0) 5.0
         else                10.0
      } else {
         if      (frac < 1.0) 1.0
         else if (frac < 2.0) 2.0
         else if (frac < 5.0) 5.0
         else                10.0
      }
      niceFrac * math.pow(10,exp)

   }

   private val range = nice(max - min, false)
   val spacing = nice( range / (n-1), true)
   val niceMin = math.floor(min/spacing)*spacing
   val niceMax = math.ceil(max/spacing)*spacing

   val niceN = ((niceMax - niceMin)/spacing).toInt+1 //this is based on the suggested `n` by Thomas Furlong

   def iterator:Iterator[Double] = for(i <- Range(0,niceN).iterator; value = niceMin + spacing*i; if value <= niceMax) yield value/gain

   // TODO remove
   // def foreach[U](func:Double => U):Unit = Range(0,niceN).foreach{i => 
   //    val value = niceMin + spacing*i
   //    if(value <= niceMax) func(value/gain)
   // }
}