module Sound.Hommage.Midi
 ( 
 -- | The action 'writeMidiFile' stores the datatype 'MidiFile' in a MIDI-File. 
 --   It consists of a field for the 'Ticks' per quarter and a 'MidiTrack' (or some). 
 --   The function 'runMidiMusic' produces such a track from a 'MidiMusic', which can
 --   be created using the functions 'noteMidiMusic', 'restMidiMusic', 'appendMidiMusic' and
 --   'mergeMidiMusic'. 
 --   'MidiNote' is used to create note or controller events; it consists of the Midi-Channel
 --   ('Chan') and two fields with a 'MidiValue'.

 -- * MidiFile
   MidiFile (..)
 , writeMidiFile 
 , Ticks 
 -- * MidiNote
 , MidiNote (..)
 , Chan 
 , MidiValue 
 -- * MidiMusic 
 , MidiMusic
 , noteMidiMusic 
 , restMidiMusic 
 , appendMidiMusic 
 , mergeMidiMusic 
 , runMidiMusic 
 -- * MidiTrack
 , MidiTrack 
 , Delta 
 -- * MidiEvent
 , MidiEvent (..)
 -- * Encoding
 , encodeMidiFile 
 , midiHeaderSync 
 , midiHeaderSingle
 , midiHeaderAsync  
 , encodeMidiTrack
 , encodeDelta 
 , encodeMidiDeltaEvent 
 , encodeEvent 
 , lastByte 
 , nextByte 
 , toBytes 
 )
 where

import Sound.Hommage.WavFile

import Data.Word
import Data.Bits
import Data.Ratio
---------------------------------------------------------------------------------------------------
type Delta = Int
-- | A kind of extended MidiTrack type, that allows delta-times without events 
--   (will be summed up by converting to 'MidiTrack').
type MidiMusic = [(Delta, Maybe MidiEvent)]
-- | A sequence of MidiEvents with a delta-time which means the number of ticks
--   before the event happens (relative to the last event). 
type MidiTrack = [(Delta, MidiEvent)]
-- | Ticks per Quarter. For some unknown reasons it does not work with any value.
--   96 seems to be ok. 
type Ticks = Int
---------------------------------------------------------------------------------------------------
-- | Converts a 'MidiMusic' to a 'MidiTrack'. 
runMidiMusic :: MidiMusic -> MidiTrack 
runMidiMusic mt = loop 0 mt
 where
  loop delta ((d, Just e)  : r) = (delta + d, e) : loop 0 r
  loop delta ((d, Nothing) : r) = loop (d+delta) r
  loop delta []                 = [(delta, MidiEndOfTrack)]

-- | Writes a 'MidiFile' to a File.
writeMidiFile :: FilePath -> MidiFile -> IO ()
writeMidiFile fp mf = writeDataFile fp $ encodeMidiFile mf
---------------------------------------------------------------------------------------------------
-- | A Midi-Channel (0-15)
type Chan = Word8
-- | A Midi-Value (0-127)
type MidiValue = Word8
---------------------------------------------------------------------------------------------------
data MidiNote = MidiNote Chan MidiValue MidiValue -- ^ A note with pitch and volume
              | MidiCtrl Chan MidiValue MidiValue -- ^ Controllernumber and value
---------------------------------------------------------------------------------------------------
-- | Creates a note or controller event with given length (relative to 'Ticks'). 
--   The length means the delta-time between note-on and note-off resp. the time after
--   the controller event.
noteMidiMusic :: Int -> MidiNote -> MidiMusic
noteMidiMusic d (MidiNote c n v) = 
 [(0, Just $ MidiNoteOn c n v), (d, Just $ MidiNoteOff c n v)]
noteMidiMusic d (MidiCtrl c n v) = 
 [(0, Just $ MidiControl c n v), (d, Nothing)]

-- | A pause with given length.
restMidiMusic :: Int -> MidiMusic
restMidiMusic d = [(d, Nothing)]
---------------------------------------------------------------------------------------------------
-- | Parallel composition of 'MidiMusic'.
mergeMidiMusic :: MidiMusic -> MidiMusic -> MidiMusic
mergeMidiMusic ((d1,m1):r1) ((d2,m2):r2) | d1 < d2   = (d1,m1) : mergeMidiMusic r1 ((d2-d1,m2):r2)
                                         | d1 > d2   = (d2,m2) : mergeMidiMusic ((d1-d2,m1):r1) r2
                                         | otherwise = (d1,m1) : (0, m2) : mergeMidiMusic r1 r2
