module Database.Seakale.FromRow
  ( RowParser
  , pmap, ppure, preturn, papply, pbind, pfail, pempty, por
  , pbackend, pconsume
  , FromRow(..)
  , parseRows
  , parseRow
  , maybeParser
  ) where

import           GHC.Generics
import           GHC.Int

import qualified Data.ByteString.Char8 as BS
import qualified Data.ByteString.Lazy  as BSL
import qualified Data.Text             as T
import qualified Data.Text.Encoding    as TE
import qualified Data.Text.Lazy        as TL

import           Database.Seakale.Types

-- | A pseudo-monad in which parsing of rows is done. Because it is counting the
-- number of fields consumed, it can't be made an instance of Monad.
data RowParser backend :: Nat -> * -> * where
  GetBackend :: RowParser backend Zero backend
  Consume    :: RowParser backend One (ColumnInfo backend, Field backend)
  Pure       :: a -> RowParser backend Zero a
  Bind       :: RowParser backend n a -> (a -> RowParser backend m b)
             -> RowParser backend (n :+ m) b
  Or         :: RowParser backend n a -> RowParser backend n a
             -> RowParser backend n a
  Fail       :: String -> RowParser backend n a

-- | For consistency with the following functions, 'pmap' is given in addition
-- to 'fmap'.
pmap :: (a -> b) -> RowParser backend n a -> RowParser backend n b
pmap f = \case
  GetBackend    -> Bind GetBackend $ \x -> Pure $ f x
  Consume       -> Bind Consume $ \x -> Pure $ f x
  Pure x        -> Pure $ f x
  Bind parser g -> Bind parser $ \x -> pmap f $ g x
  Or par1 par2  -> Or (fmap f par1) (fmap f par2)
  Fail msg      -> Fail msg

instance Functor (RowParser backend n) where
  fmap = pmap

-- | Equivalent of 'pure' and 'return'.
ppure, preturn :: a -> RowParser backend Zero a
ppure = Pure
preturn = ppure

-- | Equivalent of '(<*>)'
papply :: RowParser backend n (a -> b) -> RowParser backend m a
       -> RowParser backend (n :+ m) b
papply pf px = Bind pf $ \f -> pmap (\x -> f x) px

-- | Equivalent of '(>>=)'.
pbind :: RowParser backend n a -> (a -> RowParser backend m b)
      -> RowParser backend (n :+ m) b
pbind = Bind

-- | Equivalent of 'fail' from 'Monad'.
pfail :: String -> RowParser backend n a
pfail = Fail

-- | Equivalent of 'empty' from 'Alternative'.
pempty :: RowParser backend n a
pempty = pfail "pempty"

-- | Equivalent of '(<|>)' from 'Alternative'.
por :: RowParser backend n a -> RowParser backend n a -> RowParser backend n a
por = Or

-- | Return the underlying SQL backend.
pbackend :: RowParser backend Zero backend
pbackend = GetBackend

-- | Return the next column.
pconsume :: RowParser backend One (ColumnInfo backend, Field backend)
pconsume = Consume

execParser :: RowParser backend n a -> backend
           -> [(ColumnInfo backend, Field backend)]
           -> Either String ([(ColumnInfo backend, Field backend)], a)
