{-# LANGUAGE ForeignFunctionInterface #-}
{-# LANGUAGE MultiParamTypeClasses #-}

module Sound.File.Sndfile.Buffer
(
    MBuffer(..),
    checkSampleBounds, checkFrameBounds,
    hReadSamples, hReadFrames,
    interact,
    IOFunc,
    sf_read_double, sf_readf_double,
    sf_write_double, sf_writef_double,
    sf_read_float, sf_readf_float,
    sf_write_float, sf_writef_float
) where

import Control.Monad (liftM, when)
import Data.Array.Base (unsafeRead, unsafeWrite)
import Data.Array.MArray (Ix, MArray, getBounds, newArray_)
import Data.Ix (rangeSize)
import Foreign.Ptr (Ptr)
import Foreign.C.Types (CLLong)
import Prelude hiding (interact)
import Sound.File.Sndfile.Exception (throw)
import Sound.File.Sndfile.Interface

checkSampleBounds :: (Monad m) => Count -> Int -> Count -> m ()
checkSampleBounds size channels count
    | (count `mod` channels) /= 0   = throw 0 ("invalid channel/count combination " ++ (show count))
    | (count < 0) || (count > size) = throw 0 ("index out of bounds")
    | otherwise = return ()

checkFrameBounds :: (Monad m) => Count -> Int -> Count -> m ()
checkFrameBounds size channels count
    | (size `mod` channels) /= 0                      = throw 0 ("invalid buffer size")
    | (count < 0) || (count > (size `quot` channels)) = throw 0 ("index out of bounds")
    | otherwise = return ()

type IOFunc a = HandlePtr -> Ptr a -> CLLong -> IO CLLong

foreign import ccall unsafe "sf_read_double"   sf_read_double   :: IOFunc Double
foreign import ccall unsafe "sf_readf_double"  sf_readf_double  :: IOFunc Double
foreign import ccall unsafe "sf_write_double"  sf_write_double  :: IOFunc Double
foreign import ccall unsafe "sf_writef_double" sf_writef_double :: IOFunc Double

foreign import ccall unsafe "sf_read_float"   sf_read_float   :: IOFunc Float
foreign import ccall unsafe "sf_readf_float"  sf_readf_float  :: IOFunc Float
foreign import ccall unsafe "sf_write_float"  sf_write_float  :: IOFunc Float
foreign import ccall unsafe "sf_writef_float" sf_writef_float :: IOFunc Float

-- |The class MBuffer is used for polymorphic I\/O on a 'Handle', and is
-- parameterized on the mutable array type, the element type and the monad
-- results are returned in.
--
-- It is important to note that the data type used by the calling program and
-- the data format of the file do not need to be the same. For instance, it is
-- possible to open a 16 bit PCM encoded WAV file and read the data in
-- floating point format. The library seamlessly converts between the two
-- formats on-the-fly; the Haskell interface only supports reading and writing
-- 'Double' or 'Float' values.
--
-- When converting between integer data and floating point data, the following
-- rules apply: The default behaviour when reading floating point data
-- ('hGetSamples' or 'hGetFrames') from a file with integer data is
-- normalisation. Regardless of whether data in the file is 8, 16, 24 or 32
-- bit wide, the data will be read as floating point data in the range
-- [-1.0, 1.0]. Similarly, data in the range [-1.0, 1.0] will be written to an
-- integer PCM file so that a data value of 1.0 will be the largest allowable
-- integer for the given bit width. This normalisation can be turned on or off
-- using the command interface [TODO: implementation missing in Haskell].
--
-- 'hGetSamples' and 'hGetFrames' return the number of items read. Unless the
-- end of the file was reached during the read, the return value should equal
-- the number of items requested. Attempts to read beyond the end of the file
-- will not result in an error but will cause the read functions to return
-- less than the number of items requested or 0 if already at the end of the
-- file.
class (MArray a e m) => MBuffer a e m where
    -- |Fill the destination array with the requested number of items. The 'count'
    -- parameter must be an integer product of the number of channels or an error
    -- will occur.
    hGetSamples :: Handle -> a Index e -> Count -> m Count
    -- |Fill the destination array with the requested number of frames of data.
    -- The array must be large enough to hold the product of frames and the number
    -- of channels or an error will occur.
    hGetFrames  :: Handle -> a Index e -> Count -> m Count
    -- |Write 'count' samples from the source array to the stream. The 'count'
    -- parameter must be an integer product of the number of channels or an error
    -- will occur.
    --
    -- 'hPutSamples' returns the number of items written (which should be the same
    -- as the 'count' parameter).
    hPutSamples :: Handle -> a Index e -> Count -> m Count
    -- |Write 'count' frames from the source array to the stream.
    -- The array must be large enough to hold the product of frames and the number
    -- of channels or an error will occur.
    --
    -- 'hPutFrames' returns the number of frames written (which should be the same
    -- as the 'count' parameter).
    hPutFrames  :: Handle -> a Index e -> Count -> m Count

-- TODO: Optimize unsafeWriteRange
unsafeWriteRange :: (MArray a e m) => a Int e -> (Int, Int) -> e -> m ()
unsafeWriteRange _ (i0, i) _ | i0 > i = return ()
unsafeWriteRange a (i0, i) e          = unsafeWrite a i0 e >> unsafeWriteRange a (i0+1,i) e

-- |Return an array with the requested number of items. The 'count' parameter
-- must be an integer product of the number of channels or an error will
-- occur.
hReadSamples :: (MBuffer a e m, Num e) => Handle -> Count -> m (Maybe (a Index e))
hReadSamples h n = do
    b  <- newArray_ (0, n-1)
    n' <- hGetSamples h b n
    if n' == 0
        then return Nothing
        else do
            when (n' < n) (unsafeWriteRange b (n',n-1) 0)
            return (Just b)

-- |Return an array with the requested number of frames of data.
-- The resulting array size is equal to the product of the number of frames
-- `n' and the number of channels in the soundfile.
hReadFrames :: (MBuffer a e m, Num e) => Handle -> Count -> m (Maybe (a Index e))
hReadFrames h n = do
    b  <- newArray_ (0, si)
    n' <- hGetFrames h b n
    if n' == 0
        then return Nothing
        else do
            when (n' < n) (unsafeWriteRange b (f2s n', si) 0)
            return (Just b)
    where
        f2s = (* channels (hInfo h))
        si  = (f2s n) - 1

modifyArray :: (MArray a e m, Ix i) => (e -> e) -> a i e -> Int -> Int -> m ()
modifyArray f a i n
    | i >= n = return ()
    | otherwise = do
        e <- unsafeRead a i
        unsafeWrite a i (f e)
        modifyArray f a (i+1) n

interact :: (MBuffer a e m) => (e -> e) -> a Index e -> Handle -> Handle -> m ()
interact f buffer hIn hOut = do
    s <- liftM rangeSize $ getBounds buffer
    n <- hGetSamples hIn buffer s
    when (n > 0) $ do
        modifyArray f buffer 0 n
        hPutSamples hOut buffer n
        interact f buffer hIn hOut

-- EOF