-- | A simple drum sequencer app.
--
-- Each row plays a different note. 
-- Each note can have a velocity; to set this, press the triangle button on the
-- right corresponding to the given row; then the columns represent velocities.
--
-- When there are more than 8 step, you can scroll with the left/right buttons
-- (jumping 8 steps).

-- Example usage:
--
-- > main = runMonadicApp defaultGlobalConfig $ drumSequencer defaultCfg
--

{-# LANGUAGE BangPatterns #-}
module System.MIDI.Launchpad.Apps.DrumSeq where

--------------------------------------------------------------------------------

import Data.List

import Control.Monad
import System.MIDI

import Data.Array.Unboxed
import Data.Array.IArray

import System.MIDI.Launchpad.Control
import System.MIDI.Launchpad.AppFramework

--------------------------------------------------------------------------------
  
data Cfg = Cfg 
  { seqSteps        :: !Int   -- ^ How many steps we have (it can be more than 8!)
  , stepResolution  :: !Int   -- ^ Length of a step. 24 is quarter note, 12 is 1/8th, etc.
  , midiFrom        :: !Int   -- ^ which note should be the lowest (MIDI notes, for example 36 or 48 or 60 are C notes)
  , defaultVelocity :: !Int   -- ^ default velocity of a note (0..7)
  }
  deriving Show
  
-- | 8 steps by default, and 1/8th note per step
defaultCfg :: Cfg
defaultCfg = Cfg 
  { seqSteps         = 8 
  , stepResolution   = 12  
  , midiFrom         = 36    -- in ableton, drum racks typically start here?
  , defaultVelocity  = 5    
  }

--------------------------------------------------------------------------------

data Mode 
  = Pattern 
  | Velocities !Int
  deriving (Eq,Ord,Show)
  
data State = State 
  { _playing    :: !Bool
  , _screenPos  :: !Int
  , _notes      :: !(UArray (Int,Int) Int)    -- ^ encoding both velocities and notes
  , _playNotes  :: [PlayNote]
  }
  deriving (Eq,Ord,Show)

-- | Notes played at the moment
data PlayNote = PlayNote
  { _note     :: !Int
  , _stopAt   :: !Int
  }
  deriving (Eq,Ord,Show)

-- | A drum sequencer app.     
drumSequencer :: Cfg -> MonadicApp Cfg Mode State
drumSequencer cfg = MonadicApp
  { mAppConfig    = cfg
  , mAppIniState  = (Pattern, initialState cfg)
  , mAppStartStop = startStop
  , mAppRender    = render
  , mAppButton    = button
  , mAppSync      = sync
  } 

--------------------------------------------------------------------------------
    
initialState :: Cfg -> State    
initialState cfg@(Cfg seqSteps stepResolution _ _) = State 
  { _playing   = False
  , _screenPos = 0
  , _notes     = listArray ((0,0),(seqSteps+7,7)) (repeat (-1))
  , _playNotes = []
  }
  where
    rep = replicate seqSteps
  
startStop :: Cfg -> Bool -> State -> State
startStop cfg playing state = state { _playing = playing }

--------------------------------------------------------------------------------

button :: Cfg -> ButtonPress -> ButtonMonad Mode State ()
button _                        (Release _) = return ()
button cfg@(Cfg seqSteps _ _ _) (Press but) = do

  mode  <- getMode
  state <- getState 

  let pos     = _screenPos state
      notes   = _notes  state

  let lastScreenPos = 8 * div (seqSteps - 1) 8

  case but of

    Dir d -> case d of
      L -> setState $ state { _screenPos = max (pos-8) 0             } 
      R -> setState $ state { _screenPos = min (pos+8) lastScreenPos }
      _ -> return ()

    Side k -> case mode of
      Pattern      -> setMode $ Velocities k
      Velocities u -> setMode $ if u/=k then Velocities k else Pattern 
      
    Pad x y -> case mode of

      Pattern -> do
        let old = notes!(pos+x,y)
            new = if old>=0 then -1 else (defaultVelocity cfg) 
        setState $ state { _notes = notes // [((pos+x,y),new)] } 
        return ()
{-        
        -- also give a sound? but who will stop it?
        when (not $ _playing state) $ do
          sendMessage $ noteOnOff cfg True (7-y) (defaultVelocity cfg)
-}
            
      Velocities u -> when (notes!(pos+x,u) >= 0) $
                        setState $ state { _notes = notes // [((pos+x,u), 7-y)] } where
    
    _ -> return ()
       

--------------------------------------------------------------------------------

counterStep :: Cfg -> Int -> Int 
counterStep (Cfg seqSteps stepResolution _ _) cnt = ((div cnt stepResolution) `mod` seqSteps)

invCounterStep :: Cfg -> Int -> Int
invCounterStep (Cfg seqSteps stepResolution _ _) step = step*stepResolution

totalTicks :: Cfg -> Int
totalTicks (Cfg seqSteps stepResolution _ _) = stepResolution * seqSteps

-- velo 0..7 (ahol 0 nem nulla hanem -1 az igazi csondes)
noteOnOff :: Cfg -> Bool -> Int -> Int -> MidiMessage'
noteOnOff cfg True  y velo = NoteOn  (midiFrom cfg + 7-y) ((velo+1)*16-1)
noteOnOff cfg False y velo = NoteOff (midiFrom cfg + 7-y) 64
    
--------------------------------------------------------------------------------

sync :: Cfg -> Mode -> Int -> SyncMonad State ()    
sync cfg@(Cfg seqSteps stepResolution midiFrom _) mode counter = do
  state <- getState  

  let notes  = _notes  state

  let newIdx = [ (x,y) | x <- [0..seqSteps-1], y<-[0..7], let v = notes!(x,y), v>=0
                       , invCounterStep cfg x == mod counter (totalTicks cfg) ]

  let newNotes = [ PlayNote y (counter + stepResolution) 
                 | (x,y) <- newIdx ]
   
  let (stopNotes, contNotes) = partition (\(PlayNote note stop) -> stop == counter) (_playNotes state)
       
  sendMessages [ noteOnOff cfg False stop 64  | PlayNote stop _  <- stopNotes ]
  sendMessages [ noteOnOff cfg True  y    vel | (x,y) <- newIdx, let vel = notes!(x,y) ]
  
  setState $ state { _playNotes = newNotes ++ contNotes }


--------------------------------------------------------------------------------

renderArrows :: Cfg -> State -> [(Button,Color)]
renderArrows cfg state = concat
  [ if _screenPos state > 0                 then [(Dir L, green)] else []
  , if _screenPos state < (seqSteps cfg)-8  then [(Dir R, green)] else [] 
  ]
  
render :: Cfg -> Mode -> State -> Maybe Int -> RenderMonad ()
render cfg mode state msync = 
  do
  
    setButtonColors $ renderArrows cfg state 
    setButtonColors $ stuff 
  
  where

    pos = _screenPos state
    notes  = _notes  state
    
    steps = seqSteps cfg
    
    column = case msync of
      Nothing  -> (-1)
      Just cnt -> counterStep cfg cnt - pos
      
    stuff = case mode of
    
      Pattern -> time ++ note where
        time = if column >= 0 && column < 8 then [ (Pad column y, amber) | y<-[0..7] ] else [] 
        note = [ (Pad x y, color) | x<-[0..7], y<-[0..7], let v = notes!(pos+x,y), pos+x<steps, v>=0
                                  , let color = if column==x then orange else red ]
   
      Velocities u -> time ++ par ++ side where
        time = if column >= 0 && column < 8 then [ (Pad column 0, amber) ] else []
        par  = [ (Pad x y, color) | x<-[0..7], let p = notes!(pos+x,u), p>=0, pos+x<steps, y<-[7-p..7]
                                  , let color = if column==x && y==0 then yellow else green ] 
        side = [ (Side u, green) ]
      
--------------------------------------------------------------------------------