{-# LANGUAGE TypeSynonymInstances #-}

-- |PortAudio is a cross platform audio library which supports many different operatings systems.
module Sound.PortAudio
(
    -- Enumerations
    ErrorCode(..),
    HostApiTypeId(..),
    SampleFormat(..),

    -- Data structures
    HostApiInfo(..),
    HostErrorInfo(..),
    DeviceInfo(..),
    StreamParameters(..),
    StreamInfo(..),

    -- Types
    PaFloat32, paFloat32,
    PaInt32, paInt32,
--  PaInt24, paInt24,
    PaInt16, paInt16,
    PaInt8, paInt8,
    PaUInt8, paUInt8,
    PaTime,
    PaStream,
    paNullPtr,
    HostApiIndex,
    DeviceIndex,

    -- Constants
    paNoDevice,
    paUseHostApiSpecificDeviceSpecification,

    {- Functions -}
    getVersion,
    getVersionText,

    withPortAudio,
    initialize,
    terminate,

    withStream,
    withDefaultStream,
    openDefaultStream,
    openStream,
    closeStream,
    startStream,
    stopStream,
    abortStream,
    readStream,
    writeStream,

    getStreamInfo,
    getStreamTime,
    getStreamCpuLoad,
    getStreamReadAvailable,
    getStreamWriteAvailable,

    getHostApiCount,
    getDefaultHostApi,
    getHostApiInfo,
    getDeviceCount,
    getDefaultInputDevice,
    getDefaultOutputDevice,
    getDeviceInfo,
    getSampleSize,

    hostApiTypeIdToHostApiIndex,
    hostApiDeviceIndexToDeviceIndex,

    isFormatSupported,
    isStreamStopped,
    isStreamActive,

    paSleep,

    chunk,

    standardSampleRates
)
where

{- 
 - TODO: Change DeviceIndex into a newtype so that the actual integer isn't exposed.
 - Hide the constructor and unwrap it behind the scenes.
 -}

import Sound.PortAudio.Base
import Sound.PortAudio.Helpers

import Foreign
import Control.Monad.Error


standardSampleRates :: [Double]
standardSampleRates = [8000,
                       9600,
                       11025,
                       12000,
                       16000,
                       22050,
                       24000,
                       32000,
                       44100,
                       48000,
                       88200,
                       96000,
                       192000]

-- | Put the caller to sleep for at least n milliseconds.
-- The function may sleep longer than requested so don't rely on this for accurate
-- musical timing.
paSleep :: Int -> IO ()
paSleep = paSleep_ffi

{- This set of stuff allows us to convert an
 - type into an integer. This is used when
 - creating streams.-}
class PortAudioFormat a where
    toFmt :: a -> SampleFormat

instance PortAudioFormat PaFloat32 where
    toFmt _ = PaFloat32
instance PortAudioFormat PaInt32 where
    toFmt _ = PaInt32
{- We don't want this until PaInt24 is not
 - a synonym for PaInt32
 -
 - instance PortAudioFormat PaInt24 where
 -    toFmt _ = PaInt24
-}
instance PortAudioFormat PaInt16 where
    toFmt _ = PaInt16
instance PortAudioFormat PaInt8 where
    toFmt _ = PaInt8
instance PortAudioFormat PaUInt8 where
    toFmt _ = PaUInt8

-- |This is a format specifier to be used with functions like
-- openDefaultStream. It specifies a 32 bit floating point representation.
paFloat32 :: PaFloat32
paFloat32 = undefined

-- |This is a format specifier to be used with functions like
-- openDefaultStream. It specifies a 32 bit integer representation.
paInt32 :: PaInt32
paInt32   = undefined

-- |This is a format specifier to be used with functions like
-- openDefaultStream. It specifies a 16 bit integer representation.
paInt16 :: PaInt16
paInt16   = undefined

-- |This is a format specifier to be used with functions like
-- openDefaultStream. It specifies a 8 bit integer representation.
paInt8 :: PaInt8
paInt8    = undefined

-- |This is a format specifier to be used with functions like
-- openDefaultStream. It specifies a 8 bit unsigned integer representation.
paUInt8 :: PaUInt8
paUInt8   = undefined

-- |Library initialization function. Call this before using 
-- PortAudio. The functions getVersion, getVersionText, and
-- getErrorText may be called before initialize is called.
initialize :: IO (Either String ErrorCode) -- ^ @(Right NoError)@ on success. @(Left err)@ on failure.
initialize = runErrorT initialize'

initialize' :: ErrorT String IO ErrorCode
initialize' = do code <- liftIO $ initialize_ffi
                 case (toEnum code) of
                      NoError -> return NoError
                      otherErr -> throwError $ show otherErr

-- |Library termination function. Call this after PortAudio
-- is no longer needed.
terminate :: IO (Either String ErrorCode) -- ^ @(Right NoError)@ on success. @(Left err)@ on failure.)
terminate = runErrorT terminate'

