{-|
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.Context
import IHP.Controller.Param ( paramOrDefault, paramOrNothing )

import IHP.Pagination.Types
    ( Options(..), Pagination(..) )

import IHP.QueryBuilder
    ( HasQueryBuilder, filterWhereILike, limit, offset )
import IHP.Fetch (fetchCount)

import IHP.ModelSupport (GetModelByTableName, sqlQuery, sqlQueryScalar, Table)

import Database.PostgreSQL.Simple.ToField (toField, Action)
import Database.PostgreSQL.Simple.Types (Query(Query))

-- | 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 queryBuilderProvider joinRegister .
    (?context::ControllerContext
    , ?modelContext :: ModelContext
    , ?theAction :: controller
    , KnownSymbol table
    , HasQueryBuilder queryBuilderProvider joinRegister) =>
    queryBuilderProvider table
    -> IO (queryBuilderProvider table, Pagination)
paginate :: queryBuilderProvider table
-> IO (queryBuilderProvider table, Pagination)
paginate = Options
-> queryBuilderProvider table
-> IO (queryBuilderProvider table, Pagination)
forall controller (table :: Symbol)
       (queryBuilderProvider :: Symbol -> *) joinRegister.
(?context::ControllerContext, ?modelContext::ModelContext,
 ?theAction::controller, KnownSymbol table,
 HasQueryBuilder queryBuilderProvider joinRegister) =>
Options
-> queryBuilderProvider table
-> IO (queryBuilderProvider 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 queryBuilderProvider joinRegister .
    (?context::ControllerContext
    , ?modelContext :: ModelContext
    , ?theAction :: controller
    , KnownSymbol table
    , HasQueryBuilder queryBuilderProvider joinRegister) =>
    Options
    -> queryBuilderProvider table
    -> IO (queryBuilderProvider table, Pagination)
paginateWithOptions :: Options
-> queryBuilderProvider table
-> IO (queryBuilderProvider table, Pagination)
paginateWithOptions Options
options queryBuilderProvider table
query = do
    Int
count <- queryBuilderProvider table
query
        queryBuilderProvider table
-> (queryBuilderProvider table -> IO Int) -> IO Int
forall t1 t2. t1 -> (t1 -> t2) -> t2
|> queryBuilderProvider table -> IO Int
forall k (table :: Symbol) (queryBuilderProvider :: Symbol -> *)
       (joinRegister :: k).
(?modelContext::ModelContext, KnownSymbol table,
 HasQueryBuilder queryBuilderProvider joinRegister) =>
queryBuilderProvider table -> IO Int
fetchCount

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

    let results :: queryBuilderProvider table
results = queryBuilderProvider table
query
            queryBuilderProvider table
-> (queryBuilderProvider table -> queryBuilderProvider table)
-> queryBuilderProvider table
forall t1 t2. t1 -> (t1 -> t2) -> t2
|> Int -> queryBuilderProvider table -> queryBuilderProvider table
forall k (queryBuilderProvider :: Symbol -> *) (joinRegister :: k)
       (model :: Symbol).
HasQueryBuilder queryBuilderProvider joinRegister =>
Int -> queryBuilderProvider model -> queryBuilderProvider model
limit Int
pageSize
            queryBuilderProvider table
-> (queryBuilderProvider table -> queryBuilderProvider table)
-> queryBuilderProvider table
forall t1 t2. t1 -> (t1 -> t2) -> t2
|> Int -> queryBuilderProvider table -> queryBuilderProvider table
forall k (queryBuilderProvider :: Symbol -> *) (joinRegister :: k)
       (model :: Symbol).
HasQueryBuilder queryBuilderProvider joinRegister =>
Int -> queryBuilderProvider model -> queryBuilderProvider model
offset (Int -> Int -> Int
offset' Int
pageSize Int
(?context::ControllerContext) => Int
page)

    (queryBuilderProvider table, Pagination)
-> IO (queryBuilderProvider table, Pagination)
forall (f :: * -> *) a. Applicative f => a -> f a
pure
        ( queryBuilderProvider table
results
        , Pagination
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 queryBuilderProvider joinRegister .
    (?context::ControllerContext
    , KnownSymbol name
    , HasField name model Text
    , model ~ GetModelByTableName table
    , KnownSymbol table
    , HasQueryBuilder queryBuilderProvider joinRegister
    , Table model
    ) =>
    Proxy name
    -> queryBuilderProvider table
    -> queryBuilderProvider table
filterList :: Proxy name
-> queryBuilderProvider table -> queryBuilderProvider table
filterList Proxy name
field =
    case ByteString -> Maybe Text
forall paramType.
(?context::ControllerContext, ParamReader (Maybe paramType)) =>
ByteString -> Maybe paramType
paramOrNothing @Text ByteString
"filter" of
       Just Text
uf -> (Proxy name, Text)
-> queryBuilderProvider table -> queryBuilderProvider table
forall k (name :: Symbol) (table :: Symbol) model value
       (queryBuilderProvider :: Symbol -> *) (joinRegister :: k).
(KnownSymbol table, KnownSymbol name, ToField value,
 HasField name model value, model ~ GetModelByTableName table,
 HasQueryBuilder queryBuilderProvider joinRegister, Table model) =>
(Proxy name, value)
-> queryBuilderProvider table -> queryBuilderProvider 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 -> queryBuilderProvider table -> queryBuilderProvider table
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 :: Int -> Int -> Options
Options
        { $sel:maxItems:Options :: Int
maxItems = Int
50
        , $sel:windowSize:Options :: 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
  :: forall model
   . ( FromRow model
     , ?context :: ControllerContext
     , ?modelContext :: ModelContext
     )
  => ByteString -> [Action] -> IO ([model], Pagination)
paginatedSqlQuery :: ByteString -> [Action] -> IO ([model], Pagination)
paginatedSqlQuery = Options -> ByteString -> [Action] -> IO ([model], Pagination)
forall model.
(FromRow model, ?context::ControllerContext,
 ?modelContext::ModelContext) =>
Options -> ByteString -> [Action] -> 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
  :: forall model
   . ( FromRow model
     , ?context :: ControllerContext
     , ?modelContext :: ModelContext
     )
  => Options -> ByteString -> [Action] -> IO ([model], Pagination)
paginatedSqlQueryWithOptions :: Options -> ByteString -> [Action] -> IO ([model], Pagination)
paginatedSqlQueryWithOptions Options
options ByteString
sql [Action]
placeholders = do
    Int
count :: Int <- Query -> [Action] -> IO Int
forall q value.
(?modelContext::ModelContext, ToRow q, Show q, FromField value) =>
Query -> q -> IO value
sqlQueryScalar (ByteString -> Query
Query (ByteString -> Query) -> ByteString -> Query
forall a b. (a -> b) -> a -> b
$ ByteString
"SELECT count(subquery.*) FROM (" ByteString -> ByteString -> ByteString
forall a. Semigroup a => a -> a -> a
<> ByteString
sql ByteString -> ByteString -> ByteString
forall a. Semigroup a => a -> a -> a
<> ByteString
") as subquery") [Action]
placeholders

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

    [model]
results :: [model] <- Query -> [Action] -> IO [model]
forall q r.
(?modelContext::ModelContext, ToRow q, FromRow r, Show q) =>
Query -> q -> IO [r]
sqlQuery
        (ByteString -> Query
Query (ByteString -> Query) -> ByteString -> Query
forall a b. (a -> b) -> a -> b
$ ByteString
"SELECT subquery.* FROM (" ByteString -> ByteString -> ByteString
forall a. Semigroup a => a -> a -> a
<> ByteString
sql ByteString -> ByteString -> ByteString
forall a. Semigroup a => a -> a -> a
<> ByteString
") as subquery LIMIT ? OFFSET ?")
        ([Action]
placeholders [Action] -> [Action] -> [Action]
forall a. Semigroup a => a -> a -> a
++ (Int -> Action) -> [Int] -> [Action]
forall a b. (a -> b) -> [a] -> [b]
map Int -> Action
forall a. ToField a => a -> Action
toField [Int
pageSize, Int -> Int -> Int
offset' Int
pageSize Int
(?context::ControllerContext) => Int
page])

    ([model], Pagination) -> IO ([model], Pagination)
forall (f :: * -> *) a. Applicative f => a -> f a
pure ([model]
results, Pagination
pagination)

-- We limit the page size to a maximum of 200, to prevent users from
-- passing in query params with a value that could overload the
-- database (e.g. maxItems=100000)
pageSize' :: (?context::ControllerContext) => Options -> Int
pageSize' :: Options -> Int
pageSize' Options
options = Int -> Int -> Int
forall a. Ord a => a -> a -> a
min (Int -> Int -> Int
forall a. Ord a => a -> a -> a
max Int
1 (Int -> Int) -> Int -> Int
forall a b. (a -> b) -> a -> b
$ Int -> ByteString -> Int
forall a.
(?context::ControllerContext, ParamReader a) =>
a -> ByteString -> a
paramOrDefault @Int (Options -> Int
maxItems Options
options) ByteString
"maxItems") Int
200

-- Page and page size shouldn't be lower than 1.
page :: (?context::ControllerContext) => Int
page :: Int
page = Int -> Int -> Int
forall a. Ord a => a -> a -> a
max Int
1 (Int -> Int) -> Int -> Int
forall a b. (a -> b) -> a -> b
$ Int -> ByteString -> Int
forall a.
(?context::ControllerContext, ParamReader a) =>
a -> ByteString -> a
paramOrDefault @Int Int
1 ByteString
"page"

offset' :: Int -> Int -> Int
offset' :: Int -> Int -> Int
offset' Int
pageSize Int
page = (Int
page Int -> Int -> Int
forall a. Num a => a -> a -> a
- Int
1) Int -> Int -> Int
forall a. Num a => a -> a -> a
* Int
pageSize