/*
   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 helper class for time series signals
 */
object Signal{
  type TVS = Seq[(Time,Double)]

  private val ConsPat = """(^|\s)(-?\d)""".r //pattern to auto add a C for constant
  private val SpecPat = {
    val num = """[0-9\-.E]+"""
    val ws = """[\s,]*"""
    val kind = "[cCrRhHzZ]"
    val tspec = "[/@%]"
    //Spec pat reads like: Add piecewise signal of <kind> with param <num> with a width of by <time spec>
    //     req.     opt.         opt.
    s"$ws($kind)$ws($num)?$ws($tspec$num)?$ws".r
  }
  private val empty:TVS = Seq()

  def main(args:Array[String]):Unit = {
    if(args.isEmpty) printExampleSpecs
    else {
      val tvs = Signal.fromSpec(args.toSeq.mkSpaces, frameWidth=1.s, dt=30.hz.inv)
      Plot.println(tvs)
    }
  }
  def printExampleSpecs:Unit = {
    val specs = Kson("""
    zCCCccCcz                    desc: 3211 pulse
    z%0.5CCCccCcz                desc: 3211 pulse with frameWidth 3
    zCz                          desc: pulse (square)
    zCcz                         desc: doublet (square)
    zRrz                         desc: pulse (triangle)
    zRrrRz                       desc: doublet (triangle)
    C1 Z                         desc: start at constant drop to zero
    Z@3C1/3                      desc: start at 3s a pulse
    Z@0.2C1Z                     desc: start at 0.2s a pulse
    Z R1/.25 H H H R-1/.25 H/1   desc: doublet (trapezoid)
    C1 C0                        desc: start at constant drop to zero
    1 0                          desc: start at constant drop to zero
    """)

    for(line <- specs.lines; spec <- line.root;  desc <- line.get("desc")){
      println(s"#=== '$spec'    $desc")
      val tvs = Signal.fromSpec(spec, frameWidth=1.s, dt=30.hz.inv)
      Plot.println(tvs)
    }
  }

  /** Signal spec is a mini DSL language (i.e. svg d paths) to generated time series data in a concise string like svg path language 
   *  
   *  Spaces are irrelevant
   *  Spec strings are a sequence of piecwise signals (pieces).  
   *  Each piece reads like: 
   *     Add piecewise signal of <kind> with param <num> with a width of by <time spec>
   *     Each 
   *     [kind][value][timespec timevalue] 
   *
   *     kind:  
   *        C  desc:constant               default:1
   *        c  desc:constant inverted      default:1
   *        R  desc:Rate slope to value    default:1
   *        r  desc:constant inverted      default:1
   *        H  desc:hold last value        default:n/a
   *        z  desc:a constant of zero     default:n/a
   *
   *     timespec:
   *        /  desc:alternate frame width            
   *        @  desc:stretch frame to an absolute time   
   *        %  desc:specify future frame widths
   *
   *  */
  def fromSpec(spec:String, gain:Double=1d, frameWidth:Time=1.s, dt:Time=30.hz.inv):TVS = {
    var _frameWidth:Time = frameWidth
    val specPrime = ConsPat.replaceAllIn(spec,{m => " C" + m.group(2)}) //prepend the C the force the constant
    SpecPat.findAllMatchIn(specPrime).toSeq.foldLeft(empty){case (tvs,m) =>
      val (t0,v0) = tvs.lastOption.getOrElse(0.s -> 0d)
      def group(i:Int):Option[String] = Option(m group i)

      val kind:String = group(1).getOrElse("Z")
      val v:Double    = group(2).getOrElse("1").toDouble
      //cacluate piecewise time width
      val tw:Time = group(3).map{tspec =>
        val t:Time = tspec.drop(1).toDouble.s
        tspec.take(1) match {
          case "%" => _frameWidth=t; t //set this and all future timewidths
          case "/" => t
          case "@" => t - t0
        }
      }.getOrElse(_frameWidth)
      // println(f"tw:${tw.ms}%5.0fms  kind:$kind v:$v% 2.2f")

      val tStart = if(tvs.isEmpty) t0 else t0 + dt
      val tEnd   = t0+tw
      tvs ++ {
        kind match {
          case "C"     => Seq(tStart ->  v, tEnd -> ( v    ) )
          case "c"     => Seq(tStart -> -v, tEnd -> (-v    ) )
          case "R"     => Seq(              tEnd -> (v0 + v) )
          case "r"     => Seq(              tEnd -> (v0 - v) )
          case "h"|"H" => Seq(              tEnd -> (v0    ) )
          case "z"|"Z" => Seq(tStart -> 0d, tEnd -> (0d    ) )
        }
      }
    }
    //--optimze (remove redundancies)
    .groupRunsBy(_._2).flatMap{constants =>
      if(constants.size > 2) Seq(constants.head, constants.last)
      else constants
    }
  }.map{case (t,v) => t -> v*gain} //apply gains
}