terminate' :: ErrorT String IO ErrorCode 
terminate' = do code <- liftIO $ terminate_ffi
                case (toEnum code) of
                     NoError -> return NoError
                     otherErr -> throwError $ show otherErr

-- |Perform a port audio action in the context
-- of the PortAudio library.
--
-- This initializes the library, performs the
-- supplied actions, and then terminates the
-- library. This is the reccomended way to use
-- the library.
-- withPortAudio :: IO a -> IO (Maybe a) -- ^ Returns @(Just a)@ on success or @Nothing@ on failure.
withPortAudio :: IO a -> IO (Either String a)
withPortAudio a = runErrorT $ withPortAudio' a

withPortAudio' :: IO a -> ErrorT String IO a
withPortAudio' a = do initCode <- initialize'
                      actRet <- liftIO a
                      termCode <- terminate'
                      return actRet

-- |A special DeviceIndex value indicating that no device
-- is available or should be used.
paNoDevice :: DeviceIndex
paNoDevice = (-1)

-- |A special DeviceIndex value indicating that the device(s)
-- to be used are specified in the host api specific stream
-- info structure.
paUseHostApiSpecificDeviceSpecification :: DeviceIndex
paUseHostApiSpecificDeviceSpecification = (-2)

-- |Retrieve the number of available host APIs. Even if a host API
-- is available it may have no devices available.
getHostApiCount :: IO (Either String Int)-- ^ @(Left err)@ on failure, @(Right int)@ on success.
getHostApiCount = runErrorT getHostApiCount'

getHostApiCount' :: ErrorT String IO Int
getHostApiCount' = do c <- liftIO getHostApiCount_ffi
                      if (c < 0)
                         then throwError $ show (toEnum c :: ErrorCode)
                         else return c
    
-- |Returns the index of the default host API.
getDefaultHostApi :: IO (Either String Int)
getDefaultHostApi = runErrorT getDefaultHostApi'

getDefaultHostApi' :: ErrorT String IO Int
getDefaultHostApi' = do c <- liftIO getDefaultHostApi_ffi
                        if (c < 0)
                           then throwError $ show (toEnum c :: ErrorCode)
                           else return c


-- |Gets a structure containing information about a specific host API. FINISH
getHostApiInfo :: HostApiIndex -> IO (Maybe HostApiInfo)
getHostApiInfo = getHostApiInfo_ffi

-- |Convert a static host API uniqe identifier to a runtime host API index. FINISH
hostApiTypeIdToHostApiIndex :: HostApiTypeId -> IO HostApiIndex
hostApiTypeIdToHostApiIndex = hostApiTypeIdToHostApiIndex_ffi

-- |Convert a host-API-specific device index to a standard PortAudio device index. FINISH
hostApiDeviceIndexToDeviceIndex :: HostApiIndex -> Int -> IO DeviceIndex
hostApiDeviceIndexToDeviceIndex = hostApiDeviceIndexToDeviceIndex_ffi

