{-|
Module: IHP.Pagination.ControllerFunctions
Description: Paginate results in your actions
Copyright: (c) digitally induced GmbH, 2021
-}
module IHP.Pagination.ControllerFunctions
(   paginate,
    paginateWithOptions,
    filterList,
    defaultPaginationOptions,
    paginatedSqlQuery,
    paginatedSqlQueryWithOptions,
) where

import IHP.Prelude
import IHP.Controller.Param ( paramOrNothing )
import IHP.Pagination.Types ( Options(..), Pagination(..) )
import IHP.Pagination.Internal ( pageSize', page, offset' )
import IHP.QueryBuilder ( QueryBuilder, filterWhereILike, limit, offset )
import IHP.Fetch (fetchCount)
import IHP.ModelSupport (GetModelByTableName, sqlQueryHasql)
import IHP.Hasql.FromRow (FromRowHasql(..))
import IHP.Hasql.Encoders (ToSnippetParams(..), sqlToSnippet)
import Network.Wai (Request)
import qualified Hasql.Decoders as Decoders
import qualified Hasql.DynamicStatements.Snippet as Snippet
import Data.Text.Encoding (encodeUtf8)

-- | Paginate a query, with the following default options:
--
-- 1. Maximum items per page: 50. Each page will show at most 50 items.
-- 2. Selector window size: 5. The selector will show the current page, and 5 pages before and after it,
--    if they exist.
--
-- This function should be used inside your controller action. It will do two things:
--
--     1. Using the 'page' (current page number to display) and 'maxItems' (which overrides the set maximum
--        items per page) request parameters, this applies the the needed limit and offset to display the
--        correct page. For instance, page 3 with a maxItems of 50 would produce a limit of 50 and an offset
--        of 100 to display results 100 through 150.
--     2. Returns a 'Pagination' state which should be passed through to your view and then,
--        in turn, 'renderPagination'.
--
-- Example:
--
-- > action UsersAction = do
-- >    (userQ, pagination) <- query @User
-- >        |> orderBy #email
-- >        |> paginate
-- >    user <- userQ |> fetch
-- >    render IndexView { .. }
paginate :: forall controller table .
    (?request::Request
    , ?modelContext :: ModelContext
    , ?theAction :: controller
    , ?request :: Request) =>
    QueryBuilder table
    -> IO (QueryBuilder table, Pagination)
paginate :: forall controller (table :: Symbol).
(?request::Request, ?modelContext::ModelContext,
 ?theAction::controller, ?request::Request) =>
QueryBuilder table -> IO (QueryBuilder table, Pagination)
paginate = Options
-> QueryBuilder table -> IO (QueryBuilder table, Pagination)
forall controller (table :: Symbol).
(?request::Request, ?modelContext::ModelContext,
 ?theAction::controller, ?request::Request) =>
Options
-> QueryBuilder table -> IO (QueryBuilder table, Pagination)
paginateWithOptions Options
defaultPaginationOptions

