{- |
It is recommended to view this Haddock documentation using the @-q local@ option
so that, for example, the types @Data.EventList.Relative.TimeBody.T@,
@Sound.MIDI.File.T@, and @Sound.MIDI.File.Event.T@ don't all get displayed
simply as @T@. Otherwise, hover over the types to see what is referred to.
-}
{-# LANGUAGE BangPatterns #-}
{-# LANGUAGE CPP #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
module Sound.MIDI.Util (
-- * Types
  Beats(..), TimeSig(..), Seconds(..), BPS(..)
-- * Reading\/writing MIDI files
, decodeFile, encodeFileBeats, minResolution
-- * Tempos
, readTempo, showTempo
, makeTempo, applyTempo, unapplyTempo, applyTempoTrack, unapplyTempoTrack
, TempoMap, makeTempoMap, unmakeTempoMap, tempoMapFromBPS, tempoMapToBPS, applyTempoMap, unapplyTempoMap
-- * Measures and time signatures
, readSignature, readSignatureFull, showSignature, showSignatureFull
, MeasureMap, MeasureBeats, MeasureMode(..), measures, makeMeasureMap, unmakeMeasureMap
, measureMapFromLengths, measureMapToLengths
, measureMapFromTimeSigs, measureMapToTimeSigs
, applyMeasureMap, unapplyMeasureMap
, measureLengthToTimeSig
-- * Track names
, trackName, setTrackName, readTrackName, showTrackName
-- * Misc. track operations
, trackSplitZero, trackGlueZero, trackTakeZero, trackDropZero
, trackJoin, trackSplit, trackTake, trackDrop
, extractFirst
) where

import qualified Data.Map as Map
import Data.Maybe (listToMaybe, mapMaybe, isNothing)
#if __GLASGOW_HASKELL__ < 710
import Data.Monoid (Monoid)
#endif
import Data.Ratio (denominator)

import qualified Numeric.NonNegative.Wrapper as NN
import qualified Numeric.NonNegative.Class as NNC

import qualified Sound.MIDI.File as F
import qualified Sound.MIDI.File.Event as E
import qualified Sound.MIDI.File.Event.Meta as Meta

import qualified Data.EventList.Absolute.TimeBody as ATB
import qualified Data.EventList.Relative.TimeBody as RTB

-- | A hack to look up mappings in a 'Map.Map' using either one of
-- two key types, which increase together.
data DoubleKey a b
  = DoubleKey !a !b
  | LookupA   !a
  | LookupB      !b
  deriving (Show, Read)

instance (Ord a, Ord b) => Eq (DoubleKey a b) where
  dk1 == dk2 = compare dk1 dk2 == EQ

instance (Ord a, Ord b) => Ord (DoubleKey a b) where

  compare (DoubleKey a1 _ ) (DoubleKey a2 _ ) = compare a1 a2 -- A is arbitrary
  compare (DoubleKey a1 _ ) (LookupA   a2   ) = compare a1 a2
  compare (DoubleKey _  b1) (LookupB      b2) = compare b1 b2

  compare (LookupA   a1   ) (DoubleKey a2 _ ) = compare a1 a2
  compare (LookupA   a1   ) (LookupA   a2   ) = compare a1 a2
  compare (LookupA   _    ) (LookupB      _ ) = error
    "compare: internal error! tried to compare LookupA and LookupB"

  compare (LookupB      b1) (DoubleKey _  b2) = compare b1 b2
  compare (LookupB      _ ) (LookupA      _ ) = error
    "compare: internal error! tried to compare LookupB and LookupA"
  compare (LookupB      b1) (LookupB      b2) = compare b1 b2

-- | Musical time, measured in beats a.k.a. quarter notes.
newtype Beats = Beats { fromBeats :: NN.Rational }
  deriving (Eq, Ord, Show, Monoid, NNC.C, Num, Real, Fractional, RealFrac)

data TimeSig = TimeSig { timeSigLength :: !Beats, timeSigUnit :: !Beats }
  deriving (Eq, Show)

-- | Real time, measured in seconds.
newtype Seconds = Seconds { fromSeconds :: NN.Rational }
  deriving (Eq, Ord, Show, Monoid, NNC.C, Num, Real, Fractional, RealFrac)

-- | A ratio between musical time and real time, measured in beats per second.
newtype BPS = BPS { fromBPS :: NN.Rational }
  deriving (Eq, Ord, Show, Monoid, NNC.C, Num, Real, Fractional, RealFrac)

-- | Creates a tempo as a ratio of a music duration to a real time duration.
makeTempo :: Beats -> Seconds -> BPS
makeTempo (Beats b) (Seconds s) = BPS $ b / s

-- | Uses a tempo to convert from musical time to real time.
applyTempo :: BPS -> Beats -> Seconds
applyTempo (BPS bps) (Beats b) = Seconds $ b / bps

-- | Uses a tempo to convert from real time to musical time.
unapplyTempo :: BPS -> Seconds -> Beats
unapplyTempo (BPS bps) (Seconds s) = Beats $ bps * s

-- | Assigns units to the tracks in a MIDI file. Supports both the common
-- ticks-based files as well as real-time SMPTE-encoded files.
decodeFile :: F.T -> Either [RTB.T Beats E.T] [RTB.T Seconds E.T]
decodeFile (F.Cons _typ dvn trks) = case dvn of
  F.Ticks res -> let
    readTime tks = Beats $ fromIntegral tks / fromIntegral res
    in Left $ map (RTB.mapTime readTime) trks
  F.SMPTE fps tksPerFrame -> let
    realFps = case fps of
      29 -> 29.97
      _  -> fromIntegral fps
    readTime tks = Seconds $
      fromIntegral tks / (realFps * fromIntegral tksPerFrame)
    in Right $ map (RTB.mapTime readTime) trks

-- | Encodes the tracks' beat positions in ticks, using the given resolution.
-- Positions will be rounded if necessary; see 'minResolution'.
encodeFileBeats :: F.Type -> Integer -> [RTB.T Beats E.T] -> F.T
encodeFileBeats typ res
  = F.Cons typ (F.Ticks $ fromIntegral res)
  . map (RTB.discretize . RTB.mapTime (* fromIntegral res))

-- | To correctly encode all the given tracks without rounding,
-- the resolution must be a multiple of the returned number.
minResolution :: [RTB.T Beats E.T] -> Integer
minResolution
  = foldr lcm 1
  . map (denominator . NN.toNumber . fromBeats)
  . concatMap RTB.getTimes

-- | Extracts the tempo from a tempo change event.
readTempo :: E.T -> Maybe BPS
readTempo (E.MetaEvent (Meta.SetTempo uspqn)) = let
  spqn = fromIntegral uspqn / 1000000
  qnps = recip spqn
  in Just $ BPS qnps
readTempo _ = Nothing

-- | Creates a MIDI event to set the tempo to the given value.
-- Rounds the tempo to the nearest whole \"microseconds per beat\" if necessary.
showTempo :: BPS -> E.T
showTempo (BPS qnps) = let
  spqn = recip qnps
  uspqn = spqn * 1000000
  in E.MetaEvent $ Meta.SetTempo $ round uspqn

-- | Given a MIDI event, if it is a time signature event, returns the length
-- of one measure set by the time signature.
readSignature :: E.T -> Maybe Beats
readSignature = fmap timeSigLength . readSignatureFull

readSignatureFull :: E.T -> Maybe TimeSig
readSignatureFull (E.MetaEvent (Meta.TimeSig n d _ _)) = Just $ let
  unit = 4 / (2 ^ d)
  len = fromIntegral n * unit
  in TimeSig (Beats len) (Beats unit)
readSignatureFull _ = Nothing

-- | If the given number is @2 ^ n@ where @n@ is a non-negative integer,
-- returns @n@.
logBase2 :: Integer -> Maybe Integer
logBase2 x = go 0 1 where
  go !p !y = case compare x y of
    EQ -> Just p
    GT -> go (p + 1) (y * 2)
    LT -> Nothing

-- | Given a measure length, tries to encode it as a MIDI time signature.
showSignature :: Beats -> Maybe E.T
showSignature = showSignatureFull . measureLengthToTimeSig

showSignatureFull :: TimeSig -> Maybe E.T
showSignatureFull (TimeSig (Beats len) (Beats unit)) = case properFraction $ len / unit of
  (numer, 0) -> case properFraction $ 1 / unit of
    (denom, 0) -> do
      denomPow <- logBase2 denom
      Just $ E.MetaEvent $ Meta.TimeSig numer (fromIntegral denomPow + 2) 24 8
    _ -> Nothing
  _ -> Nothing

measureLengthToTimeSig :: Beats -> TimeSig
measureLengthToTimeSig b = let
  d = denominator $ NN.toNumber $ fromBeats b
  in TimeSig b $ 1 / fromIntegral d

-- | Should never happen. Signifies an internal error in the creation of a
-- 'Map.Map': either there was no event at position 0, or the 'Map.Map' contains
-- a 'LookupA' or 'LookupB'.
translationError :: (Show t) => String -> t -> a
translationError f t = error $
  "Sound.MIDI.Util." ++ f ++ ": internal error! couldn't translate position " ++ show t

-- | Converts between positions in musical time and real time.
newtype TempoMap = TempoMap (Map.Map (DoubleKey Beats Seconds) BPS)
  deriving (Eq)

instance Show TempoMap where
  showsPrec p = showsPrec p . tempoMapToBPS

makeTempoMap :: RTB.T Beats E.T -> TempoMap
makeTempoMap = tempoMapFromBPS . RTB.mapMaybe readTempo

unmakeTempoMap :: TempoMap -> RTB.T Beats E.T
unmakeTempoMap = fmap showTempo . tempoMapToBPS

tempoMapFromBPS :: RTB.T Beats BPS -> TempoMap
tempoMapFromBPS = TempoMap . Map.fromAscList . go 0 0 2 where
  go :: Beats -> Seconds -> BPS -> RTB.T Beats BPS -> [(DoubleKey Beats Seconds, BPS)]
  go b s bps rtb = (DoubleKey b s, bps) : case RTB.viewL rtb of
    Nothing                 -> []
    Just ((db, bps'), rtb') -> go (b + db) (s + applyTempo bps db) bps' rtb'

tempoMapToBPS :: TempoMap -> RTB.T Beats BPS
tempoMapToBPS (TempoMap m) = let
  f (DoubleKey bts _, bps) = (bts, bps)
  f _                      = error
    "Sound.MIDI.Util.tempoMapToBPS: internal error! TempoMap key wasn't DoubleKey"
  in RTB.fromAbsoluteEventList $ ATB.fromPairList $ map f $ Map.toAscList m

applyTempoMap :: TempoMap -> Beats -> Seconds
applyTempoMap (TempoMap tm) bts = case Map.lookupLE (LookupA bts) tm of
  Just (DoubleKey b s, bps) -> s + applyTempo bps (bts - b)
  _ -> translationError "applyTempoMap" bts

unapplyTempoMap :: TempoMap -> Seconds -> Beats
unapplyTempoMap (TempoMap tm) secs = case Map.lookupLE (LookupB secs) tm of
  Just (DoubleKey b s, bps) -> b + unapplyTempo bps (secs - s)
  _ -> translationError "unapplyTempoMap" secs

applyTempoTrack :: TempoMap -> RTB.T Beats a -> RTB.T Seconds a
applyTempoTrack tm
  = RTB.fromAbsoluteEventList
  . ATB.mapTime (applyTempoMap tm)
  . RTB.toAbsoluteEventList 0

unapplyTempoTrack :: TempoMap -> RTB.T Seconds a -> RTB.T Beats a
unapplyTempoTrack tm
  = RTB.fromAbsoluteEventList
  . ATB.mapTime (unapplyTempoMap tm)
  . RTB.toAbsoluteEventList 0

-- | Converts between a simple beat position,
-- and a measure offset plus a beat position.
newtype MeasureMap = MeasureMap (Map.Map (DoubleKey Beats Int) TimeSig)
  deriving (Eq)

instance Show MeasureMap where
  showsPrec p = showsPrec p . measureMapToLengths

-- | A number of measures (starting from 0), and an offset within that measure
-- (also starting from 0).
type MeasureBeats = (Int, Beats)

-- | What to do when 'makeMeasureMap' finds a misaligned time signature?
data MeasureMode
  = Error    -- ^ Throw an exception.
  | Ignore   -- ^ Ignore it.
  | Truncate -- ^ Truncate the previous measure.
  deriving (Eq, Ord, Show, Read, Enum, Bounded)

-- | The duration of a number of measures in a given time signature.
measures :: Int -> Beats -> Beats
measures m b = fromIntegral m * b

-- | Computes the measure map, given the tempo track from the MIDI.
makeMeasureMap :: MeasureMode -> RTB.T Beats E.T -> MeasureMap
makeMeasureMap mm = measureMapFromTimeSigs mm . RTB.mapMaybe readSignatureFull

unmakeMeasureMap :: MeasureMap -> RTB.T Beats E.T
unmakeMeasureMap = fmap showSignatureFull' . measureMapToTimeSigs where
  showSignatureFull' tsig = case showSignatureFull tsig of
    Just e  -> e
    Nothing -> error $ "Sound.MIDI.Util.unmakeMeasureMap: couldn't encode time signature " ++ show tsig

measureMapFromTimeSigs :: MeasureMode -> RTB.T Beats TimeSig -> MeasureMap
measureMapFromTimeSigs mm = MeasureMap . Map.fromAscList . go 0 0 (TimeSig 4 1) where
  go :: Beats -> Int -> TimeSig -> RTB.T Beats TimeSig -> [(DoubleKey Beats Int, TimeSig)]
  go b m tsig rtb = (DoubleKey b m, tsig) : case RTB.viewL rtb of
    Nothing                  -> []
    Just ((db, tsig'), rtb') -> case properFraction $ db / timeSigLength tsig of
      (dm, 0           ) -> go (b + db) (m + dm) tsig' rtb'
      (dm, leftoverMsrs) -> case mm of
        Error -> error $ unwords
          [ "makeMeasureMap: misaligned time signature found after"
          , show m
          , "measures and"
          , show $ fromBeats db
          , "beats"
          ]
        Ignore   -> go b m tsig $ RTB.delay db rtb'
        Truncate -> let
          leftoverBeats = measureLengthToTimeSig $ leftoverMsrs * timeSigLength tsig
          truncated = (DoubleKey (b + measures dm (timeSigLength tsig)) (m + dm), leftoverBeats)
          in truncated : go (b + db) (m + dm + 1) tsig' rtb'

measureMapFromLengths :: MeasureMode -> RTB.T Beats Beats -> MeasureMap
measureMapFromLengths mm = measureMapFromTimeSigs mm . fmap measureLengthToTimeSig

measureMapToTimeSigs :: MeasureMap -> RTB.T Beats TimeSig
measureMapToTimeSigs (MeasureMap m) = let
  f (DoubleKey bts _, len) = (bts, len)
  f _                      = error
    "Sound.MIDI.Util.measureMapToLengths: internal error! MeasureMap key wasn't DoubleKey"
  in RTB.fromAbsoluteEventList $ ATB.fromPairList $ map f $ Map.toAscList m

measureMapToLengths :: MeasureMap -> RTB.T Beats Beats
measureMapToLengths = fmap timeSigLength . measureMapToTimeSigs

-- | Uses the measure map to compute which measure a beat position is in.
applyMeasureMap :: MeasureMap -> Beats -> MeasureBeats
applyMeasureMap (MeasureMap mm) bts = case Map.lookupLE (LookupA bts) mm of
  Just (DoubleKey b msr, tsig) -> let
    msrs = floor $ (bts - b) / timeSigLength tsig
    leftover = (bts - b) - fromIntegral msrs * timeSigLength tsig
    in (msr + msrs, leftover)
  _ -> translationError "applyMeasureMap" bts

-- | Uses the measure map to convert a measures+beats position to just beats.
unapplyMeasureMap :: MeasureMap -> MeasureBeats -> Beats
unapplyMeasureMap (MeasureMap mm) (msr, bts) = case Map.lookupLE (LookupB msr) mm of
  Just (DoubleKey b m, tsig) -> b + fromIntegral (msr - m) * timeSigLength tsig + bts
  _ -> translationError "unapplyMeasureMap" (msr, bts)

-- | Combines 'trackTakeZero' and 'trackDropZero'.
trackSplitZero :: (NNC.C t) => RTB.T t a -> ([a], RTB.T t a)
trackSplitZero rtb = case RTB.viewL rtb of
  Just ((dt, x), rtb') | dt == NNC.zero -> case trackSplitZero rtb' of
    (xs, rtb'') -> (x : xs, rtb'')
  _ -> ([], rtb)

-- | Prepends the given events to the event list at position zero.
trackGlueZero :: (NNC.C t) => [a] -> RTB.T t a -> RTB.T t a
trackGlueZero xs rtb = foldr (RTB.cons NNC.zero) rtb xs

-- | Returns the list of events at position zero of the event list.
trackTakeZero :: (NNC.C t) => RTB.T t a -> [a]
trackTakeZero = fst . trackSplitZero

-- | Drops all events at position zero of the event list.
trackDropZero :: (NNC.C t) => RTB.T t a -> (RTB.T t a)
trackDropZero = snd . trackSplitZero

-- | Looks for a track name event at position zero.
trackName :: (NNC.C t) => RTB.T t E.T -> Maybe String
trackName = listToMaybe . mapMaybe readTrackName . trackTakeZero

-- | Removes any existing track name events at position zero and adds a new one.
setTrackName :: (NNC.C t) => String -> RTB.T t E.T -> RTB.T t E.T
setTrackName s rtb = case trackSplitZero rtb of
  (zero, rest) -> let
    zero' = showTrackName s : filter (isNothing . readTrackName) zero
    in trackGlueZero zero' rest

readTrackName :: E.T -> Maybe String
readTrackName (E.MetaEvent (Meta.TrackName s)) = Just s
readTrackName _                                = Nothing

showTrackName :: String -> E.T
showTrackName = E.MetaEvent . Meta.TrackName

-- | Equivalent to 'Control.Monad.join', except 'RTB.T' doesn't have a 'Monad'
-- instance, presumably because 'RTB.merge' has an 'Ord' constraint.
trackJoin :: (NNC.C t, Ord a) => RTB.T t (RTB.T t a) -> RTB.T t a
trackJoin rtb = case RTB.viewL rtb of
  Nothing -> RTB.empty
  Just ((dt, x), rtb') -> RTB.delay dt $ RTB.merge x $ trackJoin rtb'

-- | Combines 'trackTake' and 'trackDrop'.
trackSplit :: (NNC.C t) => t -> RTB.T t a -> (RTB.T t a, RTB.T t a)
trackSplit t rtb = case RTB.viewL rtb of
  Nothing -> (RTB.empty, RTB.empty)
  Just ((dt, x), rtb') -> case NNC.split t dt of
    (_, (True , d)) {- t <= dt -} -> (RTB.empty, RTB.cons d x rtb')
    (_, (False, d)) {- t >  dt -} -> case trackSplit d rtb' of
      (taken, dropped) -> (RTB.cons dt x taken, dropped)

-- | Drops all events at or after the given time from the event list.
trackTake :: (NNC.C t) => t -> RTB.T t a -> RTB.T t a
trackTake t rtb = fst $ trackSplit t rtb

-- | Drops the given amount of time from the start of the event list.
-- Events that are exactly at the time that is dropped will be kept in the list.
trackDrop :: (NNC.C t) => t -> RTB.T t a -> RTB.T t a
trackDrop t rtb = snd $ trackSplit t rtb

-- | Finds and extracts the first event for which the function returns 'Just'.
extractFirst :: (NNC.C t) => (a -> Maybe b) -> RTB.T t a -> Maybe ((t, b), RTB.T t a)
extractFirst f rtb = do
  ((dt, x), rtb') <- RTB.viewL rtb
  case f x of
    Just y  -> return ((dt, y), RTB.delay dt rtb')
    Nothing -> do
      ((dt_, y_), rtb_) <- extractFirst f rtb'
      return ((NNC.add dt dt_, y_), RTB.cons dt x rtb_)