module IHP.Pagination.ViewFunctions (
    module IHP.Pagination.Types,
    renderPagination,
    renderFilter,
) where

import IHP.Prelude
import IHP.Pagination.Types
import IHP.Pagination.Helpers

import IHP.ControllerSupport

import Text.Blaze.Html (Html)
import IHP.HSX.QQ (hsx)

import IHP.Controller.Param (paramOrNothing)

import IHP.View.Classes
import qualified Network.Wai as Wai
import qualified Network.HTTP.Types.URI as Query
import IHP.ViewSupport (theRequest, theCSSFramework)
import qualified Data.Containers.ListUtils as List
import IHP.View.Types (PaginationView(..), styledPagination, styledPaginationPageLink, styledPaginationDotDot, styledPaginationItemsPerPageSelector, styledPaginationLinkPrevious, styledPaginationLinkNext)
import IHP.View.CSSFramework


-- | Render a navigation for your pagination. This is to be used in your view whenever
-- to allow users to change pages, including "Next" and "Previous".
-- If there is only one page, this will not render anything.
renderPagination :: (?context::ControllerContext) => Pagination -> Html
renderPagination :: Pagination -> Html
renderPagination pagination :: Pagination
pagination@Pagination {Int
$sel:currentPage:Pagination :: Pagination -> Int
currentPage :: Int
currentPage, Int
$sel:window:Pagination :: Pagination -> Int
window :: Int
window, Int
$sel:pageSize:Pagination :: Pagination -> Int
pageSize :: Int
pageSize} =
        Bool -> Html -> Html
forall (f :: * -> *). Applicative f => Bool -> f () -> f ()
when (Pagination -> Bool
showPagination Pagination
pagination) (Html -> Html) -> Html -> Html
forall a b. (a -> b) -> a -> b
$ CSSFramework -> CSSFramework -> PaginationView -> Html
styledPagination CSSFramework
(?context::ControllerContext) => CSSFramework
theCSSFramework CSSFramework
(?context::ControllerContext) => CSSFramework
theCSSFramework PaginationView
paginationView
        where
            paginationView :: PaginationView
paginationView = PaginationView :: CSSFramework
-> Pagination
-> (Int -> ByteString)
-> Html
-> Html
-> Html
-> Html
-> PaginationView
PaginationView
                { $sel:cssFramework:PaginationView :: CSSFramework
cssFramework = CSSFramework
(?context::ControllerContext) => CSSFramework
theCSSFramework
                , $sel:pagination:PaginationView :: Pagination
pagination = Pagination
pagination
                , $sel:pageUrl:PaginationView :: Int -> ByteString
pageUrl = Int -> ByteString
forall a. (Show a, ?context::ControllerContext) => a -> ByteString
pageUrl
                , $sel:linkPrevious:PaginationView :: Html
linkPrevious = Html
linkPrevious
                , $sel:linkNext:PaginationView :: Html
linkNext = Html
linkNext
                , $sel:pageDotDotItems:PaginationView :: Html
pageDotDotItems = Html
pageDotDotItems
                , $sel:itemsPerPageSelector:PaginationView :: Html
itemsPerPageSelector = Html
itemsPerPageSelector
                }

            linkPrevious :: Html
linkPrevious =
                CSSFramework -> CSSFramework -> Pagination -> ByteString -> Html
styledPaginationLinkPrevious CSSFramework
(?context::ControllerContext) => CSSFramework
theCSSFramework CSSFramework
(?context::ControllerContext) => CSSFramework
theCSSFramework Pagination
pagination (Int -> ByteString
forall a. (Show a, ?context::ControllerContext) => a -> ByteString
pageUrl (Int -> ByteString) -> Int -> ByteString
forall a b. (a -> b) -> a -> b
$ Int
currentPage Int -> Int -> Int
forall a. Num a => a -> a -> a
- Int
1)

            linkNext :: Html
linkNext =
                CSSFramework -> CSSFramework -> Pagination -> ByteString -> Html