mergeMidiMusic [] r2 = r2
mergeMidiMusic r1 [] = r1

-- | Sequencial composition of 'MidiMusic'.
appendMidiMusic :: MidiMusic -> MidiMusic -> MidiMusic
appendMidiMusic = (++)
---------------------------------------------------------------------------------------------------
data MidiFile = MidiSync   Ticks [MidiTrack] -- ^ Some parallel (synchronous) MidiTracks. 
              | MidiSingle Ticks MidiTrack   -- ^ A single MidiTrack. (Seems not to work yet)
              | MidiAsync  Ticks [MidiTrack] -- ^ Some asynchronous MidiTracks. (Seems not to work yet)

data MidiEvent = MidiNoteOff  !Chan !MidiValue !MidiValue     -- note-nr and velocity
               | MidiNoteOn   !Chan !MidiValue !MidiValue     -- note-nr and velocity
               | MidiKeyAfter !Chan !MidiValue !MidiValue     -- note-nr and velocity
               | MidiControl  !Chan !MidiValue !MidiValue     -- controller-nr and value
               | MidiEndOfTrack
--               | MidiProgram  !Chan !MidiValue                -- program-nr
--               | MidiChannelAfter !Chan !MidiValue            -- channel-nr
--               | MidiPitchWheel   !Chan !MidiValue !MidiValue -- bottom, top
---------------------------------------------------------------------------------------------------
encodeMidiFile :: MidiFile -> [Word8]
encodeMidiFile mf =
 case mf of        -- HEADER              TRACKS                    TICKS
  MidiSync tc  mts -> midiHeaderSync   ++ toBytes 2 (length mts) ++ toBytes 2 tc ++ (mts >>= encodeMidiTrack)
  MidiSingle tc mt -> midiHeaderSingle ++ toBytes 2 1            ++ toBytes 2 tc  ++ encodeMidiTrack mt
  MidiAsync tc mts -> midiHeaderAsync  ++ toBytes 2 (length mts) ++ toBytes 2 tc ++ (mts >>= encodeMidiTrack)
---------------------------------------------------------------------------------------------------
midiHeaderSingle :: [Word8]
midiHeaderSingle = [ 77, 84, 104, 100, 0, 0, 0, 6, 0, 0]

midiHeaderSync :: [Word8]
midiHeaderSync = [ 77, 84, 104, 100, 0, 0, 0, 6, 0, 1]

midiHeaderAsync :: [Word8]
midiHeaderAsync = [ 77, 84, 104, 100, 0, 0, 0, 6, 0, 2]
---------------------------------------------------------------------------------------------------
encodeMidiTrack :: MidiTrack -> [Word8]
encodeMidiTrack ms = [ 77, 84, 114, 107 ] ++ toBytes 4 (length t) ++ t
 where
  t = (ms >>= encodeMidiDeltaEvent) 
---------------------------------------------------------------------------------------------------
encodeDelta :: Int -> [Word8]
encodeDelta n = loop n
 where 
  loop n | n < 128   = [fromIntegral n]
         | otherwise = fromIntegral (128 + mod n 128) : loop (div n 128)
---------------------------------------------------------------------------------------------------
encodeMidiDeltaEvent :: (Delta, MidiEvent) -> [Word8]
encodeMidiDeltaEvent (delta, me) = encodeDelta delta ++ encodeEvent me

encodeEvent :: MidiEvent -> [Word8]
encodeEvent me = 
 case me of
  MidiNoteOff ch no va  -> [ch + 128, no, va]
  MidiNoteOn  ch no va  -> [ch + 144, no, va]
  MidiKeyAfter ch no va -> [ch + 160, no, va]
  MidiControl ch no va  -> [ch + 176, no, va]
  MidiEndOfTrack        -> [255,47,0]
--------------------------------------------------------------------------------------------------
lastByte :: Int -> Word8
lastByte n = fromIntegral (mod n 256)

nextByte :: Int -> Int
nextByte n = div n 256

toBytes :: Int -> Int -> [Word8]
toBytes k n = reverse $ take k $ map lastByte $ iterate nextByte n  -- reverse?
---------------------------------------------------------------------------------------------------