content: {-|
  Module      : PostgREST.RangeQuery
  Description : Logic regarding the `Range`/`Content-Range` headers and `limit`/`offset` querystring arguments.
  -}
  module PostgREST.RangeQuery (
    rangeParse
  , rangeRequested
  , rangeLimit
  , rangeOffset
  , restrictRange
  , rangeGeq
  , allRange
  , limitZeroRange
  , hasLimitZero
  , convertToLimitZeroRange
  , NonnegRange
  , rangeStatusHeader
  , contentRangeH
  ) where
  
  import qualified Data.ByteString.Char8 as BS
  
  import Data.List       (lookup)
  import Text.Regex.TDFA ((=~))
  
  import Control.Applicative
  import Data.Ranged.Boundaries
  import Data.Ranged.Ranges
  import Network.HTTP.Types.Header
  import Network.HTTP.Types.Status
  
  import Protolude
  
  type NonnegRange = Range Integer
  
  rangeParse :: BS.ByteString -> NonnegRange
  rangeParse range = do
    let rangeRegex = "^([0-9]+)-([0-9]*)$" :: BS.ByteString
  
    case range =~ rangeRegex :: [[BS.ByteString]] of
      [[_, l, u]] ->
        let lower  = maybe emptyRange rangeGeq (readInteger l)
            upper  = maybe allRange rangeLeq (readInteger u) in
        rangeIntersection lower upper
      _ -> allRange
    where
      readInteger = readMaybe . BS.unpack
  
  rangeRequested :: RequestHeaders -> NonnegRange
  rangeRequested headers = maybe allRange rangeParse $ lookup hRange headers
  
  restrictRange :: Maybe Integer -> NonnegRange -> NonnegRange
  restrictRange Nothing r = r
  restrictRange (Just limit) r =
     rangeIntersection r $
       Range BoundaryBelowAll (BoundaryAbove $ rangeOffset r + limit - 1)
  
  rangeLimit :: NonnegRange -> Maybe Integer
  rangeLimit range =
    case [rangeLower range, rangeUpper range] of
      [BoundaryBelow lower, BoundaryAbove upper] -> Just (1 + upper - lower)
      _ -> Nothing
  
  rangeOffset :: NonnegRange -> Integer
  rangeOffset range =
    case rangeLower range of
      BoundaryBelow lower -> lower
      _                   -> panic "range without lower bound" -- should never happen
  
  rangeGeq :: Integer -> NonnegRange
  rangeGeq n =
    Range (BoundaryBelow n) BoundaryAboveAll
  
  allRange :: NonnegRange
  allRange = rangeGeq 0
  
  rangeLeq :: Integer -> NonnegRange
  rangeLeq n =
    Range BoundaryBelowAll (BoundaryAbove n)
  
  -- Special case to allow limit 0 queries
  -- https://github.com/PostgREST/postgrest/issues/1121
  -- 0 <= x <= -1
  limitZeroRange :: Range Integer
  limitZeroRange = Range (BoundaryBelow 0) (BoundaryAbove (-1))
  
  hasLimitZero :: Range Integer -> Bool
  hasLimitZero r = rangeUpper r == rangeUpper limitZeroRange
  
  -- Used to convert a range into a special limitZeroRange if it has a
  -- limit=0 in order to bypass validations for empty ranges.
  convertToLimitZeroRange :: Range Integer -> Range Integer -> Range Integer
  convertToLimitZeroRange range fallbackRange =
    if hasLimitZero range then limitZeroRange else fallbackRange
  
  rangeStatusHeader :: NonnegRange -> Int64 -> Maybe Int64 -> (Status, Header)
  rangeStatusHeader topLevelRange queryTotal tableTotal =
    let lower = rangeOffset topLevelRange
        upper = lower + toInteger queryTotal - 1
        contentRange = contentRangeH lower upper (toInteger <$> tableTotal)
        status = rangeStatus lower upper (toInteger <$> tableTotal)
    in (status, contentRange)
    where
      rangeStatus :: Integer -> Integer -> Maybe Integer -> Status
      rangeStatus _ _ Nothing = status200
      rangeStatus lower upper (Just total)
        | lower > total               = status416 -- 416 Range Not Satisfiable
        | (1 + upper - lower) < total = status206 -- 206 Partial Content
        | otherwise                   = status200 -- 200 OK
  
  contentRangeH :: (Integral a, Show a) => a -> a -> Maybe a -> Header
  contentRangeH lower upper total =
      ("Content-Range", toUtf8 headerValue)
      where
        headerValue   = rangeString <> "/" <> totalString :: Text
        rangeString
          | totalNotZero && fromInRange = show lower <> "-" <> show upper
          | otherwise = "*"
        totalString   = maybe "*" show total
        totalNotZero  = Just 0 /= total
        fromInRange   = lower <= upper
  