-- |
-- Module     : Data.Time.Exts.Base
-- Copyright  : 2013-2017 Enzo Haussecker
-- License    : BSD3
-- Maintainer : Enzo Haussecker <enzo@sovereign.io>
-- Stability  : Stable
--
-- Basic definitions, including type classes, data types and type families.

{-# LANGUAGE DataKinds                  #-}
{-# LANGUAGE DeriveDataTypeable         #-}
{-# LANGUAGE DeriveGeneric              #-}
{-# LANGUAGE FlexibleContexts           #-}
{-# LANGUAGE FlexibleInstances          #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE KindSignatures             #-}
{-# LANGUAGE MultiParamTypeClasses      #-}
{-# LANGUAGE RecordWildCards            #-}
{-# LANGUAGE StandaloneDeriving         #-}
{-# LANGUAGE TypeFamilies               #-}
{-# LANGUAGE UndecidableInstances       #-}

module Data.Time.Exts.Base (

  -- * Classes
       Human(..)
     , Math(..)

  -- * Chronologies
     , Calendar(..)
     , Epoch(..)
     , Era

  -- * Components
     , Year(..)
     , Month(..)
     , Day(..)
     , DayOfWeek(..)
     , Hour(..)
     , Minute(..)
     , Second(..)
     , Millis(..)
     , Micros(..)
     , Nanos(..)
     , Picos(..)

  -- * Structs
     , DateStruct(..)
     , TimeStruct(..)
     , DateTimeStruct(..)
     , LocalDateStruct(..)
     , LocalTimeStruct(..)
     , LocalDateTimeStruct(..)

  -- * Fractions
     , properFracMillis
     , properFracMicros
     , properFracNanos
     , properFracPicos

     ) where

import Control.DeepSeq (NFData(..))
import Data.Data       (Data, Typeable)
import Data.Int        (Int32, Int64)
import Data.Time       (TimeZone)
import GHC.Generics    (Generic)
import Text.Printf     (PrintfArg)

class Human x where

   -- |
   -- Define the human-readable components of a timestamp.
   type Components x :: *

   -- |
   -- Pack a timestamp from human-readable components.
   pack :: Components x -> x

   -- |
   -- Unpack a timestamp to human-readable components.
   unpack :: x -> Components x

class Math x c where

   -- |
   -- Calculate the duration between two timestamps.
   duration :: x -> x -> c

   -- |
   -- Add a duration to a timestamp.
   plus :: x -> c -> x

-- |
-- System for organizing dates.
data Calendar =
     Chinese
   | Gregorian
   | Hebrew
   | Islamic
   | Julian
   deriving (Bounded, Data, Enum, Eq, Generic, Ord, Read, Show, Typeable)

instance NFData Calendar

-- |
-- System origin.
data Epoch =
     Unix
   deriving (Bounded, Data, Enum, Eq, Generic, Ord, Read, Show, Typeable)

instance NFData Epoch

-- |
-- System for numbering years.
data family Era (cal :: Calendar) :: *

data instance Era 'Gregorian =
     BeforeChrist
   | AnnoDomini
   deriving (Bounded, Data, Enum, Eq, Generic, Ord, Read, Show, Typeable)

instance NFData (Era 'Gregorian)

-- |
-- Year.
newtype Year = Year {getYear :: Int32}
   deriving (Bounded, Data, Enum, Eq, Generic, Integral, NFData, Num, Ord, PrintfArg, Read, Real, Typeable)

instance Show Year where
   show Year {..} = show getYear

-- |
-- Month.
data family Month (cal :: Calendar) :: *

data instance Month 'Gregorian =
     January
   | February
   | March
   | April
   | May
   | June
   | July
   | August
   | September
   | October
   | November
   | December
   deriving (Bounded, Data, Eq, Generic, Ord, Read, Show, Typeable)

instance Enum (Month 'Gregorian) where

   fromEnum January   = 01
   fromEnum February  = 02
   fromEnum March     = 03
   fromEnum April     = 04
   fromEnum May       = 05
   fromEnum June      = 06
   fromEnum July      = 07
   fromEnum August    = 08
   fromEnum September = 09
   fromEnum October   = 10
   fromEnum November  = 11
   fromEnum December  = 12

   toEnum 01 = January
   toEnum 02 = February
   toEnum 03 = March
   toEnum 04 = April
   toEnum 05 = May
   toEnum 06 = June
   toEnum 07 = July
   toEnum 08 = August
   toEnum 09 = September
   toEnum 10 = October
   toEnum 11 = November
   toEnum 12 = December
   toEnum __ = error "toEnum{Month 'Gregorian}: out of range"

instance NFData (Month 'Gregorian)

-- |
-- Day.
newtype Day = Day {getDay :: Int32}
   deriving (Bounded, Data, Enum, Eq, Generic, Integral, NFData, Num, Ord, PrintfArg, Read, Real, Typeable)

instance Show Day where
   show Day {..} = show getDay

-- |
-- Day of week.
data family DayOfWeek (cal :: Calendar) :: *

data instance DayOfWeek 'Gregorian =
     Sunday
   | Monday
   | Tuesday
   | Wednesday
   | Thursday
   | Friday
   | Saturday
   deriving (Bounded, Data, Eq, Generic, Ord, Read, Show, Typeable)

instance Enum (DayOfWeek 'Gregorian) where

   fromEnum Sunday    = 1
   fromEnum Monday    = 2
   fromEnum Tuesday   = 3
   fromEnum Wednesday = 4
   fromEnum Thursday  = 5
   fromEnum Friday    = 6
   fromEnum Saturday  = 7

   toEnum 1 = Sunday
   toEnum 2 = Monday
   toEnum 3 = Tuesday
   toEnum 4 = Wednesday
   toEnum 5 = Thursday
   toEnum 6 = Friday
   toEnum 7 = Saturday
   toEnum _ = error "toEnum{DayOfWeek 'Gregorian}: out of range"

instance NFData (DayOfWeek 'Gregorian)

-- |
-- Hour.
newtype Hour = Hour {getHour :: Int64}
   deriving (Bounded, Data, Enum, Eq, Generic, Integral, NFData, Num, Ord, PrintfArg, Read, Real, Typeable)

instance Show Hour where
   show Hour {..} = show getHour

-- |
-- Minute.
newtype Minute = Minute {getMinute :: Int64}
   deriving (Bounded, Data, Enum, Eq, Generic, Integral, NFData, Num, Ord, PrintfArg, Read, Real, Typeable)

instance Show Minute where
   show Minute {..} = show getMinute

-- |
-- Second.
newtype Second = Second {getSecond :: Int64}
   deriving (Bounded, Data, Enum, Eq, Generic, Integral, NFData, Num, Ord, PrintfArg, Read, Real, Typeable)

instance Show Second where
   show Second {..} = show getSecond

-- |
-- Millisecond.
newtype Millis = Millis {getMillis :: Int64}
   deriving (Bounded, Data, Enum, Eq, Generic, Integral, NFData, Num, Ord, PrintfArg, Read, Real, Typeable)

instance Show Millis where
   show Millis {..} = show getMillis

-- |
-- Microsecond.
newtype Micros = Micros {getMicros :: Int64}
   deriving (Bounded, Data, Enum, Eq, Generic, Integral, NFData, Num, Ord, PrintfArg, Read, Real, Typeable)

instance Show Micros where
   show Micros {..} = show getMicros

-- |
-- Nanosecond.
newtype Nanos = Nanos {getNanos :: Int64}
   deriving (Bounded, Data, Enum, Eq, Generic, Integral, NFData, Num, Ord, PrintfArg, Read, Real, Typeable)

instance Show Nanos where
   show Nanos {..} = show getNanos

-- |
-- Picosecond.
newtype Picos = Picos {getPicos :: Int64}
   deriving (Bounded, Data, Enum, Eq, Generic, Integral, NFData, Num, Ord, PrintfArg, Read, Real, Typeable)

instance Show Picos where
   show Picos {..} = show getPicos

-- |
-- A struct with date components.
data DateStruct (cal :: Calendar) =
     DateStruct
     { _d_year :: {-# UNPACK #-} !Year
     , _d_mon  ::                !(Month cal)
     , _d_mday :: {-# UNPACK #-} !Day
     , _d_wday ::                !(DayOfWeek cal)
     } deriving (Generic, Typeable)

-- |
-- A struct with time components.
data TimeStruct =
     TimeStruct
     { _t_hour :: {-# UNPACK #-} !Hour
     , _t_min  :: {-# UNPACK #-} !Minute
     , _t_sec  :: {-# UNPACK #-} !Double
     } deriving (Data, Eq, Generic, Show, Typeable)

-- |
-- A struct with date and time components.
data DateTimeStruct (cal :: Calendar) =
     DateTimeStruct
     { _dt_year :: {-# UNPACK #-} !Year
     , _dt_mon  ::                !(Month cal)
     , _dt_mday :: {-# UNPACK #-} !Day
     , _dt_wday ::                !(DayOfWeek cal)
     , _dt_hour :: {-# UNPACK #-} !Hour
     , _dt_min  :: {-# UNPACK #-} !Minute
     , _dt_sec  :: {-# UNPACK #-} !Double
     } deriving (Generic, Typeable)

-- |
-- A struct with date and time zone components.
data LocalDateStruct (cal :: Calendar) =
     LocalDateStruct
     { _ld_year :: {-# UNPACK #-} !Year
     , _ld_mon  ::                !(Month cal)
     , _ld_mday :: {-# UNPACK #-} !Day
     , _ld_wday ::                !(DayOfWeek cal)
     , _ld_zone :: {-# UNPACK #-} !TimeZone
     } deriving (Generic, Typeable)

-- |
-- A struct with time and time zone components.
data LocalTimeStruct =
     LocalTimeStruct
     { _lt_hour :: {-# UNPACK #-} !Hour
     , _lt_min  :: {-# UNPACK #-} !Minute
     , _lt_sec  :: {-# UNPACK #-} !Double
     , _lt_zone :: {-# UNPACK #-} !TimeZone
     } deriving (Data, Eq, Generic, Show, Typeable)

-- |
-- A struct with date, time, and time zone components.
data LocalDateTimeStruct (cal :: Calendar) =
     LocalDateTimeStruct
     { _ldt_year :: {-# UNPACK #-} !Year
     , _ldt_mon  ::                !(Month cal)
     , _ldt_mday :: {-# UNPACK #-} !Day
     , _ldt_wday ::                !(DayOfWeek cal)
     , _ldt_hour :: {-# UNPACK #-} !Hour
     , _ldt_min  :: {-# UNPACK #-} !Minute
     , _ldt_sec  :: {-# UNPACK #-} !Double
     , _ldt_zone :: {-# UNPACK #-} !TimeZone
     } deriving (Generic, Typeable)

deriving instance (Data (Month cal), Data (DayOfWeek cal), Typeable cal) => Data (DateStruct cal)
deriving instance (Data (Month cal), Data (DayOfWeek cal), Typeable cal) => Data (DateTimeStruct cal)
deriving instance (Data (Month cal), Data (DayOfWeek cal), Typeable cal) => Data (LocalDateStruct cal)
deriving instance (Data (Month cal), Data (DayOfWeek cal), Typeable cal) => Data (LocalDateTimeStruct cal)

deriving instance (Eq (Month cal), Eq (DayOfWeek cal)) => Eq (DateStruct cal)
deriving instance (Eq (Month cal), Eq (DayOfWeek cal)) => Eq (DateTimeStruct cal)
deriving instance (Eq (Month cal), Eq (DayOfWeek cal)) => Eq (LocalDateStruct cal)
deriving instance (Eq (Month cal), Eq (DayOfWeek cal)) => Eq (LocalDateTimeStruct cal)

deriving instance (Show (Month cal), Show (DayOfWeek cal)) => Show (DateStruct cal)
deriving instance (Show (Month cal), Show (DayOfWeek cal)) => Show (DateTimeStruct cal)
deriving instance (Show (Month cal), Show (DayOfWeek cal)) => Show (LocalDateStruct cal)
deriving instance (Show (Month cal), Show (DayOfWeek cal)) => Show (LocalDateTimeStruct cal)

instance (NFData (Month cal), NFData (DayOfWeek cal)) => NFData (DateStruct cal)
instance (NFData (Month cal), NFData (DayOfWeek cal)) => NFData (DateTimeStruct cal)
instance (NFData (Month cal), NFData (DayOfWeek cal)) => NFData (LocalDateStruct cal)
instance (NFData (Month cal), NFData (DayOfWeek cal)) => NFData (LocalDateTimeStruct cal)

instance NFData TimeStruct
instance NFData LocalTimeStruct

-- |
-- Decompose a floating point number into second and millisecond components.
properFracMillis :: RealFrac a => a -> (Second, Millis)
{-# SPECIALISE properFracMillis :: Double -> (Second, Millis) #-}
properFracMillis frac = if millis == 1000 then (sec + 1, 0) else res
   where res@(sec, millis) = fmap (round . (*) 1000) $ properFraction frac

-- |
-- Decompose a floating point number into second and microsecond components.
properFracMicros :: RealFrac a => a -> (Second, Micros)
{-# SPECIALISE properFracMicros :: Double -> (Second, Micros) #-}
properFracMicros frac = if micros == 1000000 then (sec + 1, 0) else res
   where res@(sec, micros) = fmap (round . (*) 1000000) $ properFraction frac

-- |
-- Decompose a floating point number into second and nanosecond components.
properFracNanos :: RealFrac a => a -> (Second, Nanos)
{-# SPECIALISE properFracNanos :: Double -> (Second, Nanos) #-}
properFracNanos frac = if nanos == 1000000000 then (sec + 1, 0) else res
   where res@(sec, nanos) = fmap (round . (*) 1000000000) $ properFraction frac

-- |
-- Decompose a floating point number into second and picosecond components.
properFracPicos :: RealFrac a => a -> (Second, Picos)
{-# SPECIALISE properFracPicos :: Double -> (Second, Picos) #-}
properFracPicos frac = if picos == 1000000000000 then (sec + 1, 0) else res
   where res@(sec, picos) = fmap (round . (*) 1000000000000) $ properFraction frac