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


//http://howardhinnant.github.io/date_algorithms.html  MIT and public domain attributions from the summary section of the paper
object Date2{
  def ms:Long = System.currentTimeMillis //new JDate().getTime //epoch: number of ms from ~1970

  //--construction
  def fromMs(d:Long):Date2 = new Date2(d)
  def fromSec(d:Long):Date2 = fromMs(d*1000) //new JDate().getTime //epoch: number of seconds from ~1970
  /*unix epoch moment in time*/
  def now:Date2 = fromMs(ms)
  val epoch:Date2 = new Date2(0L) //new JDate().getTime //epoch: number of seconds from ~1970

  //-- epoch construction
   // def apply(d:Long):Date2 = new Date2(d)  //epoch: number of ms from ~1970
   def apply(d:Double):Date2 = new Date2(d.toLong)  //epoch: number of ms from ~1970 //TODO add auto numeric info

  //-- civil constructions
  def apply(year:Int):Date2                                                 = Date2(year,1,1)
  def apply(year:Int, month:Int):Date2                                      = Date2(year,month,1)
  def apply(year:Int, month:Int, day:Int):Date2                             = DateCivil(year,month,day).date

  //-- date and time constructions
  def apply(year:Int, month:Int, day:Int, hour:Int):Date2                   = Date2(year,month,day,hour,0,0,0)
  def apply(year:Int, month:Int, day:Int, hour:Int, min:Int):Date2          = Date2(year,month,day,hour,min,0,0)
  def apply(year:Int, month:Int, day:Int, hour:Int, min:Int, sec:Int):Date2 = Date2(year,month,day,hour,min,sec)
  def apply(year:Int, month:Int, day:Int, hour:Int, min:Int, sec:Int, ms:Int):Date2 = Date2(
    DateCivil(year,month,day).epochDay*msPerDay + DateTime24(hour,min,sec,ms).msInDay
  )

  //-- day of week constructions
  /**nth dayOfWeek of a month*/
  def apply(year:Int, month:Int, day:Date.DayOfWeek,n:Int):Date2 = DateCivil(year,month,1).date.get(day,n)
  /**nth dayOfWeek of the year*/
  def apply(year:Int, day:Date.DayOfWeek,n:Int):Date2            = DateCivil(year,1,1).date.get(day,n)
  /**first dayOfWeek of a month*/
  def apply(year:Int, month:Int, day:Date.DayOfWeek):Date2       = DateCivil(year,month,1).date.get(day,1)
  /**first dayOfWeek of a year*/
  def apply(year:Int, day:Date.DayOfWeek):Date2                  = DateCivil(year,1,1).date.get(day,1)

  //--constants
  final val msPerDay = 86400000L // ms/day => 24*3600*1000

  //--type classes
  implicit object FormatDate2 extends Format[Date2]{ override def apply(v:Date2):String = v.iso}
  //TODO  implicit object ParsableDate2 extends Parsable[Date2]{ def apply(v:String):Date2 = Date2(v) }
  implicit object OrderingDate2 extends Ordering[Date2]{
    def compare(a:Date2,b:Date2):Int = a.ms compare b.ms
  }
  implicit object BoundableDate2 extends Bound.Boundable[Date2]{
    def lessThan(a:Date2,b:Date2) =  a.ms < b.ms
    def interpolate(min:Date2,max:Date2, ratio:Double) = new Date2( (min.ms + ratio*(max.ms - min.ms)).toLong)
    def ratioOf(min:Date2,max:Date2, x:Date2) = (x.ms - min.ms).toDouble/(max.ms - min.ms)
    override def dist(min:Date2,max:Date2):Double =  (max.ms - min.ms).toDouble.abs
    override def gain(min:Date2,max:Date2):Double =  (max.ms/min.ms).toDouble.abs
    override def toString(min:Date2,max:Date2) = f"Bound(${Time(dist(min,max)*1E-3).format}%8s from ${min.iso} to ${max.iso})"
  }
  implicit object BoundDate2Step extends Bound.BoundStep[Date2,Time]{
    def next(a:Date2, step:Time):Date2 = a + step
  }

}
object DateCivil {
  /** construct from number of days from 1970-01-01 
   *  http://howardhinnant.github.io/date_algorithms.html
   * */
  def apply(epochDay:Int):DateCivil = {
    val z:Int   = epochDay + 719468  //epoch shift to March1st (the day after leap year day)
    val era:Int = (if(z >= 0)  z else z - 146096) / 146097 // era
    val doe:Int = z - era * 146097     //day of epoch [0, 146096]
    val yoe:Int = (doe - doe/1460 + doe/36524 - doe/146096) / 365  // [0, 399]
    val doy:Int = doe - (365*yoe + yoe/4 - yoe/100)                // [0, 365]
    val mp:Int  = (5*doy + 2)/153  // [0, 11]

    //--month
    val month = mp + (if(mp < 10) 3 else -9)   // [1, 12]        
    //--year
    val year = yoe + era * 400  + (if(month <= 2) 1 else 0)
    //--day
    val day = doy - (153*mp+2)/5 + 1;         // [1, 31]
    DateCivil(year, month, day)
  }

