--
-- Module      : Skeleton
-- Copyright   : (c) Conrad Parker 2006
-- License     : BSD-style
-- Maintainer  : conradp@cse.unsw.edu.au
-- Stability   : experimental
-- Portability : portable

module Codec.Container.Ogg.Skeleton (
  OggFishead (..),
  OggFisbone (..),
  emptyFishead,
  pageToFishead,
  packetToFishead,
  pageToFisbone,
  packetToFisbone,
  fisheadToPage,
  fisheadToPacket,
  fisboneToPage,
  fisboneToPacket,
  trackToFisbone,
  tracksToFisbones
) where

import qualified Data.ByteString.Lazy as L
import qualified Data.ByteString.Lazy.Char8 as C
import Data.List as List
import Data.Maybe
import Data.Word (Word32,Word64)
import Data.Ratio

import Codec.Container.Ogg.ByteFields
import Codec.Container.Ogg.ContentType
import Codec.Container.Ogg.Granulepos
import Codec.Container.Ogg.Granulerate
import Codec.Container.Ogg.MessageHeaders
import Codec.Container.Ogg.Packet
import Codec.Container.Ogg.Page
import Codec.Container.Ogg.Serial
import Codec.Container.Ogg.Timestamp
import Codec.Container.Ogg.Track

------------------------------------------------------------
-- Data
--

data OggFishead =
  OggFishead {
    fisheadPrestime :: Timestamp,
    fisheadBasetime :: Timestamp
  }

data OggFisbone =
  OggFisbone {
    fisboneSerialno :: Serial,
    fisboneNHeaders :: Int,
    fisboneGranulerate :: Granulerate,
    fisboneStartgranule :: Word64,
    fisbonePreroll :: Word32,
    fisboneGranuleshift :: Int,
    fisboneMsgHeaders :: MessageHeaders
  }

------------------------------------------------------------
-- OggSkeleton constants
--

-- fisheadIdent = 'fishead\0'
fisheadIdent :: L.ByteString
fisheadIdent = L.pack [0x66, 0x69, 0x73, 0x68, 0x65, 0x61, 0x64, 0x00]

-- fisboneIdent = 'fisbone\0'
fisboneIdent :: L.ByteString
fisboneIdent = L.pack [0x66, 0x69, 0x73, 0x62, 0x6f, 0x6e, 0x65, 0x00]

-- Skeleton major version generated by this module
vMajor :: Int
vMajor = 3

-- Skeleton minor version generated by this module
vMinor :: Int
vMinor = 0 

-- Offset to message header fields generated by this module
fisboneMHOffset :: Int
fisboneMHOffset = 44

-- Padding after granuleshift, before message headers
fisbonePadding :: L.ByteString
fisbonePadding = L.concat $ List.map u8Fill [z, z, z]

-- Helpers
z :: Int
z = 0

emptyFishead :: OggFishead
emptyFishead = OggFishead zeroTimestamp zeroTimestamp

------------------------------------------------------------
-- pageToFishead, pageToFisbone
--

pageToFishead :: OggPage -> Maybe OggFishead
pageToFishead g = case (pagesToPackets [g]) of
  [] -> Nothing
  p:_ -> packetToFishead p

pageToFisbone :: OggPage -> Maybe OggFisbone
pageToFisbone g = case (pagesToPackets [g]) of
  [] -> Nothing
  p:_ -> packetToFisbone p

------------------------------------------------------------
-- packetToFishead, bsToFishead
--

packetToFishead :: OggPacket -> Maybe OggFishead
packetToFishead (OggPacket d t _ bos _ _) = case bos of
  False -> Nothing
  True -> case (contentTypeIs skeleton t) of
    True  -> Just (bsToFishead d)
    False -> Nothing

bsToFishead :: L.ByteString -> OggFishead
bsToFishead d = OggFishead pt bt
  where
    pt = t 12 20
    bt = t 28 36

    -- | Read a timestamp encoded as a (le64,le64) rational, where a
    -- denominator of 0 is interepreted as the result being 0.
    t o1 o2 = case od of
      0 -> Timestamp (0, 1)
      _ -> Timestamp (on, od)
      where
        on = le64At o1 d
        od = le64At o2 d

------------------------------------------------------------
-- packetToFisbone, bsToFisbone
--

packetToFisbone :: OggPacket -> Maybe OggFisbone
packetToFisbone (OggPacket d t _ _ _ _) =
  case (contentTypeIs skeleton t) of
    True  -> Just (bsToFisbone d)
    False -> Nothing

bsToFisbone :: L.ByteString -> OggFisbone
bsToFisbone d = OggFisbone serial nh gr startg pr gshift mh
  where
    serial = le32At 12 d
    nh = le32At 16 d
    gr = fracRate (le64At 20 d) (le64At 28 d)
    startg = le64At 36 d
    pr = le32At 44 d
    gshift = u8At 48 d
    mh = read $ C.unpack (L.drop 52 d)