execParser parser backend pairs = case parser of
  Pure x  -> return (pairs, x)
  GetBackend -> return (pairs, backend)
  Consume -> case pairs of
    pair : pairs' -> return (pairs', pair)
    [] -> Left "not enough columns"
  Bind parser' f -> do
    (pairs', x) <- execParser parser' backend pairs
    execParser (f x) backend pairs'
  Or parser1 parser2 ->
    let eRes1 = execParser parser1 backend pairs
        eRes2 = execParser parser2 backend pairs
    in case eRes1 of
      Right _ -> eRes1
      _ -> eRes2
  Fail msg -> Left msg

class FromRow backend n a | a -> n where
  fromRow :: RowParser backend n a

  default fromRow :: (Generic a, GFromRow backend ReadCon n (Rep a))
                  => RowParser backend (n :+ Zero) a
  fromRow =
    gfromRow ReadCon Nothing `pbind` \case
      Nothing -> pfail "GFromRow backend ?: error while parsing"
      Just x  -> ppure (to x)

data ReadCon = ReadCon
newtype DontReadCon = DontReadCon BS.ByteString

class GFromRow backend con n f | f -> n where
  gfromRow :: con -> Maybe BS.ByteString -> RowParser backend n (Maybe (f a))

instance GFromRow backend con Zero V1 where
  gfromRow _ _ = pfail "GFromRow backend V1: no value for GHC.Generic.V1"

instance GFromRow backend con Zero U1 where
  gfromRow _ _ = ppure $ Just U1

instance (GFromRow backend con k a, GFromRow backend con l b, (k :+ l) ~ i)
  => GFromRow backend con i (a :*: b) where
  gfromRow dbCon brCon =
    gfromRow dbCon brCon `pbind` \ma ->
      flip pmap (gfromRow dbCon brCon) (\mb -> ((:*:) <$> ma <*> mb))

instance (GFromRow backend DontReadCon k (a :+: b), 'S k ~ i)
  => GFromRow backend ReadCon i (a :+: b) where
  gfromRow ReadCon brCon =
    pconsume `pbind` \(_, f) -> case fieldValue f of
      Nothing ->
        pfail "GFromRow backend (a :+: b): found NULL in place of constructor"
      Just con -> gfromRow (DontReadCon con) brCon

instance ( GFromRow backend DontReadCon k a, GFromRow backend DontReadCon l b
         , (k :+ l) ~ i )
  => GFromRow backend DontReadCon i (a :+: b) where
  gfromRow dbCon brCon =
    gfromRow dbCon brCon `pbind` \ml ->
      flip pmap (gfromRow dbCon brCon) $ \mr -> case (ml, mr) of
        (Just l, _) -> Just $ L1 l
        (_, Just r) -> Just $ R1 r
        _ -> Nothing

instance (FromRow backend n a, SkipColumns backend n)
  => GFromRow backend DontReadCon n (K1 i a) where
  gfromRow (DontReadCon con) = \case
    Just con' | con == con' -> pmap (Just. K1) fromRow
    _ -> pmap (const Nothing) skipColumns

instance FromRow backend n a => GFromRow backend ReadCon n (K1 i a) where
  gfromRow ReadCon _ = pmap (Just. K1) fromRow

instance (Constructor c, GFromRow backend ReadCon n a)
  => GFromRow backend ReadCon n (M1 C c a) where
  gfromRow dbCon _ = go undefined
    where
      go :: (Constructor c, GFromRow backend ReadCon n a)
         => M1 C c a b -> RowParser backend n (Maybe (M1 C c a b))
      go m1 = pmap (fmap M1) $ gfromRow dbCon $ Just $ BS.pack $ conName m1

instance (Constructor c, GFromRow backend DontReadCon n a)
  => GFromRow backend DontReadCon n (M1 C c a) where
  gfromRow dbCon _ = go undefined
    where
      go :: (Constructor c, GFromRow backend DontReadCon n a)
         => M1 C c a b -> RowParser backend n (Maybe (M1 C c a b))
      go m1 = pmap (fmap M1) $ gfromRow dbCon $ Just $ BS.pack $ conName m1

instance GFromRow backend con n a => GFromRow backend con n (M1 D c a) where
  gfromRow dbCon brCon = pmap (fmap M1) (gfromRow dbCon brCon)

instance GFromRow backend con n a => GFromRow backend con n (M1 S c a) where
  gfromRow dbCon brCon = pmap (fmap M1) (gfromRow dbCon brCon)

class SkipColumns backend n where
  skipColumns :: RowParser backend n ()

instance SkipColumns backend Zero where
  skipColumns = ppure ()

instance (SkipColumns backend n, 'S n ~ m) => SkipColumns backend m where
  skipColumns = pconsume `pbind` \_ -> skipColumns

-- | Try to parse rows given a row parser and the SQL backend.
parseRows :: RowParser backend n a -> backend -> [ColumnInfo backend]
          -> [Row backend] -> Either String [a]
parseRows parser backend cols = mapM (parseRow parser backend cols)

parseRow :: RowParser backend n a -> backend -> [ColumnInfo backend]
         -> Row backend -> Either String a
parseRow parser backend cols row = do
  let pairs = zip cols row
  snd <$> execParser parser backend pairs

instance FromRow backend One Null where
  fromRow = pconsume `pbind` \(_, f) -> case fieldValue f of
    Nothing -> preturn Null
    Just _  -> pfail "expected NULL"

instance FromRow backend Zero (Vector Zero a) where
  fromRow = preturn Nil

instance (FromRow backend One a, FromRow backend n (Vector n a))
  => FromRow backend ('S n) (Vector ('S n) a) where
  fromRow = fromRow `pbind` \x -> pmap (\xs -> x `cons` xs) fromRow

instance Backend backend => FromRow backend Zero ()

bytestringParser :: RowParser backend One BS.ByteString
bytestringParser = pconsume `pbind` \(_, f) -> case fieldValue f of
  Nothing -> pfail "unexpected NULL"
  Just bs -> preturn bs

readerParser :: Read a => RowParser backend One a
readerParser = pconsume `pbind` \(_, f) -> case fieldValue f of
  Nothing -> pfail "unexpected NULL"
  Just bs ->
    let str = BS.unpack bs
    in case reads str of
      (x,""):_ -> preturn x
      _ -> pfail $ "unreadable value: " ++ str

instance FromRow backend One BS.ByteString where
  fromRow = bytestringParser

instance FromRow backend One BSL.ByteString where
  fromRow = pmap BSL.fromStrict bytestringParser

instance FromRow backend One T.Text where
  fromRow = pmap TE.decodeUtf8 bytestringParser

instance FromRow backend One TL.Text where
  fromRow = pmap (TL.fromStrict . TE.decodeUtf8) bytestringParser

instance FromRow backend One Int where
  fromRow = readerParser

instance FromRow backend One Int8 where
  fromRow = readerParser

instance FromRow backend One Int16 where
  fromRow = readerParser

instance FromRow backend One Int32 where
  fromRow = readerParser

instance FromRow backend One Int64 where
  fromRow = readerParser

instance FromRow backend One Integer where
  fromRow = readerParser

instance FromRow backend One Double where
  fromRow = readerParser

instance FromRow backend One Float where
  fromRow = readerParser

maybeParser :: forall backend k a. FromRow backend k (Vector k Null)
            => RowParser backend k a -> RowParser backend k (Maybe a)
maybeParser parser =
  pmap Just parser
  `por`
  (pmap (\_ -> Nothing) (fromRow :: RowParser backend k (Vector k Null)))

instance (FromRow backend k a, FromRow backend k (Vector k Null))
  => FromRow backend k (Maybe a) where
  fromRow = maybeParser fromRow

instance (FromRow backend k a, FromRow backend l b, (k :+ l) ~ i)
  => FromRow backend i (a, b) where
  fromRow = (,) `pmap` fromRow `papply` fromRow

instance ( FromRow backend k a, FromRow backend l b, FromRow backend i c
         , (k :+ l :+ i) ~ j )
  => FromRow backend j (a, b, c) where
  fromRow = (,,) `pmap` fromRow `papply` fromRow `papply` fromRow