    -----------------------------------------------------------------------------
    -- |
    -- Module      :  Sound.ALSA.Mixer
    -- Copyright   :  (c) Thomas Tuegel 2011
    -- License     :  BSD
    --
    -- Maintainer  :  Thomas Tuegel <ttuegel@gmail.com>
    -- Stability   :  experimental
    -- Portability :  non-portable (Linux only)
    --
    -- This library provides bindings to the Advanced Linux Sound Architecture
    -- (ALSA) library API. The portability of this library is limited to
    -- systems with ALSA (i.e., Linux systems). The functions in this library
    -- throw errors of type 'Sound.ALSA.Exception.T' on failure.
    --
    -----------------------------------------------------------------------------

    module Sound.ALSA.Mixer
        ( -- * Types
          Control(..)
        , Mixer()
        , Channel(..)
        , PerChannel(..)
        , Volume(..)
        , Switch()
        , CUInt
        , CLong
          -- * Functions
          -- ** Mixers
        , controls
        , withMixer
          -- ** Controls
        , getControlByName
        , common
        , playback
        , capture
          -- ** PerChannels
        , channels
        , allChannels
        , joined
        , perChannel
        , getChannel
        , setChannel
          -- * Examples

          -- ** Getting and setting the volume of a Control
          -- $exampleVolume

          -- ** Getting and setting the switch of a Control
          -- $exampleSwitch
        ) where

    import Control.Monad ( forM, liftM, when )
    import Data.Maybe ( catMaybes )
    import Foreign.C.Error ( Errno(..) )
    import Foreign.C.Types
    import Sound.ALSA.Exception ( catchErrno )
    import Sound.ALSA.Mixer.Internal

    -- | 'Control' represents one of the controls belonging to an ALSA mixer
    -- element. Each control has a number of playback and capture channels.
    -- The control may also have a switch and/or a volume capability associated
    -- with it. The capability can be common to both playback and capture, or
    -- there can be separate capabilities for each.
    data Control = Control { index :: CUInt
                           , name :: String
                           , switch :: Either Switch (Maybe Switch, Maybe Switch)
                           , volume :: Either Volume (Maybe Volume, Maybe Volume)
                           }

    -- | 'PerChannel' represents a capability that with either a separate value for
    -- each channel or with a common value for all channels.
    data PerChannel e = Joined { getJoined :: IO e
                               , setJoined :: e -> IO ()
                               , joinedChannels :: [Channel]
                               }
                      | PerChannel { getPerChannel :: IO [(Channel, e)]
                                   , setPerChannel :: [(Channel, e)] -> IO ()
                                   , perChannels :: [Channel]
                                   }

    -- | True if the 'PerChannel' object has a common value for all channels.
    joined :: PerChannel e -> Bool
    joined j@(Joined _ _ _) = True
    joined _ = False

    -- | True if the 'PerChannel' object has a separate value for each channel.
    perChannel :: PerChannel e -> Bool
    perChannel p@(PerChannel _ _ _) = True
    perChannel _ = False

    -- | All channels supported by a 'PerChannel' object.
    channels :: PerChannel e -> [Channel]
    channels p | joined p = joinedChannels p
               | otherwise = perChannels p

    -- | 'Switch' represents a switch capability for controls and channels that can
    -- be muted and unmuted.
    type Switch = PerChannel Bool

    -- | 'Volume' represents a volume capability. There may be a separate value per
    -- channel, but each capability has only one range.
    data Volume = Volume { getRange :: IO (CLong, CLong)
                           -- ^ Returns the minimum and maximum volumes (unitless).
                         , setRange :: (CLong, CLong) -> IO ()
                           -- ^ Sets the minimum and maximum volumes (unitless).
                         , getRangeDb :: IO (CLong, CLong)
                           -- ^ Returns the minimum and maximum volumes in
                           -- hundredths of a decibel.
                         , value :: PerChannel CLong
                           -- ^ Volume values for each channel.
                         , dB :: PerChannel CLong
                           -- ^ Volume values for each channel in hundredths of
                           -- a decibel.
                         }

    -- | Get the value associated with a particular channel, if that channel exists.
    getChannel :: Channel -> PerChannel x -> IO (Maybe x)
    getChannel c p | joined p = let r | c `elem` channels p =
                                          liftM Just $ getJoined p
                                      | otherwise = return Nothing
                                in r
                   | otherwise = liftM (lookup c) $ getPerChannel p

    -- | Set the value associated with a particular channel, if that channel exists.
    setChannel :: Channel -> PerChannel x -> x -> IO ()
    setChannel c p v | joined p = when (c `elem` channels p) $ setJoined p v
                     | otherwise = setPerChannel p [(c, v)]

    -- | For a given capability, which may be for either playback or capture, or
    -- common to both, return the playback capability if it exists.
    playback :: Either a (Maybe a, Maybe a) -> Maybe a
    playback (Left _) = Nothing
    playback (Right (x, _)) = x

    -- | For a given capability, which may be for either playback or capture, or
    -- common to both, return the capture capability if it exists.
    capture :: Either a (Maybe a, Maybe a) -> Maybe a
    capture (Left _) = Nothing
    capture (Right (_, x)) = x

    -- | For a given capability, which may be for either playback or capture, or
    -- common to both, return the common capability if it exists.
    common :: Either a (Maybe a, Maybe a) -> Maybe a
    common (Left x) = Just x
    common (Right _) = Nothing

    mkSwitch :: SimpleElement -> IO (Either Switch (Maybe Switch, Maybe Switch))
    mkSwitch se = do
        hasPlayChan <- mapM (hasPlaybackChannel se) allChannels
        hasCaptChan <- mapM (hasCaptureChannel se) allChannels
        let pChans = map fst $ filter snd $ zip allChannels hasPlayChan
            cChans = map fst $ filter snd $ zip allChannels hasCaptChan
        hasComSw <- hasCommonSwitch se
        hasPlaySw <- hasPlaybackSwitch se
        hasPlaySwJ <- hasPlaybackSwitchJoined se
        hasCaptSw <- hasCaptureSwitch se
        hasCaptSwJ <- hasCaptureSwitchJoined se
        return $ if hasComSw
                    then Left $ if hasPlaySwJ
                                  then comJoinedSwitch pChans
                                  else comPerChannelSwitch pChans
                    else let playSw | not hasPlaySw = Nothing
                                    | otherwise = Just
                                        $ if hasPlaySwJ
                                            then playJoinedSwitch pChans
                                            else playPerChannelSwitch pChans
                             captSw | not hasCaptSw = Nothing
                                    | otherwise = Just
                                        $ if hasCaptSwJ
                                            then captJoinedSwitch cChans
                                            else captPerChannelSwitch cChans
                         in Right (playSw, captSw)
      where joined fGet fSet chans =
                Joined { getJoined = fGet se (head chans)
                       , setJoined = fSet se (head chans)
                       , joinedChannels = chans
                       }
            perChannel fGet fSet chans =
                PerChannel { getPerChannel = liftM (zip chans)
                                $ mapM (fGet se) chans
                           , setPerChannel = mapM_ (uncurry (fSet se))
                           , perChannels = chans
                           }
            comJoinedSwitch = joined getPlaybackSwitch setPlaybackSwitch
            comPerChannelSwitch = perChannel getPlaybackSwitch setPlaybackSwitch
            playJoinedSwitch = comJoinedSwitch
            playPerChannelSwitch = comPerChannelSwitch
            captJoinedSwitch = joined getCaptureSwitch setCaptureSwitch
            captPerChannelSwitch = perChannel getCaptureSwitch setCaptureSwitch

    mkVolume :: SimpleElement -> IO (Either Volume (Maybe Volume, Maybe Volume))
    mkVolume se = do
        hasPlayChan <- mapM (hasPlaybackChannel se) allChannels
        hasCaptChan <- mapM (hasCaptureChannel se) allChannels
        let pChans = map fst $ filter snd $ zip allChannels hasPlayChan
            cChans = map fst $ filter snd $ zip allChannels hasCaptChan
        hasComV <- hasCommonVolume se
        hasPlayV <- hasPlaybackVolume se
        hasPlayVJ <- hasPlaybackVolumeJoined se
        hasCaptV <- hasCaptureVolume se
        hasCaptVJ <- hasCaptureVolumeJoined se
        return $
            if hasComV
               then let (v, d) | hasPlayVJ = ( comJoinedVol pChans
                                             , comJoinedDb pChans
                                             )
                               | otherwise = ( comPerChannelVol pChans
                                             , comPerChannelDb pChans
                                             )
                    in Left $ playVolume { value = v, dB = d }
               else let playVol | not hasPlayV = Nothing
                                | otherwise =
                                    let (v, d) | hasPlayVJ =
                                                   ( playJoinedVol pChans
                                                   , playJoinedDb pChans
                                                   )
                                               | otherwise =
                                                   ( playPerChannelVol pChans
                                                   , playPerChannelDb pChans
                                                   )
                                    in Just playVolume { value = v, dB = d }
                        captVol | not hasCaptV = Nothing
                                | otherwise =
                                    let (v, d) | hasCaptVJ =
                                                   ( captJoinedVol cChans
                                                   , captJoinedDb cChans
                                                   )
                                               | otherwise =
                                                   ( captPerChannelVol cChans
                                                   , captPerChannelDb cChans
                                                   )
                                    in Just $ captVolume { value = v, dB = d }
                    in Right (playVol, captVol)
      where j fGet fSet chans =
                Joined { getJoined = fGet se (head chans)
                       , setJoined = fSet se (head chans)
                       , joinedChannels = chans
                       }
            pc fGet fSet chans =
                PerChannel { getPerChannel = liftM (zip chans)
                                $ mapM (fGet se) chans
                           , setPerChannel = mapM_ (uncurry (fSet se))
                           , perChannels = chans
                           }
            playVolume = Volume { getRange = getPlaybackVolumeRange se
                                , setRange = setPlaybackVolumeRange se
                                , getRangeDb = getPlaybackDbRange se
                                , value = undefined
                                , dB = undefined
                                }
            captVolume = Volume { getRange = getCaptureVolumeRange se
                                , setRange = setCaptureVolumeRange se
                                , getRangeDb = getCaptureDbRange se
                                , value = undefined
                                , dB = undefined
                                }
            comJoinedVol = j getPlaybackVolume setPlaybackVolume
            comJoinedDb = j getPlaybackDb setPlaybackDb
            comPerChannelVol = pc getPlaybackVolume setPlaybackVolume
            comPerChannelDb = pc getPlaybackDb setPlaybackDb
            playJoinedVol = comJoinedVol
            playPerChannelVol = comPerChannelVol
            playJoinedDb = comJoinedDb
            playPerChannelDb = comPerChannelDb
            captJoinedVol = j getCaptureVolume setCaptureVolume
            captPerChannelVol = pc getCaptureVolume setCaptureVolume
            captJoinedDb = j getCaptureDb setCaptureDb
            captPerChannelDb = pc getCaptureDb setCaptureDb

    -- | All the 'Control' objects associated with a particular 'Mixer'.
    controls :: Mixer -> IO [Control]
    controls mix = do
        es <- elements mix
        forM es $ \(idN, se) -> do
            n <- getName idN
            i <- getIndex idN
            sw <- mkSwitch se
            v <- mkVolume se
            return $! Control { name = n
                              , index = i
                              , switch = sw
                              , volume = v
                              }

    -- | Get the named 'Control', if it exists, from the named 'Mixer'.
    getControlByName :: Mixer   -- ^ Mixer
                     -> String  -- ^ Control name
                     -> IO (Maybe Control)
    getControlByName mix controlName = do
        cs <- controls mix
        return $ lookup controlName $ zip (map name cs) cs

    {- $exampleVolume
    This example demonstrates the method of accessing the volume of a Control.
    The example function reads the volume and increases it by the value supplied.

    >   changeVolumeBy :: CLong -> IO ()
    >   changeVolumeBy i =
    >       withMixer "default" $ \mixer ->
    >         do Just control <- getControlByName mixer "Master"
    >            let Just playbackVolume = playback $ volume control
    >            (min, max) <- getRange playbackVolume
    >            Just vol <- getChannel FrontLeft $ value $ playbackVolume
    >            when ((i > 0 && vol < max) || (i < 0 && vol > min))
    >              $ setChannel FrontLeft (value $ playbackVolume) $ vol + i

    -}

    {- $exampleSwitch
    This example demonstrates the method of accessing the switch of a Control.
    The example function reads the value of the switch and toggles it.

    >   toggleMute :: IO ()
    >   toggleMute =
    >       withMixer "default" $ \mixer ->
    >         do Just control <- getControlByName mixer "Master"
    >            let Just playbackSwitch = playback $ switch control
    >            Just sw <- getChannel FrontLeft playbackSwitch
    >            setChannel FrontLeft playbackSwitch $ not sw

    -}