-- |
-- = Introduction
--
-- The 'Text.Parsec.Indent' module provides an /implicit/ indentation parser.
-- For complex parsers split over many functions, this requires a good
-- understanding from the programmer which indentation is being referenced.
--
-- This module fixes that problem by explicitly passing around indentation as a
-- first-class value.  This commonly makes complex parsers less concise but
-- easier to understand.
--
-- = The problem with reference indentation
--
-- Many functions from that module (@indented@, @checkIndent@,
-- @sameOrIndented@...) use a /reference indentation/.  This
-- /reference indentation/ is stored in the @IndentParserT@ type.  It can be set
-- by calling @withPos@, but also by other functions such as @block@.
--
-- Consider the following code snippet:
--
-- > import qualified Text.Parsec.Indent as I
-- >
-- > p = I.withPos $ do
-- >   foo
-- >   I.block $ I.checkIndent >> bar
--
-- @I.checkIndent@, in this case, will always succeed, since we are comparing
-- the current indentation to the implicit reference indentation set by
-- @I.block@, not by @I.withPos@, and there is no way to do the latter.
--
-- = Explicit indentation
--
-- This module makes indentation first-class rather than implicit, so we can
-- provide a good implementation without relying on @withPos@:
--
-- > import qualified Text.Parsec.Indent as I
-- > import qualified Text.Parsec.Indent.Explicit as EI
-- >
-- > p = do
-- >   indentation <- EI.indentation
-- >   foo
-- >   I.block $ EI.checkIndent indentation >> bar
--
-- In order to preserve backwards-compatibility, the names in this module are
-- chosen to match their counterparts in "Text.Parsec.Indent".
module Text.Parsec.Indent.Explicit
    ( -- * Indentation type
      Indentation

      -- * Obtaining a reference implementation
    , indentation

      -- * Indentation-based parser combinators
    , indented
    , sameOrIndented
    , same
    , block
    , checkIndent
    , topLevel
    , notTopLevel
    ) where

import           Control.Monad               (unless, when)
import           Text.Parsec
import           Text.Parsec.Indent.Internal

-- | Obtain the current indentation, to be used as a reference later.
indentation :: Monad m => ParsecT s u m Indentation
indentation = do
    pos <- getPosition
    return $! Indentation {iLine = sourceLine pos, iColumn = sourceColumn pos}

-- | Parses only when indented past the level of the reference
indented
    :: (Monad m, Stream s m z)
    => Indentation  -- ^ Reference indentation
    -> ParsecT s u m ()
indented ref = do
    pos <- indentation
    when (iColumn pos <= iColumn ref) $ unexpected (prettyIndentation pos)

-- | Parses only when indented past the level of the reference or on the same line
sameOrIndented
    :: (Monad m, Stream s m z)
    => Indentation  -- ^ Reference indentation
    -> ParsecT s u m ()
sameOrIndented ref = do
    -- This is equal to 'same <|> indented' but gives a cleaner error message.
    pos <- indentation
    when (iColumn pos <= iColumn ref && iLine pos /= iLine ref) $
        unexpected (prettyIndentation pos)

-- | Parses only on the same line as the reference
same
    :: (Monad m, Stream s m z)
    => Indentation  -- ^ Reference indentation
    -> ParsecT s u m ()
same ref = do
    pos <- indentation
    when (iLine pos /= iLine ref) $ unexpected "line break"

-- | Parses a block of lines at the same indentation level starting at the
-- current position
block
    :: (Monad m, Stream s m z)
    => ParsecT s u m a
    -> ParsecT s u m [a]
block p = do
    ref <- indentation
    many1 (checkIndent ref >> p)

-- | Ensures the current indentation level matches that of the reference
checkIndent
    :: (Monad m, Stream s m z)
    => Indentation  -- ^ Reference indentation
    -> ParsecT s u m ()
checkIndent ref = do
    pos <- indentation
    when (iColumn pos /= iColumn ref) $
        (<?> prettyIndentation ref ++ " (started at line " ++ prettyLine ref ++ ")")
        (unexpected $ prettyIndentation pos)

-- | Ensures that there is no indentation.
topLevel
    :: (Monad m, Stream s m z)
    => ParsecT s u m ()
topLevel = do
    pos <- indentation
    unless (iColumn pos == 1) $ unexpected "indentation"

-- | Ensures that there is at least some indentation.
notTopLevel
    :: (Monad m, Stream s m z)
    => ParsecT s u m ()
notTopLevel = do
    pos <- indentation
    when (iColumn pos == 1) $ unexpected "top-level"