styledPaginationLinkNext CSSFramework
(?context::ControllerContext) => CSSFramework
theCSSFramework CSSFramework
(?context::ControllerContext) => CSSFramework
theCSSFramework Pagination
pagination (Int -> ByteString
forall a. (Show a, ?context::ControllerContext) => a -> ByteString
pageUrl (Int -> ByteString) -> Int -> ByteString
forall a b. (a -> b) -> a -> b
$ Int
currentPage Int -> Int -> Int
forall a. Num a => a -> a -> a
+ Int
1)

            itemsPerPageSelector :: Html
itemsPerPageSelector =
                CSSFramework
-> CSSFramework -> Pagination -> (Int -> ByteString) -> Html
styledPaginationItemsPerPageSelector CSSFramework
(?context::ControllerContext) => CSSFramework
theCSSFramework CSSFramework
(?context::ControllerContext) => CSSFramework
theCSSFramework Pagination
pagination Int -> ByteString
forall a. (Show a, ?context::ControllerContext) => a -> ByteString
itemsPerPageUrl

            pageDotDotItems :: Html
pageDotDotItems = [hsx|{forEach (processedPages pages) pageDotDotItem}|]

            pageDotDotItem :: PageDotDot -> Html
pageDotDotItem PageDotDot
pg =
                case PageDotDot
pg of
                    Page Int
n ->
                        CSSFramework
-> CSSFramework -> Pagination -> ByteString -> Int -> Html
styledPaginationPageLink CSSFramework
(?context::ControllerContext) => CSSFramework
theCSSFramework CSSFramework
(?context::ControllerContext) => CSSFramework
theCSSFramework Pagination
pagination (Int -> ByteString
forall a. (Show a, ?context::ControllerContext) => a -> ByteString
pageUrl Int
n) Int
n
                    DotDot Int
n ->
                        CSSFramework -> CSSFramework -> Pagination -> Html
styledPaginationDotDot CSSFramework
(?context::ControllerContext) => CSSFramework
theCSSFramework CSSFramework
(?context::ControllerContext) => CSSFramework
theCSSFramework Pagination
pagination

            pageUrl :: a -> ByteString
pageUrl a
n = ByteString
path ByteString -> ByteString -> ByteString
forall a. Semigroup a => a -> a -> a
<> Bool -> Query -> ByteString
Query.renderQuery Bool
True Query
newQueryString
                where
                    -- "?page=" ++ show n ++ maybeFilter ++ maybeMaxItems
                    path :: ByteString
path = Request -> ByteString
Wai.rawPathInfo Request
(?context::ControllerContext) => Request
theRequest
                    queryString :: Query
queryString = Request -> Query
Wai.queryString Request
(?context::ControllerContext) => Request
theRequest
                    newQueryString :: Query
newQueryString = Query
queryString
                        Query -> (Query -> Query) -> Query
forall t1 t2. t1 -> (t1 -> t2) -> t2
|> ByteString -> ByteString -> Query -> Query
setQueryValue ByteString
"page" (Text -> ByteString
forall a b. ConvertibleStrings a b => a -> b
cs (Text -> ByteString) -> Text -> ByteString
forall a b. (a -> b) -> a -> b
$ a -> Text
forall a. Show a => a -> Text
show a
n)
                        Query -> (Query -> Query) -> Query
forall t1 t2. t1 -> (t1 -> t2) -> t2
|> (?context::ControllerContext) => Query -> Query
Query -> Query
maybeFilter
                        Query -> (Query -> Query) -> Query
forall t1 t2. t1 -> (t1 -> t2) -> t2
|> (?context::ControllerContext) => Query -> Query
Query -> Query
maybeMaxItems

            itemsPerPageUrl :: a -> ByteString
itemsPerPageUrl a
n = ByteString
path ByteString -> ByteString -> ByteString
forall a. Semigroup a => a -> a -> a
<> Bool -> Query -> ByteString
Query.renderQuery Bool
True Query
newQueryString
                where
                    path :: ByteString
path = Request -> ByteString
Wai.rawPathInfo Request
(?context::ControllerContext) => Request
theRequest
                    queryString :: Query
queryString = Request -> Query
Wai.queryString Request
(?context::ControllerContext) => Request
theRequest
                    newQueryString :: Query
newQueryString = Query
queryString
                        Query -> (Query -> Query) -> Query
