/*
   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 java.util.{Date=>JDate}
import java.util.{Calendar => JCalendar}
import java.util.{TimeZone => JZone}
import java.util.Locale
import java.text.SimpleDateFormat

/*
2017-07-30("use (start~end):Bound[Date] instead", "v0.1.20")
case class DateRange(start:Date,end:Date){
    2017-07-30("use Time(start~end) instead", "v0.1.20")
    lazy val time = Time(math.abs(end.ms - start.ms)*1E-3)
    override def toString = f"${time.nice}%8s from ${start.date} to ${end.date}"
    def <(t:Time):Boolean = time < t
    def >(t:Time):Boolean = time > t
    2017-07-30("use (start~end).step(??) instead", "v0.1.20")
    def stepBy(step:Time)(func:Date=>Unit) = {
      @tailrec def next(d:Date):Unit = {
         func(d)
         val n = d + step
         if(n.ms <= end.ms) next(n)
      }
      next(start)
    }
}
*/

object Date{
   implicit object FormatDate extends Format[Date]{ override def apply(v:Date):String = v.isoLocal}
   implicit object ParsableDate extends Parsable[Date]{ def apply(v:String):Date = Date(v) }
   implicit object OrderingDate extends Ordering[Date]{
      def compare(a:Date,b:Date):Int = {
         val dt = b.ms - a.ms
         if(dt > 0) -1 else if(dt < 0) 1 else 0
      }
   }

   //--current time
   def s:Double = System.currentTimeMillis/1000.0 //new JDate().getTime //epoch: number of ms from ~1970
   def ms:Long = System.currentTimeMillis //new JDate().getTime //epoch: number of ms from ~1970
   def now:Date = fromMs(ms)

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

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

   def krypton(str:String):Date = Krypton(str).date
   /*calendar based construction*/
   private def apply(tz:JZone, y:Int, M:Int, d:Int,  H:Int,m:Int,s:Int,ms:Int):Date ={
      val jcal = JCalendar.getInstance(tz,Locale.US) //default timezone and locale lots of allocation
      jcal.set(y,M-1,d,  H,m,s)
      jcal.set(JCalendar.MILLISECOND,ms)
      new Date(jcal.getTime)
   }
   //TODO wrap this with a custom zone wrapper class
   private val UTC:JZone = zone("UTC")  //NO auto timezone or locals should be used
   private val Local:JZone = JZone.getDefault() //NO auto timezone or locals should be used
   private def zone(str:String):JZone = if(str=="local") Local else JZone.getTimeZone(
     str.toUpperCase match {
        case "EDT" => "GMT-04:00"
        case _ => str
     }
   )

   private def jcal = JCalendar.getInstance(UTC,Locale.US) //default timezone and locale
   private def jcal(tz:String) = JCalendar.getInstance(zone(tz),Locale.US)

   def apply(y:Int, M:Int, d:Int,  H:Int,m:Int,s:Int,ms:Int):Date             = apply(UTC,      y,M,d,H,m,s,ms)
   def apply(y:Int, M:Int, d:Int,  H:Int,m:Int,s:Int):Date                    = apply(UTC,      y,M,d,H,m,s,0)
   def apply(y:Int, M:Int, d:Int,  H:Int,m:Int):Date                          = apply(UTC,      y,M,d,H,m,0,0)
   def apply(y:Int, M:Int, d:Int,  H:Int):Date                                = apply(UTC,      y,M,d,H,0,0,0)
   def apply(y:Int, M:Int, d:Int):Date                                        = apply(UTC,      y,M,d,0,0,0,0)
   def apply(y:Int, M:Int):Date                                               = apply(UTC,      y,M,1,0,0,0,0)
   def apply(y:Int):Date                                                      = apply(UTC,      y,1,1,0,0,0,0)
   def apply(tz:String, y:Int, M:Int, d:Int, H:Int,m:Int,s:Int,ms:Int):Date   = apply(zone(tz), y,M,d,H,m,s,ms)
   def apply(tz:String, y:Int, M:Int, d:Int, H:Int,m:Int,s:Int):Date          = apply(zone(tz), y,M,d,H,m,s,0)
   def apply(tz:String, y:Int, M:Int, d:Int, H:Int,m:Int):Date                = apply(zone(tz), y,M,d,H,m,0,0)
   def apply(tz:String, y:Int, M:Int, d:Int, H:Int):Date                      = apply(zone(tz), y,M,d,H,0,0,0)
   def apply(tz:String, y:Int, M:Int, d:Int):Date                             = apply(zone(tz), y,M,d,0,0,0,0)
   def apply(tz:String, y:Int, M:Int):Date                                    = apply(zone(tz), y,M,1,0,0,0,0)
   def apply(tz:String, y:Int):Date                                           = apply(zone(tz), y,1,1,0,0,0,0)


