{-|
Module: Data.Astro.Time.JulianDate
Description: Julian Date
Copyright: Alexander Ignatyev, 2016


Julian date is the continuous count of days since noon on January 1, 4713 BC,
the beginning of the Julian Period.

= Examples

== /JulianDate/
@
import Data.Astro.Time.JulianDate

-- 2017-06-25 9:29:00 (GMT)
jd :: JulianDate
jd = fromYMDHMS 2017 6 25 9 29 0
-- JD 2457929.895138889
@

== /LocalCiviTime and LocalCivilDate/

@
import Data.Astro.Time.JulianDate
import Data.Astro.Types

-- 2017-06-25 10:29:00 +0100 (BST)
lct :: LocalCivilTime
lct = lctFromYMDHMS (DH 1) 2017 6 25 10 29 0
-- 2017-06-25 10:29:00.0000 +1.0

lctJD :: JulianDate
lctJD = lctUniversalTime lct
-- JD 2457929.895138889

lctTZ :: DecimalHours
lctTZ = lctTimeZone lct
-- DH 1.0

lcd :: LocalCivilDate
lcd = lcdFromYMD (DH 1) 2017 6 25

lcdJD :: JulianDate
lcdJD = lcdDate lcd
-- JD 2457929.5

lcdTZ :: DecimalHours
lcdTZ = lcdTimeZone lcd
-- DH 1.0
@
-}

module Data.Astro.Time.JulianDate
(
  JulianDate(..)
  , julianStartDateTime
  , LocalCivilTime(..)
  , LocalCivilDate(..)
  , TimeBaseType
  , numberOfDays
  , numberOfYears
  , numberOfCenturies
  , addHours
  , fromYMD
  , fromYMDHMS
  , toYMDHMS
  , dayOfWeek
  , splitToDayAndTime
  , lctFromYMDHMS
  , lctToYMDHMS
  , lcdFromYMD
  , printLctHs
)

where

import Text.Printf (printf)

import Data.Astro.Types(DecimalHours(..), fromHMS, toHMS)
import Data.Astro.Time.GregorianCalendar (gregorianDateAdjustment)
import Data.Astro.Utils (trunc, fraction)


type TimeBaseType = Double

-- | A number of days since noon of 1 January 4713 BC
newtype JulianDate = JD TimeBaseType
                     deriving (Show, Eq)


-- | Represents Local Civil Time
data LocalCivilTime = LCT {
  lctTimeZone :: DecimalHours   -- Time Zone correction
  , lctUniversalTime :: JulianDate
  } deriving (Eq)


instance Show LocalCivilTime where
  show = printLct


-- | Local Civil Date, used for time conversions when base date is needed
data LocalCivilDate = LCD {
  lcdTimeZone :: DecimalHours
  , lcdDate :: JulianDate
  } deriving (Eq)


-- | Beginning of the Julian Period
julianStartDateTime = fromYMDHMS (-4712) 1 1 12 0 0


instance Num JulianDate where
  (+) (JD d1) (JD d2) = JD (d1+d2)
  (-) (JD d1) (JD d2) = JD (d1-d2)
  (*) (JD d1) (JD d2) = JD (d1*d2)
  negate (JD d) = JD (negate d)
  abs (JD d) = JD (abs d)
  signum (JD d) = JD (signum d)
  fromInteger int = JD (fromInteger int)


-- | Return number of days since the first argument till the second one
numberOfDays :: JulianDate -> JulianDate -> TimeBaseType
numberOfDays (JD jd1) (JD jd2) = jd2 - jd1


-- | Return number of years since the first argument till the second one
numberOfYears :: JulianDate -> JulianDate -> TimeBaseType
numberOfYears (JD jd1) (JD jd2) = (jd2-jd1) / 365.25


-- | Return number of centuries since the first argument till the second one
numberOfCenturies :: JulianDate -> JulianDate -> TimeBaseType
numberOfCenturies (JD jd1) (JD jd2) = (jd2-jd1) / 36525


-- | add Decimal Hours
addHours :: DecimalHours -> JulianDate -> JulianDate
addHours (DH hours) jd = jd + (JD $ hours/24)