forall t1 t2. t1 -> (t1 -> t2) -> t2
|> ByteString -> ByteString -> Query -> Query
setQueryValue ByteString
"maxItems" (Text -> ByteString
forall a b. ConvertibleStrings a b => a -> b
cs (Text -> ByteString) -> Text -> ByteString
forall a b. (a -> b) -> a -> b
$ a -> Text
forall a. Show a => a -> Text
tshow a
n)
                        -- If we change the number of items, we should jump back to the first page
                        -- so we are not out of the items bound.
                        Query -> (Query -> Query) -> Query
forall t1 t2. t1 -> (t1 -> t2) -> t2
|> ByteString -> ByteString -> Query -> Query
setQueryValue ByteString
"page" (Text -> ByteString
forall a b. ConvertibleStrings a b => a -> b
cs (Text -> ByteString) -> Text -> ByteString
forall a b. (a -> b) -> a -> b
$ Integer -> Text
forall a. Show a => a -> Text
show Integer
1)

            maybeFilter :: Query -> Query
maybeFilter Query
queryString =
                case ByteString -> Maybe Text
forall paramType.
(?context::ControllerContext, ParamReader (Maybe paramType)) =>
ByteString -> Maybe paramType
paramOrNothing @Text ByteString
"filter" of
                    Maybe Text
Nothing -> Query
queryString
                    Just Text
"" -> Query
queryString
                    Just Text
filterValue -> Query
queryString Query -> (Query -> Query) -> Query
forall t1 t2. t1 -> (t1 -> t2) -> t2
|> ByteString -> ByteString -> Query -> Query
setQueryValue ByteString
"filter" (Text -> ByteString
forall a b. ConvertibleStrings a b => a -> b
cs Text
filterValue)

            maybeMaxItems :: Query -> Query
maybeMaxItems Query
queryString =
                case ByteString -> Maybe Int
forall paramType.
(?context::ControllerContext, ParamReader (Maybe paramType)) =>
ByteString -> Maybe paramType
paramOrNothing @Int ByteString
"maxItems" of
                    Maybe Int
Nothing -> Query
queryString
                    Just Int
m -> Query
queryString Query -> (Query -> Query) -> Query
forall t1 t2. t1 -> (t1 -> t2) -> t2
|> ByteString -> ByteString -> Query -> Query
setQueryValue ByteString
"maxItems" (Text -> ByteString
forall a b. ConvertibleStrings a b => a -> b
cs (Text -> ByteString) -> Text -> ByteString
forall a b. (a -> b) -> a -> b
$ Int -> Text
forall a. Show a => a -> Text
tshow Int
m)

            processedPages :: [Int] -> [PageDotDot]
processedPages (Int
pg0:Int
pg1:[Int]
rest) =
                if Int
pg1 Int -> Int -> Bool
forall a. Eq a => a -> a -> Bool
== Int
pg0 Int -> Int -> Int
forall a. Num a => a -> a -> a
+ Int
1 then
                    Int -> PageDotDot
Page Int
pg0 PageDotDot -> [PageDotDot] -> [PageDotDot]
forall a. a -> [a] -> [a]
: [Int] -> [PageDotDot]
processedPages (Int
pg1Int -> [Int] -> [Int]
forall a. a -> [a] -> [a]
:[Int]
rest)
                else
                    Int -> PageDotDot
Page Int
pg0 PageDotDot -> [PageDotDot] -> [PageDotDot]
forall a. a -> [a] -> [a]
: Int -> PageDotDot
DotDot ((Int
pg1Int -> Int -> Int
forall a. Num a => a -> a -> a
+Int
pg0) Int -> Int -> Int
forall a. Integral a => a -> a -> a
`div` Int
2) PageDotDot -> [PageDotDot] -> [PageDotDot]
forall a. a -> [a] -> [a]
: [Int] -> [PageDotDot]
processedPages (Int
pg1Int -> [Int] -> [Int]
forall a. a -> [a] -> [a]
:[Int]
rest)
            processedPages [Int
pg] =
                [Int -> PageDotDot
Page Int
pg]
            processedPages [] = []
            pages :: [Int]