-- |Retrieve the number of available devices or @Nothing@ if there are none.
getDeviceCount :: IO (Either ErrorCode DeviceIndex)
getDeviceCount = do c <- getDeviceCount_ffi
                    return $ if (c < 0)
                                then (Left (toEnum c :: ErrorCode))
                                else (Right c)

-- |Retrieve the index of the default input device or @Nothing@ if there are none.
getDefaultInputDevice :: IO (Maybe DeviceIndex)
getDefaultInputDevice = do d <- getDefaultInputDevice_ffi
                           return $ case d of
                                       (-1) -> Nothing -- See paNoDevice
                                       dev  -> Just dev

-- |Retrieve the index of the default output device or @Nothing@ if there are none.
getDefaultOutputDevice :: IO (Maybe DeviceIndex)
getDefaultOutputDevice = do d <- getDefaultOutputDevice_ffi
                            return $ case d of
                                       (-1) -> Nothing -- See paNoDevice
                                       dev  -> Just dev


-- |Retrieve a DeviceInfo structure containing information about the specified device.
getDeviceInfo :: DeviceIndex -> IO (Maybe DeviceInfo)
getDeviceInfo = getDeviceInfo_ffi


-- |Determines whether it is possible to open a stream with the specified parameters.
isFormatSupported :: (Maybe StreamParameters) -- ^ Input Parameters
                  -> (Maybe StreamParameters) -- ^ Output Parameters
                  -> Double                   -- ^ Sample Rate
                  -> IO (Either String Int) -- ^ @(Right 0)@ on supported format, @(Left err)@ otherwise.
isFormatSupported s1 s2 sr = runErrorT $ isFormatSupported' s1 s2 sr

isFormatSupported' :: (Maybe StreamParameters) -> (Maybe StreamParameters) -> Double -> ErrorT String IO Int
isFormatSupported' s1 s2 sr = do s <- liftIO $ isFormatSupported_ffi s1 s2 sr
                                 if (0 == s)
                                    then return 0
                                    else throwError $ show (toEnum s :: ErrorCode)


-- |Open a stream for input, output, or both.
openStream :: Maybe StreamParameters -- ^ Input Parameters
           -> Maybe StreamParameters -- ^ Output Parameters
           -> Double                 -- ^ Sample Rate
           -> Int                    -- ^ Frames Per Buffer
           -> IO (Either String (PaStream a)) -- ^ @(Right PaStream)@ on success, @(Left err)@ on failure.
openStream isp osp sr fpb = runErrorT $ openStream' isp osp sr fpb

openStream' :: Maybe StreamParameters
            -> Maybe StreamParameters
            -> Double
            -> Int
            -> ErrorT String IO (PaStream a)
openStream' isp osp sr fpb =
     do (ec,s) <- liftIO $ openStream_ffi isp osp sr fpb 0 nullPtr nullPtr
        let errCode = toEnum ec
        case errCode of
             NoError -> return s
             err -> throwError $ show err

-- | A Simplified version of openStream which opens the default input and\/or output device(s).
openDefaultStream :: (PortAudioFormat a)
                  => Int          -- ^ Number of input channels
                  -> Int          -- ^ Number of output channels
                  -> a            -- ^ Sample Format. Should only use one of: paUInt8, paInt8, paInt16, paInt32, or paFloat32.
                  -> Double       -- ^ Sample Rate
                  -> Int          -- ^ Frames Per Buffer
                  -> IO (Either String (PaStream a)) -- ^ @(Right PaStream)@ on success, @(Left err)@ on failure.
openDefaultStream nic noc sf sr fpb = runErrorT $ openDefaultStream' nic noc sf sr fpb

openDefaultStream' :: (PortAudioFormat a) => Int -> Int -> a -> Double -> Int
                   -> ErrorT String IO (PaStream a)
openDefaultStream' nic noc sf sr fpb = do
     (ec,s) <- liftIO $ openDefaultStream_ffi nic noc (toFmt sf) sr fpb nullPtr nullPtr
     let errCode = toEnum ec
     case errCode of
          NoError -> return s
          err -> throwError $ show err

