module Csound.Exp.Mix(
    -- * Container for sounds (triggered with notes and mixers)
    Mix(..), M(..), nchnls,

    effect, effectS,
    sco, mix, --, midi, pgmidi

    rescaleCsdEventListM
) where

import Data.Traversable(traverse)
import qualified Data.Map    as M

import Csound.Tfm.Tab

import Csound.Exp
import Csound.Exp.Wrapper
import Csound.Exp.SE
import Csound.Exp.GE
import Csound.Exp.Instr
import Csound.Exp.Arg
import Csound.Exp.Tuple(Out(..), CsdTuple, fromCsdTuple, toCsdTuple, outArity)
import Csound.Exp.Options
import Csound.Exp.EventList

newtype Mix a = Mix { unMix :: GE M } 

data M 
    = Snd InstrId (CsdEventList Note)
    | Eff InstrId (CsdEventList M)    

nchnls :: Out a => f (Mix a) -> Int
nchnls = outArity . proxy  
    where proxy :: f (Mix a) -> a
          proxy = undefined  

-- | Play a bunch of notes with the given instrument.
--
-- > res = sco instrument scores 
--
-- * @instrument@ is a function that takes notes and produces a 
--   tuple of signals (maybe with some side effect)
--  
-- * @scores@ are some notes (see the module "Temporal.Media" 
--   on how to build complex scores out of simple ones)
--
-- Let's try to understand the type of the output. It's @Score (Mix (NoSE a))@. 
-- What does it mean? Let's look at the different parts of this type:
--
-- * @Score a@ - you can think of it as a container of some values of 
--   type @a@ (every value of type @a@ starts at some time and lasts 
--   for some time in seconds)
--
-- * @Mix a@ - is an output of Csound instrument it can be one or several 
--   signals ('Csound.Base.Sig' or 'Csound.Base.CsdTuple'). 
--
-- *NoSE a* - it's a tricky part of the output. 'NoSE' means literaly 'no SE'. 
-- It tells to the type checker that it can skip the 'Csound.Base.SE' wrapper
-- from the type 'a' so that @SE a@ becomes just @a@ or @SE (a, SE b, c)@ 
-- becomes @(a, b, c)@. Why should it be? I need 'SE' to deduce the order of the
-- instruments that have side effects. I need it within one instrument. But when 
-- instrument is rendered i no longer need 'SE' type. So 'NoSE' lets me drop it
-- from the output type. 
sco :: (Arg a, Out b, CsdSco f) => (a -> b) -> f a -> f (Mix (NoSE b))
sco instr notes = singleCsdEvent 0 (csdEventListDur events) $ Mix $ do    
    events'  <- traverse renderNote events
    instrId <- saveSourceInstrCached instr soundSourceExp
    return $ Snd instrId events'
    where events = toCsdEventList notes

renderNote :: (Arg a) => a -> GE [Prim]
renderNote a = tfmNoteStrs =<< tfmNoteTabs (toNote a) 

tfmNoteTabs :: Note -> GE Note
tfmNoteTabs xs = do
    opt <- getOptions
    let xs' = defineNoteTabs (tabFi opt) xs
        tabs = getPrimTabs =<< xs'
    ids <- mapM saveTab tabs
    let tabMap = M.fromList $ zip tabs ids
    return $ substNoteTabs tabMap xs'

tfmNoteStrs :: Note -> GE Note
tfmNoteStrs xs = do    
    ids <- mapM saveStr strs
    let strMap = M.fromList $ zip strs ids
    return $ substNoteStrs strMap xs   
    where strs = getStrings xs


-- | Applies an effect to the sound. Effect is applied to the sound on the give track. 
--
-- > res = mix effect sco 
--
-- * @effect@ - a function that takes a tuple of signals and produces 
--   a tuple of signals.
--
-- * @sco@ - something that is constructed with 'Csound.Base.sco' or 
--   'Csound.Base.mix' or 'Csound.Base.midi'. 
--
-- With the function 'Csound.Base.mix' you can apply a reverb or adjust the 
-- level of the signal. It functions like a mixing board but unlike mixing 
-- board it produces the value that you can arrange with functions from the 
-- module "Temporal.Media". You can delay it mix with some other track and 
-- apply some another effect on top of it!
mix :: (Out a, Out b, CsdSco f) => (a -> b) -> f (Mix a) -> f (Mix (NoSE b))
mix eff sigs = singleCsdEvent 0 (csdEventListDur events) $ Mix $ do
    notes <- traverse unMix events
    instrId <- saveMixerInstr =<< effectExp eff
    return $ Eff instrId notes 
    where events = toCsdEventList sigs
    
{-
-- | Triggers a midi-instrument (like Csound's massign). The result type 
-- is a fake one. It's wrapped in the 'Csound.Base.Score' for the ease of mixing.
-- you can not delay or stretch it. The only operation that is meaningful 
-- for it is 'Temporal.Media.chord'. But you can add effects to it with 'Csound.Base.mix'!
midi :: (Out a) => Channel -> (Msg -> a) -> Score (Mix (NoSE a))
midi = genMidi Massign

-- | Triggers a - midi-instrument (like Csound's pgmassign). 
pgmidi :: (Out a) => Maybe Int -> Channel -> (Msg -> a) -> Score (Mix (NoSE a))
pgmidi mchn = genMidi (Pgmassign mchn)

genMidi :: (Out a) => MidiType -> Channel -> (Msg -> a) -> Score (Mix (NoSE a))
genMidi midiType chn f = temp $ Mid $ mkInstr getMidiArity Msg f (Just (midiType, chn))
    where getMidiArity = mkArity (const 0) outArity
-}

-- | Constructs the effect that applies a given function on every channel.
effect :: (CsdTuple a, Out a) => (Sig -> Sig) -> (a -> a)
effect f = toCsdTuple . fmap (toE . f . fromE) . fromCsdTuple

-- | Constructs the effect that applies a given function with side effect 
-- (it uses random opcodes or delays) on every channel.
effectS :: (CsdTuple a, Out a) => (Sig -> SE Sig) -> (a -> SE a)
effectS f a = fmap fromOut $ mapM f =<< toOut a

rescaleCsdEventListM :: CsdEventList M -> CsdEventList M
rescaleCsdEventListM es = 
    es { csdEventListNotes = fmap rescaleCsdEventM $ csdEventListNotes es }

rescaleCsdEventM :: CsdEvent M -> CsdEvent M
rescaleCsdEventM (start, dur, evt) = (start, dur, phi evt)
    where phi x = case x of
            Snd n evts -> Snd n $ rescaleCsdEventList (dur/localDur) evts
            Eff n evts -> Eff n $ rescaleCsdEventListM $ rescaleCsdEventList (dur/localDur) evts            
            where localDur = case x of
                    Snd _ evts -> csdEventListDur evts
                    Eff _ evts -> csdEventListDur evts