{-# LANGUAGE ConstraintKinds #-}
{-# LANGUAGE CPP #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE QuasiQuotes #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE OverloadedStrings #-}
module Language.C.Inline.ParseSpec (spec) where

import           Control.Exception (evaluate)
import           Control.Monad (void)
import           Control.Monad.Trans.Class (lift)
import qualified Data.HashSet as HashSet
import           Data.Monoid ((<>))
import qualified Test.Hspec as Hspec
import           Text.Parser.Char
import           Text.Parser.Combinators
import           Text.RawString.QQ (r)
import           Text.Regex.Posix ((=~))

#if __GLASGOW_HASKELL__ < 710
import           Control.Applicative ((<*), (*>))
#endif

import           Language.C.Inline.Context
import           Language.C.Inline.HaskellIdentifier
import           Language.C.Inline.Internal
import qualified Language.C.Types as C

spec :: Hspec.SpecWith ()
spec = do
  Hspec.describe "parsing" $ do
    Hspec.it "parses simple C expression" $ do
      (retType, params, cExp) <- goodParse [r|
          int { (int) ceil($(double x) + ((double) $(float y))) }
        |]
      retType `Hspec.shouldBe` (cty "int")
      params `shouldMatchParameters` [(cty "double", Plain "x"), (cty "float", Plain "y")]
      cExp `shouldMatchBody` " (int) ceil(x[a-z0-9_]+ \\+ ((double) y[a-z0-9_]+)) "
    Hspec.it "accepts anti quotes" $ do
      void $ goodParse [r| int { $(int x) } |]
    Hspec.it "accepts anti quotes with pointer" $ do
      void $ goodParse [r| int* { $(int* x) } |]
    Hspec.it "rejects if bad braces (1)" $ do
      badParse [r| int x |]
    Hspec.it "rejects if bad braces (2)" $ do
      badParse [r| int { x |]
    Hspec.it "parses function pointers" $ do
      void $ goodParse [r| int(int (*add)(int, int)) { add(3, 4) } |]
    Hspec.it "parses returning function pointers" $ do
      (retType, params, cExp) <-
        goodParse [r| double (*)(double) { &cos } |]
      retType `Hspec.shouldBe` (cty "double (*)(double)")
      params `shouldMatchParameters` []
      cExp `shouldMatchBody` " &cos "
    Hspec.it "parses Haskell identifier (1)" $ do
      (retType, params, cExp) <- goodParse [r| double { $(double x') } |]
      retType `Hspec.shouldBe` (cty "double")
      params `shouldMatchParameters` [(cty "double", Plain "x'")]
      cExp `shouldMatchBody` " x[a-z0-9_]+ "
    Hspec.it "parses Haskell identifier (2)" $ do
      (retType, params, cExp) <- goodParse [r| double { $(double ä') } |]
      retType `Hspec.shouldBe` (cty "double")
      params `shouldMatchParameters` [(cty "double", Plain "ä'")]
      cExp `shouldMatchBody` " [a-z0-9_]+ "
    Hspec.it "parses Haskell identifier (3)" $ do
      (retType, params, cExp) <- goodParse [r| int { $(int Foo.bar) } |]
      retType `Hspec.shouldBe` (cty "int")
      params `shouldMatchParameters` [(cty "int", Plain "Foo.bar")]
      cExp `shouldMatchBody` " Foobar[a-z0-9_]+ "
    Hspec.it "does not parse Haskell identifier in bad position" $ do
      badParse [r| double (*)(double Foo.bar) { 3.0 } |]
  where
    ctx = baseCtx <> funCtx

    assertParse ctxF p s =
      case C.runCParser (ctxF HashSet.empty) "spec" s (lift spaces *> p <* lift eof) of
        Left err -> error $ "Parse error (assertParse): " ++ show err
        Right x -> x

    -- We use show + length to fully evaluate the result -- there
    -- might be exceptions hiding.  TODO get rid of exceptions.
    strictParse
      :: String
      -> IO (C.Type C.CIdentifier, [(C.CIdentifier, C.Type C.CIdentifier, ParameterType)], String)
    strictParse s = do
      let ParseTypedC retType pars body =
            assertParse (haskellCParserContext True) (parseTypedC True (ctxAntiQuoters ctx)) s
      void $ evaluate $ length $ show (retType, pars, body)
      return (retType, pars, body)

    goodParse = strictParse
    badParse s = strictParse s `Hspec.shouldThrow` Hspec.anyException

    cty :: String -> C.Type C.CIdentifier
    cty s = C.parameterDeclarationType $
      assertParse (C.cCParserContext True) C.parseParameterDeclaration s

    shouldMatchParameters
      :: [(C.CIdentifier, C.Type C.CIdentifier, ParameterType)]
      -> [(C.Type C.CIdentifier, ParameterType)]
      -> Hspec.Expectation
    shouldMatchParameters pars pars' =
      [(x, y) | (_, x, y) <- pars] `Hspec.shouldMatchList` pars'

    shouldMatchBody :: String -> String -> Hspec.Expectation
    shouldMatchBody x y = do
      let f ch' = case ch' of
            '(' -> "\\("
            ')' -> "\\)"
            ch -> [ch]
      (x =~ concatMap f y) `Hspec.shouldBe` True
