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

module Codec.Container.Ogg.Chain (
  OggChain (..),
  chainScan,
  chainAddSkeleton
) where

import qualified Data.ByteString.Lazy as L
import Data.Maybe

import Codec.Container.Ogg.ContentType
import Codec.Container.Ogg.Granulepos
import Codec.Container.Ogg.Track
import Codec.Container.Ogg.Page
import Codec.Container.Ogg.Packet
import Codec.Container.Ogg.Serial
import Codec.Container.Ogg.Skeleton

-- | A section of a chained Ogg physical bitstream. This corresponds to
-- an entire song or video, and most Ogg files in the wild contain only
-- a single chain.
data OggChain =
  OggChain {
    chainTracks :: [OggTrack],
    chainPages :: [OggPage],
    chainPackets :: [OggPacket]
  }
  
-- | Parse a ByteString into a list of OggChains
chainScan :: L.ByteString -> [OggChain]
chainScan d
  | L.null d  = []
  | otherwise = chain : chainScan rest
  where chain = OggChain tracks pages packets
        (tracks, pages, rest) = pageScan d
        packets = pagesToPackets pages

------------------------------------------------------------
-- chainAddSkeleton
--

-- | Add a Skeleton logical bitstream to an OggChain
chainAddSkeleton :: OggChain -> IO OggChain
chainAddSkeleton chain = do
  serialno <- genSerial
  return $ chainAddSkeleton' serialno chain

-- | Add a Skeleton logical bitstream with a given serialno to an OggChain
chainAddSkeleton' :: Serial -> OggChain -> OggChain
chainAddSkeleton' serialno (OggChain tracks _ packets) = OggChain nt ng np
  where
    nt = skelTrack : tracks
    ng = packetsToPages np
    np = fh : concat [ixBoss, ixFisbones, ixHdrs, [sEOS], ixD]

    -- Construct a new track for the Skeleton
    skelTrack = (newTrack serialno){trackType = Just skeleton}

    -- Create the fishead and fisbone packets (all with pageIx 0)
    fh = fisheadToPacket skelTrack emptyFishead
    fbs = map (fisboneToPacket skelTrack) $ tracksToFisbones tracks

    -- Separate out the BOS pages of the input
    (boss, rest) = span packetBOS packets

    -- Increment the pageIx of these original BOS pages by 1, as the
    -- Skeleton fishead packet is being prepended
    ixBoss = map (incPageIx 1) boss

    -- Split the remainder of the input into headers and data
    (hdrs, d) = splitAt totHeaders rest

    -- ... for which we determine the total number of header pages
    totHeaders = sum $ map trackHeaders tracks

    -- Increment the pageIx of the original data packets by the number of
    -- Skeleton pages
    ixHdrs = map (incPageIx (1 + length fbs)) hdrs
    ixD = map (incPageIx (2 + length fbs)) d

    -- Set the pageIx of the fisbones in turn, beginning after the last
    -- BOS page
    ixFisbones = zipWith setPageIx [1+(length tracks)..] fbs

    -- Generate an EOS packet for the Skeleton track
    sEOS = (uncutPacket L.empty skelTrack sEOSgp){packetEOS = True}
    sEOSgp = Granulepos (Just 0)

-- An internal function for setting the pageIx of the segment of a packet.
-- This is only designed for working with packets which are known to only
-- and entirely span one page, such as Skeleton fisbones.
setPageIx :: Int -> OggPacket -> OggPacket
setPageIx ix p@(OggPacket _ _ _ _ _ (Just [oldSegment])) =
  p{packetSegments = Just [newSegment]}
  where
    newSegment = oldSegment{segmentPageIx = ix}
setPageIx _ _ = error "setPageIx used on non-uncut page"

-- An internal function for incrementing the pageIx of all the segments of
-- a packet.
incPageIx :: Int -> OggPacket -> OggPacket
incPageIx ixd p@(OggPacket _ _ _ _ _ (Just segments)) =
  p{packetSegments = Just (map incSegIx segments)}
  where
    incSegIx :: OggSegment -> OggSegment
    incSegIx s@(OggSegment _ oix _) = s{segmentPageIx = oix + ixd}
-- Otherwise, the packet has no segmentation info so leave it untouched
incPageIx _ p = p