{-# LANGUAGE DeriveDataTypeable #-}
{-# LANGUAGE ExistentialQuantification #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE TypeFamilies #-}
{-# OPTIONS -Wall #-}
----------------------------------------------------------------------
-- |
-- Module      : Data.ZoomCache.Types
-- Copyright   : Conrad Parker
-- License     : BSD3-style (see LICENSE)
--
-- Maintainer  : Conrad Parker <conrad@metadecks.org>
-- Stability   : unstable
-- Portability : unknown
--
-- ZoomCache packet and summary types and interfaces
----------------------------------------------------------------------

module Data.ZoomCache.Types (
    -- * Track types and specification
      Codec(..)
    , TrackMap
    , TrackSpec(..)
    , IdentifyCodec

    -- * Classes
    , Timestampable(..)
    , before

    , UTCTimestampable(..)
    , beforeUTC

    , ZoomReadable(..)
    , ZoomWritable(..)

    , ZoomRaw(..)

    , ZoomSummary(..)
    , ZoomSummaryUTC(..)
    , ZoomSummarySO(..)

    , ZoomWork(..)

    -- * Types
    , Packet(..)
    , packetFromPacketSO
    , Summary(..)
    , summaryFromSummarySO

    , PacketUTC(..)
    , packetUTCFromPacket
    , packetUTCFromPacketSO

    , SummaryUTC(..)
    , summaryUTCFromSummary
    , summaryUTCFromSummarySO

    , PacketSO(..)
    , SummarySO(..)

    , summarySODuration

    -- * CacheFile
    , CacheFile(..)
    , mkCacheFile
    , fiFull

) where

import Blaze.ByteString.Builder
import Data.ByteString (ByteString)
import Data.Dynamic
import Data.IntMap (IntMap)
import qualified Data.IntMap as IM
import Data.Iteratee (Iteratee)
import Data.Maybe (fromJust)
import Data.Time (UTCTime)
import System.Posix.Types (FileOffset)

import Data.Offset
import Data.ZoomCache.Common

------------------------------------------------------------
-- | A map of all track numbers to their 'TrackSpec'
type TrackMap = IntMap TrackSpec

-- | A specification of the type and name of each track
data TrackSpec = TrackSpec
    { specType         :: !Codec
    , specDeltaEncode  :: !Bool
    , specZlibCompress :: !Bool
    , specSRType       :: !SampleRateType
    , specRate         :: {-# UNPACK #-}!Rational
    , specName         :: !ByteString
    }
    deriving (Show)

data Codec = forall a . ZoomReadable a => Codec a

instance Show Codec where
    show = const "<<Codec>>"

-- | Identify the tracktype corresponding to a given Codec Identifier.
-- When parsing a zoom-cache file, the zoom-cache library will try each
-- of a given list ['IdentifyTrack'].
--
-- The standard zoom-cache instances are provided in 'standardIdentifiers'.
--
-- When developing your own codecs it is not necessary to build a composite
-- 'IdentifyTrack' functions; it is sufficient to generate one for each new
-- codec type. A library of related zoom-cache codecs should export its own
-- ['IdentifyTrack'] functions, usually called something like mylibIdentifiers.
--
-- These can be generated with 'identifyCodec'.
type IdentifyCodec = ByteString -> Maybe Codec

------------------------------------------------------------

-- | Global and track headers for a zoom-cache file
data CacheFile = CacheFile
    { cfGlobal :: Global
    , cfSpecs  :: IntMap TrackSpec
    , cfOffsets :: IntMap FileOffset
    }

-- | Create an empty 'CacheFile' using the given 'Global'
mkCacheFile :: Global -> CacheFile
mkCacheFile g = CacheFile g IM.empty IM.empty

-- | Determine whether all tracks of a 'CacheFile' are specified
fiFull :: CacheFile -> Bool
fiFull CacheFile{..} = IM.size cfSpecs == noTracks cfGlobal

------------------------------------------------------------

class Timestampable a where
    timestamp :: a -> Maybe TimeStamp

before :: (Timestampable a) => Maybe TimeStamp -> a -> Bool
before Nothing _ = True
before (Just b) x = t == Nothing || (fromJust t) < b
  where
    t = timestamp x

instance Timestampable (TimeStamp, a) where
    timestamp = Just . fst

instance Timestampable a => Timestampable [a] where
    timestamp []    = Nothing
    timestamp (x:_) = timestamp x

instance Timestampable a => Timestampable (Offset a) where
    timestamp (Offset _ xs) = timestamp xs

------------------------------------------------------------

class UTCTimestampable a where
    utcTimestamp :: a -> Maybe UTCTime

beforeUTC :: (UTCTimestampable a) => Maybe UTCTime -> a -> Bool
beforeUTC Nothing _ = True
beforeUTC (Just b) x = t == Nothing || (fromJust t) < b
  where
    t = utcTimestamp x

instance UTCTimestampable (UTCTime, a) where
    utcTimestamp = Just . fst

instance UTCTimestampable a => UTCTimestampable [a] where
    utcTimestamp []    = Nothing
    utcTimestamp (x:_) = utcTimestamp x

instance UTCTimestampable a => UTCTimestampable (Offset a) where
    utcTimestamp (Offset _ xs) = utcTimestamp xs

------------------------------------------------------------

data PacketSO = PacketSO
    { packetSOTrack         :: {-# UNPACK #-}!TrackNo
    , packetSOEntry         :: {-# UNPACK #-}!SampleOffset
    , packetSOExit          :: {-# UNPACK #-}!SampleOffset
    , packetSOCount         :: {-# UNPACK #-}!Int
    , packetSOData          :: !ZoomRaw
    , packetSOSampleOffsets :: ![SampleOffset]
    }

data Packet = Packet
    { packetTrack      :: {-# UNPACK #-}!TrackNo
    , packetEntry      :: {-# UNPACK #-}!TimeStamp
    , packetExit       :: {-# UNPACK #-}!TimeStamp
    , packetCount      :: {-# UNPACK #-}!Int
    , packetData       :: !ZoomRaw
    , packetTimeStamps :: ![TimeStamp]
    }

packetFromPacketSO :: Rational -> PacketSO -> Packet
packetFromPacketSO r PacketSO{..} = Packet {
      packetTrack = packetSOTrack
    , packetEntry = timeStampFromSO r packetSOEntry
    , packetExit  = timeStampFromSO r packetSOExit
    , packetCount = packetSOCount
    , packetData  = packetSOData
    , packetTimeStamps = map (timeStampFromSO r) packetSOSampleOffsets
    }

instance Timestampable Packet where
    timestamp = Just . packetEntry

data PacketUTC = PacketUTC
    { packetUTCTrack      :: {-# UNPACK #-}!TrackNo
    , packetUTCEntry      :: {-# UNPACK #-}!UTCTime
    , packetUTCExit       :: {-# UNPACK #-}!UTCTime
    , packetUTCCount      :: {-# UNPACK #-}!Int
    , packetUTCData       :: !ZoomRaw
    , packetUTCTimeStamps :: ![UTCTime]
    }

packetUTCFromPacket :: UTCTime -> Packet -> PacketUTC
packetUTCFromPacket base Packet{..} = PacketUTC {
      packetUTCTrack = packetTrack
    , packetUTCEntry = utcTimeFromTimeStamp base packetEntry
    , packetUTCExit  = utcTimeFromTimeStamp base packetExit
    , packetUTCCount = packetCount
    , packetUTCData  = packetData
    , packetUTCTimeStamps = map (utcTimeFromTimeStamp base) packetTimeStamps
    }

packetUTCFromPacketSO :: UTCTime -> Rational -> PacketSO -> PacketUTC
packetUTCFromPacketSO base r = packetUTCFromPacket base . packetFromPacketSO r

instance UTCTimestampable PacketUTC where
    utcTimestamp = Just . packetUTCEntry

------------------------------------------------------------
-- | A recorded block of summary data
data SummarySO a = SummarySO
    { summarySOTrack :: {-# UNPACK #-}!TrackNo
    , summarySOLevel :: {-# UNPACK #-}!Int
    , summarySOEntry :: {-# UNPACK #-}!SampleOffset
    , summarySOExit  :: {-# UNPACK #-}!SampleOffset
    , summarySOData  :: !(SummaryData a)
    }
    deriving (Typeable)

-- | The duration covered by a summary, in units of 1 / the track's datarate
summarySODuration :: SummarySO a -> SampleOffsetDiff
summarySODuration s = SODiff $ (unSO $ summarySOExit s) - (unSO $ summarySOEntry s)


-- | A summary block with samplecounts converted to TimeStamp
data Summary a = Summary
    { summaryTrack :: {-# UNPACK #-}!TrackNo
    , summaryLevel :: {-# UNPACK #-}!Int
    , summaryEntry :: {-# UNPACK #-}!TimeStamp
    , summaryExit  :: {-# UNPACK #-}!TimeStamp
    , summaryData  :: !(SummaryData a)
    }
    deriving (Typeable)

-- | Convert a SummarySo to a Summary, given a samplerate
summaryFromSummarySO :: Rational -> SummarySO a -> Summary a
summaryFromSummarySO r SummarySO{..} = Summary {
      summaryTrack = summarySOTrack
    , summaryLevel = summarySOLevel
    , summaryEntry = timeStampFromSO r summarySOEntry
    , summaryExit  = timeStampFromSO r summarySOExit
    , summaryData  = summarySOData
    } 

instance Timestampable (Summary a) where
    timestamp = Just . summaryEntry

-- | A summary block with timestamps converted to UTCTime
data SummaryUTC a = SummaryUTC
    { summaryUTCTrack :: {-# UNPACK #-}!TrackNo
    , summaryUTCLevel :: {-# UNPACK #-}!Int
    , summaryUTCEntry :: {-# UNPACK #-}!UTCTime
    , summaryUTCExit  :: {-# UNPACK #-}!UTCTime
    , summaryUTCData  :: !(SummaryData a)
    }
    deriving (Typeable)

-- | Convert a Summary to a SummaryUTC, given a UTC base time
summaryUTCFromSummary :: UTCTime -> Summary a -> SummaryUTC a
summaryUTCFromSummary base Summary{..} = SummaryUTC {
      summaryUTCTrack = summaryTrack
    , summaryUTCLevel = summaryLevel
    , summaryUTCEntry = utcTimeFromTimeStamp base summaryEntry
    , summaryUTCExit  = utcTimeFromTimeStamp base summaryExit
    , summaryUTCData  = summaryData
    }

summaryUTCFromSummarySO :: UTCTime -> Rational -> SummarySO a -> SummaryUTC a
summaryUTCFromSummarySO base r = summaryUTCFromSummary base . summaryFromSummarySO r

instance UTCTimestampable (SummaryUTC a) where
    utcTimestamp = Just . summaryUTCEntry

------------------------------------------------------------
-- Read

-- | A codec instance must specify a 'SummaryData' type,
-- and implement all methods of this class.
class Typeable a => ZoomReadable a where
    -- | Summaries of a subsequence of values of type 'a'. In the default
    -- instances for 'Int' and 'Double', this is a record containing values
    -- such as the maximum, minimum and mean of the subsequence.
    data SummaryData a :: *

    -- | The track identifier used for streams of type 'a'.
    -- The /value/ of the argument should be ignored by any instance of
    -- 'ZoomReadable', so that is safe to pass 'undefined' as the
    -- argument.
    trackIdentifier :: a -> ByteString

    -- | An iteratee to read one value of type 'a' from a stream of 'ByteString'.
    readRaw         :: (Functor m, Monad m)
                    => Iteratee ByteString m a

    -- | An iteratee to read one value of type 'SummaryData a' from a stream
    -- of 'ByteString'.
    readSummary        :: (Functor m, Monad m)
                       => Iteratee ByteString m (SummaryData a)

    -- | Pretty printing, used for dumping values of type 'a'.
    prettyRaw          :: a -> String

    -- | Pretty printing for values of type 'SummaryData a'.
    prettySummaryData  :: SummaryData a -> String

    -- | Delta-decode a list of values
    deltaDecodeRaw :: [a] -> [a]
    deltaDecodeRaw = id

data ZoomRaw = forall a . ZoomReadable a => ZoomRaw [a]

data ZoomSummarySO = forall a . ZoomReadable a => ZoomSummarySO (SummarySO a)

data ZoomSummary = forall a . ZoomReadable a => ZoomSummary (Summary a)

instance Timestampable ZoomSummary where
    timestamp (ZoomSummary s) = timestamp s

data ZoomSummaryUTC = forall a . ZoomReadable a => ZoomSummaryUTC (SummaryUTC a)

instance UTCTimestampable ZoomSummaryUTC where
    utcTimestamp (ZoomSummaryUTC s) = utcTimestamp s

------------------------------------------------------------
-- Write

-- | A codec instance must additionally specify a 'SummaryWork' type
class ZoomReadable a => ZoomWritable a where
    -- | Intermediate calculations
    data SummaryWork a :: *

    -- | Serialize a value of type 'a'
    fromRaw            :: a -> Builder

    -- | Serialize a 'SummaryData a'
    fromSummaryData    :: SummaryData a -> Builder

    -- | Generate a new 'SummaryWork a', given an initial timestamp.
    initSummaryWork    :: SampleOffset -> SummaryWork a

    -- | Update a 'SummaryData' with the value of 'a' occuring at the
    -- given 'SampleOffset'.
    updateSummaryData  :: SampleOffset -> a
                       -> SummaryWork a
                       -> SummaryWork a

    -- | Finalize a 'SummaryWork a', generating a 'SummaryData a'.
    toSummaryData      :: SampleOffsetDiff -> SummaryWork a -> SummaryData a

    -- | Append two 'SummaryData'
    appendSummaryData  :: SampleOffsetDiff -> SummaryData a
                       -> SampleOffsetDiff -> SummaryData a
                       -> SummaryData a

    -- | Delta-encode a value.
    deltaEncodeRaw :: SummaryWork a -> a -> a
    deltaEncodeRaw _ = id

data ZoomWork = forall a . (Typeable a, ZoomWritable a) => ZoomWork
    { levels   :: IntMap (SummarySO a)
    , currWork :: Maybe (SummaryWork a)
    }