{-# LANGUAGE BangPatterns #-}
{-# LANGUAGE BlockArguments #-}
{-# LANGUAGE DerivingStrategies #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE TypeFamilies #-}

module Hasql.Interpolate.Internal.TH
  ( sql,
    addParam,
    parseSqlExpr,
    compileSqlExpr,
    SqlExpr (..),
    SqlBuilderExp (..),
    ParamEncoder (..),
    SpliceBind (..),
  )
where

import Control.Applicative
import Control.Monad.State.Strict
import Data.Array (listArray, (!))
import Data.ByteString.Builder (Builder, stringUtf8)
import Data.Char
import Data.Functor
import Data.Functor.Contravariant
import qualified Data.IntSet as IS
import Data.Monoid (Ap (..))
import Data.Void
import qualified Hasql.Encoders as E
import Hasql.Interpolate.Internal.Encoder (EncodeField (..))
import Hasql.Interpolate.Internal.Sql
import Language.Haskell.Meta (parseExp)
import Language.Haskell.TH
import Language.Haskell.TH.Quote
import Text.Megaparsec
  ( ParseErrorBundle,
    Parsec,
    anySingle,
    chunk,
    eof,
    notFollowedBy,
    runParser,
    single,
    takeWhileP,
    try,
  )

data SqlExpr = SqlExpr
  { sqlBuilderExp :: [SqlBuilderExp],
    paramEncoder :: [ParamEncoder],
    spliceBinds :: [SpliceBind],
    bindCount :: Int
  }
  deriving stock (Show, Eq)

data SqlBuilderExp
  = Sbe'Var Int
  | Sbe'Param
  | Sbe'Quote String
  | Sbe'Ident String
  | Sbe'DollarQuote String String
  | Sbe'Cquote String
  | Sbe'Sql String
  deriving stock (Show, Eq)

data ParamEncoder
  = Pe'Exp Exp
  | Pe'Var Int
  deriving stock (Show, Eq)

data SpliceBind = SpliceBind
  { sbBuilder :: Int,
    sbParamEncoder :: Int,
    sbExp :: Exp
  }
  deriving stock (Show, Eq)

dollar :: Builder
dollar = "$"

cquote :: Builder
cquote = "E'"

sq :: Builder
sq = "'"

dq :: Builder
dq = "\""

data ParserState = ParserState
  { ps'sqlBuilderExp :: [SqlBuilderExp] -> [SqlBuilderExp],
    ps'paramEncoder :: [ParamEncoder] -> [ParamEncoder],
    ps'spliceBinds :: [SpliceBind] -> [SpliceBind],
    ps'nextUnique :: Int
  }

type Parser a = StateT (ParserState) (Parsec Void String) a

sqlExprParser :: Parser ()
sqlExprParser = go
  where
    go =
      quoted
        <|> ident
        <|> dollarQuotes
        <|> cquoted
        <|> param
        <|> splice
        <|> comment
        <|> multilineComment
        <|> someSql
        <|> eof

    nextUnique :: Parser Int
    nextUnique = do
      st <- get
      let next = ps'nextUnique st
          !nextnext = next + 1
      put st {ps'nextUnique = nextnext}
      pure next

    appendSqlBuilderExp :: SqlBuilderExp -> Parser ()
    appendSqlBuilderExp x = do
      st <- get
      put st {ps'sqlBuilderExp = ps'sqlBuilderExp st . (x :)}

    appendEncoder :: ParamEncoder -> Parser ()
    appendEncoder x = do
      st <- get
      put st {ps'paramEncoder = ps'paramEncoder st . (x :)}

    addSpliceBinding :: Exp -> Parser ()
    addSpliceBinding x = do
      exprVar <- nextUnique
      paramVar <- nextUnique
      st <- get
      put
        st
          { ps'spliceBinds =
              ps'spliceBinds st
                . (SpliceBind {sbBuilder = exprVar, sbParamEncoder = paramVar, sbExp = x} :)
          }
      appendSqlBuilderExp (Sbe'Var exprVar)
      appendEncoder (Pe'Var paramVar)

    comment = do
      _ <- chunk "--"
      void $ takeWhileP (Just "comment") (/= '\n')
      go

    multilineComment = do
      multilineCommentBegin
      go

    multilineCommentBegin = do
      _ <- chunk "/*"
      multilineCommentEnd

    multilineCommentEnd = do
      void $ takeWhileP (Just "multiline comment") (\c -> c /= '*' && c /= '/')
      (multilineCommentBegin >> multilineCommentEnd) <|> void (chunk "*/")

    escapedContent name terminal escapeChar escapeParser =
      let loop sofar = do
            content <- takeWhileP (Just name) (\c -> c /= terminal && c /= escapeChar)
            notFollowedBy eof
            (try escapeParser >>= \esc -> loop (sofar . (content ++) . (esc ++)))
              <|> (single terminal $> sofar content)
       in loop id

    betwixt name initial terminal escapeChar escapeParser = do
      _ <- chunk initial
      escapedContent name terminal escapeChar escapeParser

    quoted = do
      content <- betwixt "single quotes" "'" '\'' '\'' (chunk "''")
      appendSqlBuilderExp (Sbe'Quote content)
      go

    cquoted = do
      content <- betwixt "C-style escape quote" "E'" '\'' '\\' do
        a <- single '\\'
        b <- anySingle
        pure [a, b]
      appendSqlBuilderExp (Sbe'Cquote content)
      go

    ident = do
      content <- betwixt "identifier" "\"" '"' '"' (chunk "\"\"")
      appendSqlBuilderExp (Sbe'Ident content)
      go

    dollarQuotes = do
      _ <- single '$'
      tag <- takeWhileP (Just "identifier") isAlphaNum
      _ <- single '$'
      let bonk sofar = do
            notFollowedBy eof
            c <- takeWhileP (Just "dollar quoted content") (/= '$')
            (parseEndQuote $> (sofar . (c ++))) <|> bonk (sofar . (c ++))
          parseEndQuote = do
            _ <- single '$'
            _ <- chunk tag
            void $ single '$'
      content <- ($ "") <$> bonk id
      appendSqlBuilderExp (Sbe'DollarQuote tag content)
      go

    param = do
      _ <- chunk "#{"
      content <- takeWhileP (Just "parameter") (/= '}')
      _ <- single '}'
      alpha <-
        case parseExp content of
          Left err -> fail err
          Right x -> pure x
      appendEncoder (Pe'Exp alpha)
      appendSqlBuilderExp Sbe'Param
      go

    splice = do
      _ <- chunk "^{"
      content <- takeWhileP (Just "splice") (/= '}')
      _ <- single '}'
      alpha <-
        case parseExp content of
          Left err -> fail err
          Right x -> pure x
      addSpliceBinding alpha
      go

    breakCharsIS = IS.fromList (map fromEnum breakChars)
    breakChars =
      [ '\'',
        'E',
        '"',
        '#',
        '^',
        '$',
        '-',
        '/'
      ]

    someSql = do
      s <- anySingle
      content <- takeWhileP (Just "sql") (\c -> IS.notMember (fromEnum c) breakCharsIS)
      appendSqlBuilderExp (Sbe'Sql (s : content))
      go

addParam :: State Int Builder
addParam = state \i ->
  let !i' = i + 1
   in (dollar <> stringUtf8 (show i), i')

parseSqlExpr :: String -> Either (ParseErrorBundle String Void) SqlExpr
parseSqlExpr str = do
  ps <- runParser (execStateT sqlExprParser (ParserState id id id 0)) "" str
  pure
    SqlExpr
      { sqlBuilderExp = ps'sqlBuilderExp ps [],
        paramEncoder = ps'paramEncoder ps [],
        spliceBinds = ps'spliceBinds ps [],
        bindCount = ps'nextUnique ps
      }

-- | QuasiQuoter that supports interpolation and splices. Produces a
-- 'Sql'.
--
-- @#{..}@ interpolates a haskell expression into a sql query.
--
-- @
-- example1 :: EncodeValue a => a -> Sql
-- example1 x = [sql| select \#{x} |]
-- @
--
-- @^{..}@ introduces a splice, which allows us to inject a sql
-- snippet along with the associated parameters into another sql
-- snippet.
--
-- @
-- example2 :: Sql
-- example2 = [sql| ^{example1 True} where true |]
-- @
sql :: QuasiQuoter
sql =
  QuasiQuoter
    { quoteExp = \str -> do
        case parseSqlExpr str of
          Left err -> fail (show err)
          Right sqlExpr -> compileSqlExpr sqlExpr,
      quotePat = undefined,
      quoteType = undefined,
      quoteDec = undefined
    }

compileSqlExpr :: SqlExpr -> Q Exp
compileSqlExpr (SqlExpr sqlBuilder enc spliceBindings bindCount) = do
  nameArr <- listArray (0, bindCount - 1) <$> replicateM bindCount (newName "x")
  let spliceDecs =
        map
          ( \SpliceBind {sbBuilder, sbParamEncoder, sbExp} ->
              ValD (ConP 'Sql (map VarP [nameArr ! sbBuilder, nameArr ! sbParamEncoder])) (NormalB sbExp) []
          )
          spliceBindings
  sqlBuilderExp <-
    let go a b = case a of
          Sbe'Var i -> [e|Ap $(varE (nameArr ! i)) <> $b|]
          Sbe'Param -> [e|Ap addParam <> $b|]
          Sbe'Quote content -> [e|pure (sq <> stringUtf8 content <> sq) <> $b|]
          Sbe'Ident content -> [e|pure (dq <> stringUtf8 content <> dq) <> $b|]
          Sbe'DollarQuote tag content -> [e|pure (dollar <> stringUtf8 tag <> dollar <> stringUtf8 content <> dollar <> stringUtf8 tag <> dollar) <> $b|]
          Sbe'Cquote content -> [e|pure (cquote <> content <> sq) <> $b|]
          Sbe'Sql content -> [e|pure (stringUtf8 content) <> $b|]
     in foldr go [e|pure mempty|] sqlBuilder
  encExp <-
    let go a b = case a of
          Pe'Exp x -> [e|$(pure x) >$ E.param encodeField <> $b|]
          Pe'Var x -> [e|$(varE (nameArr ! x)) <> $b|]
     in foldr go [e|mempty|] enc
  body <- [e|Sql (getAp $(pure sqlBuilderExp)) $(pure encExp)|]
  pure case spliceDecs of
    [] -> body
    _ -> LetE spliceDecs body