{-# LANGUAGE TupleSections #-}
{-# LANGUAGE ImpredicativeTypes #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE DeriveFoldable #-}
{-# LANGUAGE DeriveTraversable #-}
{-# LANGUAGE DeriveFunctor #-}
{-# LANGUAGE FunctionalDependencies #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE ScopedTypeVariables #-}

module Fadno.Notation where

import GHC.Generics
import Data.String
import Data.Default
import Fadno.Note
import Control.Lens hiding (pre)
import Data.Typeable
import Data.Ratio
import Data.Sequence (Seq,fromList)
import Data.Foldable
import Test.HUnit
import Data.List
import Data.Maybe


-- valid time sig denoms
data Quanta = Q2|Q4|Q8|Q16|Q32|Q64
               deriving (Eq,Show,Ord,Enum,Bounded,Typeable)

qToInt :: Quanta -> Int
qToInt = (2^) . succ . fromEnum
qFromInt :: Integral i => i -> Maybe Quanta
qFromInt = fmap toEnum . (`elemIndex` [2,4,8,16,32,64])


data TimeSignature = TimeSignature { _tsLength :: Int, _tsUnit :: Quanta }
   deriving (Eq,Ord)
instance Show TimeSignature where
    show (TimeSignature l u) = show l ++ "/:" ++ show u
makeLenses ''TimeSignature
class HasTimeSignature a where timeSignature :: Lens' a (Maybe TimeSignature)

(/:) :: Int -> Quanta -> TimeSignature
(/:) = TimeSignature

-- PPQ: valid midi divisions, named after equivalent Quantum
-- as in, "1 means ..."; PQ4 is "1 means quarter note"
data PPQ = PQ4|PQ8|PQ16|PQ32|PQ64|PQ128|PQ256
         deriving (Eq,Show,Ord,Enum,Bounded)


-- convert to midi division value
ppqDiv :: Integral a => PPQ -> a
ppqDiv = (2^) . fromEnum


-- Compute duration of TS
tsToRatio :: TimeSignature -> Rational
tsToRatio (TimeSignature n d) = fromIntegral n % fromIntegral (qToInt d)

-- Derive TS from duration, with 1 denominator implying Q4
tsFromRatio :: Rational -> Maybe TimeSignature
tsFromRatio r = toTs (if d == 1 then 4 else (if n == 1 then 2 else 1))
    where toTs m = (fromIntegral (n * m) /:) <$> qFromInt (d * m)
          d = denominator r
          n = numerator r


tsFromRatio' :: TimeSignature -> Rational -> Maybe TimeSignature
tsFromRatio' (TimeSignature _ src) = fmap adjust . tsFromRatio where
    adjust t@(TimeSignature n d) | src <= d = t
                                 | otherwise = (n * m) /: src
                                 where qd = qToInt d
                                       qs = qToInt src
                                       m = qs `div` qd


-- | Duration iso, from Integral to Rational, given PPQ
ratioPPQ :: Integral a => PPQ -> Iso' a Rational
ratioPPQ p = iso toRat toInt where
    ppq4 = ppqDiv p * 4
    toRat i = fromIntegral i % fromIntegral ppq4
    toInt r = truncate (r * toRational ppq4)

-- | Adapt a type to its HasXXX "Maybe Lens'"
adaptHas :: Lens' a (Maybe a)
adaptHas f s = fromMaybe s <$> f (Just s)

-- | Adapt a non-Maybe lens to the HasXXX "Maybe Lens'"
adaptHasLens :: Lens' s a -> Lens' s (Maybe a)
adaptHasLens l f s = fmap (maybe s (\a -> set l a s)) (f (Just (view l s)))

-- | Adapt a type that does NOT support the HasXXX feature.
adaptHasNot :: Lens' s (Maybe a)
adaptHasNot f s = fmap (const s) (f Nothing)


-- | Tied notes.
data Tie = TStart | TStop | TBoth
    deriving (Eq,Bounded,Enum,Ord,Show)
makeLenses ''Tie
class HasTie a where tie :: Lens' a (Maybe Tie)
instance HasTie Tie where tie = adaptHas
instance HasTie (Note p d) where tie = adaptHasNot


-- | Slurred notes.
data Slur = SStart | SStop
    deriving (Eq,Bounded,Enum,Ord,Show)
makeLenses ''Slur
class HasSlur a where slur :: Lens' a (Maybe Slur)
instance HasSlur Slur where slur = adaptHas

-- | Note articulations.
data Articulation = Staccato | Accent
    deriving (Eq,Show,Bounded,Enum,Ord)
class HasArticulation a where articulation :: Lens' a (Maybe Articulation)
instance HasArticulation Articulation where articulation = adaptHas


-- | Bar rehearsal mark.
newtype RehearsalMark = RehearsalMark { _rehearsalText :: String }
    deriving (Eq,Ord,IsString,Generic,Monoid,Default)
makeLenses ''RehearsalMark
instance Show RehearsalMark where show = show . _rehearsalText
class HasRehearsalMark a where rehearsalMark :: Lens' a (Maybe RehearsalMark)
instance HasRehearsalMark RehearsalMark where rehearsalMark = adaptHas

-- | Musical direction.
newtype Direction = Direction { _directionText :: String }
    deriving (Eq,Ord,IsString,Generic,Monoid,Default)
makeLenses ''Direction
instance Show Direction where show = show . _directionText
class HasDirection a where direction :: Lens' a (Maybe Direction)
instance HasDirection Direction where direction = adaptHas

-- | Barline.
data Barline = Double | Final
    deriving (Eq,Show,Ord,Generic)
class HasBarline a where barline :: Lens' a (Maybe Barline)
instance HasBarline Barline where barline = adaptHas

data Repeats = RStart | REnd | RBoth
    deriving (Eq,Show,Ord,Generic)
class HasRepeats a where repeats :: Lens' a (Maybe Repeats)
instance HasRepeats Repeats where repeats = adaptHas

data Clef = TrebleClef | BassClef | AltoClef | PercClef
    deriving (Eq,Show,Ord,Generic)
makeLenses ''Clef
class HasClef a where clef :: Lens' a (Maybe Clef)
instance HasClef Clef where clef = adaptHas


-- | Part identifier, prefers 'Num' or 'IsString' values.
newtype Part a = Part { _partIdx :: a }
    deriving (Eq,Generic,Ord,Functor,Bounded,Foldable,Traversable,Real,Num,IsString)
makeLenses ''Part
instance (Show a) => Show (Part a) where show = show._partIdx
class HasPart a b | a -> b where part :: Lens' a (Maybe (Part b))

-- | Lensy show of a Maybe field, given a 'Getter' and its name.
mshow :: (Show a) => Getter s (Maybe a) -> String -> s -> String
mshow l n = maybe "" (\v -> " & " ++ n ++ " ?~ " ++ show v) . view l

-- | 'concatMap' show functions with a prelude.
mshows :: s -> String -> [s -> String] -> String
mshows s pre = (pre ++) . concatMap ($ s)

-- Example types.

-- | Note with notations.
data Note' p d = Note' {
      _nNote :: Note p d
    , _nTie :: Maybe Tie
    , _nSlur :: Maybe Slur
    , _nArticulation :: Maybe Articulation
    } deriving (Eq,Generic)

makeLenses ''Note'
instance HasNote (Note' p d) p d where
    note = nNote
    fromNote = note' . view note
instance HasTie (Note' p d) where tie = nTie
instance HasSlur (Note' p d) where slur = nSlur
instance HasArticulation (Note' p d) where articulation = nArticulation
instance (Show p, Show d) => Show (Note' p d) where
    show n = mshows n ("note' (" ++ show (view nNote n) ++ ")")
             [mshow tie "tie"
             ,mshow slur "slur"
             ,mshow articulation "articulation"
             ]


-- | Note smart ctor, used in 'Show'.
note' :: Note p d -> Note' p d
note' n = Note' n Nothing Nothing Nothing

testNote :: Note' [Int] Int
testNote = note' ([60]|:2) & tie ?~ TStart & articulation ?~ Accent

-- | Bar as list of notes, with notations.
data Bar n = Bar {
      _bNotes :: Seq n
    , _bRehearsalMark :: Maybe RehearsalMark
    , _bDirection :: Maybe Direction
    , _bBarline :: Maybe Barline
    , _bRepeats :: Maybe Repeats
    , _bTimeSignature :: Maybe TimeSignature
    , _bClef :: Maybe Clef
    } deriving (Eq,Generic,Functor,Foldable,Traversable)
makeLenses ''Bar
instance Default (Bar n) where def = bar []
instance Snoc (Bar n) (Bar n) n n where
    _Snoc = prism (\(b,n) -> over bNotes (review _Snoc . (,n)) b) $
            \b -> case firstOf _Snoc (view bNotes b) of
                    Nothing -> Left (def :: Bar n)
                    (Just (as,a)) -> Right (set bNotes as b,a)
instance Cons (Bar n) (Bar n) n n where
    _Cons = prism (\(n,b) -> over bNotes (review _Cons . (n,)) b) $
            \b -> case firstOf _Cons (view bNotes b) of
                    Nothing -> Left (def :: Bar n)
                    (Just (a,as)) -> Right (a,set bNotes as b)
instance HasRehearsalMark (Bar n) where rehearsalMark = bRehearsalMark
instance HasDirection (Bar n) where direction = bDirection
instance HasBarline (Bar n) where barline = bBarline
instance HasTimeSignature (Bar n) where timeSignature = bTimeSignature
instance HasClef (Bar n) where clef = bClef
instance HasRepeats (Bar n) where repeats = bRepeats
instance (Show n) => Show (Bar n) where
    show b = mshows b ("bar " ++ show (toList $ view bNotes b))
             [mshow rehearsalMark "rehearsalMark"
             ,mshow direction "direction"
             ,mshow barline "barline"
             ,mshow repeats "repeat"
             ,mshow timeSignature "timeSignature"
             ,mshow clef "clef"
             ]
instance Monoid (Bar n) where
    mempty = def
    a `mappend` b = over bNotes (`mappend` view bNotes b) a

-- | Bar smart ctor, used in 'Show'.
bar :: [n] -> Bar n
bar ns = Bar (fromList ns) Nothing Nothing Nothing Nothing Nothing Nothing

testBar :: Bar (Note [Int] Int)
testBar = bar [[60]|:2,[62]|:1] & timeSignature ?~ TimeSignature 4 Q4 & direction ?~ "Softly"