  private val commonMonthDays = Array(31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31)
  def isLeapYear(year:Int):Boolean = year % 4 == 0 && (year % 100 != 0 || year % 400 == 0)
  def lastDayOfMonth(year:Int, month:Int):Int =
    if(month != 2) commonMonthDays(month-1) else if(isLeapYear(year)) 28 else 29

  // /**civil date extractor from a Date2 also instead of just a DateCivil*/
  def unapply(date:Date2):Option[(Int,Int,Int)] = {
    val civil = date.civil
    Option( (civil.y, civil.m, civil.d) )
  }
}
/** year from Christ:0, month:[1,12], day:[1,31] */
case class DateCivil(y:Int, m:Int, d:Int) { //TODO this is where the time zone is required
  final def date = Date2(epochDay*Date2.msPerDay)

  final def epochDay:Int = {
    //--shift year and month with month offset for february 29 as last day of `yp`
    val yp = if(m > 2) y else y-1
    val mp = if(m > 2) m-3 else m+9

    //--error and calculations
    val era = (if(yp >= 0) yp else yp - 399) / 400  //leap year patterns are repeat every 400 years
    val yoe = yp - era*400  //year of epoch  [0,399]
    val doy = (153*mp + 2)/5 + d-1 //day of year [0, 365]
    val doe = yoe*365 + yoe/4 - yoe/100 + doy //day of epoch [0, 146096]
    era*146097 + doe - 719468
  }

  def isoYear:String    = f"$y%04d"
  def isoMonth:String   = f"$y%04d-$m%02d"
  def isoDay:String     = f"$y%04d-$m%02d-$d%02d"
  def iso:String        = isoDay

  def month:Date.Month  = Date.Month.fromIndex(m)

}