pages =
                let
                    totalPages :: Int
totalPages = Pagination -> Int
getLastPage Pagination
pagination

                    lowerBound :: Int
lowerBound
                      | Int
currentPage Int -> Int -> Int
forall a. Num a => a -> a -> a
- Int
window Int -> Int -> Bool
forall a. Ord a => a -> a -> Bool
< Int
1 =
                        Int
1
                      | Int
currentPage Int -> Int -> Int
forall a. Num a => a -> a -> a
+ Int
window Int -> Int -> Bool
forall a. Ord a => a -> a -> Bool
> Int
totalPages =
                        Int
totalPages Int -> Int -> Int
forall a. Num a => a -> a -> a
- Int
window Int -> Int -> Int
forall a. Num a => a -> a -> a
* Int
2 Int -> Int -> Int
forall a. Num a => a -> a -> a
+ Int
1
                      | Bool
otherwise =
                        Int
currentPage Int -> Int -> Int
forall a. Num a => a -> a -> a
- Int
window

                    upperBound :: Int
upperBound
                      | Int
currentPage Int -> Int -> Int
forall a. Num a => a -> a -> a
+ Int
window Int -> Int -> Bool
forall a. Ord a => a -> a -> Bool
> Int
totalPages =
                        Int
totalPages
                      | Int
currentPage Int -> Int -> Int
forall a. Num a => a -> a -> a
- Int
window Int -> Int -> Bool
forall a. Ord a => a -> a -> Bool
< Int
1 =
                        Int
window Int -> Int -> Int
forall a. Num a => a -> a -> a
* Int
2
                      | Bool
otherwise =
                        Int
currentPage Int -> Int -> Int
forall a. Num a => a -> a -> a
+ Int
window

                in
                    if Int
window Int -> Int -> Bool
forall a. Ord a => a -> a -> Bool
> Int
totalPages then
                        [Int
1..Pagination -> Int
getLastPage Pagination
pagination]
                    else
                        [Int] -> [Int]
List.nubInt ([Int] -> [Int]) -> [Int] -> [Int]
forall a b. (a -> b) -> a -> b
$ Int
1 Int -> [Int] -> [Int]
forall a. a -> [a] -> [a]
: [Int -> Int -> Int
forall a. Ord a => a -> a -> a
max Int
1 Int
lowerBound..Int -> Int -> Int
forall a. Ord a => a -> a -> a
min (Pagination -> Int
getLastPage Pagination
pagination) Int
upperBound] [Int] -> [Int] -> [Int]
forall a. Semigroup a => a -> a -> a
++ [Int
totalPages]

-- | Render a filtering box in your view. Allows the user to type in a query and filter
-- results according to what they type.
--
-- Below is an example of how this might be used in your index. Replace the existing <h1> with:
--        <div class="container">
--          <div class="row justify-content-between">
--              <div class="col-7">
--                  <h1>Users<a href={pathTo NewUserAction} class="btn btn-primary ml-4">+ New</a></h1>
--              </div>
--              <div class="col-5">
--                  {renderFilter "Username"}
--              </div>
--          </div>
--        </div>
renderFilter :: (?context::ControllerContext) =>
    Text    -- ^ Placeholder text for the text box
    -> Html
renderFilter :: Text -> Html
renderFilter Text
placeholder =
    [hsx|
        <form method="GET" action="" class="mt-2 float-right">
            <div class="form-row">
                <div class="col-auto">
                <label class="sr-only" for="inlineFormInput">Name</label>
                <input type="hidden" name="page" value="1"/>
                <input name="filter" type="text" class="form-control mb-2" id="inlineFormInput" placeholder={placeholder} value={boxValue}>
                </div>
                <div class="col-auto">
                    <button type="submit" class="btn btn-primary mb-2 mr-2">Filter</button>
                    <a class="btn btn-primary mb-2" href={clearFilterUrl}>Clear</a>
                </div>
            </div>
        </form>
    |]
        where
            boxValue :: Text
