{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE StandaloneDeriving #-}
{-# LANGUAGE UndecidableInstances #-}
module Data.HodaTime.ZonedDateTime.Internal
(
   ZonedDateTime(..)
  ,fromInstant
  ,year
  ,month
  ,day
  ,hour
  ,minute
  ,second
  ,nanosecond
)
where

import Data.HodaTime.CalendarDateTime.Internal (CalendarDateTime, IsCalendarDateTime, IsCalendar, fromAdjustedInstant, toUnadjustedInstant)
import qualified Data.HodaTime.CalendarDateTime.Internal as CDT
import qualified Data.HodaTime.LocalTime.Internal as LT
import Data.HodaTime.TimeZone.Internal (TimeZone, TZIdentifier(..), TransitionInfo, activeTransitionFor, tiUtcOffset, zoneName)
import Data.HodaTime.Offset.Internal (Offset(..), adjustInstant)
import Data.HodaTime.Instant.Internal (Instant)
import Data.Hashable (Hashable(..))

-- | A CalendarDateTime in a specific time zone. A 'ZonedDateTime' is global and maps directly to a single 'Instant'.
data ZonedDateTime cal = ZonedDateTime { forall cal. ZonedDateTime cal -> CalendarDateTime cal
zdtCalendarDateTime :: CalendarDateTime cal, forall cal. ZonedDateTime cal -> TimeZone
zdtTimeZone :: TimeZone, forall cal. ZonedDateTime cal -> TransitionInfo
zdtActiveTransition :: TransitionInfo }

deriving instance Eq (CDT.Date cal) => Eq (ZonedDateTime cal)

-- | Renders a 'ZonedDateTime' as the 'fromInstant' call that reconstructs it from its physical 'Instant' and zone.
instance IsCalendarDateTime cal => Show (ZonedDateTime cal) where
  showsPrec :: Int -> ZonedDateTime cal -> ShowS
showsPrec Int
p (ZonedDateTime CalendarDateTime cal
cdt TimeZone
tz TransitionInfo
ti) = Bool -> ShowS -> ShowS
showParen (Int
p Int -> Int -> Bool
forall a. Ord a => a -> a -> Bool
> Int
10) (ShowS -> ShowS) -> ShowS -> ShowS
forall a b. (a -> b) -> a -> b
$
      String -> ShowS
showString String
"fromInstant " ShowS -> ShowS -> ShowS
forall b c a. (b -> c) -> (a -> b) -> a -> c
. Int -> Instant -> ShowS
forall a. Show a => Int -> a -> ShowS
showsPrec Int
11 Instant
inst ShowS -> ShowS -> ShowS
forall b c a. (b -> c) -> (a -> b) -> a -> c
. Char -> ShowS
showChar Char
' ' ShowS -> ShowS -> ShowS
forall b c a. (b -> c) -> (a -> b) -> a -> c
. Int -> TimeZone -> ShowS
forall a. Show a => Int -> a -> ShowS
showsPrec Int
11 TimeZone
tz
    where
      inst :: Instant
inst = Offset -> Instant -> Instant
adjustInstant (Offset -> Offset
negateOffset (TransitionInfo -> Offset
tiUtcOffset TransitionInfo
ti)) (CalendarDateTime cal -> Instant
forall cal.
IsCalendarDateTime cal =>
CalendarDateTime cal -> Instant
toUnadjustedInstant CalendarDateTime cal
cdt)
      negateOffset :: Offset -> Offset
negateOffset (Offset Int
s) = Int -> Offset
Offset (Int -> Int
forall a. Num a => a -> a
negate Int
s)

-- | Orders 'ZonedDateTime's by the 'Instant' they represent (their global\/UTC position on the time line), falling
--   back to the zone identifier as a tie-break so that two zones observing the same instant still have a total order.
--   NOTE: this compares by physical time, not by the local wall-clock 'CalendarDateTime'.
instance (IsCalendarDateTime cal, Eq (CDT.Date cal)) => Ord (ZonedDateTime cal) where
  compare :: ZonedDateTime cal -> ZonedDateTime cal -> Ordering
compare ZonedDateTime cal
a ZonedDateTime cal
b = Instant -> Instant -> Ordering
forall a. Ord a => a -> a -> Ordering
compare (ZonedDateTime cal -> Instant
forall {cal}.
IsCalendarDateTime cal =>
ZonedDateTime cal -> Instant
instantOf ZonedDateTime cal
a) (ZonedDateTime cal -> Instant
forall {cal}.
IsCalendarDateTime cal =>
ZonedDateTime cal -> Instant
instantOf ZonedDateTime cal
b) Ordering -> Ordering -> Ordering
forall a. Semigroup a => a -> a -> a
<> String -> String -> Ordering
forall a. Ord a => a -> a -> Ordering
compare (ZonedDateTime cal -> String
forall {cal}. ZonedDateTime cal -> String
zid ZonedDateTime cal
a) (ZonedDateTime cal -> String
forall {cal}. ZonedDateTime cal -> String
zid ZonedDateTime cal
b)
    where
      instantOf :: ZonedDateTime cal -> Instant
instantOf (ZonedDateTime CalendarDateTime cal
cdt TimeZone
_ TransitionInfo
ti) = Offset -> Instant -> Instant
adjustInstant (Offset -> Offset
negateOffset (TransitionInfo -> Offset
tiUtcOffset TransitionInfo
ti)) (CalendarDateTime cal -> Instant
forall cal.
IsCalendarDateTime cal =>
CalendarDateTime cal -> Instant
toUnadjustedInstant CalendarDateTime cal
cdt)
      negateOffset :: Offset -> Offset
negateOffset (Offset Int
s) = Int -> Offset
Offset (Int -> Int
forall a. Num a => a -> a
negate Int
s)
      zid :: ZonedDateTime cal -> String
zid (ZonedDateTime CalendarDateTime cal
_ TimeZone
tz TransitionInfo
_) = case TimeZone -> TZIdentifier
zoneName TimeZone
tz of
        TZIdentifier
UTC    -> String
"UTC"
        Zone String
n -> String
n

-- | Hashes a 'ZonedDateTime' by its identity: the local 'CalendarDateTime', the zone (by identifier) and the active
--   transition.  Consistent with '(==)', which compares those same components.
-- NOTE: no 'NFData' instance is provided because 'ZonedDateTime' embeds a 'TimeZone', whose fingertree-based
-- transition maps cannot be forced (see 'Data.HodaTime.TimeZone.Internal').
instance Hashable (CDT.Date cal) => Hashable (ZonedDateTime cal) where
  hashWithSalt :: Int -> ZonedDateTime cal -> Int
hashWithSalt Int
s (ZonedDateTime CalendarDateTime cal
cdt TimeZone
tz TransitionInfo
ti) = Int
s Int -> CalendarDateTime cal -> Int
forall a. Hashable a => Int -> a -> Int
`hashWithSalt` CalendarDateTime cal
cdt Int -> TimeZone -> Int
forall a. Hashable a => Int -> a -> Int
`hashWithSalt` TimeZone
tz Int -> TransitionInfo -> Int
forall a. Hashable a => Int -> a -> Int
`hashWithSalt` TransitionInfo
ti


-- | Returns the 'ZonedDateTime' represented by the passed 'Instant' within the given 'TimeZone'.  This is always an unambiguous conversion.
fromInstant :: IsCalendarDateTime cal => Instant -> TimeZone -> ZonedDateTime cal
fromInstant :: forall cal.
IsCalendarDateTime cal =>
Instant -> TimeZone -> ZonedDateTime cal
fromInstant Instant
instant TimeZone
tz = CalendarDateTime cal
-> TimeZone -> TransitionInfo -> ZonedDateTime cal
forall cal.
CalendarDateTime cal
-> TimeZone -> TransitionInfo -> ZonedDateTime cal
ZonedDateTime CalendarDateTime cal
cdt TimeZone
tz TransitionInfo
ti
  where
    ti :: TransitionInfo
ti = Instant -> TimeZone -> TransitionInfo
activeTransitionFor Instant
instant TimeZone
tz
    offset :: Offset
offset = TransitionInfo -> Offset
tiUtcOffset TransitionInfo
ti
    instant' :: Instant
instant' = Offset -> Instant -> Instant
adjustInstant Offset
offset Instant
instant
    cdt :: CalendarDateTime cal
cdt = Instant -> CalendarDateTime cal
forall cal.
IsCalendarDateTime cal =>
Instant -> CalendarDateTime cal
fromAdjustedInstant Instant
instant'

-- | Accessor for the Year of a 'ZonedDateTime'.
year :: IsCalendar cal => ZonedDateTime cal -> CDT.Year
year :: forall cal. IsCalendar cal => ZonedDateTime cal -> Int
year (ZonedDateTime CalendarDateTime cal
cdt TimeZone
_ TransitionInfo
_) = CalendarDateTime cal -> Int
forall d. HasDate d => d -> Int
CDT.year CalendarDateTime cal
cdt

-- | Accessor for the Month of a 'ZonedDateTime'.
month :: IsCalendar cal => ZonedDateTime cal -> CDT.Month cal
month :: forall cal. IsCalendar cal => ZonedDateTime cal -> Month cal
month (ZonedDateTime CalendarDateTime cal
cdt TimeZone
_ TransitionInfo
_) = CalendarDateTime cal -> MoY (CalendarDateTime cal)
forall d. HasDate d => d -> MoY d
CDT.month CalendarDateTime cal
cdt

-- | Accessor for the Day of a 'ZonedDateTime'.
day :: IsCalendar cal => ZonedDateTime cal -> CDT.DayOfMonth
day :: forall cal. IsCalendar cal => ZonedDateTime cal -> Int
day (ZonedDateTime CalendarDateTime cal
cdt TimeZone
_ TransitionInfo
_) = CalendarDateTime cal -> Int
forall d. HasDate d => d -> Int
CDT.day CalendarDateTime cal
cdt

-- | Accessor for the Hour of a 'ZonedDateTime'.
hour :: IsCalendar cal => ZonedDateTime cal -> LT.Hour
hour :: forall cal. IsCalendar cal => ZonedDateTime cal -> Int
hour (ZonedDateTime CalendarDateTime cal
cdt TimeZone
_ TransitionInfo
_) = CalendarDateTime cal -> Int
forall lt. HasLocalTime lt => lt -> Int
LT.hour CalendarDateTime cal
cdt

-- | Accessor for the Minute of a 'ZonedDateTime'.
minute :: IsCalendar cal => ZonedDateTime cal -> LT.Minute
minute :: forall cal. IsCalendar cal => ZonedDateTime cal -> Int
minute (ZonedDateTime CalendarDateTime cal
cdt TimeZone
_ TransitionInfo
_) = CalendarDateTime cal -> Int
forall lt. HasLocalTime lt => lt -> Int
LT.minute CalendarDateTime cal
cdt

-- | Accessor for the Second of a 'ZonedDateTime'.
second :: IsCalendar cal => ZonedDateTime cal -> LT.Second
second :: forall cal. IsCalendar cal => ZonedDateTime cal -> Int
second (ZonedDateTime CalendarDateTime cal
cdt TimeZone
_ TransitionInfo
_) = CalendarDateTime cal -> Int
forall lt. HasLocalTime lt => lt -> Int
LT.second CalendarDateTime cal
cdt

-- | Accessor for the Nanosecond of a 'ZonedDateTime'.
nanosecond :: IsCalendar cal => ZonedDateTime cal -> LT.Nanosecond
nanosecond :: forall cal. IsCalendar cal => ZonedDateTime cal -> Int
nanosecond (ZonedDateTime CalendarDateTime cal
cdt TimeZone
_ TransitionInfo
_) = CalendarDateTime cal -> Int
forall lt. HasLocalTime lt => lt -> Int
LT.nanosecond CalendarDateTime cal
cdt

-- helper functions

-- TODO: We need functions that help construct this type.  Some of those functions probably need to be in OffsetDateTime so we can hide details of
-- TODO: CalendarDateTime from this module.  What we're trying to do is make sure the OffsetDateTime has the time set to the local time zone
-- TODO: and that the offset part tells us how far we are from UTC.  Nanos tell us how far we are into the current day