{-|
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 ( QueryBuilder, filterWhereILike, limit, offset )
import IHP.Fetch (fetchCount)
import IHP.ModelSupport (GetModelByTableName, sqlQueryHasql, Table)
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 .
    (?context::ControllerContext
    , ?modelContext :: ModelContext
    , ?theAction :: controller
    , ?request :: Request
    , KnownSymbol table) =>
    QueryBuilder table
    -> IO (QueryBuilder table, Pagination)
paginate :: forall controller (table :: Symbol).
(?context::ControllerContext, ?modelContext::ModelContext,
 ?theAction::controller, ?request::Request, KnownSymbol table) =>
QueryBuilder table -> IO (QueryBuilder table, Pagination)
paginate = Options
-> QueryBuilder table -> IO (QueryBuilder table, Pagination)
forall controller (table :: Symbol).
(?context::ControllerContext, ?modelContext::ModelContext,
 ?theAction::controller, ?request::Request, KnownSymbol table) =>
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 .
    (?context::ControllerContext
    , ?modelContext :: ModelContext
    , ?theAction :: controller
    , ?request :: Request
    , KnownSymbol table) =>
    Options
    -> QueryBuilder table
    -> IO (QueryBuilder table, Pagination)
paginateWithOptions :: forall controller (table :: Symbol).
(?context::ControllerContext, ?modelContext::ModelContext,
 ?theAction::controller, ?request::Request, KnownSymbol table) =>
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, KnownSymbol table) =>
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
|> Int -> QueryBuilder table -> QueryBuilder table
forall (model :: Symbol).
Int -> QueryBuilder model -> QueryBuilder model
limit Int
pageSize
            QueryBuilder table
-> (QueryBuilder table -> QueryBuilder table) -> QueryBuilder table
forall a b. a -> (a -> b) -> b
|> Int -> QueryBuilder table -> QueryBuilder table
forall (model :: Symbol).
Int -> QueryBuilder model -> QueryBuilder model
offset (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 .
    (?context::ControllerContext
    , ?request :: Request
    , KnownSymbol name
    , HasField name model Text
    , model ~ GetModelByTableName table
    , KnownSymbol table
    , Table model
    ) =>
    Proxy name
    -> QueryBuilder table
    -> QueryBuilder table
filterList :: forall (name :: Symbol) (table :: Symbol) model.
(?context::ControllerContext, ?request::Request, KnownSymbol name,
 HasField name model Text, model ~ GetModelByTableName table,
 KnownSymbol table, Table model) =>
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 table, KnownSymbol name, DefaultParamEncoder value,
 HasField name model value, model ~ GetModelByTableName table,
 Table model) =>
(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
     , ?context :: ControllerContext
     , ?modelContext :: ModelContext
     , ?request :: Request
     )
  => Text -> parameters -> IO ([model], Pagination)
paginatedSqlQuery :: forall model parameters.
(FromRowHasql model, ToSnippetParams parameters,
 ?context::ControllerContext, ?modelContext::ModelContext,
 ?request::Request) =>
Text -> parameters -> IO ([model], Pagination)
paginatedSqlQuery = Options -> Text -> parameters -> IO ([model], Pagination)
forall model parameters.
(FromRowHasql model, ToSnippetParams parameters,
 ?context::ControllerContext, ?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
     , ?context :: ControllerContext
     , ?modelContext :: ModelContext
     , ?request :: Request
     )
  => Options -> Text -> parameters -> IO ([model], Pagination)
paginatedSqlQueryWithOptions :: forall model parameters.
(FromRowHasql model, ToSnippetParams parameters,
 ?context::ControllerContext, ?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)

-- 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' :: (?request :: Request) => Options -> Int
pageSize' :: (?request::Request) => 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
$ forall a.
(?request::Request, 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 :: (?request :: Request) => Int
page :: (?request::Request) => 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
$ forall a.
(?request::Request, 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