------------------------------------------------------------
-- fisheadToPage
--

fisheadToPage :: OggTrack -> OggFishead -> OggPage
fisheadToPage t f = head $ packetsToPages [fisheadToPacket t f]

------------------------------------------------------------
-- fisheadToPacket
--

fisheadToPacket :: OggTrack -> OggFishead -> OggPacket
fisheadToPacket t f = up{packetBOS = True}
  where
    up = uncutPacket d t gp
    d = fisheadWrite f
    gp = Granulepos (Just 0)

fisheadWrite :: OggFishead -> L.ByteString
fisheadWrite (OggFishead p b) = newFisheadData
  where
    newFisheadData = L.concat [hData, pData, bData, uData]
    hData = L.concat [fisheadIdent, le16Fill vMajor, le16Fill vMinor]
    pData = timestampFill p
    bData = timestampFill b
    uData = L.concat $ List.map le32Fill $ take 5 $ repeat z

timestampFill :: Timestamp -> L.ByteString
timestampFill (Timestamp (n,d)) = L.concat $ List.map le64Fill [n, d]

------------------------------------------------------------
-- fisboneToPage
--

fisboneToPage :: OggTrack -> OggFisbone -> OggPage
fisboneToPage t f = head $ packetsToPages [fisboneToPacket t f]

------------------------------------------------------------
-- fisboneToPacket
--

fisboneToPacket :: OggTrack -> OggFisbone -> OggPacket
fisboneToPacket t f = uncutPacket d t gp
  where
    d = fisboneWrite f
    gp = Granulepos (Just 0)

fisboneWrite :: OggFisbone -> L.ByteString
fisboneWrite (OggFisbone s n (Granulerate gr) sg pr gs mhdrs) = newFisboneData
  where
    newFisboneData = L.concat [hData, fData, tData]
    hData = L.concat [fisboneIdent, le32Fill fisboneMHOffset]
    fData = L.concat [sD, nD, grD, sgD, prD, gsD]
    tData = L.concat [fisbonePadding, mhdrsD]

    sD = le32Fill s
    nD = le32Fill n
    grD = L.concat $ List.map le64Fill [numerator gr, denominator gr]
    sgD = le64Fill sg
    prD = le32Fill pr
    gsD = u8Fill gs

    mhdrsD = C.pack $ show mhdrs

------------------------------------------------------------
-- trackToFisbone
--

-- | Create a list of OggFisbones from a list of OggTracks, not including
-- | any OggTracks with unknown ContentType or Granulerate
tracksToFisbones :: [OggTrack] -> [OggFisbone]
tracksToFisbones ts = Data.Maybe.mapMaybe trackToFisbone ts

-- | Create an OggFisbone from a given OggTrack
trackToFisbone :: OggTrack -> Maybe OggFisbone
trackToFisbone (OggTrack serialno (Just ctype) nheaders (Just gr) gs mdata) =
  Just (OggFisbone serialno nheaders gr startgranule pr gsi mhdrs)
  where
    pr = fromIntegral $ preroll ctype
    startgranule = 0
    gsi = maybe 0 id gs -- A Granuleshift of None is represented by 0
    -- The first given content-type is the default to use in skeleton
    mhdrs = mhInsert "Content-Type" (head $ mime ctype) mdata

-- If the pattern match failed, ie. any of the Maybe values were Nothing,
-- then we can't produce a valid Fisbone for this
trackToFisbone _ = Nothing

------------------------------------------------------------
-- Custom Instances
--

instance ContentTypeImplied OggPage where
  contentTypeImplies = pageImplies

pageImplies :: [OggTrack] -> ContentType -> OggPage -> Bool
pageImplies tracks t g = case (pagesToPackets [g]) of
  []  -> False
  p:_ -> contentTypeImplies tracks t p

instance ContentTypeImplied OggPacket where
  contentTypeImplies = packetImplies

packetImplies :: [OggTrack] -> ContentType -> OggPacket -> Bool
packetImplies tracks t p = case (contentTypeIs skeleton p) of
    False -> contentTypeIs t p
    True  -> case (packetBOS p, packetEOS p) of
      (True, _) -> True
      (_, True) -> True
      _         -> case (packetToFisbone p) of
                     Nothing -> False
                     Just fb -> fbPacketImplies tracks t fb

fbPacketImplies :: [OggTrack] -> ContentType -> OggFisbone -> Bool
fbPacketImplies tracks t fb = case matchTracks of
    []       -> False
    sTrack:_ -> contentTypeIs t sTrack
  where
    matchTracks = filter (\x -> trackSerialno x == skelSerial) tracks
    skelSerial = fisboneSerialno fb