{-# LANGUAGE BangPatterns #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TupleSections #-}
{-# LANGUAGE LambdaCase #-}

-- | The basic parser.

module ICal.Parser
  (-- * Types
   Parser
  ,ParseError(..)
  -- * Handy starter functions
  ,parseEither
  -- * Combinators
  ,begin
  ,object
  ,objects
  ,property
  ,properties
  -- * Parser library
  ,local
  ,parseError
  ,getState
  ,putState)
  where

import Control.Applicative
import Control.Monad.State.Strict
import Control.Monad.Trans.Either
import Data.List
import Data.Maybe
import Data.Text (Text)
import ICal.Types

-- | A parse error.
data ParseError
  = ExpectedObject !Text
  | ExpectedProperty !Text
  | GeneralProblem !Text
  deriving (Show)

-- | Parse some iCalendar thing.
parseEither
  :: Monad m
  => s -> Parser m s a -> m (Either ParseError a)
parseEither s p =
  evalStateT (runEitherT (runParser p))
             s

-- | Parser type.
newtype Parser m s a =
  Parser {runParser :: EitherT ParseError (StateT s m) a}
  deriving (Monad,Applicative,Functor)

-- | Left branch failing resets the state.
instance Monad m => Alternative (Parser m s) where
  Parser x <|> Parser y =
    Parser (do s <- get
               r <-
                 EitherT (do r <- runEitherT x
                             return (Right r))
               case r of
                 Left{} -> do put s
                              y
                 Right ok -> return ok)
  empty = parseError (GeneralProblem "empty parser")

-- | Lookup a property.
property :: Monad m
         => Text -- ^ Key
         -> Parser m [Object] Text -- ^ The value of that property.
property !key =
  do os <- getState
     case listToMaybe
            (mapMaybe (\case
                         Property name value
                           | name == key -> Just value
                         _ -> Nothing)
                      os) of
       Nothing -> parseError (ExpectedProperty key)
       Just x -> return x

-- | Get all values of a property.
properties :: Monad m
           => Text -- ^ Key
           -> Parser m [Object] [Text] -- ^ The values of that property.
properties !key =
  do os <- getState
     return (mapMaybe (\case
                         Property name value
                           | name == key -> Just value
                         _ -> Nothing)
                      os)

-- | Lookup an object with this name in the current object's children,
-- then run with that object as the context.
object :: Monad m => Text -> Parser m [Object] a -> Parser m [Object] a
object !name m =
  do os <- getState
     case find (\case
                   Object name' _ -> name' == name
                   _ -> False) os of
       Just (Object _ children) ->
         local children m
       _ -> parseError (ExpectedObject name)

-- | Lookup objects with this name in the current object's children,
-- then run with that object as the context.
objects :: Monad m => Text -> Parser m [Object] a -> Parser m [Object] [a]
objects !name m =
  do os <- getState
     case mapMaybe (\case
                      Object name' children
                        | name' == name -> Just children
                      _ -> Nothing)
                   os of
       cs -> mapM (\children -> local children m) cs

-- | Require the given object name to exist and run in that context.
begin :: Monad m => Text -> Parser m [Object] a -> Parser m Object a
begin !name m =
  do o <- getState
     case o of
       Object name' children ->
         if name' == name
            then local children m
            else parseError (ExpectedObject name)
       _ -> parseError (ExpectedObject name)

-- | Use a local state of a different type.
local :: Monad m
      => t -> Parser m t a -> Parser m s a
local temp m =
  Parser (EitherT (StateT (\orig ->
                             do (result,_new) <-
                                  runStateT (runEitherT (runParser m)) temp
                                return (result,orig))))

-- | Throw a parse error.
parseError :: Monad m => ParseError -> Parser m o a
parseError = Parser . left

-- | Get the current state.
getState :: Monad m => Parser m s s
getState = Parser get

-- | Put a new state.
putState :: Monad m => s -> Parser m s ()
putState = Parser . put