{-# LANGUAGE DeriveDataTypeable #-}
{-# LANGUAGE OverloadedStrings  #-}
{-# LANGUAGE RankNTypes         #-}
{-# LANGUAGE RecursiveDo        #-}

-- | Parsing logic for the Morte language

module Annah.Parser (
    -- * Parser
    exprFromText,
    typesFromText,

    -- * Errors
    ParseError(..),
    ParseMessage(..)
    ) where

import Annah.Core
import Annah.Lexer (Position, Token, LocatedToken(..))
import Control.Applicative hiding (Const)
import Control.Exception (Exception)
import Control.Monad.Trans.Class  (lift)
import Control.Monad.Trans.Except (Except, throwE, runExceptT)
import Control.Monad.Trans.State.Strict (evalState, get)
import Data.Monoid
import Data.Text.Buildable (Buildable(..))
import Data.Text.Lazy (Text)
import Data.Text.Lazy.Builder (toLazyText)
import Data.Typeable (Typeable)
import Morte.Core (Path(..))
import Filesystem.Path.CurrentOS (FilePath)
import Prelude hiding (FilePath)
import Text.Earley

import qualified Annah.Lexer    as Lexer
import qualified Pipes.Prelude  as Pipes
import qualified Data.Text.Lazy as Text

match :: Token -> Prod r Token LocatedToken Token
match t = fmap Lexer.token (satisfy predicate) <?> t
  where
    predicate (LocatedToken t' _) = t == t'

label :: Prod r e LocatedToken Text
label = fmap unsafeFromLabel (satisfy isLabel)
  where
    isLabel (LocatedToken (Lexer.Label _) _) = True
    isLabel  _                               = False

    unsafeFromLabel (LocatedToken (Lexer.Label l) _) = l

number :: Prod r e LocatedToken Int
number = fmap unsafeFromNumber (satisfy isNumber)
  where
    isNumber (LocatedToken (Lexer.Number _) _) = True
    isNumber  _                                = False

    unsafeFromNumber (LocatedToken (Lexer.Number n) _) = n

file :: Prod r e LocatedToken FilePath
file = fmap unsafeFromFile (satisfy isFile)
  where
    isFile (LocatedToken (Lexer.File _) _) = True
    isFile  _                              = False

    unsafeFromFile (LocatedToken (Lexer.File n) _) = n

url :: Prod r e LocatedToken Text
url = fmap unsafeFromURL (satisfy isURL)
  where
    isURL (LocatedToken (Lexer.URL _) _) = True
    isURL  _                             = False

    unsafeFromURL (LocatedToken (Lexer.URL n) _) = n

sepBy1 :: Alternative f => f a -> f b -> f [a]
sepBy1 x sep = (:) <$> x <*> many (sep *> x)

sepBy :: Alternative f => f a -> f b -> f [a]
sepBy x sep = sepBy1 x sep <|> pure []

expr
    :: Grammar r
        (Prod r Token LocatedToken Expr, Prod r Token LocatedToken [Type])
expr = mdo
    expr0 <- rule
        (   Annot <$> expr1 <*> (match Lexer.Colon *> expr0)
        <|> expr1
        )
    expr1 <- rule
        (       Lam
            <$> (match Lexer.Lambda *> match Lexer.OpenParen *> label)
            <*> (match Lexer.Colon *> expr1)
            <*> (match Lexer.CloseParen *> match Lexer.Arrow *> expr1)
        <|>     Pi
            <$> (match Lexer.Pi *> match Lexer.OpenParen *> label)
            <*> (match Lexer.Colon *> expr1)
            <*> (match Lexer.CloseParen *> match Lexer.Arrow *> expr1)
        <|> Pi "_" <$> expr2 <*> (match Lexer.Arrow *> expr1)
        <|> Family <$> types <*> (match Lexer.In *> expr1)
        <|> Lets <$> lets <*> (match Lexer.In *> expr1)
        <|> expr2
        )

    vexpr <- rule
        (   V <$> label <*> (match Lexer.At *> number)
        <|> V <$> label <*> pure 0
        )

    expr2 <- rule
        (   App <$> expr2 <*> expr3
        <|> expr3
        )

    let makeExpr3 p =
            (   Var <$> vexpr
            <|> match Lexer.Star *> pure (Const Star)
            <|> match Lexer.Box  *> pure (Const Box )
            <|> Embed <$> embed
            <|> (Natural . fromIntegral) <$> number
            <|>     List
                <$> (match Lexer.OpenList *> expr0)
                <*> (many (match Lexer.Comma *> expr0) <* match Lexer.CloseBracket)
            <|>     Path
                <$> (match Lexer.OpenPath *> expr0)
                <*> many ((,) <$> object <*> expr0)
                <*> (object <* match Lexer.CloseBracket)
            <|>     Do
                <$> (match Lexer.Do *> expr0)
                <*> (match Lexer.OpenBrace *> many bind)
                <*> (bind <* match Lexer.CloseBrace)
            <|> (match Lexer.OpenParen *> p <* match Lexer.CloseParen)
            )

    expr3  <- rule (makeExpr3 expr0)
    expr3' <- rule (makeExpr3 expr1)

    arg <- rule
        (       Arg
            <$> (match Lexer.OpenParen *> label)
            <*> (match Lexer.Colon *> expr1 <* match Lexer.CloseParen)
        <|>     Arg "_" <$> expr3'
        )

    args <- rule (many arg)

    data_ <- rule (Data <$> (match Lexer.Data *> label) <*> args)

    datas <- rule (many data_)

    type_ <- rule
        (       Type
            <$> (match Lexer.Type *> label)
            <*> datas
            <*> (match Lexer.Fold *> label)
        <|>     Type
            <$> (match Lexer.Type *> label)
            <*> datas
            <*> pure "_"
        )

    types <- rule (some type_)

    let_ <- rule
        (   Let
        <$> (match Lexer.Let *> label)
        <*> args
        <*> (match Lexer.Colon *> expr0)
        <*> (match Lexer.Equals *> expr1)
        )

    lets <- rule (some let_)

    object <- rule (match Lexer.OpenBrace *> expr0 <* match Lexer.CloseBrace)

    bind <- rule
        (   (\x y z -> Bind (Arg x y) z)
        <$> label
        <*> (match Lexer.Colon *> expr0)
        <*> (match Lexer.LArrow *> expr0 <* match Lexer.Semicolon)
        )

    embed <- rule
        (   File <$> file
        <|> URL <$> url
        )

    return (expr0, types)

-- | The specific parsing error
data ParseMessage
    -- | Lexing failed, returning the remainder of the text
    = Lexing Text
    -- | Parsing failed, returning the invalid token and the expected tokens
    | Parsing Token [Token]
    deriving (Show)

-- | Structured type for parsing errors
data ParseError = ParseError
    { position     :: Position
    , parseMessage :: ParseMessage
    } deriving (Typeable)

instance Show ParseError where
    show = Text.unpack . toLazyText . build

instance Exception ParseError

instance Buildable ParseError where
    build (ParseError (Lexer.P l c) e) =
            "\n"
        <>  "Line:   " <> build l <> "\n"
        <>  "Column: " <> build c <> "\n"
        <>  "\n"
        <>  case e of
            Lexing r                                     ->
                    "Lexing: \"" <> build remainder <> dots <> "\"\n"
                <>  "\n"
                <>  "Error: Lexing failed\n"
              where
                remainder = Text.takeWhile (/= '\n') (Text.take 64 r)
                dots      = if Text.length r > 64 then "..." else mempty
            Parsing t ts ->
                    "Parsing : " <> build (show t ) <> "\n"
                <>  "Expected: " <> build (show ts) <> "\n"
                <>  "\n"
                <>  "Error: Parsing failed\n"

runParser
    :: (forall r . Grammar r (Prod r Token LocatedToken a))
    -> Text
    -> Either ParseError a
runParser p text = evalState (runExceptT m) (Lexer.P 1 0)
  where
    m = do
        (locatedTokens, mtxt) <- lift (Pipes.toListM' (Lexer.lexExpr text))
        case mtxt of
            Nothing  -> return ()
            Just txt -> do
                pos <- lift get
                throwE (ParseError pos (Lexing txt))
        let (parses, Report _ needed found) =
                fullParses (parser p) locatedTokens
        case parses of
            parse:_ -> return parse
            []      -> do
                let LocatedToken t pos = case found of
                        lt:_ -> lt
                        _    -> LocatedToken Lexer.EOF (Lexer.P 0 0)
                throwE (ParseError pos (Parsing t needed))

-- | Parse an `Expr` from `Text` or return a `ParseError` if parsing fails
exprFromText :: Text -> Either ParseError Expr
exprFromText = runParser (fmap fst expr)

{-| Parse a type definition from `Text` or return a `ParseError` if parsing
    fails
-}
typesFromText :: Text -> Either ParseError [Type]
typesFromText = runParser (fmap snd expr)