boxValue = Text -> Maybe Text -> Text
forall a. a -> Maybe a -> a
fromMaybe Text
"" (ByteString -> Maybe Text
forall paramType.
(?context::ControllerContext, ParamReader (Maybe paramType)) =>
ByteString -> Maybe paramType
paramOrNothing ByteString
"filter") :: Text
            clearFilterUrl :: ByteString
clearFilterUrl = ByteString
path ByteString -> ByteString -> ByteString
forall a. Semigroup a => a -> a -> a
<> Bool -> Query -> ByteString
Query.renderQuery Bool
True Query
newQueryString
                where
                    path :: ByteString
path = Request -> ByteString
Wai.rawPathInfo Request
(?context::ControllerContext) => Request
theRequest
                    queryString :: Query
queryString = Request -> Query
Wai.queryString Request
(?context::ControllerContext) => Request
theRequest
                    newQueryString :: Query
newQueryString = Query
queryString
                        Query -> (Query -> Query) -> Query
forall t1 t2. t1 -> (t1 -> t2) -> t2
|> ByteString -> Query -> Query
removeQueryItem ByteString
"filter"


-- | Set or replace a query string item
--
-- >>> setQueryValue "page" "1" []
-- [("page", Just "1")]
--
-- >>> setQueryValue "page" "2" [("page", Just "1")]
-- [("page", Just "2")]
--
setQueryValue :: ByteString -> ByteString -> Query.Query -> Query.Query
setQueryValue :: ByteString -> ByteString -> Query -> Query
setQueryValue ByteString
name ByteString
value Query
queryString =
    case ByteString -> Query -> Maybe (Maybe ByteString)
forall a b. Eq a => a -> [(a, b)] -> Maybe b
lookup ByteString
name Query
queryString of
        Just Maybe ByteString
existingPage -> Query
queryString
                Query -> (Query -> Query) -> Query
forall t1 t2. t1 -> (t1 -> t2) -> t2
|> ((ByteString, Maybe ByteString) -> (ByteString, Maybe ByteString))
-> Query -> Query
forall a b. (a -> b) -> [a] -> [b]
map (\(queryItem :: (ByteString, Maybe ByteString)
queryItem@(ByteString
queryItemName, Maybe ByteString
_)) -> if ByteString
queryItemName ByteString -> ByteString -> Bool
forall a. Eq a => a -> a -> Bool
== ByteString
name
                        then (ByteString
name, ByteString -> Maybe ByteString
forall a. a -> Maybe a
Just ByteString
value)
                        else (ByteString, Maybe ByteString)
queryItem
                    )
        Maybe (Maybe ByteString)
Nothing -> Query
queryString Query -> Query -> Query
forall a. Semigroup a => a -> a -> a
<> [(ByteString
name, ByteString -> Maybe ByteString
forall a. a -> Maybe a
Just ByteString
value)]

-- | Removes a query item, specified by the name
--
-- >>> removeQueryItem "filter" [("filter", Just "test")]
-- []
--
removeQueryItem :: ByteString -> Query.Query -> Query.Query
removeQueryItem :: ByteString -> Query -> Query
removeQueryItem ByteString
name Query
queryString = Query
queryString Query -> (Query -> Query) -> Query
forall t1 t2. t1 -> (t1 -> t2) -> t2
|> ((ByteString, Maybe ByteString) -> Bool) -> Query -> Query
forall a. (a -> Bool) -> [a] -> [a]
filter (\(ByteString
queryItemName, Maybe ByteString
_) -> ByteString
queryItemName ByteString -> ByteString -> Bool
forall a. Eq a => a -> a -> Bool
/= ByteString
name)

{-| Determine if a Pagination needs to be shown.
    If there is only a single page, we shouldn't show a pager.
-}
showPagination :: Pagination -> Bool
showPagination :: Pagination -> Bool
showPagination pagination :: Pagination
pagination@Pagination {Int
currentPage :: Int
$sel:currentPage:Pagination :: Pagination -> Int
currentPage} =
    Int
currentPage Int -> Int -> Bool
forall a. Eq a => a -> a -> Bool
/= Int
1 Bool -> Bool -> Bool
|| Pagination -> Bool
hasNextPage Pagination
pagination Bool -> Bool -> Bool
|| Pagination -> Bool
hasPreviousPage Pagination
pagination