{-# LANGUAGE DeriveDataTypeable         #-}
{-# LANGUAGE FlexibleInstances          #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE StandaloneDeriving         #-}
{-# LANGUAGE TypeFamilies               #-}

------------------------------------------------------------------------------------
-- |
-- Copyright   : (c) Hans Hoglund, Edward Lilley 2012–2014
--
-- License     : BSD-style
--
-- Maintainer  : hans@hanshoglund.se
-- Stability   : experimental
-- Portability : non-portable (TF,GNTD)
--
-- Provides a standard pitch representation.
--
-------------------------------------------------------------------------------------

module Music.Pitch.Common.Pitch (
        -- * Accidentals
        Accidental,
        natural,
        flat,
        sharp,
        doubleFlat,
        doubleSharp,

        -- ** Inspecting accidentals
        isNatural,
        isFlattened,
        isSharpened,
        isStandardAccidental,

        -- ** Name
        Name(..),

        -- * Pitch
        Pitch,
        mkPitch,
        name,
        accidental,

        -- ** Diatonic and chromatic pitch
        upDiatonicP,
        downDiatonicP,
        upChromaticP,
        downChromaticP,
        invertDiatonicallyP,
        invertChromaticallyP,
        
        -- ** Utility
        asPitch
  ) where

import           Control.Applicative
import           Control.Monad
import           Control.Lens hiding (simple)
import           Data.AffineSpace
import           Data.AffineSpace.Point
import qualified Data.Char                    as Char
import           Data.Either
import qualified Data.List                    as List
import           Data.Maybe
import           Data.Semigroup
import           Data.Typeable
import           Data.VectorSpace

import           Music.Pitch.Absolute
import           Music.Pitch.Alterable
import           Music.Pitch.Augmentable
import           Music.Pitch.Common.Number
import           Music.Pitch.Common.Interval
import           Music.Pitch.Common.Semitones
import           Music.Pitch.Common.Chromatic
import           Music.Pitch.Common.Diatonic
import           Music.Pitch.Literal

-- |
-- An accidental is either flat, natural or sharp.
--
-- This representation allows for an arbitrary number of flats or sharps rather than just
-- single and double.
--
-- The 'Num' and 'Enum' instances treat 'Accidental' as the number of altered semitones,
-- i.e. a double flat is @-2@, natural @0@ and so on.
--
newtype Accidental = Accidental { getAccidental :: Integer }
  deriving (Eq, Ord, Num, Enum, Real, Integral)

instance Show Accidental where
  show n | n == 0    = "natural"
         | n == 1    = "sharp"
         | n == (-1) = "flat"
         | n == 2    = "doubleSharp"
         | n == (-2) = "doubleFlat"
         | n > 0     = "sharp * " ++ show (getAccidental n)
         | n < 0     = "flat * " ++ show (negate $ getAccidental n)

instance Alterable Accidental where
  sharpen = succ
  flatten = pred

-- |
-- Magic instance that allow us to write @c sharp@ instead of @sharpen c@.
--
instance (IsPitch a, Alterable a) => IsPitch (Accidental -> a) where
  fromPitch l 1     = sharpen (fromPitch l)
  fromPitch l (-1)  = flatten (fromPitch l)
-- Requires FlexibleInstances

sharp, flat, natural, doubleFlat, doubleSharp :: Accidental

-- | The double sharp accidental.
doubleSharp = 2

-- | The sharp accidental.
sharp       = 1

-- | The natural accidental.
natural     = 0

-- | The flat accidental.
flat        = -1

-- | The double flat accidental.
doubleFlat  = -2

isNatural, isSharpened, isFlattened :: Accidental -> Bool

-- | Returns whether this is a natural accidental.
isNatural   = (== 0)

-- | Returns whether this is a sharp, double sharp etc.
isSharpened = (> 0)

-- | Returns whether this is a flat, double flat etc.
isFlattened = (< 0)


-- | Returns whether this is a standard accidental, i.e.
--   either a double flat, flat, natural, sharp or double sharp.
isStandardAccidental :: Accidental -> Bool
isStandardAccidental a = abs a < 2
-- was: isStandard

-- |
-- A pitch name.
--
data Name = C | D | E | F | G | A | B
  deriving (Eq, Ord, Show, Enum)

-- |
-- Common pitch representation.
--
-- Intervals and pitches can be added using '.+^'. To get the interval between
-- two pitches, use '.-.'.
--
-- Pitches are normally entered using the following literals.
--
-- > c d e f g a b
--
-- Notes with accidentals can be written by adding the @s@ or @b@ suffices
-- (or two for double sharps and flats).
--
-- > cs, ds, es ...    -- sharp
-- > cb, db, eb ...    -- flat
-- > css, dss, ess ... -- double sharp
-- > cbb, dbb, ebb ... -- double flat
--
-- There is also a convenience syntax for entering pitches one octave up or
-- down, using @'@ and @_@ respectively.
--
-- > g a b c'
-- > d c b_ c
--
-- Because of some overloading magic, we can actually write @sharp@ and
-- @flat@ as /postfix/ functions. This gives a better read:
--
-- > cs == c sharp
-- > db == c flat
--
-- You can of course use typical functional transformation of pitch as well.
-- For example 'sharpen' and 'flatten' are the ordinary (prefix) versions of
-- 'sharp' and 'flat'
--
-- > sharpen c             == c sharp       == cs
-- > flatten d             == d flat        == ds
-- > (sharpen . sharpen) c == c doubleSharp == css
-- > (flatten . flatten) d == d doubleFlat  == dss
--
-- Note that there is no guarantee that your pitch representation use
-- enharmonic equivalence, so @cs == db@ may or may not hold.
--
-- > c .+^ minor third == eb
-- > f .-. c           == perfect fourth
--
-- Pitches are described by name, accidental and octave number.
--
-- > c   == fromIntegral 0
-- > _P4 == perfect fourth   == interval Perfect 5
-- > d5  == diminished fifth == diminish (perfect fifth)
--
newtype Pitch = Pitch { getPitch :: Interval }
  deriving (Eq, Ord, Typeable)

instance IsPitch Pitch where
  fromPitch (PitchL (c, a, o)) =
    Pitch $ (\a b -> (fromIntegral a, fromIntegral b)^.interval') (qual a) c ^+^ (_P8^* fromIntegral o)
    where
      qual Nothing  = 0
      qual (Just n) = round n

instance Enum Pitch where
  toEnum = Pitch . (\a b -> (fromIntegral a, fromIntegral b)^.interval') 0 . fromIntegral
  fromEnum = fromIntegral . pred . number . (.-. c)

instance Alterable Pitch where
  sharpen (Pitch a) = Pitch (augment a)
  flatten (Pitch a) = Pitch (diminish a)

instance Show Pitch where
  show p = showName (name p) ++ showAccidental (accidental p) ++ showOctave (octaves $ getPitch p)
    where
      showName = fmap Char.toLower . show
      showOctave n
        | n > 0     = replicate (fromIntegral n) '\''
        | otherwise = replicate (negate $ fromIntegral n) '_'
      showAccidental n
        | n > 0     = replicate (fromIntegral n) 's'
        | otherwise = replicate (negate $ fromIntegral n) 'b'

instance Num Pitch where
  Pitch a + Pitch b = Pitch (a + b)
  negate (Pitch a)  = Pitch (negate a)
  abs (Pitch a)     = Pitch (abs a)
  (*)           = error  "Music.Pitch.Common.Pitch: no overloading for (*)"
  signum        = error "Music.Pitch.Common.Pitch: no overloading for signum"
  fromInteger   = error "Music.Pitch.Common.Pitch: no overloading for fromInteger"

instance AffineSpace Pitch where
  type Diff Pitch     = Interval
  Pitch a .-. Pitch b = a ^-^ b
  Pitch a .+^ b       = Pitch (a ^+^ b)

-- |
-- Creates a pitch from name accidental.
--
mkPitch :: Name -> Accidental -> Pitch
mkPitch name acc = Pitch $ (\a b -> (fromIntegral a, fromIntegral b)^.interval') (fromIntegral acc) (fromEnum name)

-- |
-- Returns the name of a pitch.
--
-- To convert a pitch to a numeric type, use 'octaves', 'steps' or 'semitones'
-- on the relevant interval type, for example:
--
-- @
-- semitones ('a\'' .-. 'c')
-- @
--
name :: Pitch -> Name
name x
  | i == 7           = toEnum 0 -- Arises for flat C etc.
  | 0 <= i && i <= 6 = toEnum i
  | otherwise        = error $ "Pitch.name: Bad value " ++ show i
  where
    i = (fromIntegral . pred . number . simple . getPitch) x

-- |
-- Returns the accidental of a pitch.
--
-- See also 'octaves', and 'steps' and 'semitones'.
--
accidental :: Pitch -> Accidental
accidental = fromIntegral . intervalDiff . simple . getPitch
  where
    intervalDiff = view (from interval'._1)

-- |
-- This is just the identity function, but is useful to fix the type of 'Pitch'.
--
asPitch :: Pitch -> Pitch
asPitch = id

upChromaticP :: Pitch -> ChromaticSteps -> Pitch -> Pitch
upChromaticP origin n = relative origin $ (_alteration +~ n)

downChromaticP :: Pitch -> ChromaticSteps -> Pitch -> Pitch
downChromaticP origin n = relative origin $ (_alteration -~ n)

upDiatonicP :: Pitch -> DiatonicSteps -> Pitch -> Pitch
upDiatonicP origin n = relative origin $ (_steps +~ n)

downDiatonicP :: Pitch -> DiatonicSteps -> Pitch -> Pitch
downDiatonicP origin n = relative origin $ (_steps -~ n)

invertDiatonicallyP :: Pitch -> Pitch -> Pitch
invertDiatonicallyP origin = relative origin $ (_steps %~ negate)

invertChromaticallyP :: Pitch -> Pitch -> Pitch
invertChromaticallyP origin = relative origin $ (_alteration %~ negate)