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

import scala.collection.Iterable

trait Fitter[T]{
  def fit[A](proto:T, ds:Iterable[A], f:A=>(Double,Double)):T
}
/*
trait Univariate[T]{
    def apply(x:Double):Double  //evaluate
    def grad(x:Double):T //define the gradient
    def coefs:ListMap[Symbol,Double]

    //--derived
    def toKson:String = coefs map {(k,v) => s"$k:$v"} mkString " "
    def fromCoefArray(coefs:Array[Double]):T = new T()
    def coefArray:Array[Double] = coeffs
}
*/

//TODO add an implicit fitting function here using Chomsky decomposition and forward/backward solving. For now use external libs wrappers like drx.Math._ that wraps apache commons
// object Fitter{
//   implicit object FitterPolynomial extends cc.drx.Fitter[Polynomial]{
//     def fit[A](proto:Polynomial, ds:Iterable[A], f:A=>(Double,Double)):Polynomial =  ???
//   }
// }
case class FirstOrderStep(a:Double, w:Double){
  def apply(x:Double):Double = a*(1.0 - math.exp(-w*x))
  override def toString = f"$a*(1 - e^(-$w*x))"
  def toKson   = f"FirstOrderStep a:$a w:$w"
  def fit[A](ds:Iterable[A])(implicit f:A=>(Double,Double), fitter:Fitter[FirstOrderStep]):FirstOrderStep = fitter.fit(this,ds,f) //if A = (Double,Double) then use the implicit  Predef.identity to pluck out x and y values
}
case class Exponential(b:Double, a:Double, k:Double, t:Double){
  def apply(x:Double):Double = b + a*math.exp(k*(x-t))
  override def toString = f"$b + $a e^($k (x-$t))"
  def toKson   = f"Exponential: b:$b a:$a k:$k t:$t"
  def fit[A](ds:Iterable[A])(implicit f:A=>(Double,Double), fitter:Fitter[Exponential]):Exponential = fitter.fit(this,ds,f) //if A = (Double,Double) then use the implicit  Predef.identity to pluck out x and y values
}

object Polynomial{
  /**coeffs(0) is order 1*/
  def apply(coeffs:Double*):Polynomial = Polynomial(coeffs.toArray)
  def order(n:Int) = Polynomial(Array.fill[Double](n+1)(1))
}
case class Polynomial(coeff:Array[Double]){
  def order = coeff.size-1
  //def apply(x:Double) = coeff.zipWithIndex.foldLeft(0.0){case (a,(c,i)) => a + c*(math.pow(x,i))}
  def apply(x:Double):Double = coeff.foldLeft(0.0->1.0){case ((a,xi),c) => a + c*xi -> xi*x}._1
  def toKson = "Polynomial coefs:" + (coeff mkString " ")
  override def toString = coeff.zipWithIndex.foldLeft(""){case (a,(c,i)) =>
    if(i==0) f"$c" else {
       val exp = if(i>1) "^"+i else ""
       val sgn = if(c < 0) "-" else "+"
       f"$a $sgn ${c.abs} x$exp"
    }
  }
  def fit[A](ds:Iterable[A])(implicit f:A=>(Double,Double), fitter:Fitter[Polynomial]):Polynomial = fitter.fit(this,ds,f) //if A = (Double,Double) then use the implicit  Predef.identity to pluck out x and y values
}

object Logistic{
  def apply():Logistic = Logistic(k=1,m=1,b=1,  q=1,a=1,n=1) //reasonably well starting conditions for fitting problems
  def ones = Logistic(k=1,m=1,b=1,  q=1,a=1,n=1) //reasonably well starting conditions for fitting problems
}
case class Logistic(k:Double,  //ending value: asymptote to \inf (or -\inf if b<0)
                    m:Double,  //start time: (of max derivative)
                    b:Double,  //growth rate
                    q:Double,  //pos of curve (related to apply(0))
                    a:Double,  //starting value: lower asymptote
                    n:Double   //\nu pos of max growth ( >0 )
){
  require(n > 0)
  require(k >= a)

  private val kMinusA = k - a
  private val nInv = 1.0/n
  def apply(x:Double) = a + kMinusA / math.pow(1.0 + q * math.exp(b * (m-x)), nInv)

  override def toString = f"$a + $kMinusA / (1 + $q * e^($b * ($m-x)))^$nInv";
  def toKson = f"Logistic k:$k m:$m b:$b q:$q a:$a n:$n"
  def fit[A](ds:Iterable[A])(implicit f:A=>(Double,Double), fitter:Fitter[Logistic]):Logistic = fitter.fit(this,ds,f)

  def shift(x:Double) = copy(m = m+x)
}