   /**smart construction from multiple formats of date strings
     */
   def apply(str:String):Date = {
      val s = str.trim
      if(isNum(s)){
         val d = Try(s.toDouble).toOption getOrElse Double.NaN
         if     (3E11 < d && d < 19E11) Date(d)
         else if(3E8 < d && d < 19E8) Date(d*1E3)                // 3E8~20E8   --> 1979~2033 TODO add alternate epoch moduls ranges from 2022-04-27
         else parseIso(s).get
         //1.979E9 1.979E11
      }
      //TODO add krypton parse attempt
      else if(s startsWith "@") krypton(s)
      else Form.autoParse(s) getOrElse parseIso(s).get
   }

   //--day of the week
   sealed trait DayOfWeek{
     def index:Int
     def name:String = this.toString
     def short:String = this.toString take 3

     def isWeekend = index == 0 || index == 6
     def isWeekday = !isWeekend

     def deltaTo(that:DayOfWeek):Int = {
       val x:Int = that.index - this.index
       if(x <= 6) x else x+7
     }
     // def daysUntil(that:DayOfWeek):Int = {
     //   val delta = this deltaTo that
     //   if(delta < 0) delta + 7 else delta
     // }
     final def next:DayOfWeek = DayOfWeek.days( if(index < 6) index+1 else 0       )
     final def prev:DayOfWeek = DayOfWeek.days( if(index > 0) index-1 else 6       )
   }
   case object Sunday extends DayOfWeek{val index=0}
   case object Monday extends DayOfWeek{val index=1}
   case object Tuesday extends DayOfWeek{val index=2}
   case object Wednesday extends DayOfWeek{val index=3}
   case object Thursday extends DayOfWeek{val index=4}
   case object Friday extends DayOfWeek{val index=5}
   case object Saturday extends DayOfWeek{val index=6}
   object DayOfWeek{
     val days = Array(Sunday, Monday, Tuesday, Wednesday, Thursday, Friday, Saturday)
     def parse(str:String):Option[DayOfWeek] = days.find{_.name.toLowerCase startsWith str.trim.toLowerCase}
     // require(index >=0 && index <= 6, "index of week must be [0,6] -> [Sunday,Saturday]")
     def fromIndex(index:Int):DayOfWeek = days(index)
   }

   private lazy val days = Seq("sun", "mon", "tue", "wed", "thu", "fri", "sat")
   private def isDay(str:String):Boolean = DayOfWeek.parse(str).isDefined

   //--months
   sealed trait Month{
     def index:Int
     def name = this.toString
     def short = this.toString take 3
     def numDays:Int
   }
   case object January   extends Month{val index = 1; val numDays = 31}
   case object February  extends Month{val index = 2; val numDays = 28} //sometimes 29 for leap
   case object March     extends Month{val index = 3; val numDays = 31}
   case object April     extends Month{val index = 4; val numDays = 30}
   case object May       extends Month{val index = 5; val numDays = 31}
   case object June      extends Month{val index = 6; val numDays = 30}
   case object July      extends Month{val index = 7; val numDays = 31}
   case object August    extends Month{val index = 8; val numDays = 31}
   case object September extends Month{val index = 9; val numDays = 30}
   case object October   extends Month{val index =10; val numDays = 31}
   case object November  extends Month{val index =11; val numDays = 30}
   case object December  extends Month{val index =12; val numDays = 31}
   object Month{
     val months = Array(January, February, March, April, May, June, July, August, September, October, November, December)
     def parse(str:String):Option[Month] = months.find{_.name.toLowerCase startsWith str.trim.toLowerCase}
     // require(index >=0 && index <= 6, "index of week must be [0,6] -> [Sunday,Saturday]")
     def fromIndex(index:Int):Month = months(index-1)
   }
   //--leaps
   def isLeapYear(y:Int):Boolean = (y %? 400) || ( (y %? 4) && !(y %? 100) ) // ever 4 years unless centennial unless 400


   private def isNum(str:String):Boolean = str.forall{c => (c >= '0' && c <= '9') || c == '.' || c == 'E' || c =='e'}

