{-# LANGUAGE BlockArguments #-}
{-# LANGUAGE DefaultSignatures #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE MonoLocalBinds #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE TypeApplications #-}
{-# LANGUAGE TypeOperators #-}
{-# LANGUAGE UndecidableInstances #-}

module Hasql.Interpolate.Internal.EncodeRow
  ( EncodeRow (..),
    GEncodeRow (..),
    toTable,
  )
where

import Control.Monad
import Data.Functor.Contravariant
import Data.List (intersperse)
import Data.Monoid
import GHC.Generics
import qualified Hasql.Encoders as E
import Hasql.Interpolate.Internal.EncodeRow.TH
import Hasql.Interpolate.Internal.Encoder
import Hasql.Interpolate.Internal.Sql
import Hasql.Interpolate.Internal.TH (addParam)

class EncodeRow a where
  -- | The continuation @(forall x. (a -> x -> x) -> x -> E.Params x
  -- -> Int -> r)@ is given cons @(a -> x -> x)@ and nil @(x)@ for some
  -- existential type @x@ and an encoder (@'E.Params' x@) for @x@. An
  -- Int is also given to tally up how many sql fields are in the
  -- unzipped structure.
  --
  -- ==== __Example__
  --
  -- Consider the following manually written instance:
  --
  -- @
  -- data Blerg = Blerg Int64 Bool Text Char
  --
  -- instance EncodeRow Blerg where
  --   unzipWithEncoder k = k cons nil enc 4
  --     where
  --       cons (Blerg a b c d) ~(as, bs, cs, ds) =
  --         (a : as, b : bs, c : cs, d : ds)
  --       nil = ([], [], [], [])
  --       enc =
  --              ((\(x, _, _, _) -> x) >$< param encodeField)
  --           <> ((\(_, x, _, _) -> x) >$< param encodeField)
  --           <> ((\(_, _, x, _) -> x) >$< param encodeField)
  --           <> ((\(_, _, _, x) -> x) >$< param encodeField)
  -- @
  --
  -- We chose @([Int64], [Bool], [Text], [Char])@ as our existential
  -- type. If we instead use the default instance based on
  -- 'GEncodeRow' then we would produce the same code as the
  -- instance below:
  --
  -- @
  -- instance EncodeRow Blerg where
  --   unzipWithEncoder k = k cons nil enc 4
  --     where
  --       cons (Blerg a b c d) ~(~(as, bs), ~(cs, ds)) =
  --         ((a : as, b : bs), (c : cs, d : ds))
  --       nil = (([], []), ([], []))
  --       enc =
  --              ((\((x, _),      _) -> x) >$< param encodeField)
  --           <> ((\((_, x),      _) -> x) >$< param encodeField)
  --           <> ((\(_     , (x, _)) -> x) >$< param encodeField)
  --           <> ((\(_     , (_, x)) -> x) >$< param encodeField)
  -- @
  --
  -- The notable difference being we don't produce a flat tuple, but
  -- instead produce a balanced tree of tuples isomorphic to the
  -- balanced tree of @':*:'@ from the generic 'Rep' of @Blerg@.
  unzipWithEncoder :: (forall x. (a -> x -> x) -> x -> E.Params x -> Int -> r) -> r
  default unzipWithEncoder ::
    (Generic a, GEncodeRow (Rep a)) =>
    (forall x. (a -> x -> x) -> x -> E.Params x -> Int -> r) ->
    r
  unzipWithEncoder k = gUnzipWithEncoder \cons nil enc fc ->
    k (cons . from) nil enc fc
  {-# INLINE unzipWithEncoder #-}

class GEncodeRow a where
  gUnzipWithEncoder :: (forall x. (a p -> x -> x) -> x -> E.Params x -> Int -> r) -> r

-- | 'toTable' takes some list of products into the corresponding
-- relation in sql. It is applying the @unnest@ based technique
-- described [in the hasql
-- documentation](https://hackage.haskell.org/package/hasql-1.4.5.1/docs/Hasql-Statement.html#g:2).
--
-- ==== __Example__
--
-- Here is a small example that takes a haskell list and inserts it
-- into a table @blerg@ which has columns @x@, @y@, and @z@ of type
-- @int8@, @boolean@, and @text@ respectively.
--
-- @
-- toTableExample :: [(Int64, Bool, Text)] -> Statement () ()
-- toTableExample rowsToInsert =
--   interp [sql| insert into blerg (x, y, z) select * from ^{toTable rowsToInsert} |]
-- @
--
-- This is driven by the 'EncodeRow' type class that has a
-- default implementation for product types that are an instance of
-- 'Generic'. So the following also works:
--
-- @
-- data Blerg
--   = Blerg Int64 Bool Text
--   deriving stock (Generic)
--   deriving anyclass (EncodeRow)
--
-- toTableExample :: [Blerg] -> Statement () ()
-- toTableExample blergs =
--   interp [sql| insert into blerg (x, y, z) select * from ^{toTable blergs} |]
-- @
toTable :: EncodeRow a => [a] -> Sql
toTable xs = unzipWithEncoder \cons nil enc i ->
  let unzippedEncoder = foldr cons nil xs >$ enc
      queryString = getAp $ pure "unnest(" <> (mconcat . intersperse ", " <$> Ap (replicateM i addParam)) <> pure ")"
   in Sql queryString unzippedEncoder
{-# INLINE toTable #-}

instance GEncodeRow x => GEncodeRow (M1 t i x) where
  gUnzipWithEncoder k = gUnzipWithEncoder \cons nil enc i ->
    k (\(M1 a) -> cons a) nil enc i
  {-# INLINE gUnzipWithEncoder #-}

instance (GEncodeRow a, GEncodeRow b) => GEncodeRow (a :*: b) where
  gUnzipWithEncoder k = gUnzipWithEncoder \consa nila enca ia -> gUnzipWithEncoder \consb nilb encb ib ->
    k
      ( \(a :*: b) ~(as, bs) ->
          (consa a as, consb b bs)
      )
      (nila, nilb)
      (contramap fst enca <> contramap snd encb)
      (ia + ib)
  {-# INLINE gUnzipWithEncoder #-}

instance EncodeField a => GEncodeRow (K1 i a) where
  gUnzipWithEncoder k =
    k (\(K1 a) b -> a : b) [] (E.param (E.nonNullable (E.foldableArray encodeField))) 1
  {-# INLINE gUnzipWithEncoder #-}

$(traverse genEncodeRowInstance [2 .. 8])