-- Generic Stream -> Error wrapper
streamToError :: (PaStream a -> IO Int) -> PaStream a -> ErrorT String IO ErrorCode
streamToError a s = do r <- liftIO $ a s
                       return (toEnum r)

-- | Close a PortAudio stream. If the audio streem is active,
-- any pending buffers are discarded as if abortStream had been called.
closeStream :: PaStream a -> IO (Either String ErrorCode)
closeStream s = runErrorT $ closeStream' s

closeStream' :: PaStream a -> ErrorT String IO ErrorCode
closeStream' = streamToError closeStream_ffi

{- Skipping PaStreamFinishedCallback -}    
{- Skipping Pa_SetStreamFinishedCallback -}    

-- | Commences audio processing.
startStream :: PaStream a -> IO (Either String ErrorCode)
startStream s = runErrorT $ startStream' s

startStream' :: PaStream a -> ErrorT String IO ErrorCode
startStream' = streamToError startStream_ffi

-- | Terminates audio processing. It blocks until all pending
-- audio buffers have been played.
stopStream :: PaStream a -> IO (Either String ErrorCode)
stopStream s = runErrorT $ stopStream' s

stopStream' :: PaStream a -> ErrorT String IO ErrorCode
stopStream' = streamToError stopStream_ffi

-- | Terminates audio processing immediately without waiting for 
-- pending buffers to complete.
abortStream :: PaStream a -> IO (Either String ErrorCode)
abortStream s = runErrorT $ abortStream' s

abortStream' :: PaStream a -> ErrorT String IO ErrorCode
abortStream' = streamToError abortStream_ffi

withDefaultStream :: (PortAudioFormat a)
                  => Int
                  -> Int
                  -> a
                  -> Double
                  -> Int
                  -> (PaStream a -> (Int, Int, Double, Int) -> IO b)
                  -> IO (Either String b)
withDefaultStream nic noc sf sr fpb a = runErrorT $ withDefaultStream' nic noc sf sr fpb a

withDefaultStream' :: (PortAudioFormat a) 
                   => Int
                   -> Int
                   -> a
                   -> Double
                   -> Int
                   -> (PaStream a -> (Int, Int, Double, Int) -> IO b)
                   -> ErrorT String IO b
withDefaultStream' nic noc sf sr fpb a = do
    ds <- openDefaultStream' nic noc sf sr fpb
    start <- startStream' ds
    r  <- liftIO $ a ds (nic, noc, sr, fpb)
    stop <- stopStream' ds
    cs <- closeStream' ds
    return r

-- | Perform an action with a stream.
withStream :: (PaStream a -> IO a) -> PaStream a -> IO (Either String a)
withStream f s = runErrorT $ withStream' f s

withStream' :: (PaStream a -> IO a) -> PaStream a -> ErrorT String IO a 
withStream' a s = do start <- startStream' s
                     r <- liftIO $ a s
                     stop  <- stopStream' s
                     return r

-- | Determines whether the stream is stopped.
isStreamStopped :: PaStream a -> IO (Either String Bool) -- ^ (Right bool) on success, (Left err) on failure.
isStreamStopped s = do r <- isStreamStopped_ffi s
                       return $ case r of
                                     0 -> (Right False)
                                     1 -> (Right True)
                                     e -> fail $ show (toEnum e :: ErrorCode)

-- | Determines whether the stream is active.
isStreamActive :: PaStream a -> IO (Either String Bool) -- ^ (Right bool) on success, (Left err) on failure.
isStreamActive s = do r <- isStreamActive_ffi s
                      return $ case r of
                                    0 -> (Right False)
                                    1 -> (Right True)
                                    e -> fail $ show (toEnum e :: ErrorCode)


-- | Retrieve a StreamInfo containing information about the specified stream.
getStreamInfo :: PaStream a -> IO (Maybe StreamInfo)
getStreamInfo = getStreamInfo_ffi

