{-# LANGUAGE CPP               #-}
{-# LANGUAGE ConstraintKinds   #-}
{-# LANGUAGE FlexibleContexts  #-}
{-# LANGUAGE GADTs             #-}
{-# LANGUAGE KindSignatures    #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE PackageImports    #-}
{-# LANGUAGE QuasiQuotes       #-}
{-# LANGUAGE RankNTypes        #-}
{-# LANGUAGE TypeApplications  #-}

import Criterion      ( Benchmark, bench, bgroup, env, nf )
import Criterion.Main ( defaultMain )

import qualified Data.ByteString      as B
import qualified Data.ByteString.Lazy as LB
import qualified Data.String          as S
import qualified Data.Text            as T
import qualified Data.Text.Lazy       as LT

import qualified "string-interpolate" Data.String.Interpolate            as SI
import qualified "string-interpolate" Data.String.Interpolate.Conversion as SI
import qualified "interpolate" Data.String.Interpolate.IsString          as I
import           "formatting" Formatting                                 ( (%) )
import qualified "formatting" Formatting                                 as F
import qualified "formatting" Formatting.ShortFormatters                 as F
import qualified "neat-interpolation" NeatInterpolation                  as NI

import Control.DeepSeq

import Test.QuickCheck

#ifdef EXTENDED_BENCHMARKS
import "Interpolation" Data.String.Interpolation                as N
import "interpolatedstring-perl6" Text.InterpolatedString.Perl6 as P
#endif

type SIInterpolatable str flag =
  ( SI.IsCustomSink str ~ flag
  , SI.InterpSink flag str
  , SI.Interpolatable flag str str
  , SI.Interpolatable flag Int str
  , SI.Interpolatable flag Bool str
  )

type AllInterpolatable str flag =
  ( SIInterpolatable str flag
  , Show str
  , S.IsString str
  , Monoid str
  )

--------------------
-- string-interpolate
--------------------

singleInterpSI :: SIInterpolatable str flag => str -> str
singleInterpSI str = [SI.i|A fine day to die, #{str}.|]

multiInterpSI :: SIInterpolatable str flag => (Int, str, Bool) -> str
multiInterpSI (x, y, z) = [SI.i| foo #{x} bar #{y} baz #{z} quux |]

--------------------
-- interpolate
--------------------

singleInterpI :: (Show str, S.IsString str) => str -> str
singleInterpI str = [I.i|A fine day to die, #{str}.|]

multiInterpI :: (Show str, S.IsString str) => (Int, str, Bool) -> str
multiInterpI (x, y, z) = [I.i| foo #{x} bar #{y} baz #{z} quux |]

--------------------
-- formatting
--------------------

stringF :: String -> String
stringF = F.formatToString ("A fine day to die, " % F.s % ".")

multiStringF :: (Int, String, Bool) -> String
multiStringF (x, y, z) =
  F.formatToString (" foo " % F.d % " bar " % F.s % " baz " % F.sh % " quux ") x y z

textF :: T.Text -> T.Text
textF = F.sformat ("A fine day to die, " % F.st % ".")

multiTextF :: (Int, T.Text, Bool) -> T.Text
multiTextF (x, y, z) =
  F.sformat (" foo " % F.d % " bar " % F.st % " baz " % F.sh % " quux ") x y z

lazyTextF :: LT.Text -> LT.Text
lazyTextF = F.format ("A find day to die, " % F.t % ".")

multiLazyTextF :: (Int, LT.Text, Bool) -> LT.Text
multiLazyTextF (x, y, z) =
  F.format (" foo " % F.d % " bar " % F.t % " baz " % F.sh % " quux ") x y z

--------------------
-- neat-interpolation
--------------------

textNI :: T.Text -> T.Text
textNI t = [NI.text|A fine day to die, $t.|]

multiTextNI :: (Int, T.Text, Bool) -> T.Text
multiTextNI (x, y, z) =
  let x' = T.pack $ show x
      z' = T.pack $ show z
  in [NI.text| foo $x' bar $y baz $z' quux |]

#ifdef EXTENDED_BENCHMARKS

--------------------
-- Interpolation
--------------------

singleInterpN :: (Monoid str, S.IsString str) => str -> str
singleInterpN t = [str|A fine day to die, $t$.|]

multiInterpN ::(Monoid str, S.IsString str) => (Int, str, Bool) -> str
multiInterpN (x, y, z) = [str| foo $:x$ bar $y$ baz $:z$ quux |]

--------------------
-- interpolatedstring-perl6
--------------------

singleInterpP :: (Monoid str, S.IsString str) => str -> str
singleInterpP t = [qc|A fine day to die, {t}.|]

multiInterpP :: (Monoid str, S.IsString str) => (Int, str, Bool) -> str
multiInterpP (x, y, z) = [qc| foo {x} bar {y} baz {z} quux |]

#endif

--------------------
-- BENCHMARK GROUPS
--------------------

singleInterpBenches :: AllInterpolatable str flag
                    => [(String, (str -> str))]
singleInterpBenches =
  [ ("string-interpolate"      , singleInterpSI)
  , ("interpolate"             , singleInterpI)
#ifdef EXTENDED_BENCHMARKS
  , ("interpolatedstring-perl6", singleInterpP)
  , ("Interpolation"           , singleInterpN)
#endif
  ]

multiInterpBenches :: AllInterpolatable str flag
                   => [(String, ((Int, str, Bool) -> str))]
multiInterpBenches =
  [ ("string-interpolate"      , multiInterpSI)
  , ("interpolate"             , multiInterpI)
#ifdef EXTENDED_BENCHMARKS
  , ("interpolatedstring-perl6", multiInterpP)
  , ("Interpolation"           , multiInterpN)
#endif
  ]

main :: IO ()
main = defaultMain $
  [ benches @String "Small Strings Bench" "William" $
      singleInterpBenches ++
        [ ("formatting", stringF) ]
  , benches @T.Text "Small Text Bench" "William" $
      singleInterpBenches ++
        [ ("formatting"        , textF)
        , ("neat-interpolation", textNI)
        ]
  , benches @LT.Text "Small Lazy Text Bench" "William" $
      singleInterpBenches ++
        [ ("formatting", lazyTextF) ]
  , benches @B.ByteString "Small ByteStrings Bench" "William" $
      singleInterpBenches
  , benches @LB.ByteString "Small Lazy ByteStrings Bench" "William" $
      singleInterpBenches
  , benches @String "Multiple Interpolations String Bench" (42, "CATALLAXY", True) $
      multiInterpBenches ++
        [ ("formatting", multiStringF) ]
  , benches @T.Text "Multiple Interpolations Text Bench" (42, "CATALLAXY", True) $
      multiInterpBenches ++
        [ ("formatting"        , multiTextF)
        , ("neat-interpolation", multiTextNI)
        ]
  , benches @LT.Text "Multiple Interpolations Lazy Text Bench" (42, "CATALLAXY", True) $
      multiInterpBenches ++
        [ ("formatting", multiLazyTextF) ]
  , benches @B.ByteString "Multiple Interpolations ByteString Bench" (42, "CATALLAXY", True) $
      multiInterpBenches
  , benches @LB.ByteString "Multiple Interpolations Lazy ByteString Bench" (42, "CATALLAXY", True) $
      multiInterpBenches
  , env largeishText $ \ ~t -> benches @T.Text "Largeish Text Bench" t $
      singleInterpBenches ++
        [ ("formatting"        , textF)
        , ("neat-interpolation", textNI)
        ]
  , env largeishLazyText $ \ ~lt -> benches @LT.Text "Largeish Lazy Text Bench" lt $
      singleInterpBenches ++
        [ ("formatting", lazyTextF) ]
  , env largeishByteString $ \ ~bs -> benches @B.ByteString "Largeish ByteString Bench" bs $
      singleInterpBenches
  , env largeishLazyByteString $ \ ~lbs -> benches @LB.ByteString "Largeish Lazy ByteString Bench" lbs $
      singleInterpBenches
  ]

largeishText :: IO T.Text
largeishText =
  generate $ T.pack <$> Prelude.take 100000 <$> infiniteListOf arbitrary

largeishLazyText :: IO LT.Text
largeishLazyText =
  generate $ LT.pack <$> Prelude.take 100000 <$> infiniteListOf arbitrary

largeishByteString :: IO B.ByteString
largeishByteString =
  generate $ B.pack <$> Prelude.take 100000 <$> infiniteListOf arbitrary

largeishLazyByteString :: IO LB.ByteString
largeishLazyByteString =
  generate $ LB.pack <$> Prelude.take 100000 <$> infiniteListOf arbitrary

--------------------
-- BENCHMARK UTIL
--------------------

benches :: forall b a. NFData b => String -> a -> [(String, a -> b)] -> Benchmark
benches groupname arg fs = bgroup groupname (fmap benchF fs)
  where benchF (bname, f) = bench bname $ nf f arg
