-- |
-- Module      :  Control.Monad.Combinators
-- Copyright   :  © 2017 Mark Karpov
-- License     :  BSD 3 clause
--
-- Maintainer  :  Mark Karpov <markkarpov92@gmail.com>
-- Stability   :  experimental
-- Portability :  portable
--
-- The module provides more efficient versions of the combinators from
-- "Control.Applicative.Combinators" defined in terms of 'Monad' and
-- 'MonadPlus' instead of 'Control.Applicative.Applicative' and
-- 'Control.Applicative.Alternative'. When there is no difference in
-- performance we just re-export the combinators from
-- "Control.Applicative.Combinators".
--
-- @since 0.4.0

{-# LANGUAGE BangPatterns #-}
{-# LANGUAGE CPP          #-}

module Control.Monad.Combinators
  ( -- * Re-exports from "Control.Applicative"
    (C.<|>)
    -- $assocbo
  , C.optional
    -- $optional
  , C.empty
    -- $empty

    -- * Original combinators
  , C.between
  , C.choice
  , count
  , count'
  , C.eitherP
  , endBy
  , endBy1
  , many
  , manyTill
  , some
  , someTill
  , C.option
  , sepBy
  , sepBy1
  , sepEndBy
  , sepEndBy1
  , skipMany
  , skipSome
  , skipCount
  , skipManyTill
  , skipSomeTill )
where

import Control.Monad
import qualified Control.Applicative.Combinators as C

----------------------------------------------------------------------------
-- Re-exports from "Control.Applicative"

-- $assocbo
--
-- This combinator implements choice. The parser @p 'C.<|>' q@ first applies
-- @p@. If it succeeds, the value of @p@ is returned. If @p@ fails, parser
-- @q@ is tried.

-- $optional
--
-- @'C.optional' p@ tries to apply the parser @p@. It will parse @p@ or
-- 'Nothing'. It only fails if @p@ fails after consuming input. On success
-- result of @p@ is returned inside of 'Just', on failure 'Nothing' is
-- returned.
--
-- See also: 'C.option'.

-- $empty
--
-- This parser fails unconditionally without providing any information about
-- the cause of the failure.

----------------------------------------------------------------------------
-- Original combinators

-- | @'count' n p@ parses @n@ occurrences of @p@. If @n@ is smaller or equal
-- to zero, the parser equals to @'return' []@. Returns a list of @n@
-- values.
--
-- See also: 'skipCount', 'count''.

count :: Monad m => Int -> m a -> m [a]
count n' p = liftM ($ []) (go id n')
  where
    go f !n =
      if n <= 0
        then return f
        else do
          x <- p
          go (f . (x:)) (n - 1)
{-# INLINE count #-}

-- | @'count'' m n p@ parses from @m@ to @n@ occurrences of @p@. If @n@ is
-- not positive or @m > n@, the parser equals to @'return' []@. Returns a
-- list of parsed values.
--
-- Please note that @m@ /may/ be negative, in this case effect is the same
-- as if it were equal to zero.
--
-- See also: 'skipCount', 'count'.

count' :: MonadPlus m => Int -> Int -> m a -> m [a]
count' m' n' p =
  if n' > 0 && n' >= m'
    then liftM ($ []) (gom id m')
    else return []
  where
    gom f !m =
      if m > 0
        then do
          x <- p
          gom (f . (x:)) (m - 1)
        else god f (if m' <= 0 then n' else n' - m')
    god f !d =
      if d > 0
        then do
          r <- optional p
          case r of
            Nothing -> return f
            Just  x -> god (f . (x:)) (d - 1)
        else return f
{-# INLINE count' #-}

-- | @'endBy' p sep@ parses /zero/ or more occurrences of @p@, separated and
-- ended by @sep@. Returns a list of values returned by @p@.
--
-- > cStatements = cStatement `endBy` semicolon

endBy :: MonadPlus m => m a -> m sep -> m [a]
endBy p sep = many (p >>= \x -> re x sep)
{-# INLINE endBy #-}

-- | @'endBy1' p sep@ parses /one/ or more occurrences of @p@, separated and
-- ended by @sep@. Returns a list of values returned by @p@.

endBy1 :: MonadPlus m => m a -> m sep -> m [a]
endBy1 p sep = some (p >>= \x -> re x sep)
{-# INLINE endBy1 #-}

-- | @'many' p@ applies the parser @p@ /zero/ or more times and returns a
-- list of the values returned by @p@.
--
-- > identifier = (:) <$> letter <*> many (alphaNumChar <|> char '_')

many :: MonadPlus m => m a -> m [a]
many p = liftM ($ []) (go id)
  where
    go f = do
      r <- optional p
      case r of
        Nothing -> return f
        Just  x -> go (f . (x:))
{-# INLINE many #-}

-- | @'manyTill' p end@ applies parser @p@ /zero/ or more times until parser
-- @end@ succeeds. Returns the list of values returned by @p@.
--
-- See also: 'skipMany', 'skipManyTill'.

manyTill :: MonadPlus m => m a -> m end -> m [a]
manyTill p end = liftM ($ []) (go id)
  where
    go f = do
      done <- option False (re True end)
      if done
        then return f
        else do
          x <- p
          go (f . (x:))
{-# INLINE manyTill #-}

-- | @'some' p@ applies the parser @p@ /one/ or more times and returns a
-- list of the values returned by @p@.
--
-- > word = some letter

some :: MonadPlus m => m a -> m [a]
some p = liftM2 (:) p (many p)
{-# INLINE some #-}

-- | @'someTill' p end@ works similarly to @'manyTill' p end@, but @p@
-- should succeed at least once.
--
-- See also: 'skipSome', 'skipSomeTill'.

someTill :: MonadPlus m => m a -> m end -> m [a]
someTill p end = liftM2 (:) p (manyTill p end)
{-# INLINE someTill #-}

-- | @'sepBy' p sep@ parses /zero/ or more occurrences of @p@, separated by
-- @sep@. Returns a list of values returned by @p@.
--
-- > commaSep p = p `sepBy` comma

sepBy :: MonadPlus m => m a -> m sep -> m [a]
sepBy p sep = do
  r <- optional p
  case r of
    Nothing -> return []
    Just  x -> liftM (x:) (many (sep >> p))
{-# INLINE sepBy #-}

-- | @'sepBy1' p sep@ parses /one/ or more occurrences of @p@, separated by
-- @sep@. Returns a list of values returned by @p@.

sepBy1 :: MonadPlus m => m a -> m sep -> m [a]
sepBy1 p sep = do
  x <- p
  liftM (x:) (many (sep >> p))
{-# INLINE sepBy1 #-}

-- | @'sepEndBy' p sep@ parses /zero/ or more occurrences of @p@, separated
-- and optionally ended by @sep@. Returns a list of values returned by @p@.

sepEndBy :: MonadPlus m => m a -> m sep -> m [a]
sepEndBy p sep = liftM ($ []) (go id)
  where
    go f = do
      r <- optional p
      case r of
        Nothing -> return f
        Just  x -> do
          more <- option False (re True sep)
          if more
            then go (f . (x:))
            else return (f . (x:))
{-# INLINE sepEndBy #-}

-- | @'sepEndBy1' p sep@ parses /one/ or more occurrences of @p@, separated
-- and optionally ended by @sep@. Returns a list of values returned by @p@.

sepEndBy1 :: MonadPlus m => m a -> m sep -> m [a]
sepEndBy1 p sep = do
  x <- p
  more <- option False (re True sep)
  if more
    then liftM (x:) (sepEndBy p sep)
    else return [x]
{-# INLINE sepEndBy1 #-}

-- | @'skipMany' p@ applies the parser @p@ /zero/ or more times, skipping
-- its result.
--
-- See also: 'manyTill', 'skipManyTill'.

skipMany :: MonadPlus m => m a -> m ()
skipMany p = go
  where
    go = do
      more <- option False (re True p)
      when more go
{-# INLINE skipMany #-}

-- | @'skipSome' p@ applies the parser @p@ /one/ or more times, skipping its
-- result.
--
-- See also: 'someTill', 'skipSomeTill'.

skipSome :: MonadPlus m => m a -> m ()
skipSome p = p >> skipMany p
{-# INLINE skipSome #-}

-- | @'skipCount' n p@ parses @n@ occurrences of @p@, skipping its result.
-- If @n@ is smaller or equal to zero, the parser equals to @'return' []@.
-- Returns a list of @n@ values.
--
-- See also: 'count', 'count''.

skipCount :: Monad m => Int -> m a -> m ()
skipCount n' p = go n'
  where
    go !n =
      unless (n <= 0) $
        p >> go (n - 1)
{-# INLINE skipCount #-}

-- | @'skipManyTill' p end@ applies the parser @p@ /zero/ or more times
-- skipping results until parser @end@ succeeds. Result parsed by @end@ is
-- then returned.
--
-- See also: 'manyTill', 'skipMany'.

skipManyTill :: MonadPlus m => m a -> m end -> m end
skipManyTill p end = go
  where
    go = do
      r <- optional end
      case r of
        Nothing -> p >> go
        Just  x -> return x
{-# INLINE skipManyTill #-}

-- | @'skipSomeTill' p end@ applies the parser @p@ /one/ or more times
-- skipping results until parser @end@ succeeds. Result parsed by @end@ is
-- then returned.
--
-- See also: 'someTill', 'skipSome'.

skipSomeTill :: MonadPlus m => m a -> m end -> m end
skipSomeTill p end = p >> skipManyTill p end
{-# INLINE skipSomeTill #-}

----------------------------------------------------------------------------
-- Compat helpers (for older GHCs)

re :: Monad m => a -> m b -> m a
re x = liftM (const x)
{-# INLINE re #-}

option :: MonadPlus m => a -> m a -> m a
option x p = p `mplus` return x
{-# INLINE option #-}

optional :: MonadPlus m => m a -> m (Maybe a)
optional p = liftM Just p `mplus` return Nothing
{-# INLINE optional #-}