-- | Time and timing utilities.
module Data.ByteString.IsoBaseFileFormat.Util.Time
       (referenceTime, utcToMp4, mp4CurrentTime, durationFromSeconds,
        oneSecond32, oneSecond64, diffTimeToTicks,ticksToDiffTime,
        TimeScale(..), Timing, TS(..), type TS32, type TS64, Ticks(..), Ticks32(..))
       where

import Data.Time.Clock
import Data.Time.Calendar
import Data.Ratio
import Data.ByteString.IsoBaseFileFormat.Box
import Data.ByteString.IsoBaseFileFormat.Util.BoxFields
import Data.ByteString.IsoBaseFileFormat.Util.Versioned
import Data.ByteString.IsoBaseFileFormat.ReExports
import Data.Typeable
import Foreign.Storable

-- * Absolute Dates
-- | According to the standard, fields with absolute dates and times are in
-- seconds since 1904/01/01 at midnight (UTC). This is this reference time.
referenceTime :: UTCTime
referenceTime =
  let startDay = fromGregorian 1904 1 1
      startTime = 0
  in UTCTime startDay startTime

-- | Convert a 'UTCTime' to a number of seconds since 'referenceTime'.
utcToMp4 :: Num t
         => UTCTime -> t
utcToMp4 u =
  let picoSecondsDiff = toRational $ diffUTCTime u referenceTime
      picoSecondsDiffNumerator = numerator picoSecondsDiff
      picoSecondsDiffDenominator = denominator picoSecondsDiff
      secondsSinceReferenceTime =
        div picoSecondsDiffNumerator picoSecondsDiffDenominator
  in fromIntegral secondsSinceReferenceTime


-- | Convert a 'NominalDiffTime' to the number of 'Ticks' with respect
-- to a given 'TimeScale'.
diffTimeToTicks :: Integral t
                => NominalDiffTime -> TimeScale -> t
diffTimeToTicks !diff (TimeScale !scale) =
  round (diff * (fromIntegral scale))

-- | Convert a 'NominalDiffTime' to the number of 'Ticks' with respect
-- to a given 'TimeScale'.
ticksToDiffTime :: Integral t
                => t -> TimeScale -> NominalDiffTime
ticksToDiffTime !t (TimeScale !scale) =
  fromRational (toInteger t % toInteger scale)


-- | Get the current time as number of seconds since 'referenceTime'
mp4CurrentTime :: Num t
               => IO t
mp4CurrentTime = utcToMp4 <$> getCurrentTime

-- * Time-Scale and Durations
-- | Default time-scale value
--   Based on history and tradition this value is @90000@.
--   MPEG-2 TS defines a single clock for each program, running at 27MHz. The
--   timescale of MPEG-2 TS Hint Tracks should be divisable by 90000.
newtype TimeScale = TimeScale {fromTimeScale :: Word32}
  deriving (Show,Eq,Num,Bounded,Ord,Bits,Integral,Typeable,Storable,Enum,Real)

instance Default TimeScale where
  def = TimeScale 90000

instance IsBoxContent TimeScale where
  boxSize = boxSize . fromTimeScale
  boxBuilder = boxBuilder . fromTimeScale

-- | Utility function to convert seconds (Integers) to any 'Num' using a
-- 'TimeScale', Since 'Scalar' has a 'Num' instance this can be used to generate
-- @duration@ fields.
durationFromSeconds :: Num t
                    => TimeScale -> Integer -> t
durationFromSeconds timeScale seconds =
  let timeScaleI = fromIntegral timeScale
  in timeScaleI * fromInteger seconds

-- | Utility function to generate the equivalent of one second (@1 s@)
oneSecond32 = Scalar . flip durationFromSeconds 1
oneSecond32 :: TimeScale -> TS32 label

-- | Utility function to generate the equivalent of one second (@1 s@)
oneSecond64 :: TimeScale -> TS64 label
oneSecond64 = Scalar . flip durationFromSeconds 1

-- | A type that denotes a time relative to a 'TimeScale' which is included in
-- its type. 'Ticks' is the number of time units passed, where each time unit
-- has a physical duration of @timescale * 1/s@ i.e. @timescale@ 'Ticks' last
-- about @1 s@.
-- TODO use this instead of raw Word32s
newtype Ticks (timeScale :: Nat) = MkTicks {fromTicks :: Word64}
  deriving (Show,Eq,Num,Bounded,Ord,Bits,Integral,Typeable,Storable,Enum,Real)

instance IsBoxContent (Ticks n) where
  boxSize = boxSize . fromTicks
  boxBuilder = boxBuilder . fromTicks

newtype Ticks32 (timeScale :: Nat) = MkTicks32 {fromTicks32 :: Word32}
  deriving (Show,Eq,Num,Bounded,Ord,Bits,Integral,Typeable,Storable,Enum,Real)

instance IsBoxContent (Ticks32 n) where
  boxSize = boxSize . fromTicks32
  boxBuilder = boxBuilder . fromTicks32

-- | Time and timing information about a movie.
--
-- The creation/modification times are in seconds since midnight, Jan. 1, 1904,
-- in UTC time. Time scale declares the time coordinate system, it specifies the
-- number of time units that pass one second. The time coordinate system is used
-- by e.g. the duration field, which by the way contains the duration of the
-- longest track, if known, or simply the equivalent of 1s.
type Timing (version :: Nat) = Versioned TimingV0 TimingV1 version

type TS32 = Scalar Word32
type TS64 = Scalar Word64

data TS (version :: Nat) (label :: Symbol) where
  TSv0 :: !Word32 -> TS 0 label
  TSv1 :: !Word64 -> TS 1 label

instance IsBoxContent (TS v n) where
  boxSize (TSv0 _t) = 4
  boxSize (TSv1 _t) = 8
  boxBuilder (TSv0 !t) = word32BE t
  boxBuilder (TSv1 !t) = word64BE t

type TimingV0 = TimingImpl (Scalar Word32) (TS 0 "duration")

type TimingV1 = TimingImpl (Scalar Word64) (TS 1 "duration")

type TimingImpl uint dur =
     uint "creation_time"
  :+ uint "modification_time"
  :+ TimeScale
  :+ dur