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

//---a typeclass that adds nice spaced tick marks at important beacon points of a Bound
trait Tickable[A] {
  /**return  on the order of `n` nice tick points at beacon values*/
  def ticks(n:Int, bound:LerpInv[A]):Iterable[A]

  def logTicks(n:Int, bound:LerpInv[A], base:Double):Iterable[A] = ticks(n,bound)  //default to linear scale if not overridden

  /**an extended range version of the bound that extends to nice values*/
  def extend(n:Int, bound:LerpInv[A])(implicit b:Bound.Boundable[A]):Bound[A] //the boundable is required to generically make new bounds

  def formatter(n:Int, bound:LerpInv[A]):(A => String) = Format.apply[A] //the default implementation case ignores the counts and ranges

  //--the following are required to generalized the tickables
  //def add(a:A,b:A):A = ???
  //def sub(a:A,b:A):A = ???
}
object Tickable {
  // import Bound.Boundable._  //include a bunch of Boundable implicit objects
  trait FromDouble[A] extends Tickable[A]{
    //--required abstract definitions
    def fromDouble(x:Double):A
    def toDouble(a:A):Double
    def unit:String = ""

    override def formatter(n:Int,b:LerpInv[A]):(A=> String) = {(a:A) =>
      TickableDouble.toSI(toDouble(a)) + unit
    }

    def toBoundDouble(l:Lerp[A]):Bound[Double] = Bound( toDouble(l.min), toDouble(l.max) )

    //--supporting
    def ticks(n:Int, bound:LerpInv[A]):Iterable[A] =
      TickableDouble.ticks(n, toBoundDouble(bound)) map fromDouble
    override def logTicks(n:Int, bound:LerpInv[A],base:Double):Iterable[A] =
      TickableDouble.logTicks(n, toBoundDouble(bound),base) map fromDouble
    def extend(n:Int, bound:LerpInv[A])(implicit b:Bound.Boundable[A]):Bound[A] =
      TickableDouble.extend(n, toBoundDouble(bound)) map fromDouble
  }
  implicit object TickableInt extends FromDouble[Int]{
    def fromDouble(x:Double):Int  =  x.round.toInt
    def toDouble(a:Int):Double  = a.toDouble
  }
  implicit object TickableLong extends FromDouble[Long]{
    def fromDouble(x:Double):Long  =  x.round.toLong
    def toDouble(a:Long):Double  = a.toDouble
  }
  implicit object TickableShort extends FromDouble[Short]{
    def fromDouble(x:Double):Short =  x.round.toShort
    def toDouble(a:Short):Double  = a.toDouble
  }
  //the name of this object is hilarious unless you live in Northern Virginia
  implicit object TickableByte extends FromDouble[Byte]{
    def fromDouble(x:Double):Byte =  x.round.toByte
    def toDouble(a:Byte):Double  = a.toDouble
  }
  implicit object TickableFrequency extends FromDouble[Frequency]{
    def fromDouble(x:Double):Frequency  =  Frequency(x)
    def toDouble(a:Frequency):Double  = a.hz
    override def unit:String = "Hz"
  }

  // implicit object TickableBaseValue extends BaseValue[_]{
  //   def fromDouble(x:Double):Frequency  =  Frequency(x)
  //   def toDouble(a:Frequency):Double  = a.baseValue
  // }

