---------------------------------------------------------------
-- Copyright (c) 2014, Enzo Haussecker. All rights reserved. --
---------------------------------------------------------------

{-# LANGUAGE DeriveDataTypeable         #-}
{-# LANGUAGE DeriveGeneric              #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE MultiParamTypeClasses      #-}
{-# LANGUAGE RecordWildCards            #-}
{-# LANGUAGE ScopedTypeVariables        #-}
{-# LANGUAGE StandaloneDeriving         #-}
{-# LANGUAGE TemplateHaskell            #-}
{-# LANGUAGE TupleSections              #-}
{-# OPTIONS -Wall                       #-}

-- | Local timestamps of varying granularity.
module Data.Time.Exts.Local (

 -- ** Local Class
       Local(..)

 -- ** Local Timestamps
     , LocalDate(..)
     , LocalDateTime(..)
     , LocalDateTimeMillis(..)
     , LocalDateTimeMicros(..)
     , LocalDateTimeNanos(..)
     , LocalDateTimePicos(..)

 -- ** Create Local Timestamps
     , createLocalDate
     , createLocalDateTime
     , createLocalDateTimeMillis
     , createLocalDateTimeMicros
     , createLocalDateTimeNanos
     , createLocalDateTimePicos

 -- ** Get Current Local Timestamps
     , getCurrentLocalDate
     , getCurrentLocalDateTime
     , getCurrentLocalDateTimeMillis
     , getCurrentLocalDateTimeMicros
     , getCurrentLocalDateTimeNanos
     , getCurrentLocalDateTimePicos

 -- ** Time Zone Transition Times
     , Transition(..)
     , getTransitions
     , lastTransition

 -- ** Get Current Local Timestamps Using Preloaded Time Zone Transitions Times
     , getCurrentLocalDate'
     , getCurrentLocalDateTime'
     , getCurrentLocalDateTimeMillis'
     , getCurrentLocalDateTimeMicros'
     , getCurrentLocalDateTimeNanos'
     , getCurrentLocalDateTimePicos'

 -- ** Pretty Local Timestamps
     , prettyLocalDate
     , prettyLocalDateTime

 -- ** Base Conversions
     , baseUnixToUTC
     , baseUTCToUnix

     ) where

import Control.Arrow    ((***), first)
import Control.DeepSeq  (NFData)
import Data.Aeson       (FromJSON, ToJSON)
import Data.Convertible (Convertible(..), convert)
import Data.Function    (on)
import Data.Int         (Int16, Int32, Int64)
import Data.Label       (get, mkLabels, modify, set)
import Data.List        (groupBy, sortBy)
import Data.Maybe       (listToMaybe)
import Data.Monoid      ((<>))
import Data.Ord         (comparing)
import Data.Time        (DiffTime, UTCTime(..))
import Data.Time.Exts.Base
import Data.Time.Exts.Unix
import Data.Tuple       (swap)
import Data.Typeable    (Typeable)
import Data.Word        (Word8)
import Foreign.Ptr      (plusPtr)
import Foreign.Storable (Storable(..))
import GHC.Generics     (Generic)
import System.Random    (Random(..))
import Text.Printf      (printf)

import qualified Data.Time.Calendar  as Cal
import qualified Data.Time.Exts.Zone as TZ
import qualified Data.Time.LocalTime.TimeZone.Olson as Olson

-- | The local timestamp type class.
class Local u where

    -- | Get the base component of a local timestamp.
    localBase :: u -> Int64

    -- | Get the zone component of a local timestamp.
    localZone :: u -> Word8

-- | Days since Unix epoch.
data LocalDate = LocalDate {
    _ld_day_base :: {-# UNPACK #-} !Int32
  , _ld_day_zone :: {-# UNPACK #-} !Word8
  } deriving (Eq,Generic,Typeable)

-- | Seconds since Unix epoch (including leap seconds).
data LocalDateTime = LocalDateTime {
    _ldt_sec_base :: {-# UNPACK #-} !Int64
  , _ldt_sec_zone :: {-# UNPACK #-} !Word8
  } deriving (Eq,Generic,Typeable)

-- | Milliseconds since Unix epoch (including leap seconds).
data LocalDateTimeMillis = LocalDateTimeMillis {
    _ldt_mil_base :: {-# UNPACK #-} !Int64
  , _ldt_mil_zone :: {-# UNPACK #-} !Word8
  } deriving (Eq,Generic,Typeable)

-- | Microseconds since Unix epoch (including leap seconds).
data LocalDateTimeMicros = LocalDateTimeMicros {
    _ldt_mic_base :: {-# UNPACK #-} !Int64
  , _ldt_mic_zone :: {-# UNPACK #-} !Word8
  } deriving (Eq,Generic,Typeable)

-- | Nanoseconds since Unix epoch (including leap seconds).
data LocalDateTimeNanos = LocalDateTimeNanos {
    _ldt_nan_base :: {-# UNPACK #-} !Int64
  , _ldt_nan_nano :: {-# UNPACK #-} !Int16
  , _ldt_nan_zone :: {-# UNPACK #-} !Word8
  } deriving (Eq,Generic,Typeable)

-- | Picoseconds since Unix epoch (including leap seconds).
data LocalDateTimePicos = LocalDateTimePicos {
    _ldt_pic_base :: {-# UNPACK #-} !Int64
  , _ldt_pic_pico :: {-# UNPACK #-} !Int32
  , _ldt_pic_zone :: {-# UNPACK #-} !Word8
  } deriving (Eq,Generic,Typeable)

-- | Time zone transition time.
newtype Transition = Transition {
    unboxTrans :: LocalDateTime
  } deriving (Eq,Generic,Typeable)

instance FromJSON LocalDate
instance FromJSON LocalDateTime
instance FromJSON LocalDateTimeMillis
instance FromJSON LocalDateTimeMicros
instance FromJSON LocalDateTimeNanos
instance FromJSON LocalDateTimePicos

instance NFData LocalDate
instance NFData LocalDateTime
instance NFData LocalDateTimeMillis
instance NFData LocalDateTimeMicros
instance NFData LocalDateTimeNanos
instance NFData LocalDateTimePicos

instance Storable LocalDate where
    sizeOf  _ = 5
    alignment = sizeOf
    peekElemOff ptr n = do
      let off = 5 * n
      base <- peek . plusPtr ptr $ off
      zone <- peek . plusPtr ptr $ off + 4
      return $! LocalDate base zone
    pokeElemOff ptr n LocalDate{..} = do
      let off = 5 * n
      poke (plusPtr ptr $ off    ) _ld_day_base
      poke (plusPtr ptr $ off + 4) _ld_day_zone

instance Storable LocalDateTime where
    sizeOf  _ = 9
    alignment = sizeOf
    peekElemOff ptr n = do
      let off = 9 * n
      base <- peek . plusPtr ptr $ off
      zone <- peek . plusPtr ptr $ off + 8
      return $! LocalDateTime base zone
    pokeElemOff ptr n LocalDateTime{..} = do
      let off = 9 * n
      poke (plusPtr ptr $ off    ) _ldt_sec_base
      poke (plusPtr ptr $ off + 8) _ldt_sec_zone

instance Storable LocalDateTimeMillis where
    sizeOf  _ = 9
    alignment = sizeOf
    peekElemOff ptr n = do
      let off = 9 * n
      base <- peek . plusPtr ptr $ off
      zone <- peek . plusPtr ptr $ off + 8
      return $! LocalDateTimeMillis base zone
    pokeElemOff ptr n LocalDateTimeMillis{..} = do
      let off = 9 * n
      poke (plusPtr ptr $ off    ) _ldt_mil_base
      poke (plusPtr ptr $ off + 8) _ldt_mil_zone

instance Storable LocalDateTimeMicros where
    sizeOf  _ = 9
    alignment = sizeOf
    peekElemOff ptr n = do
      let off = 9 * n
      base <- peek . plusPtr ptr $ off
      zone <- peek . plusPtr ptr $ off + 8
      return $! LocalDateTimeMicros base zone
    pokeElemOff ptr n LocalDateTimeMicros{..} = do
      let off = 9 * n
      poke (plusPtr ptr $ off    ) _ldt_mic_base
      poke (plusPtr ptr $ off + 8) _ldt_mic_zone

instance Storable LocalDateTimeNanos where
    sizeOf  _ = 11
    alignment = sizeOf
    peekElemOff ptr  n = do
      let off = 11 * n
      base <- peek . plusPtr ptr $ off
      nano <- peek . plusPtr ptr $ off + 08
      zone <- peek . plusPtr ptr $ off + 10
      return $! LocalDateTimeNanos base nano zone
    pokeElemOff ptr  n LocalDateTimeNanos{..} = do
      let off = 11 * n
      poke (plusPtr ptr $ off     ) _ldt_nan_base
      poke (plusPtr ptr $ off + 08) _ldt_nan_nano
      poke (plusPtr ptr $ off + 10) _ldt_nan_zone

instance Storable LocalDateTimePicos where
    sizeOf  _ = 13
    alignment = sizeOf
    peekElemOff ptr  n = do
      let off = 13 * n
      base <- peek . plusPtr ptr $ off
      pico <- peek . plusPtr ptr $ off + 08
      zone <- peek . plusPtr ptr $ off + 12
      return $! LocalDateTimePicos base pico zone
    pokeElemOff ptr  n LocalDateTimePicos{..} = do
      let off = 13 * n
      poke (plusPtr ptr $ off     ) _ldt_pic_base
      poke (plusPtr ptr $ off + 08) _ldt_pic_pico
      poke (plusPtr ptr $ off + 12) _ldt_pic_zone

instance ToJSON LocalDate
instance ToJSON LocalDateTime
instance ToJSON LocalDateTimeMillis
instance ToJSON LocalDateTimeMicros
instance ToJSON LocalDateTimeNanos
instance ToJSON LocalDateTimePicos

mkLabels [ ''LocalDate
         , ''LocalDateTime
         , ''LocalDateTimeMillis
         , ''LocalDateTimeMicros
         , ''LocalDateTimeNanos
         , ''LocalDateTimePicos
         ]

instance Bounded LocalDate where
    minBound = LocalDate 0 0
    maxBound = LocalDate 2932896 maxzone

instance Bounded LocalDateTime where
    minBound = LocalDateTime 0 0
    maxBound = LocalDateTime 253402257624 maxzone

instance Bounded LocalDateTimeMillis where
    minBound = LocalDateTimeMillis 0 0
    maxBound = LocalDateTimeMillis 253402257624999 maxzone

instance Bounded LocalDateTimeMicros where
    minBound = LocalDateTimeMicros 0 0
    maxBound = LocalDateTimeMicros 253402257624999999 maxzone

instance Bounded LocalDateTimeNanos where
    minBound = LocalDateTimeNanos 0 0 0
    maxBound = LocalDateTimeNanos 253402257624999999 999 maxzone

instance Bounded LocalDateTimePicos where
    minBound = LocalDateTimePicos 0 0 0
    maxBound = LocalDateTimePicos 253402257624999999 999999 maxzone

instance Local LocalDate where
    localBase = fromIntegral . get ld_day_base
    localZone = get ld_day_zone

instance Local LocalDateTime where
    localBase = get ldt_sec_base
    localZone = get ldt_sec_zone

instance Local LocalDateTimeMillis where
    localBase = get ldt_mil_base
    localZone = get ldt_mil_zone

instance Local LocalDateTimeMicros where
    localBase = get ldt_mic_base
    localZone = get ldt_mic_zone

instance Local LocalDateTimeNanos where
    localBase = get ldt_nan_base
    localZone = get ldt_nan_zone

instance Local LocalDateTimePicos where
    localBase = get ldt_pic_base
    localZone = get ldt_pic_zone

deriving instance Local Transition

instance Ord LocalDate where
    compare = comparing localBase

instance Ord LocalDateTime where
    compare = comparing localBase

instance Ord LocalDateTimeMillis where
    compare = comparing localBase

instance Ord LocalDateTimeMicros where
    compare = comparing localBase

instance Ord LocalDateTimeNanos where
    compare = comparing localBase <> comparing (get ldt_nan_nano)

instance Ord LocalDateTimePicos where
    compare = comparing localBase <> comparing (get ldt_pic_pico)

instance DateTimeMath LocalDate Day where
    date `plus` Day day =
      check "plus{LocalDate,Day}" $
        modify ld_day_base (+ day) date

instance DateTimeMath LocalDateTime Second where
    time `plus` Second second =
      check "plus{LocalDateTime,Second}" $
        modify ldt_sec_base (+ second) time

instance DateTimeMath LocalDateTimeMillis Second where
    time `plus` Second second =
      check "plus{LocalDateTimeMillis,Second}" $
        modify ldt_mil_base (+ second * 1000) time

instance DateTimeMath LocalDateTimeMicros Second where
    time `plus` Second second =
      check "plus{LocalDateTimeMicros,Second}" $
        modify ldt_mic_base (+ second * 1000000) time

instance DateTimeMath LocalDateTimeNanos Second where
    time `plus` Second second =
      check "plus{LocalDateTimeNanos,Second}" $
        modify ldt_nan_base (+ second * 1000000) time

instance DateTimeMath LocalDateTimePicos Second where
    time `plus` Second second =
      check "plus{LocalDateTimePicos,Second}" $
        modify ldt_pic_base (+ second * 1000000) time

instance DateTimeMath LocalDateTimeMillis Millis where
    time `plus` Millis millis =
      check "plus{LocalDateTimeMillis,Millis}" $
        modify ldt_mil_base (+ millis) time

instance DateTimeMath LocalDateTimeMicros Millis where
    time `plus` Millis millis =
      check "plus{LocalDateTimeMicros,Millis}" $
        modify ldt_mic_base (+ millis * 1000) time

instance DateTimeMath LocalDateTimeNanos Millis where
    time `plus` Millis millis =
      check "plus{LocalDateTimeNanos,Millis}" $
        modify ldt_nan_base (+ millis * 1000) time

instance DateTimeMath LocalDateTimePicos Millis where
    time `plus` Millis millis =
      check "plus{LocalDateTimePicos,Millis}" $
        modify ldt_pic_base (+ millis * 1000) time

instance DateTimeMath LocalDateTimeMicros Micros where
    time `plus` Micros micros =
      check "plus{LocalDateTimeMicros,Micros}" $
        modify ldt_mic_base (+ micros) time

instance DateTimeMath LocalDateTimeNanos Micros where
    time `plus` Micros micros =
      check "plus{LocalDateTimeNanos,Micros}" $
        modify ldt_nan_base (+ micros) time

instance DateTimeMath LocalDateTimePicos Micros where
    time `plus` Micros micros =
      check "plus{LocalDateTimePicos,Micros}" $
        modify ldt_pic_base (+ micros) time

instance DateTimeMath LocalDateTimeNanos Nanos where
    time `plus` Nanos nanos =
      check "plus{UnixDateTimeNanos,Nanos}" .
        modify ldt_nan_base (+ micros) $
           set ldt_nan_nano nano time
           where nsum = fromIntegral (get ldt_nan_nano time) + nanos
                 (micros, nano) = fmap fromIntegral $ divMod nsum 1000

instance DateTimeMath LocalDateTimePicos Nanos where
    time `plus` Nanos nanos =
      check "plus{LocalDateTimePicos,Nanos}" .
        modify ldt_pic_base (+ micros) $
           set ldt_pic_pico pico time
           where psum = fromIntegral (get ldt_pic_pico time) + nanos * 1000
                 (micros, pico) = fmap fromIntegral $ divMod psum 1000000

instance DateTimeMath LocalDateTimePicos Picos where
    time `plus` Picos picos =
      check "plus{LocalDateTimePicos,Picos}" .
        modify ldt_pic_base (+ micros) $
           set ldt_pic_pico pico time
           where psum = fromIntegral (get ldt_pic_pico time) + picos
                 (micros, pico) = fmap fromIntegral $ divMod psum 1000000

instance Enum LocalDate where
    succ = flip plus $ Day 1
    pred = flip plus . Day $ - 1
    toEnum = check "toEnum{LocalDate}" . uncurry LocalDate . doubleInt . flip divMod 1000
    fromEnum LocalDate{..} = fromIntegral _ld_day_base * 1000 + fromIntegral _ld_day_zone
    enumFrom v = [t | t <- v : enumFrom (succ v)]
    enumFromTo v1 v2 
      | v1 == v2  = [v1]
      | v1  < v2  = [t | t <- v1 : enumFromTo (succ v1) v2]
      | otherwise = [t | t <- v1 : enumFromTo (pred v1) v2]

instance Enum LocalDateTime where
    succ = flip plus $ Second 1
    pred = flip plus . Second $ - 1
    toEnum = check "toEnum{LocalDateTime}" . uncurry LocalDateTime . doubleInt . flip divMod 1000
    fromEnum LocalDateTime{..} = fromIntegral _ldt_sec_base * 1000 + fromIntegral _ldt_sec_zone
    enumFrom v = [t | t <- v : enumFrom (succ v)]
    enumFromTo v1 v2 
      | v1 == v2  = [v1]
      | v1  < v2  = [t | t <- v1 : enumFromTo (succ v1) v2]
      | otherwise = [t | t <- v1 : enumFromTo (pred v1) v2]

instance Enum LocalDateTimeMillis where
    succ = flip plus $ Millis 1
    pred = flip plus . Millis $ - 1
    toEnum = check "toEnum{LocalDateTimeMillis}" . uncurry LocalDateTimeMillis . doubleInt . flip divMod 1000
    fromEnum LocalDateTimeMillis{..} = fromIntegral _ldt_mil_base * 1000 + fromIntegral _ldt_mil_zone
    enumFrom v = [t | t <- v : enumFrom (succ v)]
    enumFromTo v1 v2 
      | v1 == v2  = [v1]
      | v1  < v2  = [t | t <- v1 : enumFromTo (succ v1) v2]
      | otherwise = [t | t <- v1 : enumFromTo (pred v1) v2]

-- | Convert Unix seconds into a UTC seconds.
baseUnixToUTC :: Int64 -> Int64
baseUnixToUTC base
   | base >= 1341100800 = base + 25
   | base >= 1230768000 = base + 24
   | base >= 1136073600 = base + 23
   | base >= 0915148800 = base + 22
   | base >= 0867715200 = base + 21
   | base >= 0820454400 = base + 20
   | base >= 0773020800 = base + 19
   | base >= 0741484800 = base + 18
   | base >= 0709948800 = base + 17
   | base >= 0662688000 = base + 16
   | base >= 0631152000 = base + 15
   | base >= 0567993600 = base + 14
   | base >= 0489024000 = base + 13
   | base >= 0425865600 = base + 12
   | base >= 0394329600 = base + 11
   | base >= 0362793600 = base + 10
   | base >= 0315532800 = base + 09
   | base >= 0283996800 = base + 08
   | base >= 0252460800 = base + 07
   | base >= 0220924800 = base + 06
   | base >= 0189302400 = base + 05
   | base >= 0157766400 = base + 04
   | base >= 0126230400 = base + 03
   | base >= 0094694400 = base + 02
   | base >= 0078796800 = base + 01
   | otherwise          = base + 00

-- | Create a local date.
-- 
-- > >>> createLocalDate 2013 November 03 Pacific_Standard_Time
-- > 2013-11-03 PST
--
createLocalDate :: Year -> Month -> Day -> TZ.TimeZone -> LocalDate
createLocalDate year month day zone =
   check "createLocalDate" . LocalDate base $ tz2w8 zone
   where Day base = epochToDate year month day

-- | Create a local date and time.
--
-- > >>> createLocalDateTime 2013 November 03 22 55 52 South_Africa_Standard_Time
-- > 2013-11-03 22:55:52 SAST
--
createLocalDateTime :: Year -> Month -> Day -> Hour -> Minute -> Second -> TZ.TimeZone -> LocalDateTime
createLocalDateTime year month day hour minute (Second second) zone =
   check "createLocalDateTime" . LocalDateTime base $ tz2w8 zone
   where base = baseUnixToUTC (unix - offset) + second
         Second unix = epochToTime year month day hour minute 0
         offset = TZ.getUTCOffset zone * 60

-- | Create a local date and time with millisecond granularity.
--
-- > >>> createLocalDateTimeMillis 2013 November 03 13 57 43 830 Mountain_Standard_Time
-- > 2013-11-03 13:57:43.830 MST
--
createLocalDateTimeMillis :: Year -> Month -> Day -> Hour -> Minute -> Second -> Millis -> TZ.TimeZone -> LocalDateTimeMillis
createLocalDateTimeMillis year month day hour minute (Second second) (Millis millis) zone =
   check "createLocalDateTimeMillis" . LocalDateTimeMillis base $ tz2w8 zone
   where base = 1000 * (baseUnixToUTC (unix - offset) + second) + millis
         Second unix = epochToTime year month day hour minute 0
         offset = TZ.getUTCOffset zone * 60

-- | Create a local date and time with microsecond granularity.
--
-- > >>> createLocalDateTimeMicros 2013 November 03 21 01 42 903539 Coordinated_Universal_Time 
-- > 2013-11-03 21:01:42.903539 UTC
--
createLocalDateTimeMicros :: Year -> Month -> Day -> Hour -> Minute -> Second -> Micros -> TZ.TimeZone -> LocalDateTimeMicros
createLocalDateTimeMicros year month day hour minute (Second second) (Micros micros) zone =
   check "createLocalDateTimeMicros" . LocalDateTimeMicros base $ tz2w8 zone
   where base = 1000000 * (baseUnixToUTC (unix - offset) + second) + micros
         Second unix = epochToTime year month day hour minute 0
         offset = TZ.getUTCOffset zone * 60

-- | Create a local date and time with nanosecond granularity.
--
-- > >>> createLocalDateTimeNanos 2013 November 04 06 05 07 016715087 Japan_Standard_Time
-- > 2013-11-04 06:05:07.016715087 JST
--
createLocalDateTimeNanos :: Year -> Month -> Day -> Hour -> Minute -> Second -> Nanos -> TZ.TimeZone -> LocalDateTimeNanos
createLocalDateTimeNanos year month day hour minute (Second second) (Nanos nanos) zone =
   check "createLocalDateTimeNanos" . LocalDateTimeNanos base nano $ tz2w8 zone
   where base = 1000000 * (baseUnixToUTC (unix - offset) + second) + micros
         (micros, nano) = fmap fromIntegral $ divMod nanos 0001000
         Second unix = epochToTime year month day hour minute 0
         offset = TZ.getUTCOffset zone * 60

-- | Create a local date and time with picosecond granularity.
--
-- > >>> createLocalDateTimePicos 2013 November 03 23 13 56 838238648311 Eastern_European_Time
-- > 2013-11-03 23:13:56.838238648311 EET
--
createLocalDateTimePicos :: Year -> Month -> Day -> Hour -> Minute -> Second -> Picos -> TZ.TimeZone -> LocalDateTimePicos
createLocalDateTimePicos year month day hour minute (Second second) (Picos picos) zone =
   check "createLocalDateTimePicos" . LocalDateTimePicos base pico $ tz2w8 zone
   where base = 1000000 * (baseUnixToUTC (unix - offset) + second) + micros
         (micros, pico) = fmap fromIntegral $ divMod picos 1000000
         Second unix = epochToTime year month day hour minute 0
         offset = TZ.getUTCOffset zone * 60

-- | Convert UTC seconds into Unix and leap seconds.
baseUTCToUnix :: Int64 -> (Int64, Double)
baseUTCToUnix base
   | base >= 1341100825 = (base - 0025, 0)
   | base == 1341100824 = (01341100799, 1)
   | base >= 1230768024 = (base - 0024, 0)
   | base == 1230768023 = (01230767999, 1)
   | base >= 1136073623 = (base - 0023, 0)
   | base == 1136073622 = (01136073599, 1)
   | base >= 0915148822 = (base - 0022, 0)
   | base == 0915148821 = (00915148799, 1)
   | base >= 0867715221 = (base - 0021, 0)
   | base == 0867715220 = (00867715199, 1)
   | base >= 0820454420 = (base - 0020, 0)
   | base == 0820454419 = (00820454399, 1)
   | base >= 0773020819 = (base - 0019, 0)
   | base == 0773020818 = (00773020799, 1)
   | base >= 0741484818 = (base - 0018, 0)
   | base == 0741484817 = (00741484799, 1)
   | base >= 0709948817 = (base - 0017, 0)
   | base == 0709948816 = (00709948799, 1)
   | base >= 0662688016 = (base - 0016, 0)
   | base == 0662688015 = (00662687999, 1)
   | base >= 0631152015 = (base - 0015, 0)
   | base == 0631152014 = (00631151999, 1)
   | base >= 0567993614 = (base - 0014, 0)
   | base == 0567993613 = (00567993599, 1)
   | base >= 0489024013 = (base - 0013, 0)
   | base == 0489024012 = (00489023999, 1)
   | base >= 0425865612 = (base - 0012, 0)
   | base == 0425865611 = (00425865599, 1)
   | base >= 0394329611 = (base - 0011, 0)
   | base == 0394329610 = (00394329599, 1)
   | base >= 0362793610 = (base - 0010, 0)
   | base == 0362793609 = (00362793599, 1)
   | base >= 0315532809 = (base - 0009, 0)
   | base == 0315532808 = (00315532799, 1)
   | base >= 0283996808 = (base - 0008, 0)
   | base == 0283996807 = (00283996799, 1)
   | base >= 0252460807 = (base - 0007, 0)
   | base == 0252460806 = (00252460799, 1)
   | base >= 0220924806 = (base - 0006, 0)
   | base == 0220924805 = (00220924799, 1)
   | base >= 0189302405 = (base - 0005, 0)
   | base == 0189302404 = (00189302399, 1)
   | base >= 0157766404 = (base - 0004, 0)
   | base == 0157766403 = (00157766399, 1)
   | base >= 0126230403 = (base - 0003, 0)
   | base == 0126230402 = (00126230399, 1)
   | base >= 0094694402 = (base - 0002, 0)
   | base == 0094694401 = (00094694399, 1)
   | base >= 0078796801 = (base - 0001, 0)
   | base == 0078796800 = (00078796799, 1)
   | otherwise          = (base - 0000, 0)

-- | Decompose a local date into a human-readable format.
decompLocalDate :: LocalDate -> DateZoneStruct
decompLocalDate LocalDate{..} =
   DateZoneStruct _d_year _d_mon _d_mday _d_wday zone
   where DateStruct{..} = toDateStruct $ UnixDate _ld_day_base
         zone = w82tz _ld_day_zone

-- | Decompose a local date and time into a human-readable format.
decompLocalDateTime :: LocalDateTime -> DateTimeZoneStruct
decompLocalDateTime LocalDateTime{..} =
   DateTimeZoneStruct _dt_year _dt_mon _dt_mday _dt_wday _dt_hour _dt_min sec zone
   where DateTimeStruct{..} = toDateTimeStruct $ UnixDateTime base `plus` offset
         (base, leap) = baseUTCToUnix _ldt_sec_base
         sec = _dt_sec + leap
         offset = TZ.getUTCOffset zone :: Minute
         zone = w82tz _ldt_sec_zone

-- | Decompose a local date and time with millisecond granularity into a human-readable format.
decompLocalDateTimeMillis :: LocalDateTimeMillis -> DateTimeZoneStruct
decompLocalDateTimeMillis LocalDateTimeMillis{..} =
   DateTimeZoneStruct _dt_year _dt_mon _dt_mday _dt_wday _dt_hour _dt_min sec zone
   where DateTimeStruct{..} = toDateTimeStruct $ UnixDateTime base `plus` offset
         ((base, leap), millis) = baseUTCToUnix *** realToFrac $ divMod _ldt_mil_base 0001000
         sec = _dt_sec + leap + millis / 0001000
         offset = TZ.getUTCOffset zone :: Minute
         zone = w82tz _ldt_mil_zone

-- | Decompose a local date and time with microsecond granularity into a human-readable format.
decompLocalDateTimeMicros :: LocalDateTimeMicros -> DateTimeZoneStruct
decompLocalDateTimeMicros LocalDateTimeMicros{..} =
   DateTimeZoneStruct _dt_year _dt_mon _dt_mday _dt_wday _dt_hour _dt_min sec zone
   where DateTimeStruct{..} = toDateTimeStruct $ UnixDateTime base `plus` offset
         ((base, leap), micros) = baseUTCToUnix *** realToFrac $ divMod _ldt_mic_base 1000000
         sec = _dt_sec + leap + micros / 1000000
         offset = TZ.getUTCOffset zone :: Minute
         zone = w82tz _ldt_mic_zone

-- | Decompose a local date and time with nanosecond granularity into a human-readable format.
decompLocalDateTimeNanos :: LocalDateTimeNanos -> DateTimeZoneStruct
decompLocalDateTimeNanos LocalDateTimeNanos{..} =
   DateTimeZoneStruct _dt_year _dt_mon _dt_mday _dt_wday _dt_hour _dt_min sec zone
   where DateTimeStruct{..} = toDateTimeStruct $ UnixDateTime base `plus` offset
         ((base, leap), micros) = baseUTCToUnix *** realToFrac $ divMod _ldt_nan_base 1000000
         sec = _dt_sec + leap + micros / 1000000 + realToFrac _ldt_nan_nano / 1000000000
         offset = TZ.getUTCOffset zone :: Minute
         zone = w82tz _ldt_nan_zone

-- | Decompose a local date and time with picosecond granularity into a human-readable format.
decompLocalDateTimePicos :: LocalDateTimePicos -> DateTimeZoneStruct
decompLocalDateTimePicos LocalDateTimePicos{..} =
   DateTimeZoneStruct _dt_year _dt_mon _dt_mday _dt_wday _dt_hour _dt_min sec zone
   where DateTimeStruct{..} = toDateTimeStruct $ UnixDateTime base `plus` offset
         ((base, leap), micros) = baseUTCToUnix *** realToFrac $ divMod _ldt_pic_base 1000000
         sec = _dt_sec + leap + micros / 1000000 + realToFrac _ldt_pic_pico / 1000000000000
         offset = TZ.getUTCOffset zone :: Minute
         zone = w82tz _ldt_pic_zone

-- | Decompose a local base (in seconds) into day and second components.
decompLocalBase :: (Int64, TZ.TimeZone) -> (Int32, DiffTime)
decompLocalBase = uncurry (flip f) . first (uncurry g . baseUTCToUnix)
   where f x = first $ fromIntegral . flip div 86400  . (+ 60 * TZ.getUTCOffset x)
         g x = (x, ) . fromIntegral . (+ mod x 86400) . truncate

instance Convertible LocalDateTime LocalDate where
    safeConvert LocalDateTime{..} = Right $ LocalDate base _ldt_sec_zone
      where base = fst $ decompLocalBase (_ldt_sec_base, w82tz _ldt_sec_zone)

instance Convertible LocalDateTimeMillis LocalDate where
    safeConvert LocalDateTimeMillis{..} = Right $ LocalDate base _ldt_mil_zone
      where base = fst (decompLocalBase (div _ldt_mil_base 0001000, w82tz _ldt_mil_zone))

instance Convertible LocalDateTimeMicros LocalDate where
    safeConvert LocalDateTimeMicros{..} = Right $ LocalDate base _ldt_mic_zone
      where base = fst (decompLocalBase (div _ldt_mic_base 1000000, w82tz _ldt_mic_zone))

instance Convertible LocalDateTimeNanos LocalDate where
    safeConvert LocalDateTimeNanos{..} = Right $ LocalDate base _ldt_nan_zone
      where base = fst (decompLocalBase (div _ldt_nan_base 1000000, w82tz _ldt_nan_zone))

instance Convertible LocalDateTimePicos LocalDate where
    safeConvert LocalDateTimePicos{..} = Right $ LocalDate base _ldt_pic_zone
      where base = fst (decompLocalBase (div _ldt_pic_base 1000000, w82tz _ldt_pic_zone))

instance Convertible LocalDate Cal.Day where
    safeConvert = Right . Cal.ModifiedJulianDay . toInteger . (+ 40587) . _ld_day_base

instance Convertible LocalDateTime UTCTime where
    safeConvert LocalDateTime{..} = Right $ UTCTime julian pico
      where julian = Cal.ModifiedJulianDay $ toInteger day + 40587
            (day, pico) = decompLocalBase (_ldt_sec_base, w82tz _ldt_sec_zone)

instance Convertible LocalDateTimeMillis UTCTime where
    safeConvert LocalDateTimeMillis{..} = Right $ UTCTime julian pico
      where julian = Cal.ModifiedJulianDay $ toInteger day + 40587
            frac = millis / 1000
            (base, millis) = fmap fromIntegral $ _ldt_mil_base `divMod` 0001000
            (day , pico  ) = fmap (+ frac) $ decompLocalBase (base, w82tz _ldt_mil_zone)

instance Convertible LocalDateTimeMicros UTCTime where
    safeConvert LocalDateTimeMicros{..} = Right $ UTCTime julian pico
      where julian = Cal.ModifiedJulianDay $ toInteger day + 40587
            frac = micros / 1000000
            (base, micros) = fmap fromIntegral $ _ldt_mic_base `divMod` 1000000
            (day , pico  ) = fmap (+ frac) $ decompLocalBase (base, w82tz _ldt_mic_zone)

instance Convertible LocalDateTimeNanos UTCTime where
    safeConvert LocalDateTimeNanos{..} = Right $ UTCTime julian pico
      where julian = Cal.ModifiedJulianDay $ toInteger day + 40587
            frac = micros / 1000000 + fromIntegral _ldt_nan_nano / 0001000000000
            (base, micros) = fmap fromIntegral $ _ldt_nan_base `divMod` 1000000
            (day , pico  ) = fmap (+ frac) $ decompLocalBase (base, w82tz _ldt_nan_zone)

instance Convertible LocalDateTimePicos UTCTime where
    safeConvert LocalDateTimePicos{..} = Right $ UTCTime julian pico
      where julian = Cal.ModifiedJulianDay $ toInteger day + 40587
            frac = micros / 1000000 + fromIntegral _ldt_pic_pico / 1000000000000
            (base, micros) = fmap fromIntegral $ _ldt_pic_base `divMod` 1000000
            (day , pico  ) = fmap (+ frac) $ decompLocalBase (base, w82tz _ldt_pic_zone)

instance Convertible Cal.Day LocalDate where
    safeConvert = Right . check "safeConvert{Data.Time.Calendar.Day,LocalDate}" .
      flip LocalDate (tz2w8 TZ.utc) . fromInteger . (subtract 40587) . Cal.toModifiedJulianDay

instance Convertible UTCTime LocalDateTime where
    safeConvert UTCTime{..} = Right . check "safeConvert{Data.Time.Clock.UTCTime,LocalDateTime}" $
      LocalDateTime base (tz2w8 TZ.utc)
      where base = baseUnixToUTC $ day * 86400 + truncate utctDayTime
            day  = fromInteger (Cal.toModifiedJulianDay utctDay) - 40587

instance Convertible UTCTime LocalDateTimeMillis where
    safeConvert UTCTime{..} = Right . check "safeConvert{Data.Time.Clock.UTCTime,LocalDateTimeMillis}" $
      LocalDateTimeMillis base (tz2w8 TZ.utc)
      where base = baseUnixToUTC (day * 86400 + sec) * 0001000 + millis
            day  = fromInteger (Cal.toModifiedJulianDay utctDay) - 40587
            (sec , millis) = fmap (truncate . (* 0000001000)) $ properFraction utctDayTime

instance Convertible UTCTime LocalDateTimeMicros where
    safeConvert UTCTime{..} = Right . check "safeConvert{Data.Time.Clock.UTCTime,LocalDateTimeMicros}" $
      LocalDateTimeMicros base (tz2w8 TZ.utc)
      where base = baseUnixToUTC (day * 86400 + sec) * 1000000 + micros
            day  = fromInteger (Cal.toModifiedJulianDay utctDay) - 40587
            (sec , micros) = fmap (truncate . (* 0001000000)) $ properFraction utctDayTime

instance Convertible UTCTime LocalDateTimeNanos where
    safeConvert UTCTime{..} = Right . check "safeConvert{Data.Time.Clock.UTCTime,LocalDateTimeNanos}"  $
      LocalDateTimeNanos base nano (tz2w8 TZ.utc)
      where base = baseUnixToUTC (day * 86400 + sec) * 1000000 + micros
            day  = fromInteger (Cal.toModifiedJulianDay utctDay) - 40587
            (sec , nanos ) = fmap (truncate . (* 1000000000)) $ properFraction utctDayTime
            (nano, micros) = swap . fmap fromIntegral $ divMod nanos 1000

instance Convertible UTCTime LocalDateTimePicos where
    safeConvert UTCTime{..} = Right . check "safeConvert{Data.Time.Clock.UTCTime,LocalDateTimePicos}"  $
      LocalDateTimePicos base pico (tz2w8 TZ.utc)
      where base = baseUnixToUTC (day * 86400 + sec) * 1000000 + micros
            day  = fromInteger (Cal.toModifiedJulianDay utctDay) - 40587
            (sec , picos ) = fmap (truncate . (* 1000000000000)) $ properFraction utctDayTime
            (pico, micros) = swap . fmap fromIntegral $ divMod picos 1000000

instance Zone LocalDate where
    toTimeZone time = flip (set  ld_day_zone) time . tz2w8

instance Zone LocalDateTime where
    toTimeZone time = flip (set ldt_sec_zone) time . tz2w8

instance Zone LocalDateTimeMillis where
    toTimeZone time = flip (set ldt_mil_zone) time . tz2w8

instance Zone LocalDateTimeMicros where
    toTimeZone time = flip (set ldt_mic_zone) time . tz2w8

instance Zone LocalDateTimeNanos where
    toTimeZone time = flip (set ldt_nan_zone) time . tz2w8

instance Zone LocalDateTimePicos where
    toTimeZone time = flip (set ldt_pic_zone) time . tz2w8

instance DateZone LocalDate where
    toDateZoneStruct = decompLocalDate
    fromDateZoneStruct DateZoneStruct{..} =
      createLocalDate _dz_year _dz_mon _dz_mday _dz_zone

instance DateZone LocalDateTime where
    toDateZoneStruct = decompLocalDate . convert
    fromDateZoneStruct DateZoneStruct{..} =
      createLocalDateTime _dz_year _dz_mon _dz_mday 0 0 0 _dz_zone

instance DateZone LocalDateTimeMillis where
    toDateZoneStruct = decompLocalDate . convert
    fromDateZoneStruct DateZoneStruct{..} =
      createLocalDateTimeMillis _dz_year _dz_mon _dz_mday 0 0 0 0 _dz_zone

instance DateZone LocalDateTimeMicros where
    toDateZoneStruct = decompLocalDate . convert
    fromDateZoneStruct DateZoneStruct{..} =
      createLocalDateTimeMicros _dz_year _dz_mon _dz_mday 0 0 0 0 _dz_zone

instance DateZone LocalDateTimeNanos where
    toDateZoneStruct = decompLocalDate . convert
    fromDateZoneStruct DateZoneStruct{..} =
      createLocalDateTimeNanos _dz_year _dz_mon _dz_mday 0 0 0 0 _dz_zone

instance DateZone LocalDateTimePicos where
    toDateZoneStruct = decompLocalDate . convert
    fromDateZoneStruct DateZoneStruct{..} =
      createLocalDateTimePicos _dz_year _dz_mon _dz_mday 0 0 0 0 _dz_zone

instance DateTimeZone LocalDateTime where
    toDateTimeZoneStruct = decompLocalDateTime
    fromDateTimeZoneStruct DateTimeZoneStruct{..} =
      createLocalDateTime _dtz_year _dtz_mon _dtz_mday _dtz_hour _dtz_min sec _dtz_zone
      where sec = round _dtz_sec :: Second

instance DateTimeZone LocalDateTimeMillis where
    toDateTimeZoneStruct = decompLocalDateTimeMillis
    fromDateTimeZoneStruct DateTimeZoneStruct{..} =
      createLocalDateTimeMillis _dtz_year _dtz_mon _dtz_mday _dtz_hour _dtz_min sec mil _dtz_zone
      where (sec, mil) = properFracMillis _dtz_sec

instance DateTimeZone LocalDateTimeMicros where
    toDateTimeZoneStruct = decompLocalDateTimeMicros
    fromDateTimeZoneStruct DateTimeZoneStruct{..} =
      createLocalDateTimeMicros _dtz_year _dtz_mon _dtz_mday _dtz_hour _dtz_min sec mic _dtz_zone
      where (sec, mic) = properFracMicros _dtz_sec

instance DateTimeZone LocalDateTimeNanos where
    toDateTimeZoneStruct = decompLocalDateTimeNanos
    fromDateTimeZoneStruct DateTimeZoneStruct{..} =
      createLocalDateTimeNanos _dtz_year _dtz_mon _dtz_mday _dtz_hour _dtz_min sec nan _dtz_zone
      where (sec, nan) = properFracNanos _dtz_sec

instance DateTimeZone LocalDateTimePicos where
    toDateTimeZoneStruct = decompLocalDateTimePicos
    fromDateTimeZoneStruct DateTimeZoneStruct{..} =
      createLocalDateTimePicos _dtz_year _dtz_mon _dtz_mday _dtz_hour _dtz_min sec pic _dtz_zone
      where (sec, pic) = properFracPicos _dtz_sec

instance Show LocalDate where
    show date = printf str _dz_year mon _dz_mday abbr
      where DateZoneStruct{..} = toDateZoneStruct date
            str  = "%04d-%02d-%02d %s"
            mon  = fromEnum _dz_mon + 1
            abbr = show $ TZ.abbreviate _dz_zone

instance Show LocalDateTime where
    show time = printf str _dtz_year mon _dtz_mday _dtz_hour _dtz_min sec abbr
      where DateTimeZoneStruct{..} = toDateTimeZoneStruct time
            str  = "%04d-%02d-%02d %02d:%02d:%02d %s"
            abbr = show $ TZ.abbreviate _dtz_zone
            mon  = fromEnum _dtz_mon + 1
            sec  = round _dtz_sec :: Second

instance Show LocalDateTimeMillis where
    show time = printf str _dtz_year mon _dtz_mday _dtz_hour _dtz_min sec mil abbr
      where DateTimeZoneStruct{..} = toDateTimeZoneStruct time
            str  = "%04d-%02d-%02d %02d:%02d:%02d.%03d %s"
            abbr = show $ TZ.abbreviate _dtz_zone
            mon  = fromEnum _dtz_mon + 1
            (sec, mil) = properFracMillis _dtz_sec

instance Show LocalDateTimeMicros where
    show time = printf str _dtz_year mon _dtz_mday _dtz_hour _dtz_min sec mic abbr
      where DateTimeZoneStruct{..} = toDateTimeZoneStruct time
            str  = "%04d-%02d-%02d %02d:%02d:%02d.%06d %s"
            abbr = show $ TZ.abbreviate _dtz_zone
            mon  = fromEnum _dtz_mon + 1
            (sec, mic) = properFracMicros _dtz_sec

instance Show LocalDateTimeNanos where
    show time = printf str _dtz_year mon _dtz_mday _dtz_hour _dtz_min sec nan abbr
      where DateTimeZoneStruct{..} = toDateTimeZoneStruct time
            str  = "%04d-%02d-%02d %02d:%02d:%02d.%09d %s"
            abbr = show $ TZ.abbreviate _dtz_zone
            mon  = fromEnum _dtz_mon + 1
            (sec, nan) = properFracNanos _dtz_sec

instance Show LocalDateTimePicos where
    show time = printf str _dtz_year mon _dtz_mday _dtz_hour _dtz_min sec pic abbr
      where DateTimeZoneStruct{..} = toDateTimeZoneStruct time
            str  = "%04d-%02d-%02d %02d:%02d:%02d.%012d %s"
            abbr = show $ TZ.abbreviate _dtz_zone
            mon  = fromEnum _dtz_mon + 1
            (sec, pic) = properFracPicos _dtz_sec

instance Show Transition where
    show = show . unboxTrans

-- | The next leap second insertion date.
nextLeap :: Maybe UnixDate
nextLeap = Nothing

-- | Get a list of time zone transition times for the given city.
getTransitions :: TZ.City -> IO [Transition]
getTransitions city = do
   let file = TZ.getOlsonFile city
   Olson.OlsonData{..} <- Olson.getOlsonFromFile file
   let ttimes = uniquetimes $ sortBy future2past olsonTransitions
   return $! foldr (step olsonTypes) [] $ map last ttimes
   where uniquetimes = groupBy $ on (==) Olson.transTime
         future2past = comparing $ negate . Olson.transTime
         step types Olson.Transition{..} accum =
           if transTime < 0
           then [Transition (LocalDateTime 43200 zone)]
           else  Transition (LocalDateTime  base zone) : accum
           where Olson.TtInfo{..} = types !! transIndex
                 abbr = TZ.TimeZoneAbbr city tt_abbr
                 base = baseUnixToUTC $ fromIntegral transTime
                 zone = tz2w8 $ TZ.unabbreviate abbr

-- | Get the last time zone transition time for the given city and time.
lastTransition :: (DateTime dt, Unix dt) => TZ.City -> dt -> IO (Maybe Transition)
lastTransition city time = do
   ttimes <- getTransitions city
   return $! listToMaybe $ dropWhile f ttimes
   where base = baseUnixToUTC $ unixNorm time
         f tt = localBase tt > base

-- | Get the current local date from the system clock.
--
-- > >>> getCurrentLocalDate London 
-- > 2013-11-03 GMT
--
getCurrentLocalDate :: TZ.City -> IO LocalDate
getCurrentLocalDate city = getTransitions city >>= getCurrentLocalDateTime' >>= return . convert

-- | Get the current local date from the system clock using preloaded transition times.
--
-- > >>> ttimes <- getTransitions Tokyo 
-- > >>> getCurrentLocalDate' ttimes
-- > 2013-11-04 JST
--
--   Use this function if you need to get the current local date more than once. The
--   use of preloaded transition times will avoid unnecessary parsing of Olson files. 
getCurrentLocalDate' :: [Transition] -> IO LocalDate
getCurrentLocalDate' ttimes = getCurrentLocalDateTime' ttimes >>= return . convert

-- | Get the current local date and time from the system clock.
--
-- > >>> getCurrentLocalDateTime New_York 
-- > 2013-11-03 16:38:16 EST
--
getCurrentLocalDateTime :: TZ.City -> IO LocalDateTime
getCurrentLocalDateTime city = getTransitions city >>= getCurrentLocalDateTime'

-- | Get the current local date and time from the system clock using preloaded transition
--   times.
--
-- > >>> ttimes <- getTransitions Moscow
-- > >>> getCurrentLocalDateTime' ttimes
-- > 2013-11-04 01:41:50 MSK
--
--   Use this function if you need to get the current local date and time more than once.
--   The use of preloaded transition times will avoid unnecessary parsing of Olson files.
getCurrentLocalDateTime' :: [Transition] -> IO LocalDateTime
getCurrentLocalDateTime' ttimes = do
   time@UnixDateTime{..} <- getCurrentUnixDateTime
   let  base = baseUnixToUTC _udt_sec_base
        f tt = localBase tt > base
        mval = listToMaybe $ dropWhile f ttimes
        zone = maybe (tz2w8 TZ.utc) localZone mval
   if   maybe True (/= convert time) nextLeap
   then return $! LocalDateTime base zone
   else let leap = round (realToFrac (_udt_sec_base `mod` 86400) / 86400 :: Double)
        in  return $! LocalDateTime base zone `plus` Second leap

-- | Get the current local date and time with millisecond granularity from the system clock.
--
-- > >>> getCurrentLocalDateTimeMillis Auckland
-- > 2013-11-04 10:46:13.123 NZDT
--
getCurrentLocalDateTimeMillis :: TZ.City -> IO LocalDateTimeMillis
getCurrentLocalDateTimeMillis city = getTransitions city >>= getCurrentLocalDateTimeMillis'

-- | Get the current local date and time with millisecond granularity from the system clock
--   using preloaded transition times.
--
-- > >>> ttimes <- getTransitions Tehran
-- > >>> getCurrentLocalDateTimeMillis' ttimes
-- > 2013-11-04 01:20:49.435 IRST
--
--   Use this function if you need to get the current local date and time with millisecond
--   granularity more than once. The use of preloaded transition times will avoid unnecessary
--   parsing of Olson files.
getCurrentLocalDateTimeMillis' :: [Transition] -> IO LocalDateTimeMillis
getCurrentLocalDateTimeMillis' ttimes = do
   time@UnixDateTimeMillis{..} <- getCurrentUnixDateTimeMillis
   let  (seconds, millis) = first baseUnixToUTC $ _udt_mil_base `divMod` 1000
        f tt = localBase tt > seconds
        mval = listToMaybe $ dropWhile f ttimes
        zone = maybe (tz2w8 TZ.utc) localZone mval
        base = seconds * 1000 + millis
   if   maybe True (/= convert time) nextLeap
   then return $! LocalDateTimeMillis base zone
   else let leap = round (realToFrac (_udt_mil_base `mod` 86400) / 86.4 :: Double)
        in  return $! LocalDateTimeMillis base zone `plus` Millis leap

-- | Get the current local date and time with microsecond granularity from the system clock.
--
-- > >>> getCurrentLocalDateTimeMicros Tel_Aviv 
-- > 2013-11-03 23:55:30.935387 IST
--
getCurrentLocalDateTimeMicros :: TZ.City -> IO LocalDateTimeMicros
getCurrentLocalDateTimeMicros city = getTransitions city >>= getCurrentLocalDateTimeMicros'

-- | Get the current local date and time with microsecond granularity from the system clock
--   using preloaded transition times.
--
-- > >>> ttimes <- getTransitions Sao_Paulo
-- > >>> getCurrentLocalDateTimeMicros' ttimes
-- > 2013-11-03 19:58:50.405806 BRST
--
--   Use this function if you need to get the current local date and time with microsecond
--   granularity more than once. The use of preloaded transition times will avoid unnecessary
--   parsing of Olson files.
getCurrentLocalDateTimeMicros' :: [Transition] -> IO LocalDateTimeMicros
getCurrentLocalDateTimeMicros' ttimes = do
   time@UnixDateTimeMicros{..} <- getCurrentUnixDateTimeMicros
   let  (seconds, micros) = first baseUnixToUTC $ _udt_mic_base `divMod` 1000000
        f tt = localBase tt > seconds
        mval = listToMaybe $ dropWhile f ttimes
        zone = maybe (tz2w8 TZ.utc) localZone mval
        base = seconds * 1000000 + micros
   if   maybe True (/= convert time) nextLeap
   then return $! LocalDateTimeMicros base zone
   else let leap = round (realToFrac (_udt_mic_base `mod` 86400) / 0.0864 :: Double)
        in  return $! LocalDateTimeMicros base zone `plus` Micros leap

-- | Get the current local date and time with nanosecond granularity from the system clock.
--
-- > >>> getCurrentLocalDateTimeNanos Brussels 
-- > 2013-11-03 23:01:07.337488000 CET
--
--   Note that this functions calls @gettimeofday@ behind the scenes. Therefore, the resultant
--   timestamp will have nanosecond granularity, but only microsecond resolution.
getCurrentLocalDateTimeNanos :: TZ.City -> IO LocalDateTimeNanos
getCurrentLocalDateTimeNanos city = getTransitions city >>= getCurrentLocalDateTimeNanos'

-- | Get the current local date and time with nanosecond granularity from the system clock
--   using preloaded transition times.
--
-- > >>> ttimes <- getTransitions Mogadishu
-- > >>> getCurrentLocalDateTimeNanos' ttimes
-- > 2013-11-04 01:15:08.664426000 EAT
--
--   Use this function if you need to get the current local date and time with nanosecond
--   granularity more than once. The use of preloaded transition times will avoid unnecessary
--   parsing of Olson files.
--
--   Note that this functions calls @gettimeofday@ behind the scenes. Therefore, the resultant
--   timestamp will have nanosecond granularity, but only microsecond resolution.
getCurrentLocalDateTimeNanos' :: [Transition] -> IO LocalDateTimeNanos
getCurrentLocalDateTimeNanos' ttimes = do
   time@UnixDateTimeNanos{..} <- getCurrentUnixDateTimeNanos
   let  (seconds, micros) = first baseUnixToUTC $ _udt_nan_base `divMod` 1000000
        f tt = localBase tt > seconds
        mval = listToMaybe $ dropWhile f ttimes
        zone = maybe (tz2w8 TZ.utc) localZone mval
        base = seconds * 1000000 + micros
   if   maybe True (/= convert time) nextLeap
   then return $! LocalDateTimeNanos base _udt_nan_nano zone
   else let leap = round (realToFrac (_udt_nan_base `mod` 86400) / 0.0000864 :: Double)
        in  return $! LocalDateTimeNanos base _udt_nan_nano zone `plus` Nanos leap

-- | Get the current local date and time with picosecond granularity from the system clock.
--
-- > >>> getCurrentLocalDateTimePicos Karachi
-- > 2013-11-04 22:05:17.556043000000 PKT
--
--   Note that this functions calls @gettimeofday@ behind the scenes. Therefore, the resultant
--   timestamp will have picosecond granularity, but only microsecond resolution.
getCurrentLocalDateTimePicos :: TZ.City -> IO LocalDateTimePicos
getCurrentLocalDateTimePicos city = getTransitions city >>= getCurrentLocalDateTimePicos'

-- | Get the current local date and time with picosecond granularity from the system clock using
--   preloaded transition times.
--
-- > >>> ttimes <- getTransitions Baghdad
-- > >>> getCurrentLocalDateTimePicos' ttimes
-- > 2013-11-04 01:20:57.502906000000 AST
--
--   Use this function if you need to get the current local date and time with picosecond
--   granularity more than once. The use of preloaded transition times will avoid unnecessary
--   parsing of Olson files.
--
--   Note that this functions calls @gettimeofday@ behind the scenes. Therefore, the resultant
--   timestamp will have picosecond granularity, but only microsecond resolution.
getCurrentLocalDateTimePicos' :: [Transition] -> IO LocalDateTimePicos
getCurrentLocalDateTimePicos' ttimes = do
   time@UnixDateTimePicos{..} <- getCurrentUnixDateTimePicos
   let  (seconds, micros) = first baseUnixToUTC $ _udt_pic_base `divMod` 1000000
        f tt = localBase tt > seconds
        mval = listToMaybe $ dropWhile f ttimes
        zone = maybe (tz2w8 TZ.utc) localZone mval
        base = seconds * 1000000 + micros
   if   maybe True (/= convert time) nextLeap
   then return $! LocalDateTimePicos base _udt_pic_pico zone
   else let picos = round (realToFrac (_udt_pic_base `mod` 86400) / 0.0000000864 :: Double)
        in  return $! LocalDateTimePicos base _udt_pic_pico zone `plus` Picos picos

-- | Convert a local date and time with nanosecond granularity into an integer.
fromNanos :: LocalDateTimeNanos -> Integer
fromNanos (LocalDateTimeNanos base nano _) = toInteger base * 1000 + toInteger nano

-- | Convert an integer into a local date and time with nanosecond granularity.
toNanos :: Integer -> (Word8 -> LocalDateTimeNanos)
toNanos n = LocalDateTimeNanos base nano
   where (base, nano) = doubleInt $ n `divMod` 1000

-- | Convert a local date and time with picosecond granularity into an integer.
fromPicos :: LocalDateTimePicos -> Integer
fromPicos (LocalDateTimePicos base pico _) = toInteger base * 1000000 + toInteger pico

-- | Convert an integer into a local date and time with picosecond granularity.
toPicos :: Integer -> (Word8 -> LocalDateTimePicos)
toPicos n = LocalDateTimePicos base pico
   where (base, pico) = doubleInt $ n `divMod` 1000000

instance Duration LocalDate Day where
    duration (LocalDate old _) (LocalDate new _) = Day (new - old)

instance Duration LocalDateTime Second where
    duration (LocalDateTime old _) (LocalDateTime new _) = Second (new - old)

instance Duration LocalDateTimeMillis Second where
    duration (LocalDateTimeMillis old _  ) (LocalDateTimeMillis new _  ) = Second (new - old) `div` 1000

instance Duration LocalDateTimeMicros Second where
    duration (LocalDateTimeMicros old _  ) (LocalDateTimeMicros new _  ) = Second (new - old) `div` 1000000

instance Duration LocalDateTimeNanos Second where
    duration (LocalDateTimeNanos  old _ _) (LocalDateTimeNanos  new _ _) = Second (new - old) `div` 1000000

instance Duration LocalDateTimePicos Second where
    duration (LocalDateTimePicos  old _ _) (LocalDateTimePicos  new _ _) = Second (new - old) `div` 1000000

instance Duration LocalDateTimeMillis Millis where
    duration (LocalDateTimeMillis old _  ) (LocalDateTimeMillis new _  ) = Millis (new - old)

instance Duration LocalDateTimeMicros Millis where
    duration (LocalDateTimeMicros old _  ) (LocalDateTimeMicros new _  ) = Millis (new - old) `div` 1000

instance Duration LocalDateTimeNanos Millis where
    duration (LocalDateTimeNanos  old _ _) (LocalDateTimeNanos  new _ _) = Millis (new - old) `div` 1000

instance Duration LocalDateTimePicos Millis where
    duration (LocalDateTimePicos  old _ _) (LocalDateTimePicos  new _ _) = Millis (new - old) `div` 1000

instance Duration LocalDateTimeMicros Micros where
    duration (LocalDateTimeMicros old _  ) (LocalDateTimeMicros new _  ) = Micros (new - old)

instance Duration LocalDateTimeNanos Micros where
    duration (LocalDateTimeNanos  old _ _) (LocalDateTimeNanos  new _ _) = Micros (new - old)

instance Duration LocalDateTimePicos Micros where
    duration (LocalDateTimePicos  old _ _) (LocalDateTimePicos  new _ _) = Micros (new - old)

instance Duration LocalDateTimeNanos Nanos where
    duration old new =
      if res < toInteger (maxBound::Int64) then Nanos $ fromInteger res
      else error "duration{LocalDateTimeNanos,Nanos}: integer overflow"
      where res = fromNanos new - fromNanos old

instance Duration LocalDateTimePicos Nanos where
    duration old new =
      if res < toInteger (maxBound::Int64) then Nanos $ fromInteger res `div` 1000
      else error "duration{LocalDateTimePicos,Nanos}: integer overflow"
      where res = fromPicos new - fromPicos old

instance Duration LocalDateTimePicos Picos where
    duration old new =
      if res < toInteger (maxBound::Int64) then Picos $ fromInteger res
      else error "duration{LocalDateTimePicos,Picos}: integer overflow"
      where res = fromPicos new - fromPicos old

instance Random LocalDate where
    random g =
      case randomR (0, 2932896) g  of { (base, g' ) ->
      case randomR (0, maxzone) g' of { (zone, g'') -> (LocalDate base zone, g'') } }
    randomR (a,b) g =
      case randomR (get ld_day_base a, get ld_day_base b) g  of { (base, g' ) ->
      case randomR (get ld_day_zone a, get ld_day_zone b) g' of { (zone, g'') -> (LocalDate base zone, g'') } }

instance Random LocalDateTime where
    random g =
      case randomR (43200, 253402257624) g  of { (base, g' ) ->
      case randomR (    0,      maxzone) g' of { (zone, g'') -> (LocalDateTime base zone, g'') } }
    randomR (a,b) g =
      case randomR (get ldt_sec_base a, get ldt_sec_base b) g  of { (base, g' ) ->
      case randomR (get ldt_sec_zone a, get ldt_sec_zone b) g' of { (zone, g'') -> (LocalDateTime base zone, g'') } }

instance Random LocalDateTimeMillis where
    random g =
      case randomR (43200, 253402257624999) g  of { (base, g' ) ->
      case randomR (    0,         maxzone) g' of { (zone, g'') -> (LocalDateTimeMillis base zone, g'') } }
    randomR (a,b) g =
      case randomR (get ldt_mil_base a, get ldt_mil_base b) g  of { (base, g' ) ->
      case randomR (get ldt_mil_zone a, get ldt_mil_zone b) g' of { (zone, g'') -> (LocalDateTimeMillis base zone, g'') } }

instance Random LocalDateTimeMicros where
    random g =
      case randomR (43200, 253402257624999999) g  of { (base, g' ) ->
      case randomR (    0,            maxzone) g' of { (zone, g'') -> (LocalDateTimeMicros base zone, g'') } }
    randomR (a,b) g =
      case randomR (get ldt_mic_base a, get ldt_mic_base b) g  of { (base, g' ) ->
      case randomR (get ldt_mic_zone a, get ldt_mic_zone b) g' of { (zone, g'') -> (LocalDateTimeMicros base zone, g'') } }

instance Random LocalDateTimeNanos where
    random g =
      case randomR (43200, 253402257624999999999) g  of { (base, g' ) ->
      case randomR (    0,               maxzone) g' of { (zone, g'') -> (toNanos base zone, g'') } }
    randomR (a,b) g =
      case randomR (fromNanos        a, fromNanos        b) g  of { (base, g' ) ->
      case randomR (get ldt_nan_zone a, get ldt_nan_zone b) g' of { (zone, g'') -> (toNanos base zone, g'') } }

instance Random LocalDateTimePicos where
    random g =
      case randomR (43200, 253402257624999999999999) g  of { (base, g' ) ->
      case randomR (    0,                  maxzone) g' of { (zone, g'') -> (toPicos base zone, g'') } }
    randomR (a,b) g =
      case randomR (fromPicos        a, fromPicos        b) g  of { (base, g' ) ->
      case randomR (get ldt_pic_zone a, get ldt_pic_zone b) g' of { (zone, g'') -> (toPicos base zone, g'') } }

-- | Show a local date as a pretty string.
--
-- > >>> prettyLocalDate $ createLocalDate 2014 September 25 Japan_Standard_Time 
-- > "Thursday, September 25th, 2014 (JST)"
--
prettyLocalDate :: LocalDate -> String
prettyLocalDate date =
   printf "%s, %s %s, %04d (%s)" wday mon mday _dz_year abbr
   where DateZoneStruct{..} = toDateZoneStruct date
         wday = show _dz_wday
         mon  = show _dz_mon
         mday = show _dz_mday ++ showSuffix _dz_mday
         abbr = show $ TZ.abbreviate _dz_zone

-- | Show a local date and time as a pretty string.
--
-- > >>> getCurrentLocalDateTime Los_Angeles >>= return . prettyLocalDateTime 
-- > "2:17 AM, Wednesday, January 1st, 2014 (PST)"
--
prettyLocalDateTime :: DateTimeZone dtz => dtz -> String
prettyLocalDateTime time =
   printf str hour _dtz_min ampm wday mon mday _dtz_year abbr
   where DateTimeZoneStruct{..} = toDateTimeZoneStruct time
         str  = "%d:%02d %s, %s, %s %s, %04d (%s)"
         wday = show _dtz_wday
         mon  = show _dtz_mon
         mday = show _dtz_mday ++ showSuffix _dtz_mday
         abbr = show $ TZ.abbreviate _dtz_zone
         ampm = showPeriod _dtz_hour
         hour | _dtz_hour == 00 = 12
              | _dtz_hour <= 12 = _dtz_hour
              | otherwise       = _dtz_hour - 12

-- | Maximum enumerated time zone value.
maxzone :: Word8
maxzone = tz2w8 maxBound

-- | Convert an integral type into a time zone.
w82tz ::  Word8 -> TZ.TimeZone
w82tz = toEnum . fromIntegral

-- | Convert a time zone into a numeric value.
tz2w8 :: TZ.TimeZone -> Word8
tz2w8 = fromIntegral . fromEnum

-- | Perform a bounds check on the given local timestamp.
check :: forall a . Bounded a => Local a => Ord a => String -> a -> a
check f x =
   if minBound <= x && x <= maxBound then x
   else error $ f ++ ": base (" ++ base ++ ") out of bounds (" ++ bounds ++ ")"
   where base   = show (localBase x)
         bounds = show (localBase (minBound::a)) ++ "," ++ show (localBase (maxBound::a))

-- | Coerce a tuple of integral types.
doubleInt :: (Integral a, Integral b, Num c, Num d) => (a, b) -> (c, d)
doubleInt = fromIntegral *** fromIntegral
