module Codec.Audio.Vorbis (readAudioInfoAndComment, AudioInfo(..)) where

import qualified Codec.Binary.UTF8.String as UTF8
--import Control.Monad (when)
import Control.Monad.Error -- For Either monad instance
--import qualified Data.ByteString.Lazy as B
import qualified Data.ByteString as B
--import qualified Data.ByteString.Lazy.Char8 as Char8
import qualified Data.ByteString.Char8 as Char8
import Data.Binary.Strict.Get
import Data.Char (toLower)
import Data.Word

data AudioInfo = AudioInfo {channels :: Word8, sampleRate :: Word32, maxBitrate :: Word32,
                            nominalBitrate :: Word32, minBitrate :: Word32,
                            playtime :: Maybe Double}
  deriving Show

readAudioInfoAndComment :: FilePath -> IO (Either String (AudioInfo, [(String, String)]))
readAudioInfoAndComment fname = do
  content <- B.readFile fname
  return $ case readVorbisAudioInfo content of
             Left e -> Left e
             Right audioInfo -> case readVorbisCommentFromOgg content of
                                  Left e -> Left e
                                  Right comment -> Right (audioInfo, comment)

readVorbisCommentFromOgg :: Char8.ByteString -> Either String [(String, String)]
readVorbisCommentFromOgg fileContent = case runGet readVorbisCommentOggPage fileContent of
    (Left e, _) -> Left e
    (Right p2, _) ->
      -- XXX: This won't work right if packet is spanned across multiple pages...
      case runGet (readVorbisComment False) commentData of
        (Left e, _) -> Left e
        (Right comment, _) -> Right comment
      where commentData = B.drop 7 $ packetData p2 -- 'drop 7' strips off "\x03vorbis"

readVorbisCommentOggPage = do
  p1 <- readOggPage
  p2 <- readOggPage
  when (not $ isVorbisPage p1) $ fail "First page is not a vorbis page"
  when (serialNum p1 /= serialNum p2) $ fail "First two pages do not have same serial number"
  --when (not $ isComplete p2) $ fail "Second vorbis page is not a complete packet"
  return p2

{-
readVorbisCommentFromOgg :: Get [(String, String)]
readVorbisCommentFromOgg = do
  p1 <- readOggPage
  p2 <- readOggPage
  when (not $ isVorbisPage p1) $ fail "First page is not a vorbis page"
  when (serialNum p1 /= serialNum p2) $ fail "First two pages do not have same serial number"
  when (not $ isComplete p2) $ fail "Second vorbis page is not a complete packet"
  -- XXX: This won't work right if packet is spanned across multiple pages...
  let commentData = B.drop 7 $ packetData p2 -- 'drop 7' strips off "\x03vorbis"
  let (comment, _) = runGet (readVorbisComment False) commentData
  return comment
-}



{-
readVorbisCommentFromOgg = do
  allPages <- readOggPages
  -- XXX: This could be wrong, if the first page is not a "\x01vorbis" one...
  let serial = serialNum $ head allPages
  let relevantPages = filter (\p -> serialNum p == serial) allPages
  let secondVorbisPage = head $ tail relevantPages
  -- XXX: This won't work right if packet is spanned across multiple pages...
  let commentData = B.drop 7 $ packetData secondVorbisPage -- 'drop 7' strips off "\x03vorbis"
  return $ runGet (readVorbisComment False) commentData
-}

readVorbisComment framingXXX = do
  vendorLength <- getWord32le
  vendor <- getAByteString $ fromIntegral vendorLength -- XXX: Need to decode from utf-8
  numItems <- getWord32le
  readEntries numItems
  --if framing and not ord(fileobj.read(1)) & 0x01:
  --   raise VorbisUnsetFrameError("framing bit was unset")
  where
    readEntries 0 = return []
    readEntries numItems = do
      len <- getWord32le
      rawStr <- getAByteString $ fromIntegral len -- XXX: Need to decode from utf-8
      let ustr = UTF8.decode $ B.unpack rawStr
      restOfEntries <- readEntries (numItems - 1)
      return $ (downcase $ takeWhile (/= '=') ustr, tail $ dropWhile (/= '=') ustr) : restOfEntries
      -- XXX: tag, value = string.split('=', 1)
      -- XXX: tag = tag.encode('ascii', errors)
    downcase = map toLower

