{-# LANGUAGE
    OverloadedStrings
  , BangPatterns
  , UnboxedTuples
  , UnboxedSums
  , MagicHash
  , ScopedTypeVariables
  , LambdaCase
  , RecordWildCards
  , NamedFieldPuns
  , ApplicativeDo
  , TemplateHaskell
#-}

-- | Note: this library parses, but does not validate urls
module Url
  ( -- * Types
    Url(urlSerialization)
  , ParseError(..)
    -- * Parsing
  , decodeUrl
    -- * Slicing
  , getScheme
  , getUsername
  , getAuthority
  , getPassword
  , getHost
  , getPath
  , getQuery
  , getFragment
  , getExtension
  , getPort
  , constructUrl
  , literalUrl
  ) where

import Data.Word (Word16)
import Data.Bytes.Types (Bytes(..))
import Url.Rebind (decodeUrl)
import Url.Unsafe (Url(..),ParseError(..))
import GHC.Exts (Int(I#),(==#),Int#,int2Word#)
import GHC.Word (Word16(..))
import Language.Haskell.TH
import Language.Haskell.TH.Syntax (TExp(TExp))
import Data.List (intercalate)
-- import GHC.Integer.GMP.Internals (Integer(..))
import qualified Data.Bytes as Bytes

-- | Slice into the 'Url' and retrieve the scheme, if it's present
getScheme :: Url -> Maybe Bytes
getScheme Url{urlSerialization,urlSchemeEnd} =
  if I# urlSchemeEnd == 0
    then Nothing
    else Just $ Bytes.unsafeTake (I# urlSchemeEnd) urlSerialization

-- | Slice into the 'Url' and retrieve the username, if it's present
getUsername :: Url -> Maybe Bytes
getUsername Url{urlSerialization,urlSchemeEnd,urlUsernameEnd,urlHostStart} =
  case urlUsernameEnd ==# urlHostStart of
    0# -> Just $ unsafeSlice (I# urlSchemeEnd + 3) (I# urlUsernameEnd) urlSerialization
    _ -> Nothing

getAuthority :: Url -> Maybe Bytes
getAuthority Url{urlSerialization,urlSchemeEnd,urlUsernameEnd,urlHostStart} =
  case urlUsernameEnd ==# urlHostStart of
    0# -> Just $ unsafeSlice (I# urlSchemeEnd + 3) (I# urlHostStart - 1) urlSerialization
    _ -> Nothing

getPassword :: Url -> Maybe Bytes
getPassword Url{urlSerialization,urlUsernameEnd,urlHostStart} =
  case urlUsernameEnd ==# urlHostStart of
    0# ->
      let mpass = unsafeSlice (I# urlUsernameEnd) (I# urlHostStart - 1) urlSerialization
      in case Bytes.uncons mpass of
        Just (58,password) -> Just password
        _ -> Nothing
    _ -> Nothing

-- | Slice into the 'Url' and retrieve the host, if it's present
getHost :: Url -> Maybe Bytes
getHost Url{urlSerialization,urlHostStart,urlHostEnd} =
  case urlHostStart ==# urlHostEnd of
    0# -> Just $ unsafeSlice (I# urlHostStart) (I# urlHostEnd) urlSerialization
    _ -> Nothing

-- | Slice into the 'Url' and retrieve the path starting with @\'/'@, if it's present
getPath :: Url -> Maybe Bytes
getPath Url{urlSerialization,urlPathStart,urlQueryStart} =
  case urlPathStart ==# len of
    0# -> Just $ unsafeSlice (I# urlPathStart) (I# urlQueryStart) urlSerialization
    _ -> Nothing
  where
  !(I# len) = Bytes.length urlSerialization

-- | Slice into the 'Url' and retrieve the query string starting with @\'?'@, if it's present
getQuery :: Url -> Maybe Bytes
getQuery Url{urlSerialization,urlQueryStart,urlFragmentStart} =
  case len ==# urlQueryStart of
    0# -> Just $ unsafeSlice (I# urlQueryStart) (I# urlFragmentStart) urlSerialization
    _ -> Nothing
  where
  !(I# len) = Bytes.length urlSerialization

-- | Slice into the 'Url' and retrieve the fragment starting with @\'#'@, if it's present
getFragment :: Url -> Maybe Bytes
getFragment Url{urlSerialization,urlFragmentStart} =
  case len ==# urlFragmentStart of
    0# -> Just $ unsafeSlice (I# urlFragmentStart) (I# len) urlSerialization
    _ -> Nothing
  where
  !(I# len) = Bytes.length urlSerialization

getPort :: Url -> Maybe Word16
getPort Url{urlPort} =
  case urlPort of
    0x10000# -> Nothing
    x -> Just $ W16# (int2Word# x)

-- | This function is intentionally imprecise. 
-- E.g. @getExtension "google.com/facebook.com" == Just ".com"@
getExtension :: Url -> Maybe Bytes
getExtension url = do
  path <- getPath url
  if not (Bytes.elem 0x2e path)
  then Nothing
  else case Bytes.split 0x2e path of
    [] -> Nothing
    xs -> Just $ last xs

{-# INLINE unsafeSlice #-}
unsafeSlice :: Int -> Int -> Bytes -> Bytes
unsafeSlice begin end (Bytes arr _ _) =
  Bytes arr begin (end - begin)

literalUrl :: String -> Q (TExp Url)
literalUrl ser = case decodeUrl $ Bytes.fromLatinString ser of
  Left e -> fail $ "Invalid url. Parse error: " <> show e
  Right Url{..} -> do
    pure $ TExp $
      ConE 'Url
        `AppE` (ParensE $ (VarE 'Bytes.fromLatinString) `AppE` (LitE $ StringL ser))
        `AppE` (liftInt# urlSchemeEnd)
        `AppE` (liftInt# urlUsernameEnd)
        `AppE` (liftInt# urlHostStart)
        `AppE` (liftInt# urlHostEnd)
        `AppE` (liftInt# urlPort)
        `AppE` (liftInt# urlPathStart)
        `AppE` (liftInt# urlQueryStart)
        `AppE` (liftInt# urlFragmentStart)
  where
  liftInt# :: Int# -> Exp
  liftInt# x = LitE (IntPrimL (fromIntegral $ I# x))

constructUrl ::
     Maybe String -- ^ scheme
  -> String -- ^ host
  -> Maybe Word16 -- ^ port
  -> String -- ^ path
  -> [(String,String)] -- query string params
  -> Maybe String -- ^ framgent
  -> Q (TExp Url)
constructUrl mscheme host mport path qps mfrag = literalUrl ser
  where
  ser = scheme <> host <> port <> path <> rqps <> frag
  scheme = case mscheme of
    Nothing -> mempty
    Just x -> x <> "://"
  port = case mport of
    Nothing -> mempty
    Just x -> ':' : show x
  rqps :: String
  rqps = "?" <> (intercalate "&" $ fmap (\(a,b) -> a <> "=" <> b) qps)
  frag = case mfrag of
    Nothing -> mempty
    Just x -> "#" <> x