{-# OPTIONS_GHC -Wall           #-}
{-# LANGUAGE FlexibleContexts   #-}

--------------------------------------------------------------------------------
-- |
-- Module      :  HarmTrace.Base.Parse.ChordParser
-- Copyright   :  (c) 2012--2016, Chordify BV
-- License     :  LGPL-3
--
-- Maintainer  :  haskelldevelopers@chordify.net
-- Stability   :  experimental
-- Portability :  non-portable
--
-- Summary: Some general parsing utilities used for parsing textual chord
-- representations.
--------------------------------------------------------------------------------

module HarmTrace.Base.Parse.ChordParser (
  -- * Parsing (elements of) chords
    pChord
  , pShorthand
  , pRoot
  , pAdditions
  , pAddition
  , pKey
  , pBeat
  ) where

import HarmTrace.Base.Parse.General
import HarmTrace.Base.Chord
import HarmTrace.Base.Time

--------------------------------------------------------------------------------
-- Parsing String of Musical Chords
--------------------------------------------------------------------------------

-- | Parses a 'ChordLabel' in Harte et al. syntax including possible additions,
-- and removal of chord additions. If a chord has no 'Shorthand', the 'Degree'
-- list (if any) is analysed and depending on the 'Triad' (if any) a
-- 'Maj', 'Min','Aug', or 'Dim' 'Shorthand' is stored. By default all the
-- duration stored in every 'Chord' is 1 (where the unit is application
-- depended, often these are beats, but they can also be eighth notes)
pChord :: Parser ChordLabel
{-# INLINE pChord #-}
pChord =     pChordLabel
         <|> (NoChord    <$ (pString "N"  <|> pString "&pause"))
         <|> (UndefChord <$ (pSym '*'     <|> pSym 'X'))
         <?> "Chord"

-- Parses a chord label
-- TODO add support for inversion
pChordLabel :: Parser ChordLabel
{-# INLINE pChordLabel #-}
pChordLabel = mkChord <$> pRoot <* (pSym ':' `opt` ':')
                      <*> pMaybe pShorthand
                      <*> (pAdditions `opt` [])
                      <*> pInversion where

  mkChord :: Root -> Maybe Shorthand -> [Addition] -> Maybe Interval
          -> ChordLabel
  -- if there are no degrees and no shorthand, following Harte it
  -- should be labelled a Maj chord
  mkChord r Nothing [] b = Chord   r Maj             [] (inversion b)
  mkChord r Nothing  a b = toChord r (addToIntSet a)    b
  mkChord r (Just s) a b = Chord   r s               a  (inversion b)

  -- prepares an inversion, if any
  inversion :: Maybe Interval -> Interval
  inversion = maybe (Note Nat I1) id

-- Parses an inversion, but inversions are ignored for now.
pInversion :: Parser (Maybe Interval)
pInversion = pMaybe (pSym '/' *> pIntNote) <?> "/Inversion"

-- | parses a musical key description, e.g. @C:maj@, or @D:min@
pKey :: Parser Key
pKey = f <$> pRoot <* pSym ':' <*> pShorthand <?> "Key"
  where f r m | m == Maj = Key r MajMode
              | m == Min = Key r MinMode
              | otherwise = error ("Tokenizer: key must be Major or Minor, "
                          ++ "found: " ++ show m)

-- | Parses a shorthand following Harte et al. syntax, but also the 'Shorthand's
-- added to the Billboard dataset, e.g. @maj@, @min@, or @9@.
pShorthand :: Parser Shorthand
{-# INLINE pShorthand #-}
pShorthand =     Maj      <$ pString "maj"
             <|> Min      <$ pString "min"
             <|> Dim      <$ pString "dim"
             <|> Aug      <$ pString "aug"
             <|> Maj7     <$ pString "maj7"
             <|> Min7     <$ pString "min7"
             <|> Sev      <$ pString "7"
             <|> Dim7     <$ pString "dim7"
             <|> HDim7    <$ pString "hdim" <* opt (pSym '7') '7'
             <|> MinMaj7  <$ pString "minmaj7"
             <|> Aug7     <$ pString "aug7"
             <|> Maj6     <$ pString "maj6"
             <|> Maj6     <$ pString "6"
             <|> Min6     <$ pString "min6"
             <|> Nin      <$ pString "9"
             <|> Maj9     <$ pString "maj9"
             <|> Min9     <$ pString "min9"
             <|> Five     <$ pString "5"
             <|> Sus2     <$ pString "sus2"
             <|> Sus4     <$ pString "sus4"
             <|> SevSus4  <$ pString "7sus4"
             -- additional Billboard shorthands
             <|> Min11    <$ pString "min11"
             <|> Min13    <$ pString "min13"
             <|> Maj13    <$ pString "maj13"
             <|> Eleven   <$ pString "11"
             <|> Thirteen <$ pString "13"
             <|> None     <$ pString "1" -- no shorthand: used in billboard to
                                         -- denote a rootnote only
             <?> "Shorthand"

-- | Parses a list of 'Chord' 'Addition's within parenthesis
pAdditions :: Parser [Addition]
pAdditions = pPacked (pSym '(') (pSym ')') ( pListSep (pSym ',') pAddition )
             <?> "Addition List"

-- | Parses the a 'Chord' 'Addition' (or the removal of a chord addition,
-- prefixed by  a @*@)
pAddition :: Parser Addition
pAddition = (Add   <$>             pIntNote)
        <|> (NoAdd <$> (pSym '*'*> pIntNote))
        <?> "Addition"

-- | Parses an 'Interval'
pIntNote :: Parser Interval
pIntNote = Note <$> pAccidental <*> pInterval

-- | Parses in 'Accidental'
pAccidental :: Parser Accidental
pAccidental =    Sh <$ pSym    's'
             <|> Sh <$ pSym    '#'
             <|> Fl <$ pSym    'b'
             <|> SS <$ pString "ss"
             <|> SS <$ pString "##"
             <|> FF <$ pString "bb"
             <|> pure Nat <?> "Accidental"

-- | Parses an 'Interval'
pInterval :: Parser IntNat
pInterval =  foldr (<|>) pFail opts <?> "Interval" where
  opts = [i <$ pString (show i) | i <- [minBound..] ]

-- | Parses a 'Root' 'Note', e.g. @A@, @Bb@, or @F#@.
pRoot :: Parser Root
{-# INLINE pRoot #-}
pRoot = (flip Note) <$> pDiaNat <*> pAccidental

pDiaNat :: Parser DiatonicNatural
{-# INLINE pDiaNat #-}
pDiaNat =    A  <$ pSym 'A'
         <|> B  <$ pSym 'B'
         <|> C  <$ pSym 'C'
         <|> D  <$ pSym 'D'
         <|> E  <$ pSym 'E'
         <|> F  <$ pSym 'F'
         <|> G  <$ pSym 'G'

-- | Parses a 'Beat'.
pBeat :: Parser Beat
pBeat =   One    <$ pSym '1'
      <|> Two    <$ pSym '2'
      <|> Three  <$ pSym '3'
      <|> Four   <$ pSym '4'
      <|> NoBeat <$ pSym 'x'
      <?> "Beat"