{-# LANGUAGE KindSignatures #-}
{-# LANGUAGE FunctionalDependencies #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE DeriveFunctor #-}

module Fadno.Note
    (Note(..),pitch,dur
    ,HasNote(..),toPair,(|:)
    ,Mono(..),maybeMono,mono,mono',mPitch,unMono,catMonos,_M
    ,rest,isRest
    ,Spelling(..),fromChroma,toChroma,spelling
    ,PitchRep(..),prPitch,prOctave,(@:),pitchRep
    ,sumDurs,mapTime
    ,tied,tied',legato,legato',merge
    ,transpose,transpose'
    ,(%)
    )
where

import Control.Lens
import Control.Arrow
import Data.Ratio
import GHC.Generics (Generic)
import Data.Traversable
import Data.Function
import Data.Foldable

-- | Note = pitch and duration.
data Note p d = Note { _pitch :: p, _dur :: d }
                deriving (Eq,Generic)
$(makeLenses ''Note)

instance (Show p, Show d) => Show (Note p d) where
    show (Note p d) = show p ++ "|:" ++ show d
instance Bifunctor Note where
    bimap f g (Note a b) = Note (f a) (g b)
instance Field1 (Note a b) (Note a' b) a a'
instance Field2 (Note a b) (Note a b') b b'

-- | Hand-rolled class providing monomorphic lenses.
class HasNote s p d | s -> p d where
  note :: Lens' s (Note p d)
  fromNote :: (HasNote n p d) => n -> s
  notePitch :: Lens' s p
  notePitch = note.pitch
  noteDur :: Lens' s d
  noteDur = note.dur
instance HasNote (Note p d) p d where
    note = ($)
    fromNote = view note

-- iso with pair
toPair :: Iso' (Note p d) (p,d)
toPair = iso (\(Note p d) -> (p,d)) (uncurry Note)


infixl 5 |:
-- | 'Note' smart constructor.
(|:) :: p -> d -> Note p d
(|:) = Note

-- | Monophonic pitch functor, i.e. Maybe with a sum monoid.
data Mono p = Rest | M { _mPitch :: p }
    deriving (Eq,Ord,Functor)
instance (Show p)=>Show (Mono p) where
    show Rest = "Rest"
    show (M p) = "M " ++ show p
makeLenses ''Mono
makePrisms ''Mono
instance Num p => Monoid (Mono p) where
    mempty = Rest
    mappend Rest b = b
    mappend a Rest = a
    mappend (M a) (M b) = M (a + b)
-- | Mono/Maybe isomorphism.
maybeMono :: Iso' (Maybe a) (Mono a)
maybeMono = iso toMono toMaybe
    where toMono Nothing = Rest
          toMono (Just a) = M a
          toMaybe Rest = Nothing
          toMaybe (M a) = Just a

-- | Mono 'HasNote'
mono :: HasNote n (Mono p) d => p -> d -> n
mono p = fromNote . mono' p

-- | Mono 'Note'.
mono' :: p -> d -> Note (Mono p) d
mono' p = Note (M p)

-- | Mono eliminator
unMono :: b -> (a -> b) -> Mono a -> b
unMono b _ Rest = b
unMono _ f (M a) = f a

-- | cf 'catMaybe'. Grab all non-rest values.
catMonos :: Foldable f => f (Mono a) -> [a]
catMonos = foldMap (unMono [] pure)

-- | 'Note' from duration, given 'Monoid' pitch.
-- Interoperates with 'chord' and 'mono'.
-- Useful for batch duration conversion.
rest :: (HasNote n p d, Monoid p) => d -> n
rest = fromNote . rest'

rest' :: Monoid p => d -> Note p d
rest' = Note mempty

isRest :: (Monoid p, Eq p, HasNote n p d) => n -> Bool
isRest = (mempty ==) . view notePitch




-- | Chroma as enharmonic names.
data Spelling = C|Cs|Db|D|Ds|Eb|E|F|Fs|Gb|G|Gs|Ab|A|As|Bb|B
            deriving (Eq,Show,Read,Enum,Ord,Bounded,Generic)

-- | Convert to 'Spelling' with 0==C, using 'Cs','Eb','Fs','Gs','Bb' enharmonics.
fromChroma :: Integral a => a -> Spelling
fromChroma 0 = C
fromChroma 1 = Cs
fromChroma 2 = D
fromChroma 3 = Eb
fromChroma 4 = E
fromChroma 5 = F
fromChroma 6 = Fs
fromChroma 7 = G
fromChroma 8 = Gs
fromChroma 9 = A
fromChroma 10 = Bb
fromChroma 11 = B
fromChroma n | n > 11 = fromChroma $ n `mod` 12
             | otherwise = fromChroma $ n `mod` 12 + 12

-- | 'Spelling' to 0-11.
toChroma :: Integral a => Spelling -> a
toChroma C = 0
toChroma Cs = 1
toChroma Db = 1
toChroma D = 2
toChroma Ds = 3
toChroma Eb = 3
toChroma E = 4
toChroma F = 5
toChroma Fs = 6
toChroma Gb = 6
toChroma G = 7
toChroma Gs = 8
toChroma Ab = 8
toChroma A = 9
toChroma As = 10
toChroma Bb = 10
toChroma B = 11

-- | 'Spelling'-to-chroma degenerate 'Iso'.
spelling :: Integral a => Iso' a Spelling
spelling = iso fromChroma toChroma

-- | Represent pitch as chroma and octave.
-- It's a full 'Num', 'Integral' instance, so negative octave values OK.
-- Instances use C4 == 60.
data PitchRep = PitchRep { _prPitch :: Spelling, _prOctave :: Int  }
              deriving (Eq,Bounded,Generic)
instance Show PitchRep where show (PitchRep s o) = show s ++ "@:" ++ show o
$(makeLenses ''PitchRep)

infixl 6 @:
(@:) :: Spelling -> Int -> PitchRep
(@:) = PitchRep

instance Num PitchRep where
    fromInteger i = fromChroma i @: ((fromIntegral i `div` 12) - 1)
    a * b = fromIntegral (toInteger a * toInteger b)
    a + b = fromIntegral (toInteger a + toInteger b)
    abs = fromIntegral . abs . toInteger
    signum = fromIntegral . signum . toInteger
    negate = fromIntegral . negate . toInteger

instance Enum PitchRep where
    toEnum = fromInteger . fromIntegral
    fromEnum = fromIntegral . toInteger

instance Ord PitchRep where
    a <= b = fromIntegral a <= fromIntegral b

instance Real PitchRep where
    toRational (PitchRep s o) = (((fromIntegral o + 1) * 12) + toChroma s) % 1

instance Integral PitchRep where
    toInteger = truncate . toRational
    a `quotRem` b = (fromInteger *** fromInteger) (toInteger a `quotRem` toInteger b)

-- | Iso to integrals.
pitchRep :: Integral a => Iso' a PitchRep
pitchRep = iso fromIntegral (fromIntegral . toInteger)


--
-- Utilities
--

-- | compute total duration of notes
sumDurs :: (Num d, HasNote a p d, Traversable t) => t a -> d
sumDurs = sumOf (traverse.noteDur)

-- | map notes to arrival time
mapTime :: (Num d, Ord d, HasNote a p d, Traversable t) => t a -> [(d,a)]
mapTime = toList . snd .
          mapAccumL (\t n -> (t + view noteDur n,(t,n))) 0

-- | merge same-pitch notes
tied :: (Eq p,Num d,HasNote a p d,Traversable t,
          Traversable u,Snoc (u a) (u a) a a,Monoid (u a)) => t a -> u a
tied = merge ((==) `on` view notePitch)

tied' :: (Eq p,Num d,HasNote a p d,Traversable t) => t a -> [a]
tied' = tied

-- | merge rests with prior note
legato :: (Eq p,Monoid p,Num d,HasNote a p d,Traversable t,
          Traversable u,Snoc (u a) (u a) a a,Monoid (u a)) => t a -> u a
legato = merge $ \_ n -> view notePitch n == mempty

legato' :: (Eq p,Monoid p,Num d,HasNote a p d,Traversable t) => t a -> [a]
legato' = legato


-- | merge notes meeting some comparison by accumulating durations
merge :: (Num d,HasNote a p d,Traversable t,
          Traversable u,Snoc (u a) (u a) a a,Monoid (u a)) => (a -> a -> Bool) -> t a -> u a
merge cmp = acc mempty . toListOf traverse where
    acc rs [] = rs
    acc (rs :> r) (n:ns) | cmp r n = acc (rs |> over noteDur (+ view noteDur n) r) ns
    acc rs (n:ns) = acc (rs |> n) ns


-- | Pitch addition
transpose :: (Num p,HasNote a p d,Traversable t) => p -> t a -> t a
transpose by = over (traverse.notePitch) (+by)

-- | Pitch addition over a functor
transpose' :: (Num p,Functor f, HasNote a (f p) d,Traversable t) => p -> t a -> t a
transpose' by = over (traverse.notePitch.mapped) (+by)