{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE TupleSections #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# OPTIONS_GHC -fno-warn-warnings-deprecations #-}


module Fadno.Midi where

import Sound.MIDI.File as MFile
import Data.EventList.Relative.TimeBody as EList hiding (concat,traverse)
import Sound.MIDI.File.Event as MEvent
import Sound.MIDI.File.Event.Meta as MMeta
import Sound.MIDI.Message.Channel as MChan
import Sound.MIDI.Message.Channel.Voice as MVoice
import Sound.MIDI.File.Load
import Sound.MIDI.File.Save
import Sound.MIDI.General
import Fadno.Note
import Data.List (mapAccumL)
import Control.Lens
import Control.Arrow
import System.Process
import Control.Monad
import Data.Ratio

-- | Serializable midi data.
type MidiData = MFile.T

type IPitch = Int
type IDur = Int

-- | Convert some note value to midi-ready values.
class MidiNotes a where
    toMidiNotes :: a -> [([IPitch],IDur)]

instance MidiNotes [([IPitch],IDur)] where toMidiNotes = id

instance {-# OVERLAPPING #-} (Integral p, Traversable c, Integral d, Traversable t) => MidiNotes (t (Note (c p) d))  where
    toMidiNotes = map ((map fromIntegral . toListOf traverse) *** fromIntegral) .
                  toListOf (traverse.toPair)

instance (Integral p, Integral d, Traversable t) => MidiNotes (t (Note p d))  where
    toMidiNotes = map (return.fromIntegral *** fromIntegral) . toListOf (traverse.toPair)


-- | Tempo in microseconds per quarter. See 'fromBPM'.
newtype MidiTempo = MidiTempo Int
    deriving (Eq,Show,Enum,Bounded,Ord,Num,Real,Integral)

-- | Midi channel, 1-16 presumably.
newtype MidiChan = MidiChan Int
    deriving (Eq,Show,Enum,Bounded,Ord,Num,Real,Integral)

-- | note velocity, 0-127
newtype MidiVelocity = MidiVelocity Int
    deriving (Eq,Show,Enum,Bounded,Ord,Num,Real,Integral)

-- | Midi program. See 'fromInstrument'.
newtype MidiProgram = MidiProgram Int
    deriving (Eq,Show,Enum,Bounded,Ord,Num,Real,Integral)

-- | Midi ticks per quarter.
newtype MidiTicks = MidiTicks Int
    deriving (Eq,Show,Enum,Bounded,Ord,Num,Real,Integral)

-- | Rational to ticks
toTicks :: MidiTicks -> Iso' Rational IDur
toTicks t = iso to' from' where
    to' = truncate . (* fromIntegral (t*4))
    from' = (% fromIntegral (t*4)) . fromIntegral


-- | Internal type for midi event or pad.
data MidiEvent = Pad IDur | Event MEvent.T

-- | cover our tracks
type MidiTrack = Track


-- | write to disk.
writeMidiFile :: FilePath -> MidiData -> IO ()
writeMidiFile = toFile

-- | debug midi file.
showMidiFile :: FilePath -> IO ()
showMidiFile = showFile

-- | Make midi file data
midi :: MidiTicks -> [MidiTrack] -> MidiData
midi ticks = MFile.Cons Parallel (Ticks (toTempo $ fromIntegral ticks))

-- | make a standard track which specifies tempo and program.
-- | see 'makeTrack' for more control.
makeTrackFull
  :: (MidiNotes notes) =>
     MidiTempo
     -> MidiChan
     -> MidiProgram
     -> MidiVelocity
     -> notes
     -> MidiTrack
makeTrackFull tempo chan prog vel notes =
    makeTrack $ setTempo tempo:
                programChange chan prog:
                toNoteEvents chan vel notes


-- | BPM to microseconds per quarter note.
fromBPM :: (Real a, Show a) => a -> MidiTempo
fromBPM b | b > 0 = floor (60 * 1000000 / toRational b)
          | otherwise = error $ "fromBPM: must be > 0: " ++ show b

-- | convert a General MIDI 'Instrument'.
fromInstrument :: Instrument -> MidiProgram
fromInstrument = fromIntegral . fromEnum

-- | make a track from track events.
makeTrack :: [MidiEvent] -> MidiTrack
makeTrack = fromPairList . concat . snd . mapAccumL conv 0
    where conv :: IDur -> MidiEvent -> (IDur,[(ElapsedTime,MEvent.T)])
          conv _ (Pad dur) = (dur,[])
          conv off (Event e) = (0,[(toElapsedTime $ fromIntegral off,e)])


-- | turn notes into track events.
toNoteEvents :: MidiNotes notes => MidiChan -> MidiVelocity -> notes -> [MidiEvent]
toNoteEvents chan vel = concatMap (noteEvents chan vel) . toMidiNotes


-- | create a "Voice" MIDI event
voiceEvent :: MidiChan -> MVoice.T -> MidiEvent
voiceEvent chan = midiEvent chan . Voice

-- | tempo meta event.
setTempo :: MidiTempo -> MidiEvent
setTempo = metaEvent . SetTempo . toTempo . fromIntegral

-- | create a "Meta" MIDI event
metaEvent :: MMeta.T -> MidiEvent
metaEvent = Event . MetaEvent

-- | create a "Voice" or "Mode" MIDI event.
midiEvent :: MidiChan -> MChan.Body -> MidiEvent
midiEvent chan = Event . MIDIEvent . MChan.Cons (toChannel $ fromIntegral chan)

-- TODO: sysex.

-- | program change MIDI Voice event.
programChange :: MidiChan -> MidiProgram -> MidiEvent
programChange chan prog = voiceEvent chan (ProgramChange (toProgram $ fromIntegral prog))

-- | note on + note off events, using 'Pad' to carve out space.
noteEvents :: MidiChan -> MidiVelocity -> ([IPitch],IDur) -> [MidiEvent]
noteEvents chan vel (ps,dur) = evs noteOn ++ [Pad dur] ++ evs noteOff
    where evs f = map (f chan vel . fromIntegral) ps

-- TODO: figure out polymorphic way to attach velocity and anything else to notes.

-- | note on or note off event.
noteEvent :: (Pitch -> Velocity -> MVoice.T) ->
             MidiChan -> MidiVelocity -> IPitch -> MidiEvent
noteEvent f chan vel pitch = voiceEvent chan
                             (f (toPitch (fromIntegral pitch))
                                    (toVelocity $ fromIntegral vel))
noteOn :: MidiChan -> MidiVelocity -> IPitch -> MidiEvent
noteOn = noteEvent NoteOn

noteOff :: MidiChan -> MidiVelocity -> IPitch -> MidiEvent
noteOff = noteEvent NoteOff


test1 :: IO ()
test1 = playMidi "/tmp/first.midi" 120 [(AcousticGrandPiano,
         map (uncurry Note)
                 [([60 :: Int],48 :: Int),([61],48),([62],24),([64],64),
                  ([],96),([60,66],96)])]


playMidi :: MidiNotes n => FilePath -> Int -> [(Instrument,n)] -> IO ()
playMidi file bpm tracks = do
    writeMidiFile file $ midi 96 $ map (\(inst,notes) -> makeTrackFull (fromBPM bpm) 0 (fromInstrument inst) 127 notes) tracks
    void $ createProcess (shell $ "scripts/qt7play.applescript " ++ file)



-- playMidi "/tmp/boston.mid" DrawbarOrgan notes
-- let boston = [Db@:5,F@:4,Db@:5,Eb@:5,Ab@:4,C@:5]
-- map (\p -> (p - 60) * 2 + 60)
-- let notes = concat $ replicate 8 $ map (`Note` (1 % 16)) boston
-- playMidi "/tmp/boston.mid" DrawbarOrgan 140
--    (toListOf (traverse.seconding (toTicks 96)) notes)