--readVorbisAudioInfo :: Get (Word8, Word32, Word32, Word32, Word32)
readVorbisAudioInfo :: Char8.ByteString -> Either String AudioInfo
readVorbisAudioInfo fileContent = do
  firstPage <- readOggPageOrBust fileContent "Failed to parse first Ogg page"
  let reversed = B.reverse fileContent
  let oggs = Char8.pack "OggS"
  let lastPageData = B.concat [oggs, B.reverse $ fst $ B.breakSubstring (B.reverse oggs) reversed]
  {- lastPageData <- case findSubstring (B.reverse $ Char8.pack "OggS") reversed of
                    Nothing -> Left "Could not find any Ogg pages in file"
                    --Just pos -> Left $ (show $ snd $ B.breakSubstring (B.reverse $ Char8.pack "OggS") reversed) 
                    Just pos -> Right $ B.reverse $ B.take (B.length fileContent - pos) reversed -}
  lastPage <- readOggPageOrBust lastPageData "Failed to parse last Ogg page"
  let (maybeInfo, _) = runGet (readFromVorbisPacket (granulePosition lastPage)) (packetData firstPage)
  maybeInfo

  --pages <- readOggPages
  --let vorbisPages = filter isVorbisPage pages
  --when (length vorbisPages < 1) $ fail "No Vorbis pages found in file"
  --when (length vorbisPages > 1) $ fail "More than one Vorbis page found!"
  
  --vorbisPage <- readOggPage
  --return $ case runGet readFromVorbisPacket (packetData vorbisPage) of
  --  (Left e, _) -> e
  --  (Right audioInfo, _) -> audioInfo

  where
    readFromVorbisPacket totalNumSamples = do
      skip 11
      channels <- getWord8
      sampleRate <- getWord32le
      maxBitrate <- getWord32le
      nominalBitrate <- getWord32le
      minBitrate <- getWord32le
      let playtime = (toDouble totalNumSamples) / (toDouble sampleRate) :: Double
      return $ AudioInfo channels sampleRate maxBitrate nominalBitrate minBitrate (Just playtime)
    toDouble :: Integral a => a -> Double
    toDouble = fromInteger . toInteger

readOggPageOrBust :: Char8.ByteString -> String -> Either String OggPage
readOggPageOrBust content err = case maybePage of
                                  Left e -> Left $ err ++ ": " ++ e
                                  Right p -> Right p
  where (maybePage, _) = runGet readOggPage content

{- findSubstring s l =
    if B.null s then Just 0
              else case B.breakSubstring s l of
                     (x,y) | B.null y  -> Nothing
                           | otherwise -> Just (B.length x) -}

--serialNum (_, _, _, _, sn, _, _, _, _) = sn
--packetData (_, _, _, _, _, _, _, _, d) = d

{- readOggVorbisPages = do
  p <- readOggPage
  if isVorbisPage p
    then do
      otherPages <- readPagesWithSerialNum $ serialNum p
      return $ p : otherPages
    else fail "first page wasn't a vorbis page"
      --readOggVorbisPages
  where
    readPagesWithSerialNum sn = do
      p <- readOggPage
      if serialNum p == sn
        then do
          others <- readPagesWithSerialNum sn
          return $ p : others
        else return [] -}

isVorbisPage page = B.take 7 (packetData page) == Char8.pack "\x01vorbis"

{- readOggPages = do
  p <- readOggPage
  rest <- if lastPage p then (return []) else readOggPages
  return $ p : rest
  where lastPage (_, _, flags, _, _, _, _, _, _) = flags == 4 -}

data OggPage = OggPage { version :: Word8, typeFlags :: Word8, granulePosition :: Word64,
                         serialNum :: Word32, pageNum :: Word32, checkSum :: Word32,
                         segmentsCount :: Word8, packetData :: B.ByteString
                         {- isComplete :: Bool -} }

--readOggPage :: Get (ByteString, Word8, xxx)
readOggPage = do
  oggs <- getAByteString 4
  version <- getWord8
  typeFlags <- getWord8
  granulePosition <- getWord64le
  serialNum <- getWord32le
  pageNum <- getWord32le
  checkSum <- getWord32le
  segmentsCount <- getWord8
  lacingBytes <- getAByteString $ fromIntegral $ toInteger segmentsCount
  when (B.length lacingBytes /= (fromIntegral $ toInteger segmentsCount)) $
    fail "Unable to read sufficient \"lacing\" bytes"
  --let isCompletePacket = B.last lacingBytes < 255
  packetData <- getAByteString $ fromIntegral $ byteSum lacingBytes
  return $ OggPage version typeFlags granulePosition serialNum pageNum checkSum segmentsCount
                   packetData {-(isCompletePacket)-}

byteSum :: B.ByteString -> Integer
byteSum bs = B.foldl (\total byte -> total + (toInteger byte)) 0 bs

getAByteString = getByteString