module Data.BoolExpr.Parser
  (-- * Parsing function
   parseBoolExpr
   -- * Language definition and components
  ,languageDef
  ,lexer
  ,identifier
  ,whiteSpace
  ,symbol
  )
where

import Control.Monad
import Control.Applicative hiding ((<|>))
import Data.BoolExpr
import Text.ParserCombinators.Parsec
import qualified Text.ParserCombinators.Parsec.Token as P

{- | Parse a search query as a boolean tree using the following grammar.
     Note that this parser is parameterized over the parser of query simple
     terms (const).

@
  bt ::= bt AND bt
        | bt bt -- same as AND
        | bt OR bt
        | - bt
        | ( bt )
        | const
   const ::= \<given as argument\>
@
-}
parseBoolExpr :: CharParser st a -> CharParser st (BoolExpr a)
parseBoolExpr parseConst = disj
   where disj   = conj   `chainl1` orOp
         conj   = factor `chainl1` andOp
         factor = parens disj <|>
                  (((symbol "-" >> return BNot) <|> return id) `ap` (BConst `fmap` parseConst))

         andOp = BAnd <$ option "" (symbol "AND")
         orOp  = BOr <$ symbol "OR"

-- | Underlying lexer of 'languageDef'
lexer :: P.TokenParser st
lexer = P.makeTokenParser languageDef

-- | Shorthand for 'P.parens lexer'.
parens :: CharParser st a -> CharParser st a
parens = P.parens lexer

-- | Shorthand for 'P.symbol' 'lexer'.
symbol :: String -> CharParser st String
symbol = P.symbol lexer

-- | Shorthand for 'P.whiteSpace' 'lexer'.
whiteSpace :: CharParser st ()
whiteSpace = P.whiteSpace lexer

-- | Shorthand for 'P.identifier' 'lexer'.
identifier :: CharParser st String
identifier = P.identifier lexer

wordLetter :: CharParser st Char
wordLetter = alphaNum <|> oneOf "_:;`,~@.!#$%^&*=+?|\\{}[]<>"

-- | Basic language definition for search queries.
-- Reserved names are @\"AND\"@ @\"OR\"@ and @\"-\"@.
-- Identifiers accepts almost every ASCII sequences without blanks nor @\'-\'@.
languageDef :: P.LanguageDef st
languageDef = P.LanguageDef
               { P.commentStart   = ""
               , P.commentEnd     = ""
               , P.commentLine    = ""
               , P.nestedComments = True
               , P.identStart     = wordLetter
               , P.identLetter    = wordLetter <|> char '-'
               , P.opStart        = mzero
               , P.opLetter       = mzero
               , P.reservedOpNames= []
               , P.reservedNames  = ["AND", "OR", "-"]
               , P.caseSensitive  = True
               }