{-# LANGUAGE ConstraintKinds            #-}
{-# LANGUAGE DeriveDataTypeable         #-}
{-# LANGUAGE DeriveFoldable             #-}
{-# LANGUAGE DeriveFunctor              #-}
{-# LANGUAGE FlexibleContexts           #-}
{-# LANGUAGE FlexibleInstances          #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE NoMonomorphismRestriction  #-}
{-# LANGUAGE TypeFamilies               #-}
{-# LANGUAGE ViewPatterns               #-}

-------------------------------------------------------------------------------------
-- |
-- Copyright   : (c) Hans Hoglund 2012
--
-- License     : BSD-style
--
-- Maintainer  : hans@hanshoglund.se
-- Stability   : experimental
-- Portability : non-portable (TF,GNTD)
--
-- Provides conversion betwen temporal container types.
--
-------------------------------------------------------------------------------------


module Music.Score.Convert (
        -- * Conversion
        noteToVoice,
        noteToScore,
        -- scoreToNotes,
        -- notesToScore,
        voiceToScore,
        voicesToScore,
        trackToScore,
        trackToScore',
        scoreToVoice,
        reactiveToVoice,
        reactiveToVoice',
        noteToReactive,
        splitReactive,
        activate,
  ) where

import           Control.Applicative
import           Control.Lens
import           Control.Monad
import           Control.Monad.Plus
import           Data.AffineSpace
import           Data.AffineSpace.Point
import           Data.Foldable          (Foldable (..))
import           Data.Ord
import           Data.Ratio
import           Data.Semigroup
import           Data.String
import           Data.Traversable
import           Data.VectorSpace

import           Music.Score.Note
import           Music.Score.Part
import           Music.Score.Score
import           Music.Score.Track
import           Music.Score.Voice
import           Music.Time
import           Music.Time.Reactive

import qualified Data.Foldable          as Foldable
import qualified Data.List              as List


-- | Convert a note to an onset and a voice.
noteToVoice :: Note a -> (Time, Voice a)
noteToVoice (getNote -> (s,x)) = (onset s, stretchTo (duration s) $ return x)

-- | Convert a note to a score.
noteToScore :: Note a -> Score a
noteToScore (getNote -> (s,x)) = s `sapp` return x

-- scoreToNotes :: Score a -> [Note a]
-- scoreToNotes = Foldable.toList . reifyScore

-- notesToScore :: [Note a] -> Score a
-- notesToScore = pcat . fmap noteToScore

reactiveToVoice :: Duration -> Reactive a -> Voice a
reactiveToVoice d r = (^. voice) $ durs `zip` (fmap (r ?) times)
    where
        times = origin : filter (\t -> origin < t && t < origin .+^ d) (occs r)
        durs  = toRelN' (origin .+^ d) times

reactiveToVoice' :: Span -> Reactive a -> Voice a
reactiveToVoice' (view range -> (u,v)) r = (^. voice) $ durs `zip` (fmap (r ?) times)
    where
        times = origin : filter (\t -> u < t && t < v) (occs r)
        durs  = toRelN' v times

-- |
-- Convert a score to a voice. Fails if the score contain overlapping events.
--
scoreToVoice :: Score a -> Voice (Maybe a)
scoreToVoice = (^. voice) . fmap throwTime . addRests . (^. events)
    where
       throwTime (t,d,x) = (d,x)
       addRests = concat . snd . mapAccumL g origin
           where
               g u (t, d, x)
                   | u == t    = (t .+^ d, [(t, d, Just x)])
                   | u <  t    = (t .+^ d, [(u, t .-. u, Nothing), (t, d, Just x)])
                   | otherwise = error "addRests: Strange prevTime"


-- |
-- Convert a voice to a score.
--
voiceToScore :: Voice a -> Score a
voiceToScore = scat . fmap g . (^. from voice)
    where
        g (d,x) = stretch d (return x)

-- | Join voices in a given part into a score.
voicesToScore :: HasPart a => [(Part a, Voice a)] -> Score a
voicesToScore = pcat . fmap (voiceToScore . uncurry (\n -> fmap (setPart n)))

-- |
-- Convert a voice which may contain rests to a score.
--
voiceToScore' :: Voice (Maybe a) -> Score a
voiceToScore' = mcatMaybes . voiceToScore

-- |
-- Convert a track to a score where each event is given a fixed duration.
--
trackToScore :: Duration -> Track a -> Score a
trackToScore x = trackToScore' (const x)

-- |
-- Convert a track to a score, using durations determined by the values.
--
trackToScore' :: (a -> Duration) -> Track a -> Score a
trackToScore' f = (^. from events) . fmap (\(t,x) -> (t,f x,x)) . (^. from track)


-- Convert to delta (time to wait before this note)
toRel :: [Time] -> [Duration]
toRel = snd . mapAccumL g origin where g prev t = (t, t .-. prev)

-- Convert to delta (time to wait before next note)
toRelN :: [Time] -> [Duration]
toRelN [] = []
toRelN xs = snd $ mapAccumR g (last xs) xs where g prev t = (t, prev .-. t)

-- Convert to delta (time to wait before next note)
toRelN' :: Time -> [Time] -> [Duration]
toRelN' end xs = snd $ mapAccumR g end xs where g prev t = (t, prev .-. t)

-- 0 x,1 x,1 x,1 x
  -- x 1,x 1,x 1,x 0

-- Convert from delta (time to wait before this note)
toAbs :: [Duration] -> [Time]
toAbs = snd . mapAccumL g origin where g now d = (now .+^ d, now .+^ d)


-- TODO rename during
noteToReactive :: Monoid a => Note a -> Reactive a
noteToReactive n = (pure <$> n) `activate` pure mempty

-- | Split a reactive into notes, as well as the values before and after the first/last update
splitReactive :: Reactive a -> Either a ((a, Time), [Note a], (Time, a))
splitReactive r = case updates r of
    []          -> Left  (initial r)
    (t,x):[]    -> Right ((initial r, t), [], (t, x))
    (t,x):xs    -> Right ((initial r, t), fmap note $ mrights (res $ (t,x):xs), head $ mlefts (res $ (t,x):xs))

    where

        note (t,u,x) = t <-> u =: x

        -- Always returns a 0 or more Right followed by one left
        res :: [(Time, a)] -> [Either (Time, a) (Time, Time, a)]
        res rs = let (ts,xs) = unzip rs in
            flip fmap (withNext ts `zip` xs) $
                \ ((t, mu), x) -> case mu of
                    Nothing -> Left (t, x)
                    Just u  -> Right (t, u, x)

        -- lenght xs == length (withNext xs)
        withNext :: [a] -> [(a, Maybe a)]
        withNext = go
            where
                go []       = []
                go [x]      = [(x, Nothing)]
                go (x:y:rs) = (x, Just y) : withNext (y : rs)

activate :: Note (Reactive a) -> Reactive a -> Reactive a
activate (getNote -> (view range -> (start,stop), x)) y = y `turnOn` (x `turnOff` y)
    where
        turnOn  = switch start
        turnOff = switch stop