   def parseIso(str:String):Try[Date] = {
      val prep = if(str.toUpperCase endsWith "Z") str.init + "+00" else str
      def isZoneChar(c:Char):Boolean = c.isLetter || c == '-' || c == '+' || c.isDigit
      val (t,z) = prep.foldLeft(("","")){
         case ((t,""),c) if c.isDigit                   => (t+c, "") //is digit and no zone info obtained
         case ((t,z),c)  if t.size > 9 && isZoneChar(c) => (t,  z+c) //enough date found and a zone character was started
         case ((t,z),_)                                 => (t,    z) //strip everything else
      }
      //val (t,z) = s span {c => (c != '+') && (c != '-')}
      val formatString = ("yyyyMMddHHmmssSSSSSSSSSSS" take t.size) + (if(z=="") "" else if (z.head == '+' || z.head == '-') "X" else "z")
      val s = t+z
      Form(formatString) apply s
   }

   //this is an alias like method to make the format constructuors Time.format(dt) and Date.format(dt) similar
   def format(dt:Time):Format[Date] = Form.resolution(dt)

   class Form(val formatter:SimpleDateFormat) extends Format[Date]{
     //parse and format
     def apply(s:String):Try[Date] = Try{Date(formatter parse s)} //parse
     override def apply(d:Date):String = formatter.format(d.date)  //format
     //alternates
     def zone(tz:String):Form = Form(formatter.toPattern, tz)
   }
   object Form{
     private def apply(fmt:String,tz:JZone=UTC, lc:Locale=Locale.US):Form = {
        val sf = new SimpleDateFormat(fmt,lc)
        sf setTimeZone tz
        new Form(sf)
     }
     def apply(fmt:String):Form = apply(fmt,UTC) //no auto timezone
     def apply(fmt:String,tz:String):Form = apply(fmt,zone(tz)) //no auto timezone

     //TODO use the same simple names for all types, i.e. Date.Form.year and Time.yr should not be different 1.s or Form.sec
     //SimpleDateFormat results are not thread safe, so ugh we need these to be defs even though they are an immutable concept
     def year      = apply("yyyy")
     def month     = apply("yyyy-MM")
     def day       = apply("yyyy-MM-dd")
     def hour      = apply("yyyy-MM-dd'T'HH'Z'")
     def minute    = apply("yyyy-MM-dd'T'HH:mm'Z'")
     def sec       = apply("yyyy-MM-dd'T'HH:mm:ss'Z'")
     def ms        = apply("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'")
     def compact   = apply("yyyyMMdd'T'HHmmss.SSS'Z'")
     def local     = apply("yyyy-MM-dd'T'HH:mm:ssX",  Local)
     def http      = apply("EEE, dd MMM yyyy HH:mm:ss z")
     def java      = apply("EEE MMM dd HH:mm:ss Z yyyy", Local)

     def usMonthDay = apply("M/d")
     def usDay      = apply("M/d/yyyy")

     def autoParse(s:String):Try[Date] = java(s) || http(s) //| local(s) | compact(s) | ms(s) | sec(s) | day(s) | month(s) | year(s) //TODO add the alternates back in

     // def tag            = apply("yyyyMMdd'T'HHmmss'Z'"          , "UTC")
     // def simple         = apply("hh:mm z (EEE)")
     // def simpleDateTime = apply("yyyy MMMd h:mm:ssa z")
     // def prettyDateTime = apply("yyyy EEE MMMd h:mma z")
     // def prettyDate     = apply("yyyy-MM-dd hhaa EEE z")
     //
     def resolution(dt:Time):Form = {
       if     (dt >= 1.yr)      Form.year
       else if(dt >= 1.yr/12)   Form.month
       else if(dt >= 1.day)     Form.day
       else if(dt >= 1.hr)      Form.hour
       else if(dt >= 1.minute)  Form.minute
       else if(dt >= 1.s)       Form.sec
       else                     Form.ms
     }
   }

   object Calendar{ //where is the zone set here??
      def apply(date:Date, tz:String="UTC"):Calendar = {
         val c = jcal(tz) //use this jcal based UTC first version instead of the default //JCalendar.getInstance()
         c.setTime(date.date)
         new Calendar(c)
      }
   }
   /**clean wrapper around java Calendar upper case indicates index from the beginning of the year or relevant range?*/
   class Calendar(val cal:JCalendar) extends AnyVal{
      def y  = cal.get(JCalendar.YEAR)
      /**month index 1 = January*/
      def M  = cal.get(JCalendar.MONTH)+1
      def month = Month.fromIndex(M)
      def d  = cal.get(JCalendar.DAY_OF_MONTH)
      /**day of Year*/
      def D  = cal.get(JCalendar.DAY_OF_YEAR)