-- | Determine the current time for the stream according to the sample clock used to generate the buffer timestamps.
getStreamTime :: PaStream a -> IO (Maybe PaTime)
getStreamTime s = do r <- getStreamTime_ffi s
                     return $ case r of
                                0.0 -> Nothing
                                t -> (Just t)
                  

-- | Retrieve CPU usage information (value between 0.0 and 1.0) for the specified stream.
--
-- Note: A usage level of 0.0 is potentially an error (no specific error condition is defined by PortAudio).
getStreamCpuLoad :: PaStream a -> IO Double
getStreamCpuLoad s = getStreamCpuLoad_ffi s

-- So this one sucks because 0.0 is both a valid CPU usage value *or* an error.
-- We'll sprinkle some pixie dust here and assume 0.0 means HAPPY! all the time.
                        
-- | 32 bit floating point representation.
type PaFloat32 = Float

-- | 32 bit integer representation.
type PaInt32 = Int32

-- | 24 bit integer representation (NOT SUPPORTED YET)
type PaInt24 = Int32

-- | 16 bit integer representation
type PaInt16 = Int16

-- | 8 bit integer representation
type PaInt8 = Int8

-- | 8 bit unsigned integer representation
type PaUInt8 = Word8


{- BEGIN READERS AND WRITERS -}
-- Warning: these are scary, pointer wielding functions which are not to be trifled with.


-- | Read a sample from an input stream.
readStream :: (Storable a)
           => PaStream a             -- ^ The input stream
           -> Int                    -- ^ The number of channels
           -> Int                    -- ^ The number of frames
           -> IO (Either String [[a]])
readStream s c f = runErrorT $ readStream' s c f

readStream' :: (Storable a) => PaStream a -> Int -> Int -> ErrorT String IO [[a]]
readStream' str chns frms = do
         let len = frms * chns
             withPtr act ptr = do err <- act ptr
                                  case (toEnum err) of
                                       NoError -> peekArray len ptr >>= return . Right . chunk chns
                                       err -> return (Left err)
         r <- liftIO $ allocaArray len (withPtr (\ptr -> readStream_ffi str (castPtr ptr) len))
 
         case r of
              (Left err) -> throwError (show err)
              (Right v) -> return v
-- Thanks a bunch to paczesiowa in #haskell.


writeStream :: (Storable a) => PaStream a -- ^ The output stream
            -> [[a]]                    -- ^ The samples to be played
            -> Int                      -- ^ Number of frames
            -> IO (Either String ErrorCode) -- ^ The return status of the write
writeStream s l f = runErrorT $ writeStream' s l f


writeStream' :: (Storable a) => PaStream a -> [[a]] -> Int -> ErrorT String IO ErrorCode
writeStream' str frames numFrames = do
         r <- liftIO $ (withArray . concat) frames (\ptr -> writeStream_ffi str (castPtr ptr) (length frames))
         return (toEnum r)

{- END READERS AND WRITERS -}

-- | Get the number of frames that can be read from the stream without blocking.
getStreamReadAvailable :: PaStream a -> IO (Either String Int)
getStreamReadAvailable s = do r <- getStreamReadAvailable_ffi s
                              return $ if (r >= 0) then (Right r)
                                                   else fail $ show $ (toEnum r :: ErrorCode)

-- | Get the number of frames which can be written to the stream without blocking.
getStreamWriteAvailable :: PaStream a -> IO (Either String Int)
getStreamWriteAvailable s = do r <- getStreamWriteAvailable_ffi s
                               return $ if (r >= 0) then (Right r)
                                                    else fail $ show $ (toEnum r :: ErrorCode)

-- | Retrieve the size of a given sample format in bytes.
getSampleSize :: SampleFormat -> IO (Either String Int)
getSampleSize f = do r <- getSampleSize_ffi f
                     return $ if (r < 0) then fail $ show (toEnum r :: ErrorCode)
                                         else (Right r)