-- | Paginate with ability to override the default options for maximum items per page and selector window size.
--
-- This function should be used inside your controller action. It will do two things:
--
--     1. Using the 'page' (current page number to display) and 'maxItems' (which overrides the set maximum
--        items per page) request parameters, this applies the the needed limit and offset to display the
--        correct page. For instance, page 3 with a maxItems of 50 would produce a limit of 50 and an offset
--        of 100 to display results 100 through 150.
--     2. Returns a 'Pagination' state which should be passed through to your view and then,
--        in turn, 'renderPagination'.
--
-- Example:
--
-- > action UsersAction = do
-- >    (userQ, pagination) <- query @User
-- >        |> orderBy #email
-- >        |> paginateWithOptions
-- >            (defaultPaginationOptions
-- >                |> set #maxItems 10)
-- >    user <- userQ |> fetch
-- >    render IndexView { .. }
paginateWithOptions :: forall controller table .
    (?request::Request
    , ?modelContext :: ModelContext
    , ?theAction :: controller
    , ?request :: Request) =>
    Options
    -> QueryBuilder table
    -> IO (QueryBuilder table, Pagination)
paginateWithOptions :: forall controller (table :: Symbol).
(?request::Request, ?modelContext::ModelContext,
 ?theAction::controller, ?request::Request) =>
Options
-> QueryBuilder table -> IO (QueryBuilder table, Pagination)
paginateWithOptions Options
options QueryBuilder table
query = do
    count <- QueryBuilder table
query
        QueryBuilder table -> (QueryBuilder table -> IO Int) -> IO Int
forall a b. a -> (a -> b) -> b
|> QueryBuilder table -> IO Int
forall (table :: Symbol).
(?modelContext::ModelContext) =>
QueryBuilder table -> IO Int
fetchCount

    let pageSize = (?request::Request) => Options -> Int
Options -> Int
pageSize' Options
options
        pagination = Pagination
            { currentPage :: Int
currentPage = Int
(?request::Request) => Int
page
            , totalItems :: Int
totalItems = Int -> Int
forall a b. (Integral a, Num b) => a -> b
fromIntegral Int
count
            , pageSize :: Int
pageSize = Int
pageSize
            , window :: Int
window = Options -> Int
windowSize Options
options
            }

    let results = QueryBuilder table
query
            QueryBuilder table
-> (QueryBuilder table -> QueryBuilder table) -> QueryBuilder table
forall a b. a -> (a -> b) -> b
|> Int64 -> QueryBuilder table -> QueryBuilder table
forall (model :: Symbol).
Int64 -> QueryBuilder model -> QueryBuilder model
limit (Int -> Int64
forall a b. (Integral a, Num b) => a -> b
fromIntegral Int
pageSize)
            QueryBuilder table
-> (QueryBuilder table -> QueryBuilder table) -> QueryBuilder table
forall a b. a -> (a -> b) -> b
|> Int64 -> QueryBuilder table -> QueryBuilder table
forall (model :: Symbol).
Int64 -> QueryBuilder model -> QueryBuilder model
offset (Int -> Int64
forall a b. (Integral a, Num b) => a -> b
fromIntegral (Int -> Int -> Int
offset' Int
pageSize Int
(?request::Request) => Int
page))

    pure
        ( results
        , pagination
        )

-- | Reading from the 'filter' query parameter, filters a query according to the string entered in the
--   filter box by the user (if any), on a given text-based field. Will return any results containing the
--   string in a case-insensitive fashion.
--
-- Example:
--
-- > action UsersAction = do
-- >    (userQ, pagination) <- query @User
-- >        |> orderBy #email
-- >        |> paginate
-- >        |> filterList #email
-- >    user <- userQ |> fetch
-- >    render IndexView { .. }
filterList :: forall name table model .
    (?request::Request
    , ?request :: Request
    , KnownSymbol name
    , HasField name model Text
    , model ~ GetModelByTableName table
    ) =>
    Proxy name
    -> QueryBuilder table
    -> QueryBuilder table
filterList :: forall (name :: Symbol) (table :: Symbol) model.
(?request::Request, ?request::Request, KnownSymbol name,
 HasField name model Text, model ~ GetModelByTableName table) =>
Proxy name -> QueryBuilder table -> QueryBuilder table
filterList Proxy name
field =
    case forall paramType.
(?request::Request, ParamReader (Maybe paramType)) =>
ByteString -> Maybe paramType
paramOrNothing @Text ByteString
"filter" of
       Just Text
uf -> (Proxy name, Text) -> QueryBuilder table -> QueryBuilder table
forall (name :: Symbol) (table :: Symbol) model value.
(KnownSymbol name, DefaultParamEncoder value,
 HasField name model value, model ~ GetModelByTableName table) =>
(Proxy name, value) -> QueryBuilder table -> QueryBuilder table
filterWhereILike (Proxy name
field, Text
"%" Text -> Text -> Text
forall a. Semigroup a => a -> a -> a
<> Text
uf Text -> Text -> Text
forall a. Semigroup a => a -> a -> a
<> Text
"%")
       Maybe Text
Nothing -> QueryBuilder table -> QueryBuilder table
forall a. a -> a
forall {k} (cat :: k -> k -> *) (a :: k). Category cat => cat a a
id

-- | Default options for a pagination. Can be passed into 'paginateOptions'. The defaults are as follows:
--
-- 1. Maximum items per page: 50. Each page will show at most 50 items.
-- 2. Selector window size: 5. The selector will show the current page, and up to 5 pages before and after it,
--    if they exist.
defaultPaginationOptions :: Options
defaultPaginationOptions :: Options
defaultPaginationOptions =
    Options
        { maxItems :: Int
maxItems = Int
50
        , windowSize :: Int
windowSize = Int
5
        }

-- | Runs a raw sql query and adds pagination to it.
--
-- By default, the pagination uses the following options:
-- 1. Maximum items per page: 50. Each page will show at most 50 items.
-- 2. Selector window size: 5. The selector will show the current page, and 5 pages before and after it,
--    if they exist.
--
-- This function should be used inside your controller action. It will do three things:
--
--     1. Using the 'page' (current page number to display) and 'maxItems' (which overrides the set maximum
--        items per page) request parameters, this applies the the needed limit and offset to display the
--        correct page. For instance, page 3 with a maxItems of 50 would produce a limit of 50 and an offset
--        of 100 to display results 100 through 150.
--     2. Returns a 'Pagination' state which should be passed through to your view and then,
--        in turn, 'renderPagination'.
--     3. Actually run the query and return the result.
--
-- __Example:__
--
-- > (users, pagination) <- paginatedSqlQuery "SELECT id, firstname, lastname FROM users" ()
--
-- Take a look at "IHP.QueryBuilder" for a typesafe approach on building simple queries.
--
-- *AutoRefresh:* When using 'paginatedSqlQuery' with AutoRefresh, you need to use 'trackTableRead' to let AutoRefresh know that you have accessed a certain table. Otherwise AutoRefresh will not watch table of your custom sql query.
paginatedSqlQuery
  :: ( FromRowHasql model
     , ToSnippetParams parameters
     , ?request :: Request
     , ?modelContext :: ModelContext
     , ?request :: Request
     )
  => Text -> parameters -> IO ([model], Pagination)
paginatedSqlQuery :: forall model parameters.
(FromRowHasql model, ToSnippetParams parameters, ?request::Request,
 ?modelContext::ModelContext, ?request::Request) =>
Text -> parameters -> IO ([model], Pagination)
paginatedSqlQuery = Options -> Text -> parameters -> IO ([model], Pagination)
forall model parameters.
(FromRowHasql model, ToSnippetParams parameters, ?request::Request,
 ?modelContext::ModelContext, ?request::Request) =>
Options -> Text -> parameters -> IO ([model], Pagination)
paginatedSqlQueryWithOptions Options
defaultPaginationOptions

-- | Runs a raw sql query and adds pagination to it.
--
-- This function accepts the same Options as 'paginateWithOptions', but otherwise behaves like 'paginatedSqlQuery'.
--
-- __Example:__
--
-- > (users, pagination) <- paginatedSqlQueryWithOptions
-- >     (defaultPaginationOptions |> set #maxItems 10)
-- >     "SELECT id, firstname, lastname FROM users"
-- >     ()
--
-- Take a look at "IHP.QueryBuilder" for a typesafe approach on building simple queries.
--
-- *AutoRefresh:* When using 'paginatedSqlQuery' with AutoRefresh, you need to use 'trackTableRead' to let AutoRefresh know that you have accessed a certain table. Otherwise AutoRefresh will not watch table of your custom sql query.
paginatedSqlQueryWithOptions
  :: ( FromRowHasql model
     , ToSnippetParams parameters
     , ?request :: Request
     , ?modelContext :: ModelContext
     , ?request :: Request
     )
  => Options -> Text -> parameters -> IO ([model], Pagination)
paginatedSqlQueryWithOptions :: forall model parameters.
(FromRowHasql model, ToSnippetParams parameters, ?request::Request,
 ?modelContext::ModelContext, ?request::Request) =>
Options -> Text -> parameters -> IO ([model], Pagination)
paginatedSqlQueryWithOptions Options
options Text
sql parameters
placeholders = do
    let pool :: Pool
pool = ?modelContext::ModelContext
ModelContext
?modelContext.hasqlPool
    let baseParams :: [Snippet]
baseParams = parameters -> [Snippet]
forall a. ToSnippetParams a => a -> [Snippet]
toSnippetParams parameters
placeholders

    let countSnippet :: Snippet
countSnippet = ByteString -> [Snippet] -> Snippet
sqlToSnippet (ByteString
"SELECT count(subquery.*) FROM (" ByteString -> ByteString -> ByteString
forall a. Semigroup a => a -> a -> a
<> Text -> ByteString
encodeUtf8 Text
sql ByteString -> ByteString -> ByteString
forall a. Semigroup a => a -> a -> a
<> ByteString
") as subquery") [Snippet]
baseParams
    count :: Int <- Pool -> Snippet -> Result Int -> IO Int
forall a.
(?modelContext::ModelContext) =>
Pool -> Snippet -> Result a -> IO a
sqlQueryHasql Pool
pool Snippet
countSnippet (Row Int -> Result Int
forall a. Row a -> Result a
Decoders.singleRow (NullableOrNot Value Int -> Row Int
forall a. NullableOrNot Value a -> Row a
Decoders.column (Value Int -> NullableOrNot Value Int
forall (decoder :: * -> *) a. decoder a -> NullableOrNot decoder a
Decoders.nonNullable (Int64 -> Int
forall a b. (Integral a, Num b) => a -> b
fromIntegral (Int64 -> Int) -> Value Int64 -> Value Int
forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
<$> Value Int64
Decoders.int8))))

    let pageSize = (?request::Request) => Options -> Int
Options -> Int
pageSize' Options
options
        pagination = Pagination
            { pageSize :: Int
pageSize = Int
pageSize
            , totalItems :: Int
totalItems = Int -> Int
forall a b. (Integral a, Num b) => a -> b
fromIntegral Int
count
            , currentPage :: Int
currentPage = Int -> Int
forall a b. (Integral a, Num b) => a -> b
fromIntegral Int
(?request::Request) => Int
page
            , window :: Int
window = Options -> Int
windowSize Options
options
            }

    let resultsSnippet = ByteString -> [Snippet] -> Snippet
sqlToSnippet (ByteString
"SELECT subquery.* FROM (" ByteString -> ByteString -> ByteString
forall a. Semigroup a => a -> a -> a
<> Text -> ByteString
encodeUtf8 Text
sql ByteString -> ByteString -> ByteString
forall a. Semigroup a => a -> a -> a
<> ByteString
") as subquery LIMIT ? OFFSET ?") ([Snippet]
baseParams [Snippet] -> [Snippet] -> [Snippet]
forall a. Semigroup a => a -> a -> a
<> [Int -> Snippet
forall param. DefaultParamEncoder param => param -> Snippet
Snippet.param Int
pageSize, Int -> Snippet
forall param. DefaultParamEncoder param => param -> Snippet
Snippet.param (Int -> Int -> Int
offset' Int
pageSize Int
(?request::Request) => Int
page)])
    results :: [model] <- sqlQueryHasql pool resultsSnippet (Decoders.rowList hasqlRowDecoder)

    pure (results, pagination)