      /**day of week Calendar{SUNDAY:1 ... SATURDAY:7} , */
      def w  = cal.get(JCalendar.DAY_OF_WEEK)
      def dayOfWeek:DayOfWeek = DayOfWeek.days(w-1)
      /**week of year*/
      def W  = cal.get(JCalendar.WEEK_OF_YEAR)
      //def e  = format("EEE")
      //def E  = format("EEEE")

      def h  = cal.get(JCalendar.HOUR)
      def H  = cal.get(JCalendar.HOUR_OF_DAY)
      def m  = cal.get(JCalendar.MINUTE)
      def s  = cal.get(JCalendar.SECOND)
      def ms = cal.get(JCalendar.MILLISECOND)

      def z  = cal.get(JCalendar.ZONE_OFFSET).ms //offset in ms
      //def Z  = format("Z")

      def am = cal.get(JCalendar.AM_PM) == JCalendar.AM
      def pm = cal.get(JCalendar.AM_PM) == JCalendar.PM
      def ampm = if(am) "am" else "pm"

   }

   //--convenience methods
   val msPerDay:Long = 86400000L// 24L*60*60*1000
   val sPerDay:Long = 86400L //24L*60*60
   /**triangle wave from 0 to 1*/
   def ramp(t:Time):Double = (ms.toDouble % t.ms)/t.ms
}
case class Date(val date:JDate) extends AnyVal with scala.math.Ordered[Date] {
  import Date.Form
/*
   def simpleDateTime:String = Date.simpleDateTimeFormatter.format(date)
   def prettyDateTime:String = Date.prettyDateTimeFormatter.format(date)
   def simpleTime:String = Date.simpleTimeFormatter.format(date)
   def isoDateTime:String = Date.isoDateTimeFormatter.format(date)
   def isoDateTimeLocal:String = Date.isoDateTimeLocalFormatter.format(date)
   def isoDateTimeMs:String = Date.isoDateTimeMsFormatter.format(date)
   def isoDate:String = Date.isoDateFormatter.format(date)
   def isoTag:String = Date.isoTagFormatter.format(date)
   def iso:String = Date.isoDateTimeMsFormatter.format(date)
   def isoCompact:String = Date.isoCompactFormatter.format(date)
   def pretty:String = Date.prettyDateFormatter.format(date)
*/
   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 niceWork:String   = {
     val d = cal
     d.dayOfWeek.short + " " + d.month.short + " " + ("%-2d" format d.d)
   }
   def usMonthDay        =  Form.usMonthDay  apply this
   def usDay             =  Form.usDay       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"Date($iso)"
   def toRelativeString = {val dt = Date.now - this; dt.format(dt,0) + (if(dt > 0.s) " ago" else " from now")}

   def ~(that:Date):Bound[Date] = if(this < that) Bound(this,that) else Bound(that,this)
   def ~>(time:Time):Bound[Date] = Bound(this, this+time)
   def +-(time:Time):Bound[Date] = Bound(this - time/2, this + time/2)
   def +(time:Time):Date = Date(this.ms + time.ms)
   def -(time:Time):Date = Date(this.ms - time.ms)

   def -(that:Date):Time = Time((this.ms - that.ms)*1E-3)

   def compare(that:Date):Int = if(this.date before that.date) -1 else if(this.date after that.date) 1 else 0 //Date.Calendar(this)
   //TODO remove the following since the ordered trait gets these operators through `compare`
   // def <(that:Date):Boolean = this.date before that.date
   // def >(that:Date):Boolean = this.date after  that.date

   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: (Date.now - Date.unixEpoch).ms.toLong */
   def ms:Long = date.getTime      //ms since epoch
   /**Convenience function for the more direct: (Date.now - Date.unixEpoch).s.toLong */
   def s:Long = date.getTime/1000  //seconds since epoch

   def ramp(t:Time):Double = ((Date.ms - ms).toDouble % t.ms)/t.ms

   def krypton:Krypton = Krypton(this)

   def floorDay:Date = Date(ms/Date.msPerDay*Date.msPerDay)
   /**round down to the nearest time common time beacon points*/
   def round(dt:Time):Date = {
      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)    Date((c.y/100.0).round.toInt*100)      //nearest century
      else if(dt0 >= 10.yr)     Date((c.y/10.0).round.toInt*10)        //nearest decade
      else if(dt0 >= 1.yr)      Date((c.y + c.D/365.24).round.toInt)   //nearest new year
      else if(dt0 >= 1.yr/12)   Date(c.y, (c.M + c.d/30d).round.toInt) //nearest month
      else if(dt0 >= 1.day)     Date(c.y, c.M, (c.d + c.H/24d).round.toInt)
      else if(dt0 >= 1.hr)      Date(c.y, c.M, c.d, (c.H + c.m/60d).round.toInt)
      else if(dt0 >= 1.minute)  Date(c.y, c.M, c.d, c.H, (c.m + c.s/60d).round.toInt)
      else if(dt0 >= 1.s)       Date(c.y, c.M, c.d, c.H, c.m, (c.s + c.ms/1000d).round.toInt)
      else                     Date(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)

}

