{-# LANGUAGE FlexibleContexts #-}
{-|

In general, an indentation structure is a predicate on the position
which tells us whether the token is acceptable or not. Besides the
predicate to check if a token at a given position is acceptable, we
also need to specify how indentations can be nested. This is captured
by the type class `Indentation`.

-}

module Text.Parsec.IndentParsec.Prim
       ( Indentation(..)
       , GenIndentParsecT
       , tokeniser
       , nest
       , neglectIndent
       , HaskellLike(..)
       -- * Running parsers
       , runGIPT', runGIPT
       -- * Some convenient type aliases.
       , IndentT
       , GenIndentParsec
       , IndentParsecT
       , IndentParsec
       ) where

import Control.Monad.State
import Control.Monad.Identity

import Text.Parsec.Prim
import Text.Parsec.Error
import Text.Parsec.Pos
import Text.Parsec.Combinator

{-|

Type class that captures generic indentation rule. It should follow
the condition that @`acceptable` `never` = `const` `False`@.

-}

class Indentation i where
      never      :: i  -- ^ an indentation state where no tokens are
                       -- accepted.
      always     :: i  -- ^ an indentation that will always accept
                       -- tokens.
      acceptable :: i -> SourcePos -> Bool -- ^ Check if the current
                                           -- position is acceptable.
      nestableIn :: i -- ^ Inner indentation.
                 -> i -- ^ Outer indentation.
                 -> Bool -- ^ True if the inner indentation can nest
                         -- inside the outer indentations.

-- | The inner monad for indent parsers.
type IndentT i m = StateT i m

-- | The indentation parser.
type GenIndentParsecT i s u m a = ParsecT s u (IndentT i m) a

-- | Build indentation awareness into the parser
tokeniser :: (Indentation i, Monad m)
             => GenIndentParsecT i s u m a
             -> GenIndentParsecT i s u m a
tokeniser p = do pos <- getPosition
                 i   <- lift get
                 if acceptable i pos then p
                    else fail $ "unexpected token at " ++ show pos

{-|

Any nested indentation starts at a position. Given an indentor
function, i.e. a function to compute the indentation state from the
current position, and a parser to parse the body of the indentation,
runs the parser inside the nested indentation context given by the
indentor.

-}

nest :: (Indentation i, Show i, Monad m, Stream s (IndentT i m) t, Show t)
     => (SourcePos -> i) -- ^ indentor function.
     -> GenIndentParsecT i s u m body -- ^ The nested parser to run
     -> GenIndentParsecT i s u m body
nest indentor p = do outerI <- lift get
                     curPos <- getPosition
                     let innerI = indentor curPos
                         in if innerI `nestableIn` outerI
                            then nestP innerI outerI p
                            else nestP never  outerI p

nestP :: (Indentation i, Show i, Monad m, Stream s (IndentT i m) t, Show t)
      => i -- ^ Inner indentation
      -> i -- ^ Outer indentation
      -> GenIndentParsecT i s u m body -- ^ body parser
      -> GenIndentParsecT i s u m body

nestP i o p = do lift $ put i
                 x <- p
                 notFollowedBy (tokeniser anyToken)
                               <?> "unterminated " ++ show i
                 lift $ put o
                 return x

-- | run a given parser neglecting indentation.
neglectIndent :: (Monad m, Show t, Show i, Indentation i,
                        Stream s (IndentT i m) t)
              => GenIndentParsecT i s u m a
              -> GenIndentParsecT i s u m a

neglectIndent p = do o <- get
                     lift $ put always
                     x <- p
                     lift $ put o
                     return x

-- | Run a given indentation aware parser with a starting indentation.
runGIPT' :: (Monad m, Stream s (IndentT i m) t)
         => i                          -- ^ The starting indentation,
         -> GenIndentParsecT i s u m a -- ^ The parser to run,
         -> u                          -- ^ The user state,
         -> SourceName                 -- ^ Name of the input source,
         -> s                          -- ^ The actual stream,
         -> m (Either ParseError a)    -- ^ The result

runGIPT' i p u sname s = evalStateT (runPT p u sname s) i

-- | Same as @`runGIPT'` always@.
runGIPT :: (Monad m, Indentation i, Stream s (IndentT i m) t)
        => GenIndentParsecT i s u m a -- ^ The parser to run,
        -> u                          -- ^ The user state,
        -> SourceName                 -- ^ Name of the input source,
        -> s                          -- ^ The actual stream,
        -> m (Either ParseError a)    -- ^ The result.

runGIPT  = runGIPT' always

{-|

Type to capture Haskell like indentation. Besides `always` and `never`
the important indentations are blocks and line folds. A block starting
at position @p@ consists of all tokens that have indentation at least
as much as @p@. A folded like starting at position @p@ is

-}
data HaskellLike   = Never
                   | Neglect
                   | Block SourcePos
                   | LineFold SourcePos

instance Indentation HaskellLike where
         never  = Never
         always = Neglect

         acceptable Never          _ = False
         acceptable Neglect        _ = True
         acceptable (Block bp)     p = sourceColumn bp <= sourceColumn p
         acceptable (LineFold lp)  p = sourceColumn lp < sourceColumn p ||
                                            lp == p
         nestableIn (Block i)    (Block o)    = sourceColumn o < sourceColumn i
         nestableIn (Block i)    (LineFold o) = sourceColumn o < sourceColumn i
         nestableIn (LineFold i) (LineFold o) = sourceColumn o < sourceColumn i
         nestableIn (LineFold i) (Block o)    = sourceColumn o <= sourceColumn i
         nestableIn _            _            = True

instance Show HaskellLike where
         show Never   = "empty block"
         show Neglect = "unindented chunk"
         show (Block pos)    = "block started at " ++ show pos
         show (LineFold pos) = "line fold started at " ++ show pos

type GenIndentParsec i s u a = GenIndentParsecT i s u Identity a
type IndentParsecT s u m a = GenIndentParsecT HaskellLike s u m a
type IndentParsec  s u a   = IndentParsecT s u Identity a