/**Time of day in hour:min:sec*/
object DateTime24{
  val msPerHour:Long = 60*60*1000L
  val msPerMin:Int   = 60*1000
  val msPerSec:Int   = 1000
  def apply(msInDay:Long):DateTime24 = {
    val (h, msInHour) = msInDay  divMod msPerHour
    val (m, msInMin)  = msInHour divMod msPerMin
    val (s, ms)       = msInMin  divMod msPerSec
    DateTime24(h.toInt, m.toInt, s.toInt, ms.toInt)
  }
}
case class DateTime24(hour:Int, min:Int, sec:Int, ms:Int) {
  import DateTime24._
  def isoMin:String = f"$hour%02d:$min%02d"
  def isoSec:String = f"$hour%02d:$min%02d:$sec%02d"
  def isoMs:String  = f"$hour%02d:$min%02d:$sec%02d.$ms%03d"
  def msInDay:Long  = hour*msPerHour + min*msPerMin + sec*msPerSec + ms
}
case class Date2(val ms:Long) extends AnyVal with scala.math.Ordered[Date2] {
  import Date.DayOfWeek
  import Date2._
  import DateTime24._

  private def epochDay:Int = (ms/Date2.msPerDay).toInt
  private def msInDay:Long = ms % Date2.msPerDay   //TODO FIXME add time zone dependency

  def civil:DateCivil = DateCivil(epochDay)
  def y = civil.y
  def d = civil.d     //halfway lazy val for civil
  def m = civil.m
  def month = civil.month
  private def time:DateTime24 = DateTime24(msInDay)
  // def h = time.hour
  // def min = time.min
  // def s = time.sec

  /** day of week 0 to 6 -> [Sun, Sat] */
  def dowIndex:Int = { val z = epochDay; if(z >= -4) (z+4) % 7 else (z+5) % 7 + 6 }

  def dayOfWeek:DayOfWeek = DayOfWeek.fromIndex(dowIndex)
  def dow:DayOfWeek = dayOfWeek

  /** get the n'th (1 = first) day of week from a given date (current day is the first)*/
  def get(that:DayOfWeek,n:Int=1):Date2 = {
    val d = this.dow deltaTo that
    val daysTo = {
      val weekShift = (n.abs-1)*7
      if(n > 0) (if(d < 0) d + 7 else d) + weekShift
      else      (if(d > 0) d - 7 else d) - weekShift
    }
    Date2(ms + daysTo*Date2.msPerDay)
  }
  /** get the NEXT n'th (1 = first) day of week from a given date (current day is not counted)*/
  def next(that:DayOfWeek,n:Int=1):Date2 = {
    val nOffset = if(this.dow == that) 1 else 0
    get(that, n.sgn*(n.abs + nOffset) )
  }
  /** get the Previous n'th (1 = first) day of week from a given date (current day is not counted)*/
  def prev(that:DayOfWeek,n:Int=1):Date2 = next(that, -n)

  //--ISO 8601 formats https://en.wikipedia.org/wiki/ISO_8601
//   def zoneOffset:Time = Time(0) //TODO make this configurable (maybe an implicit ???, but it shoudl be explicit)
//   def isoZone = if(zoneOffset==Time(0)) "Z" else {
//     val (h,m) = zoneOffset.min.round.toInt divMod 60
//     f"$h%+02d:$m%02d"
//   }
  def isoZone = "Z"
  def isoYear:String = civil.isoYear
  def isoDay:String  = civil.isoDay
  def isoMin:String  = civil.isoDay + "T" + time.isoMin + isoZone
  def isoSec:String  = civil.isoDay + "T" + time.isoSec + isoZone
  def isoMs:String   = civil.isoDay + "T" + time.isoMs  + isoZone
  def iso:String     = isoSec

  override def toString = s"Date2($iso)"

//   def isoYr:String      = Form.year    apply this
//   def isoDay:String     = Form.day     apply this
//   def iso:String        = Form.sec     apply this
//   def isoHour:String    = Form.hour    apply this
//   def isoMinute:String  = Form.minute  apply this
//   def isoMs:String      = Form.ms      apply this
//   def isoCompact:String = Form.compact apply this
//   def isoLocal:String   = Form.local   apply this
//   def rfcJava:String    = Form.java    apply this
//   def rfcHttp:String    = Form.http    apply this
//
//   def %(fmt:String):String = this format fmt
//   private def lowerAMPM(s:String) = s.replace("AM","am").replace("PM","pm")
//   def format(fmt:String):String = Form(fmt) apply this
//   def format(fmt:String,tz:String):String = Form(fmt,tz) apply this
//   def format(f:Form):String = f apply this
//   // def format(formatter:SimpleDateFormat):String = formatter.format(date).replace("AM","am").replace("PM","pm")
//   //
//   //def zone = ??? //TODO why isn't a zone associated with a time already?
//
//   override def toString = s"Date2($iso)"
//   def toRelativeString = {val dt = Date2.now - this; dt.format(dt,0) + (if(dt > 0.s) " ago" else " from now")}
//
   def ~(that:Date2):Bound[Date2] = if(this < that) Bound(this,that) else Bound(that,this)
   def ~>(time:Time):Bound[Date2] = Bound(this, this+time)
   def +-(time:Time):Bound[Date2] = Bound(this - time/2, this + time/2)
   def +(time:Time):Date2 = Date2(this.ms + time.ms)
   def -(time:Time):Date2 = Date2(this.ms - time.ms)

   def -(that:Date2):Time = Time((this.ms - that.ms)*1E-3)
//
     def compare(that:Date2):Int = if(this.ms < that.ms) -1 else if(this.ms > that.ms) 1 else 0
//
//   def cal = Date.Calendar(this)
//   def cal(tz:String) = Date.Calendar(this,tz)
//   def dayOfWeek:Date.DayOfWeek = cal.dayOfWeek
//
//   2017-07-30("use ms instead (epoch is a point in time 1970 if epoch is time since epoch it can confusingly then be ms or seconds)","v0.2.13")
//   def epoch:Long = ms             //ms since epoch
//
//   /**Convenience function for the more direct: (Date2.now - Date2.unixEpoch).ms.toLong */
//   def ms:Long = date.getTime      //ms since epoch
//   /**Convenience function for the more direct: (Date2.now - Date2.unixEpoch).s.toLong */
//   def s:Long = date.getTime/1000  //seconds since epoch
//
//   def ramp(t:Time):Double = ((Date2.ms - ms).toDouble % t.ms)/t.ms
//
//   def krypton:Krypton = Krypton(this)
//
   def roundDay:Date2 = Date2(ms/msPerDay*msPerDay)
   def roundHour:Date2 = Date2(ms/msPerHour*msPerHour)
   def roundMin:Date2 = Date2(ms/msPerMin*msPerMin)
   def roundSec:Date2 = Date2(ms/msPerSec*msPerSec)
//   /**round down to the nearest time common time beacon points*/
//   def round(dt:Time):Date2 = {
//      val dt0 = dt.abs //make sure delta is an absolute value of time
//      val c = this.cal("UTC") //cal is a def so make it stable here and smaller for reference...
//      if     (dt0 >= 100.yr)    Date2((c.y/100.0).round.toInt*100)      //nearest century
//      else if(dt0 >= 10.yr)     Date2((c.y/10.0).round.toInt*10)        //nearest decade
//      else if(dt0 >= 1.yr)      Date2((c.y + c.D/365.24).round.toInt)   //nearest new year
//      else if(dt0 >= 1.yr/12)   Date2(c.y, (c.M + c.d/30d).round.toInt) //nearest month
//      else if(dt0 >= 1.day)     Date2(c.y, c.M, (c.d + c.H/24d).round.toInt)
//      else if(dt0 >= 1.hr)      Date2(c.y, c.M, c.d, (c.H + c.m/60d).round.toInt)
//      else if(dt0 >= 1.minute)  Date2(c.y, c.M, c.d, c.H, (c.m + c.s/60d).round.toInt)
//      else if(dt0 >= 1.s)       Date2(c.y, c.M, c.d, c.H, c.m, (c.s + c.ms/1000d).round.toInt)
//      else                     Date2(c.y, c.M, c.d, c.H, c.m, c.s, c.ms)
//   }
//   /**format a date at delta time resolution*/
//   def format(dt:Time):String = Date.Form.resolution(dt.abs)(this)

}