-- | Create Julian Date.
-- It takes year, month [1..12], Day [1..31].
fromYMD :: Integer -> Int -> Int -> JulianDate
fromYMD year month day =
  let (y, m) = if month < 3 then (year-1, month+12) else (year, month)
      y' = fromIntegral y
      m' = fromIntegral m
      b = gregorianDateAdjustment year month day
      c = if y < 0
          then truncate (365.25*y' - 0.75)  -- 365.25 - number of solar days in a year
          else truncate (365.25*y')
      d = truncate (30.6001 * (m'+1))
      jd = fromIntegral (b + c + d + day) + 1720994.5  -- add 1720994.5 to process BC/AC border
  in JD jd


-- | Create Julian Date.
-- It takes year, month [1..12], Day [1..31], hours, minutes, seconds.
fromYMDHMS :: Integer -> Int -> Int -> Int -> Int -> TimeBaseType -> JulianDate
fromYMDHMS year month day hs ms ss = addHours (fromHMS hs ms ss) (fromYMD year month day)


-- | It returns year, month [1..12], Day [1..31], hours, minutes, seconds.
toYMDHMS :: JulianDate -> (Integer, Int, Int, Int, Int, TimeBaseType)
toYMDHMS (JD jd) =
  let (i, time) = fraction (jd + 0.5)
      b = if i > 2299160  -- 2299161 - first day of Georgian Calendar
          then let a = trunc $ (i-1867216.25)/36524.25
               in i + a - trunc (a*0.25) + 1
          else i
      c = b + 1524
      d = trunc $ (c-122.1)/365.25
      e = trunc $ d * 365.25
      g = trunc $ (c-e)/30.6001
      day = truncate $ c - e - trunc (30.6001*g)
      month = truncate $ if g < 13.5 then g - 1 else g - 13
      year = truncate $ if month > 2 then d-4716 else d-4715
      (h, m, s) = toHMS $ DH $ 24*time
   in (year, month, day, h, m, s)


-- | Get Day of the Week
-- 0 is for Sunday, 1 for manday and 6 for Saturday
dayOfWeek :: JulianDate -> Int
dayOfWeek jd =
  let JD d = removeHours jd
      (_, f) = properFraction $ (d+1.5) / 7
  in round (7*f)


-- | Extract Day and Time parts of Date
splitToDayAndTime :: JulianDate -> (JulianDate, JulianDate)
splitToDayAndTime jd@(JD n) =
  let day = JD $ 0.5 + trunc (n - 0.5)
      time = jd - day
  in (day, time)


-- | Get Julian date corresponding to midnight
removeHours :: JulianDate -> JulianDate
removeHours jd =
  let (d, _) = splitToDayAndTime jd
  in d


-- | Create LocalCivilTime from tize zone, local year, local month, local day, local hours, local minutes and local secunds.
lctFromYMDHMS :: DecimalHours ->Integer -> Int -> Int -> Int -> Int -> TimeBaseType -> LocalCivilTime
lctFromYMDHMS tz y m d hs ms ss =
  let jd = fromYMDHMS y m d hs ms ss
      jd' = addHours (-tz) jd
  in LCT tz jd'


-- | Get from LocalCivilTime local year, local month, local day, local hours, local minutes and local secunds.
lctToYMDHMS :: LocalCivilTime -> (Integer, Int, Int, Int, Int, TimeBaseType)
lctToYMDHMS (LCT tz jd)= toYMDHMS (addHours tz jd)


-- Create LocalCivilDate from time zone, local year, local month, local day
lcdFromYMD :: DecimalHours -> Integer -> Int -> Int -> LocalCivilDate
lcdFromYMD tz y m d = LCD tz (fromYMD y m d)


-- | Print Local Civil Time in human-readable format
printLct :: LocalCivilTime -> String
printLct lct =
  printf "%d-%02d-%02d %02d:%02d:%07.4f %+03.1f" y m d hs ms ss tz
  where (y, m, d, hs, ms, ss) = lctToYMDHMS lct
        DH tz = lctTimeZone lct


-- | Print local civil time in machine readable format
printLctHs :: LocalCivilTime -> String
printLctHs lct =
  printf "lctFromYMDHMS (%1.0f) %d %d %d %d %d %.4f" tz y m d hs ms ss
  where (y, m, d, hs, ms, ss) = lctToYMDHMS lct
        DH tz = lctTimeZone lct