{-# LANGUAGE Rank2Types #-}

-- Haskell expression parser.  Big hack, but only uses documented APIs so it
-- should be more robust than the previous hack.
module Lambdabot.Parser (parseExpr, parseDecl, withParsed, prettyPrintInLine) where

import Control.Monad.Error () -- Monad Either instance
import Data.Char
import Data.Generics
import Language.Haskell.Parser
import Language.Haskell.Pretty
import Language.Haskell.Syntax

import Lambdabot.FixPrecedence

parseExpr :: String -> Either String HsExp
parseExpr s
    | not (balanced 0 ' ' s) = Left "Unbalanced parentheses"
    | otherwise          = case parseModule wrapped of
        ParseOk (HsModule _ _ _ _ [HsPatBind _ _ (HsUnGuardedRhs e) _])
            -> Right $ fixPrecedence $ unparen e
        ParseFailed (SrcLoc _ _ col) msg
            -> Left $ showParseError msg (col - length prefix) s
        _ -> Left $ "Unexpected result of parsing an expression"
  where
    prefix  = "module Main where { main = ("
    wrapped = prefix ++ s ++ "\n)}"

    unparen (HsParen e) = e
    unparen e           = e

    -- balanced (open-parentheses) (previous-character) (remaining-string)
    balanced :: Int -> Char -> String -> Bool
    balanced n _ ""           = n == 0
    balanced n _ ('(':cs)     =           balanced (n+1) '(' cs
    balanced n _ (')':cs)     = n > 0  && balanced (n-1) ')' cs
    balanced n p (c  :cs)
      | c `elem` "\"'" && (not  (isAlphaNum p) || c /= '\'')
                              =           balancedString c n cs
    balanced n p ('-':'-':_)
      | not (isSymbol p)      = n == 0
    balanced n _ ('{':'-':cs) =           balancedComment 1 n cs
    balanced n _ (c  :cs)     =           balanced n     c   cs

    balancedString :: Char -> Int -> String -> Bool
    balancedString _     n ""          = n == 0 -- the parse error will be reported by L.H.Parser
    balancedString delim n ('\\':c:cs)
      | isSpace c                      = case dropWhile isSpace cs of
                                            '\\':cs' -> balancedString delim n cs'
                                            cs'      -> balancedString delim n cs'
      | otherwise                      = balancedString delim n cs
    balancedString delim n (c     :cs)
      | delim == c                     = balanced n c cs
      | otherwise                      = balancedString delim n cs

    balancedComment :: Int -> Int -> String -> Bool
    balancedComment 0 n cs           = balanced n ' ' cs
    balancedComment _ _ ""           = True -- the parse error will be reported by L.H.Parser
    balancedComment m n ('{':'-':cs) = balancedComment (m+1) n cs
    balancedComment m n ('-':'}':cs) = balancedComment (m-1) n cs
    balancedComment m n (_      :cs) = balancedComment m     n cs


parseDecl :: String -> Either String HsDecl
parseDecl s = case parseModule s of
        ParseOk (HsModule _ _ _ _ [d])   -> Right $ fixPrecedence d
        ParseFailed (SrcLoc _ _ col) msg -> Left $ showParseError msg col s
        _                                -> Left $ "Unexpected result of parsing a declaration"

showParseError :: String -> Int -> String -> String
showParseError msg col s = " " ++ msg
                       ++ case (col < 0, drop (col - 1) s) of
                            (True, _) -> " at end of input" -- on the next line, which has no prefix
                            (_,[]   ) -> " at end of input"
                            (_,ctx  ) -> let ctx' = takeWhile (/= ' ') ctx
                                         in " at \"" ++ (take 5 ctx')
                                         ++ (if length ctx' > 5 then "..." else "")
                                         ++ "\" (column " ++ show col ++ ")"

-- Not really parsing

withParsed :: (forall a. (Data a, Eq a) => a -> a) -> String -> String
withParsed f s = case (parseExpr s, parseDecl s) of
                    (Right a, _) -> prettyPrintInLine $ f a
                    (_, Right a) -> prettyPrintInLine $ f a
                    (Left e,  _) -> e

prettyPrintInLine :: Pretty a => a -> String
prettyPrintInLine = prettyPrintWithMode (defaultMode { layout = PPInLine })
