{- |
Module      : Data.Time.Zones
Copyright   : (C) 2014 Mihaly Barasz
License     : Apache-2.0, see LICENSE
Maintainer  : Mihaly Barasz <klao@nilcons.com>
Stability   : experimental
-}

{-# LANGUAGE BangPatterns #-}
{-# LANGUAGE DeriveDataTypeable #-}

module Data.Time.Zones (
  TZ,
  Data.Time.Zones.Types.utcTZ,
  -- * Universal -> Local direction
  diffForPOSIX,
  timeZoneForPOSIX,
  timeZoneForUTCTime,
  utcToLocalTimeTZ,
  -- * Local -> Universal direction
  LocalToUTCResult(..),
  localTimeToUTCFull,
  localTimeToUTCTZ,
  -- TODO(klao): do we want to export these?
  FromLocal(..),
  localToPOSIX,
  -- * Acquiring `TZ` information
  loadTZFromFile,
  loadTZFromDB,
  loadSystemTZ,
  loadLocalTZ,
  -- * Utilities
  diffForAbbr,
  ) where

import           Control.DeepSeq
import           Data.Bits (shiftR)
import           Data.Data
import           Data.Int (Int64)
import           Data.Time
import           Data.Time.Zones.Internal
import           Data.Time.Zones.Read
import           Data.Time.Zones.Types
import qualified Data.Vector as VB
import qualified Data.Vector.Unboxed as VU

-- | Returns the time difference (in seconds) for TZ at the given
-- POSIX time.
diffForPOSIX :: TZ -> Int64 -> Int
{-# INLINE diffForPOSIX #-}
diffForPOSIX (TZ trans diffs _) t = VU.unsafeIndex diffs $ binarySearch trans t

timeZoneForIx :: TZ -> Int -> TimeZone
{-# INLINE timeZoneForIx #-}
timeZoneForIx (TZ _ diffs infos) i = TimeZone diffMins isDst name
  where
    diffMins = VU.unsafeIndex diffs i `div` 60
    (isDst, name) = VB.unsafeIndex infos i

-- | Returns the `TimeZone` for the `TZ` at the given POSIX time.
timeZoneForPOSIX :: TZ -> Int64 -> TimeZone
{-# INLINABLE timeZoneForPOSIX #-}
timeZoneForPOSIX tz@(TZ trans _ _) t = timeZoneForIx tz i
  where
    i = binarySearch trans t

-- | Returns the `TimeZone` for the `TZ` at the given `UTCTime`.
timeZoneForUTCTime :: TZ -> UTCTime -> TimeZone
{-# INLINABLE timeZoneForUTCTime #-}
timeZoneForUTCTime tz ut = timeZoneForPOSIX tz $ utcTimeToInt64 ut

-- | Returns the `LocalTime` corresponding to the given `UTCTime` in `TZ`.
--
-- @utcToLocalTimeTZ tz ut@ is equivalent to @`utcToLocalTime`
-- (`timeZoneForPOSIX` tz ut) ut@ except when the time difference is not
-- an integral number of minutes
utcToLocalTimeTZ :: TZ -> UTCTime -> LocalTime
utcToLocalTimeTZ tz utcT = int64PairToLocalTime ut' ps
  where
    (ut, ps) = utcTimeToInt64Pair utcT
    ut' = ut + fromIntegral (diffForPOSIX tz ut)

-- | Returns the largest index `i` such that `v ! i <= t`.
--
-- Assumes that `v` is sorted, has at least one element and `v ! 0 <= t`.
binarySearch :: (VU.Unbox a, Ord a) => VU.Vector a -> a -> Int
{-# INLINE binarySearch #-}
binarySearch v t | n == 1    = 0
                 | otherwise = t `seq` go 1 n
  where
    n = VU.length v
    go !l !u | l >= u = (l - 1)
             | VU.unsafeIndex v k <= t  = go (k+1) u
             | otherwise  = go l k
      where
        k = (l + u) `shiftR` 1

--------------------------------------------------------------------------------
-- LocalTime to UTCTime

-- Representing LocalTimes as Int64s.
--
-- We want a simple and convenient mapping of
-- (year,month,day,hour,minutes,seconds) tuples onto a linear
-- scale. We adopt the following convention: an Int64 maps to the
-- tuple that a POSIX time represented by the same number would map in
-- UTC time zone. That is, 0 maps to '1970-01-01 00:00:00' and
-- 1395516860 maps to '2014-03-22 19:34:20'.
--
-- This is independent and without reference to any particular time
-- zone, it is completely analogous to LocalTime in that you need a
-- time zone to be able to interpret this as a point in time. And then
-- it may refer to an invalid time (spring time forward clock jump) or
-- two possible times (fall time backward jump).
--
-- So, it's basically another representation for LocalTimes (those
-- that can be represented within an Int64 and ignoring the fractional
-- seconds), except that LocalTime can contain invalid time-of-day
-- part (like 27:-05:107) and the Int64 representation can't and is
-- always unambiguous.
--
-- With that in mind, a UTCTime to LocalTime conversion can be seen as
-- 'TZ -> Int64 -> Int64', where the first Int64 is POSIX time and the
-- second is this kind of LocalTime representation.


-- | Internal representation of LocalTime -> UTCTime conversion result:
data FromLocal
  = FLGap    { _flIx :: {-# UNPACK #-} !Int
             , _flRes :: {-# UNPACK #-} !Int64 }
  | FLUnique { _flIx :: {-# UNPACK #-} !Int
             , _flRes :: {-# UNPACK #-} !Int64 }
  | FLDouble { _flIx :: {-# UNPACK #-} !Int
             , _flRes1 :: {-# UNPACK #-} !Int64
             , _flRes2 :: {-# UNPACK #-} !Int64 }
  deriving (Eq, Show, Read, Typeable, Data)

instance NFData FromLocal where
  rnf !_ = ()

-- We make the following two assumptions here:
--
-- 1. No diff in any time zone is ever bigger than 24 hours.
--
-- 2. No two consecutive transitions in any zone file ever fall within
--    48 hours of each other.
--
-- As a consequence, a local time might correspond to two different
-- points in time, but never three.
--
-- TODO(klao): check that these assuptions hold.
localToPOSIX :: TZ -> Int64 -> FromLocal
{-# INLINABLE localToPOSIX #-}
localToPOSIX (TZ trans diffs _) !lTime = res
  where
    lBound = lTime - 86400
    ix = binarySearch trans lBound
    cand1 = lTime - fromIntegral (VU.unsafeIndex diffs ix)
    res = if ix == VU.length trans - 1
          then FLUnique ix cand1 -- TODO(klao): extend when rule handling is added
          else res'

    ix' = ix + 1
    nextTrans = VU.unsafeIndex trans ix'
    cand2 = lTime - fromIntegral (VU.unsafeIndex diffs ix')
    res' = case (cand1 < nextTrans, cand2 >= nextTrans) of
      (False, False) -> FLGap ix cand1
      (True,  False) -> FLUnique ix cand1
      (False, True)  -> FLUnique ix' cand2
      (True,  True)  -> FLDouble ix cand1 cand2

-- | Fully descriptive result of a LocalTime to UTCTime conversion.
--
-- In case of LTUAmbiguous the first result is always earlier than the
-- second one. Generally this only happens during the daylight saving
-- -> standard time transition (ie. summer -> winter). So, the first
-- result corresponds to interpreting the LocalTime as a daylight
-- saving time and the second result as standard time in the given
-- location.
--
-- But, if the location had some kind of administrative time
-- transition during which the clocks jumped back, then both results
-- can correspond to standard times (or daylight saving times) just
-- before and after the transition. You can always inspect the
-- 'timeZoneSummerOnly' field of the returned 'TimeZone's to get an
-- idea what kind of transition was taking place.
--
-- TODO(klao): document the LTUNone behavior.
data LocalToUTCResult
  = LTUNone { _ltuResult :: UTCTime
            , _ltuZone :: TimeZone }
  | LTUUnique { _ltuResult :: UTCTime
              , _ltuZone :: TimeZone }
  | LTUAmbiguous { _ltuFirst      :: UTCTime
                 , _ltuSecond     :: UTCTime
                 , _ltuFirstZone  :: TimeZone
                 , _ltuSecondZone :: TimeZone
                 } deriving (Eq, Show, Read, Typeable, Data)

instance NFData LocalToUTCResult where
  rnf !_ = ()

-- TODO(klao): better name
localTimeToUTCFull :: TZ -> LocalTime -> LocalToUTCResult
localTimeToUTCFull tz@(TZ _ diffs _) localT = res
  where
    (t,ps) = localTimeToInt64Pair localT
    addDiff i = int64PairToUTCTime t' ps
      where
        diff = VU.unsafeIndex diffs i
        t' = t - fromIntegral diff
    res = case localToPOSIX tz t of
      FLGap i _ -> LTUNone (addDiff i) (timeZoneForIx tz i)
      FLUnique i _ -> LTUUnique (addDiff i) (timeZoneForIx tz i)
      FLDouble i _ _ -> LTUAmbiguous (addDiff i) (addDiff (i+1))
                          (timeZoneForIx tz i) (timeZoneForIx tz (i+1))

localTimeToUTCTZ :: TZ -> LocalTime -> UTCTime
localTimeToUTCTZ tz lt =
  case localTimeToUTCFull tz lt of
    LTUNone ut _ -> ut
    LTUUnique ut _ -> ut
    LTUAmbiguous _ ut _ _ -> ut

--------------------------------------------------------------------------------
-- Random utility functions

-- | Returns /a/ time difference (in seconds) corresponding to the
-- abbreviation in the given time zone.
--
-- If there are multiple time differences associated with the same
-- abbreviation, the one corresponding to the latest use is
-- returned. (The latest use might be in the past or the future
-- depending on whether the abbreviation is still in use.)
--
-- This function is here for informational purpose only, do not use it
-- for time conversion. (Instead, use 'localTimeToUTCFull', and if the
-- result is ambiguous disambiguate between the possible results based
-- on the abbreviation.)
diffForAbbr :: TZ -> String -> Maybe Int
{-# INLINABLE diffForAbbr #-}
diffForAbbr (TZ _ diffs infos) s =
  case VB.findIndex ((==) s . snd) $ VB.reverse infos of
    Nothing -> Nothing
    Just i -> Just $ VU.unsafeIndex diffs (VU.length diffs - 1 - i)