  /**This is a generic numeric implementation that many numeric type can use via `Tickable.FromDouble`
   *  Wilkenson algorithm???
   *  http://vis.stanford.edu/papers/tick-labels
   * */
  implicit object TickableDouble extends Tickable[Double]{

    private val intervals = Array(0d, 1d, 2d, 2.5d, 5d, 10d)
    //-->--generic interval code....
    /**nice human sampling interval*/
    private def search(dt:Double,offset:Int):Double = {
      val idx:Int = intervals.searchBy(dt){t => t}
      intervals.lift(idx-offset) getOrElse intervals.last
    }
    /**nice human value below*/
    def floorCoef(dt:Double):Double = dt.mapAbs{search(_,1)}
    /**nice human value above*/
    def ceilCoef(dt:Double):Double =  dt.mapAbs{search(_,0)}
    /**round to the closest floor or ceil*/
    def roundCoef(dt:Double):Double = {
      val dtFloor = floorCoef(dt)
      val dtCeil  = ceilCoef(dt)
      if( (dt - dtFloor).abs < (dt - dtCeil).abs ) dtFloor else dtCeil
    }
    //---<---generic interval code....

    private def split(x:Double):(Double,Double) = {
      // require(x != 0)
      if(x == 0) (0,1)
      else {
        val mag = x.abs.log10.floor.exp10
        (x/mag, mag)
      }
    }

    private def roundGeneric(x:Double, step:Double, coefFunc:Double => Double):Double = {
      require(step != 0) //can't take a log here and doesn't make sense
      val dtMag = step.abs.log10.floor.exp10
      val coef = x/dtMag
      coefFunc(coef) * dtMag
    }

    //--public methods of interface
    def round(x:Double, step:Double):Double = roundGeneric(x, step, roundCoef)
    def floor(x:Double, step:Double):Double = roundGeneric(x, step, floorCoef)
    def ceil(x:Double,  step:Double):Double = roundGeneric(x, step, ceilCoef)

    //TODO make these sync up with the ticks...???
    override def extend(n:Int, bound:LerpInv[Double])(implicit b:Bound.Boundable[Double]):Bound[Double] = {
      val d = delta(n,bound)
      val min = math.floor(bound.min/d)*d
      val max = math.ceil(bound.max/d)*d
      Bound(min,max)
    }
    private def delta(n:Int, bound:LerpInv[Double]):Double = {
      val dt = (bound.max - bound.min)/n
      val (coef, mag) = split(dt)
      ceilCoef(coef)*mag
    }

    //TODO make a nice formatter based on si units and the fractional components here (the factional component should only be shown if dt=2.5
    //TODO add this as a generic, but specify the exact step size starting from a coef of zero
    // def ticksBy(dt:Double):Iterable[Double] = ???

    /**return at most `n` nice tick points at beacon values*/
    def ticks(n:Int, bound:LerpInv[Double]):Iterable[Double] = {
      if(n<2) List(bound lerp 0.5) //require(n > 1, "2 or more ticks are required")
      else {
        val dtFull = (bound.max - bound.min)/(n-1) //exact steps
        val mag = dtFull.abs.log10.floor.exp10
        val dt = ceilCoef(dtFull/mag)  //find the next largest coef to use
        val coef0 = ((bound.min/mag)/dt).ceil*dt
        assert(dt != 0, "step size is zero") //TODO remove assertion
        if(dtFull == 0d || dt == 0d) List(bound lerp 0.5)  // assert(dt != 0.s, "step size is zero") //TODO remove assertion
        else {
          @tailrec def step(coef:Double, ds:List[Double]):List[Double] = {
            val d = coef*mag
            if(bound contains d) step(coef + dt, d :: ds) else ds
          }
          val ds = step(coef0, Nil).reverse
          ds
        }
      }
    }

    //-note:this is required since ciel frustratingly jumps all the way to the top even if very close to the floor
    private def ceilMost(x:Double):Double = if(x - x.floor < 0.0001) x.floor else x.ceil

    //TODO log scale formatting (exponential instead of depending on linear double formatting)

    /**return at most `n` nice tick points at beacon values*/
    override def logTicks(n:Int, lerp:LerpInv[Double], base:Double):Iterable[Double] = {
      if(n<2) List(lerp lerp 0.5) // require(n > 1, "2 or more ticks are required")
      else {
        //although this zero case squishing is not mathematically accurate it does the right thing in the sense of a bound based for visualizations (in fact the squishing may be prefereable as a means of finding a starting tick instead of using ceilMost
        val bound  = Bound(lerp.min, lerp.max)
        val bound0 = if(bound.min == 0 || bound.max == 0) bound*0.99 else bound

        val isPositive = bound0.min > 0 && bound0.max > 0
        val isNegative = bound0.min < 0 && bound0.max < 0
        require(isPositive || isNegative, "both the min and max values must both be postiive or negative (no zero crossings for a log scale)")

        val sgn = if(isNegative) -1d else 1d //note: 0 not possible
        val minExp = bound0.min.abs.log(base)
        val maxExp = bound0.max.abs.log(base)
        val dtExp = (maxExp - minExp)/(n-1)  //exponential steps

        val mag  = dtExp.abs.log10.floor.exp10 //base 10 system of steps
        val dt   = ceilCoef(dtExp/mag)  //find the next largest nice sized exponential component
        val exp0 = ceilMost((minExp/mag)/dt)*dt //jump to a starting point just inside the bound
        @tailrec def step(exp:Double, ds:List[Double]):List[Double] = {
          val d = sgn*base.pow(exp*mag)
          if(bound contains d) step(exp + dt, d :: ds) else ds
        }
        val ds = step(exp0, Nil).reverse
        ds
      }
    }

    def toSI(d:Double):String= {
      if(d == 0) "0"
      else{
        val s = if(d>0)""else"-"//sgn
        val d0 = d.abs
        val b = 1000d //base
        val e0 = d0.log(b)
        val e  = d0.log(b).floor.toInt //if(e0 > 0) e0.floor.toInt else e0.ceil.toInt .mapAbs{_.floor}.toInt  //exponent
        val u = siPrefix.lift(e+6) getOrElse {s"E${e*3}"}   //unit prefix (si)
        val m = b**e      //magnitude
        val c = (d0/m)//.round.toInt //coefficient (integer)
        // Log(b,d0,e,u,m,c)
        val cf = nice3(c)
        s"$s$cf$u" //coef and unit (string)
      }
    }

    private def nice3(d:Double):String =
      f"$d%6.3f".trim.applyIf(_ contains "."){
        _.reverse.dropWhile{_ == '0'}.dropWhile{_ == '.'}.reverse
      }

    private val siPrefix = Array("a","f","p","n","μ","m","","k","M","G","T","P","E")
    override def formatter(n:Int,b:LerpInv[Double]):(Double => String) = {(d:Double) =>
      //Format.apply[Double](toDouble(a)) //TODO try this formatter
      // toSI(d)
      nice3(d)
    }

  }
  implicit object TickableTime extends Tickable[Time]{
    //intervals roughly guided by d3/d3-scale/src/time.js
    private val intervals:Array[Time] = Array(
      1.s/1E12, 1.s/1E9, 1.s/1E6,
      1.ms, 25.ms, 100.ms, 500.ms,
      1.s, 5.s, 15.s, 30.s,
      1.minute, 5.minute, 15.minute, 30.minute,
      1.hr, 3.hr, 6.hr, 12.hr,
      1.day, 2.day, 7.day,
      1.yr/12, 1.yr/4, 1.yr/2,
      1.yr, 10.yr, 100.yr, 1000.yr, 1000.yr  //why does this last value need to be repeated
    )
    /**nice human sampling interval*/
    private def search(dt:Time,offset:Int):Time = {
      val idx:Int = intervals.searchBy(dt){t => t}
      intervals.lift(idx-offset) getOrElse intervals.last
    }

    /**nice human sampling interval*/
    def floor(dt:Time):Time = search(dt.abs,1)*dt.s.sgn
    /**nice human sampling interval*/
    def ceil(dt:Time):Time = search(dt.abs,0)*dt.s.sgn
    /**round to the closest floor or ceil*/
    def round(dt:Time):Time = {
      val dtFloor = floor(dt)
      val dtCeil  = ceil(dt)
      if( (dt - dtFloor).abs < (dt - dtCeil).abs ) dtFloor else dtCeil
    }

    override def extend(n:Int, bound:LerpInv[Time])(implicit b:Bound.Boundable[Time]):Bound[Time] = extend(delta(n,bound), bound)

    private def extend(delta:Time, bound:LerpInv[Time]):Bound[Time] = {
      val minRound = bound.min.round(delta)
      val minDt = bound.min - minRound  // negative means  round is above min, positive means round is below
      val min = minRound + delta*(minDt/delta).round

      val maxRound = bound.max.round(delta)
      val maxDt = bound.max - maxRound  // negative means  round is above min, positive means round is below
      val max = maxRound - delta*(maxDt/delta).round

      // Macro.pp(delta, minDt, maxDt)
      Bound(min, max)
    }

    private def delta(n:Int, bound:LerpInv[Time]):Time = TickableTime.ceil((bound.max - bound.min)/n)

    //- TODO Time.round(dt)
    //- TODO Format.resolution[Time]
    //- TODO Time.format(dt)

    //TODO generalize this tick generation based on a Delta like typeclass
    def ticks(n:Int, bound:LerpInv[Time]):Iterable[Time] = {
      val dt = delta(n, bound) //step size based on number of desired ticks
      if(dt == 0.s) List(bound lerp 0.5)  // assert(dt != 0.s, "step size is zero") //TODO remove assertion
      else {
        @tailrec def findStart(d:Time):Time = if(bound contains d) d else findStart(d + dt) //step into the bound
        val d0 = findStart(bound.min.round(dt) -dt) //human round then step n times back to make sure all of the start is caught
        @tailrec def step(d:Time, ds:List[Time]):List[Time] = if(bound contains d) step(d + dt, d :: ds) else ds
        val ds = step(d0, Nil).reverse
        ds map {_ round dt} //make sure the values are rounded to the meaningful resolution
      }
    }
    override def formatter(n:Int,b:LerpInv[Time]):(Time => String) = Time.format((b.max - b.min).abs, 1).apply
  }
  /**tickable date depends on tickable time steps*/
  implicit object TickableDate extends Tickable[Date]{
     //Note: makes common use the following; so use of these `intervals` all need to be coordinated
     //- Date.round(dt)
     //- Date.Form.resolution(dt)
     //- Date.format(dt)
    private def delta(n:Int, bound:LerpInv[Date]):Time = TickableTime.ceil((bound.max - bound.min)/n) //negative if min > max

    //TODO the logic is fraught with edge cases(i.e. reveresed min, max), it would be nice o generlize this stepping process for the other tickables
    override def ticks(n:Int, bound:LerpInv[Date]):Iterable[Date] = {
      val dt = delta(n, bound) //step size based on number of desired ticks
      if(dt == 0.s) List(bound lerp 0.5)  // assert(dt != 0.s, "step size is zero") //TODO remove assertion
      else{
        @tailrec def findStart(d:Date):Date = if(bound contains d) d else findStart((d + dt).round(dt)) //step into the bound
        val d0 = findStart(bound.min.round(dt) - dt) //human round then step n times back to make sure all of the start is caught
        @tailrec def step(d:Date, ds:List[Date]):List[Date] = if(bound contains d) step(d + dt, d :: ds) else ds
        val ds = step(d0, Nil).reverse
        ds map {_ round dt} //make sure the values are rounded to the meaningful resolution
      }
    }

    override def extend(n:Int, bound:LerpInv[Date])(implicit b:Bound.Boundable[Date]):Bound[Date] =  extend(delta(n,bound), bound)
    private def extend(delta:Time, bound:LerpInv[Date]):Bound[Date] = {
      //TODO clean this up maybe generlize and use ceil and floor?
      val minRound = bound.min.round(delta)
      val minDt = bound.min - minRound  // negative means  round is above min, positive means round is below
      val min = minRound + delta*(minDt/delta).round

      val maxRound = bound.max.round(delta)
      val maxDt = bound.max - maxRound  // negative means  round is above min, positive means round is below
      val max = maxRound - delta*(maxDt/delta).round

      // Macro.pp(delta, minDt, maxDt)
      Bound(min, max)
    }

    override def formatter(n:Int,b:LerpInv[Date]):(Date => String) = Date.format((b.max - b.min).abs).apply
  }

}