module Sound.MIDI.Writer.Basic where

import qualified Numeric.NonNegative.Wrapper as NonNeg

import qualified Sound.MIDI.Bit as Bit
import qualified Sound.MIDI.IO as MIO
import qualified Sound.MIDI.Monoid as M
import qualified Data.Monoid as Monoid

import Data.Bits ((.|.))
import Sound.MIDI.IO (listByteFromChar, )
import Data.Monoid (Monoid, mempty, mappend, mconcat, )
import Sound.MIDI.Monoid ((+#+), genAppend, genConcat, )

import Control.Monad.Trans.Reader (ReaderT, runReaderT, ask, )
import Control.Monad.Trans.Class (lift, )

import Data.List (genericLength, )
import Data.Word (Word8, )
import Data.Char (chr, )

import qualified Data.ByteString.Lazy as B
import qualified Data.Binary.Builder as Builder
import Data.Binary.Builder (Builder, fromLazyByteString, )

import Control.Exception (bracket, )
import qualified System.IO as IO
import System.IO (openBinaryFile, hClose, hPutChar, Handle, IOMode(WriteMode))
-- import System.IO.Error (ioError, userError)

import Prelude hiding (putStr, )



class Monoid m => C m where
   putByte :: Word8 -> m
   {- |
   @putLengthBlock n writeBody@
   write @n@ bytes indicating the number of bytes written by @writeBody@
   and then it runs @writeBody@.
   -}
   putLengthBlock :: Int -> m -> m


-- differences list
newtype ByteList =
   ByteList {unByteList :: Monoid.Endo MIO.ByteList}

instance Monoid ByteList where
   mempty = ByteList mempty
   mappend = genAppend ByteList unByteList
   mconcat = genConcat ByteList unByteList

instance C ByteList where
   putByte = ByteList . Monoid.Endo . (:)
   putLengthBlock n writeBody =
      let body = runByteList writeBody
      in  putInt n (length body) `mappend`
          putByteListSpec body  -- we could call 'writeBody' but this would recompute the data


-- | 'putByteList' specialised to 'ByteList'
putByteListSpec :: MIO.ByteList -> ByteList
putByteListSpec = ByteList . Monoid.Endo . (++)


runByteList :: ByteList -> MIO.ByteList
runByteList = flip Monoid.appEndo [] . unByteList




newtype ByteString =
   ByteString {unByteString :: Builder}

instance Monoid ByteString where
   mempty = ByteString $ mempty
   mappend = genAppend ByteString unByteString
   mconcat = genConcat ByteString unByteString

instance C ByteString where
   putByte = ByteString . Builder.singleton
   putLengthBlock n writeBody =
      let body = runByteString writeBody
          len = B.length body
          errLen =
             if len >= div (256^n) 2
               then error "Chunk too large"
               else fromIntegral len
      in  putInt n errLen +#+ ByteString (fromLazyByteString body)


runByteString :: ByteString -> B.ByteString
runByteString = Builder.toLazyByteString . unByteString




newtype SeekableFile =
   SeekableFile {unSeekableFile :: ReaderT Handle IO ()}

instance Monoid SeekableFile where
   mempty = SeekableFile $ return ()
   mappend x y = SeekableFile $ unSeekableFile x >> unSeekableFile y
   mconcat xs  = SeekableFile $ mapM_ unSeekableFile xs

instance C SeekableFile where
   putByte c =
      SeekableFile $
         ask >>= \h ->
         lift $ hPutChar h (chr $ fromIntegral c)
   putLengthBlock n writeBody =
      SeekableFile $
      ask >>= \h -> lift $
      do lenPos <- IO.hGetPosn h
         IO.hPutStr h (replicate n '\000')
         startPos <- IO.hTell h
         runSeekableHandle h writeBody
         stopPos <- IO.hTell h
         contPos <- IO.hGetPosn h
         IO.hSetPosn lenPos
         let len = stopPos - startPos
         if len >= 2^(31::Int)
           then ioError (userError ("chunk too large, size " ++ show len))
           else runSeekableHandle h (putInt n (fromInteger len))
         IO.hSetPosn contPos


runSeekableFile :: FilePath -> SeekableFile -> IO ()
runSeekableFile name w =
   bracket
      (openBinaryFile name WriteMode)
      hClose
      (flip runSeekableHandle w)

runSeekableHandle :: Handle -> SeekableFile -> IO ()
runSeekableHandle h w =
   runReaderT (unSeekableFile w) h




putInt :: C writer => Int -> Int -> writer
putInt a = putByteList . map fromIntegral . Bit.someBytes a

putStr :: C writer => String -> writer
putStr = putByteList . listByteFromChar

putIntAsByte :: C writer => Int -> writer
putIntAsByte x = putByte $ fromIntegral x

putByteList :: C writer => MIO.ByteList -> writer
putByteList = M.concatMap putByte

putLenByteList :: C writer => MIO.ByteList -> writer
putLenByteList bytes =
   putVar (genericLength bytes) +#+
   putByteList bytes

{- |
Numbers of variable size are represented by sequences of 7-bit blocks
tagged (in the top bit) with a bit indicating:
(1) that more data follows; or
(0) that this is the last block.
-}

putVar :: C writer => NonNeg.Integer -> writer
putVar n =
   let bytes = map fromIntegral $ Bit.toBase 128 n
   in  case bytes of
          [] -> putInt 1 0
          (_:bs) ->
             let highBits = map (const 128) bs ++ [0]
             in  putByteList (zipWith (.|.) highBits bytes)
