module Haskore.Interface.SuperCollider.Performance where

import qualified Haskore.Interface.SuperCollider.Note     as Note
import qualified Haskore.Interface.SuperCollider.SoundMap as InstrMap

import qualified Haskore.Music.Standard as StdMusic
import qualified Haskore.Music.Rhythmic as RhyMusic
import qualified Haskore.Music          as Music
import qualified Haskore.Melody         as Melody

import qualified Haskore.Performance         as Pf
import qualified Haskore.Performance.BackEnd as PfBE
import qualified Haskore.Performance.Fancy   as FancyPf

import qualified Haskore.RealTime.EventList.TimeBody    as TimeList
import qualified Haskore.RealTime.EventList.TimeTime as TimeListPad
import qualified Haskore.General.IdGenerator      as IdGen

import qualified Numeric.NonNegative.Class as NonNeg



fromMelody :: (NonNeg.C time, Floating time, RealFrac time) =>
   Melody.T () -> PfBE.Padded time Note.T
fromMelody =
{-
   PfBE.fromPaddedPerformance
      (Note.fromRhythmicNote
          (const "no drum")
          (const "no instrument")) .
-}
   PfBE.fromPaddedPerformance
      (Note.fromRhythmicNote
          (error "no drum defined")
          (error "no instrument defined")) .
   fancyPaddedPerformanceFromMusic .
   StdMusic.fromMelodyNullAttr undefined


fancyPaddedPerformanceFromMusic ::
   (Ord note, NonNeg.C time, RealFrac time) =>
   Music.T note -> Pf.Padded time Double note
fancyPaddedPerformanceFromMusic =
   FancyPf.paddedFromMusic



type NodeId = Int   -- cf. PlayEasy
type NodeIdGen a = IdGen.T NodeId a
type T time = TimeListPad.T time (NodeId, Maybe (time, Note.T))


eventsFromNotes, eventsFromNotesQueue, eventsFromNotesEither ::
   (NonNeg.C time)
   => PfBE.Padded time Note.T
   -> NodeIdGen (T time)
eventsFromNotes = eventsFromNotesEither



eventsFromNotesQueue =
   eventsFromNotesQueueAux TimeList.empty

{- |
This variant does not need a list of 'Either's,
instead it uses two separate lists for start and stop events.
We would like to use a standard datatype for priority queues,
however it must be efficient to remove the first element
which means that the time stamp of all following elements must be decreased.
Since we have no such data structure, we simply use a TimeList.
But then again, without the 'Either' list,
we have to merge the queue of stop events and the list of start events manually.
This makes the implementation less beautiful.
-}
eventsFromNotesQueueAux :: (NonNeg.C time)
   => TimeList.T time NodeId
   -> PfBE.Padded time Note.T
   -> NodeIdGen (T time)
eventsFromNotesQueueAux queue xtt =
   let (qExists, (~(qTime,oldSId), qs)) =
          maybe
             (False, (error "no q", error "no qs"))
             (\q' -> (True, q'))
             (TimeList.viewL queue)
       (xTime,xs0) = TimeListPad.viewTimeL xtt
       (xExists, (note, xs)) =
          maybe
             (False, (error "no x", error "no xs"))
             (\x' -> (True, x'))
             (TimeListPad.viewBodyL xs0)
       qFirst =
          do IdGen.free oldSId
             fmap
                (TimeListPad.cons qTime (oldSId, Nothing))
                (eventsFromNotesQueueAux qs
                   (TimeListPad.decreaseStart qTime xtt))
       xFirst =
          do newSId <- IdGen.alloc
             let noteDur = PfBE.eventDur note
             fmap
                (TimeListPad.cons xTime
                   (newSId, Just (noteDur, PfBE.eventNote note)))
                (eventsFromNotesQueueAux
                   (TimeList.insert noteDur newSId
                      (TimeList.decreaseStart xTime queue))
                   xs)
   in  if qExists && (not xExists || qTime <= xTime)
         then qFirst
         else
           if xExists
             then xFirst
             else return (TimeListPad.pause xTime)


eventsFromNotesEither =
   eventsFromNotesEitherAux . TimeListPad.mapBody Right

{-
It seems not to be possible to implement that with foldM,
because one recursive call to eventsFromNotes
requires modifying the events list.
-}
eventsFromNotesEitherAux :: (NonNeg.C time)
   => TimeListPad.T time (Either NodeId (PfBE.Event time Note.T))
   -> NodeIdGen (T time)
eventsFromNotesEitherAux =
   (\ (dur, xss) ->
       fmap (TimeListPad.consTime dur) $
       maybe
          (return TimeListPad.empty)
          (\(x,xs) ->
             let doRest ev restAct =
                    fmap (TimeListPad.consBody ev) restAct
             in  either
                   (\sid ->
                       do IdGen.free sid
                          doRest
                             (sid, Nothing)
                             (eventsFromNotesEitherAux xs))
                   (\note ->
                       do sid <- IdGen.alloc
                          let noteDur = PfBE.eventDur note
                          doRest
                             (sid, Just (noteDur, PfBE.eventNote note))
                             (eventsFromNotesEitherAux
                                (TimeListPad.insert noteDur (Left sid) xs)))
                   x) $
       TimeListPad.viewBodyL xss)
    . TimeListPad.viewTimeL


instrStartNodeId :: NodeId
instrStartNodeId = 2 -- succ Sound.SC3.Server.Play.homeId

fixNodeIds :: NodeIdGen a -> a
fixNodeIds = IdGen.run instrStartNodeId

fromMusic :: (Ord note, NonNeg.C time, RealFrac time) =>
   Note.FromNote time note ->
   Music.T note ->
   NodeIdGen (T time)
fromMusic makeNote =
   eventsFromNotes .
   PfBE.fromPaddedPerformance makeNote .
   FancyPf.paddedFromMusic

{- needed for Play.Install, Play.Channel, Life -}
{-
fromRhythmicMusic ::
   (Show drum, Show instr, Ord drum, Ord instr,
    Floating time, RealFrac time) =>
   RhyMusic.T drum instr ->
   NodeIdGen (T time)
fromRhythmicMusic =
   fromMusic (Note.fromRhythmicNote show show)
-}

fromRhythmicMusicWithAttributes ::
   (Ord drum, Ord instr,
    NonNeg.C time, Floating time, RealFrac time) =>
   InstrMap.ToSound drum ->
   InstrMap.ToSound instr ->
   RhyMusic.T drum instr ->
   NodeIdGen (T time)
fromRhythmicMusicWithAttributes dMap iMap =
   fromMusic (Note.fromRhythmicNoteWithAttributes dMap iMap)