/**The planet of kyrpton provides concise specific date tagging and linear versioning*/
object Krypton{

  object Span{
    lazy val stage:Time = 1.day*36   // = 3 years
    lazy val month:Time = 1.day*36   // = 36 days exactly
    lazy val hour:Time = 1.day/36   // = 40.min exactly
    lazy val minute:Time = 1.day/(36**2) //~ 1.1.min 
    lazy val s:Time = 1.day/(36**3) //~ 1.9s
    lazy val ms:Time = 1.day/(36**5) //~ 1.4.ms
    lazy val us:Time = 1.day/(36**7) //~ 1.1.us
    // lazy val ns:Time = 1.day/(36**9) //~ 1.1.us
  }

  def now:Krypton = Date.now.krypton

  def apply(kyrpton:String):Krypton = {
    val s = kyrpton filter {_.isLetterOrDigit}
    //Note: naive approach does not include leap seconds and relies on unix time...
    def padRight(num:String, size:Int) = num + "0"*(size-num.size)
    val daysSinceEpoch:Long = s take 3 base 36 //period-month-day
    val krMsIntoDay:Long = padRight(s drop 3,5) base 36   //hour-min-sec-flicker-ms
    Krypton(daysSinceEpoch.toInt, krMsIntoDay.toInt)
  }
  def apply(msEarthSinceEpoch:Long):Krypton = {
    val daysSinceEpoch = msEarthSinceEpoch/Date.msPerDay
    val kryptonMsIntoDay = msEarthSinceEpoch%Date.msPerDay*2187/3125
    Krypton(daysSinceEpoch.toInt, kryptonMsIntoDay)
  }
  def apply(date:Date):Krypton = apply(date.ms)

  implicit object OrderingKrypton extends Ordering[Krypton]{
    def compare(a:Krypton,b:Krypton):Int = {
       val dt = b.daysSinceEpoch - a.daysSinceEpoch
       if(dt > 0) -1 else if(dt < 0) 1 else {
         val dt = b.kryptonMsIntoDay - a.kryptonMsIntoDay
         if(dt > 0) -1 else if(dt < 0) 1 else 0
       }
    }
  }
}

case class Krypton(daysSinceEpoch:Int, kryptonMsIntoDay:Long) extends scala.math.Ordered[Krypton]{  //note long is required for resolution of interest in the day
  lazy val krypton:String = daysSinceEpoch.base(36,3) + kryptonMsIntoDay.base(36,5)

  def stage:String = this take 1 // =3 Earth years
  def month:String = this take 2 // =36 Earth days
  def day:String   = this take 3 // =1 Earth day
  def min:String   = this take 5 // ~= 1.1 Earth min
  def s:String     = this take 6 // ~= 1.9 Earth s
  def ms:String    = this take 8 // ~= 1.4 Earth ms

  2017-07-30("apply with an int usually means indexed charcter return, so use take instead","v0.2.16")
  def apply(res:Int):String = krypton take res
  def take(res:Int):String = krypton take res
  override def toString = krypton
  def compare(that:Krypton):Int = Krypton.OrderingKrypton.compare(this,that)

  //usEarth/usKrytpon => Ratio(24L*60*60*1000000, 36L**7) =>  Ratio(86400000000,78364164096) => Ratio(390625, 354294)
  //msEarth/msKrypton => Ratio(24L*60*60*1000, 36L**5) => Ratio(86400000,60466176) => Ratio(3125,2187)
  //s/kyptonS => Ratio(24L*60*60, 36L**3) => Ratio(50, 27)

  private def msEarthIntoDay:Long =  kryptonMsIntoDay*3125/2187
  private def msEarthSinceEpoch:Long = daysSinceEpoch*Date.msPerDay + msEarthIntoDay
  def date:Date = Date(msEarthSinceEpoch)

  //note |Version| contains considerable krypton features to get the next shortest uniq version
}