content: {-|
  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 (FromRow, ToRow, Query(..), Only(Only), (:.)(..))
  
  -- | 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 = paginateWithOptions 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 query = do
      count <- query
          |> fetchCount
  
      let pageSize = pageSize' options
          pagination = Pagination
              { currentPage = page
              , totalItems = fromIntegral count
              , pageSize = pageSize
              , window = windowSize options
              }
  
      let results = query
              |> limit pageSize
              |> offset (offset' pageSize 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 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 field =
      case paramOrNothing @Text "filter" of
         Just uf -> filterWhereILike (field, "%" <> uf <> "%")
         Nothing -> 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
          { maxItems = 50
          , windowSize = 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
    :: ( FromRow model
       , ToRow parameters
       , ?context :: ControllerContext
       , ?modelContext :: ModelContext
       )
    => Query -> parameters -> IO ([model], Pagination)
  paginatedSqlQuery = paginatedSqlQueryWithOptions 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
    :: ( FromRow model
       , ToRow parameters
       , ?context :: ControllerContext
       , ?modelContext :: ModelContext
       )
    => Options -> Query -> parameters -> IO ([model], Pagination)
  paginatedSqlQueryWithOptions options sql placeholders = do
      count :: Int <- sqlQueryScalar ("SELECT count(subquery.*) FROM (" <> sql <> ") as subquery") placeholders
  
      let pageSize = pageSize' options
          pagination = Pagination
              { pageSize = pageSize
              , totalItems = fromIntegral count
              , currentPage = fromIntegral page
              , window = windowSize options
              }
  
      results :: [model] <- sqlQuery
          ("SELECT subquery.* FROM (" <> sql <> ") as subquery LIMIT ? OFFSET ?")
          (placeholders :. Only pageSize :. Only (offset' pageSize page))
  
      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' :: (?context::ControllerContext) => Options -> Int
  pageSize' options = min (max 1 $ paramOrDefault @Int (maxItems options) "maxItems") 200
  
  -- Page and page size shouldn't be lower than 1.
  page :: (?context::ControllerContext) => Int
  page = max 1 $ paramOrDefault @Int 1 "page"
  
  offset' :: Int -> Int -> Int
  offset' pageSize page = (page - 1) * pageSize
  