{-# LANGUAGE OverloadedStrings #-}

{-|
Module: Data.Gedcom.Internal.Common
Description: Common utility functions for parsing
Copyright: (c) Callum Lowcay, 2017
License: BSD3
Maintainer: cwslowcay@gmail.com
Stability: experimental
Portability: GHC
-}
module Data.Gedcom.Internal.Common
  ((<&>), withDefault, trim, Parser, timeToPicos, timeValue,
  dateExact, month, monthFr, monthHeb, yearGreg
  )
where

import Control.Applicative
import Data.Char
import Data.Maybe
import Data.Time.Clock
import Data.Void
import qualified Data.Text.All as T
import Text.Megaparsec
import Text.Megaparsec.Char

-- | Parsers from 'T.Text' using the default error component.
type Parser = Parsec Void T.Text

infixl 1 <&>

-- | Flipped version of '<$>'
(<&>) :: Functor f => f a -> (a -> b) -> f b
as <&> f = f <$> as
{-# INLINE (<&>) #-}

-- | Replace failure with a default value
withDefault :: Alternative f => a -> f a -> f a
withDefault def = fmap (fromMaybe def).optional
{-# INLINE withDefault #-}

-- | Trim leading and trailing whitespace off a string
trim :: T.Text -> T.Text
trim = T.dropWhile isSpace . T.dropWhileEnd isSpace

-- | Convert h:m:s.fs time to 'DiffTime'
timeToPicos :: (Int, Int, Int, Double) -> DiffTime
timeToPicos (h, m, s, fs) =
  picosecondsToDiffTime$
    (fromIntegral h * hm)
    + (fromIntegral m * mm)
    + (fromIntegral s * sm)
    + (round$ fs * (fromIntegral sm))
  where
    hm = mm * 60
    mm = sm * 60
    sm = 1000000000

-- | Parse a GEDCOM exact time value into (h, m, s, fs) format
timeValue :: Parser (Maybe (Int, Int, Int, Double))
timeValue = optional$ (,,,)
  <$> (read <$> count' 1 2 digitChar)
  <*> (char ':' *> (read <$> count' 1 2 digitChar))
  <*> withDefault 0 (char ':' *> (read <$> count' 1 2 digitChar))
  <*> withDefault 0 (char '.' *> (read.("0." ++) <$> count' 1 3 digitChar))

-- | Parse a GEDCOM exact date into (day, month, year).  Months are number from
-- 1 to 12.
dateExact :: Parser (Int, Word, Int)
dateExact = (,,)
  <$> (read <$> count' 1 2 digitChar)
  <*> (space *> month)
  <*> (space *> yearGreg)

-- | Parse a Gregorian/Julian month
month :: Parser Word
month =
  (string' "JAN" *> pure 1) <|>
  (string' "FEB" *> pure 2) <|>
  (string' "MAR" *> pure 3) <|>
  (string' "APR" *> pure 4) <|>
  (string' "MAY" *> pure 5) <|>
  (string' "JUN" *> pure 6) <|>
  (string' "JUL" *> pure 7) <|>
  (string' "AUG" *> pure 8) <|>
  (string' "SEP" *> pure 9) <|>
  (string' "OCT" *> pure 10) <|>
  (string' "NOV" *> pure 11) <|>
  (string' "DEC" *> pure 12)

-- | Parse a French calendar month
monthFr :: Parser Word
monthFr =
  (string' "VEND" *> pure 1) <|>
  (string' "BRUM" *> pure 2) <|>
  (string' "FRIM" *> pure 3) <|>
  (string' "NIVO" *> pure 4) <|>
  (string' "PLUV" *> pure 5) <|>
  (string' "VENT" *> pure 6) <|>
  (string' "GERM" *> pure 7) <|>
  (string' "FLOR" *> pure 8) <|>
  (string' "PRAI" *> pure 9) <|>
  (string' "MESS" *> pure 10) <|>
  (string' "THER" *> pure 11) <|>
  (string' "FRUC" *> pure 12) <|>
  (string' "COMP" *> pure 13)

-- | Parse a Hebrew calendar month
monthHeb :: Parser Word
monthHeb =
  (string' "TSH" *> pure 1) <|>
  (string' "CSH" *> pure 2) <|>
  (string' "KSL" *> pure 3) <|>
  (string' "TVT" *> pure 4) <|>
  (string' "SHV" *> pure 5) <|>
  (string' "ADR" *> pure 6) <|>
  (string' "ADS" *> pure 7) <|>
  (string' "NSN" *> pure 8) <|>
  (string' "IYR" *> pure 9) <|>
  (string' "SVN" *> pure 10) <|>
  (string' "TMZ" *> pure 11) <|>
  (string' "AAV" *> pure 12) <|>
  (string' "ELL" *> pure 13)

-- | Parse a Gregorian year.  GEDCOM allows one to specify two versions of the
-- same year for cases where the historical year started in March instead of
-- January.  This function attempts to return the modern year number (assuming
-- the year starts in January.
yearGreg :: Parser Int
yearGreg = do
  y <- read <$> count' 1 4 digitChar
  malt <- optional$ char '/' *> (read <$> count 2 digitChar)
  case malt of
    Just alt -> return$ if (y + 1) `mod` 100 == alt then y + 1 else y
    Nothing -> return y