{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}

module Network.Mattermost.Util
( ConnectionType(..)
, assertE
, noteE
, hoistE
, (~=)
, withConnection
, mkConnection
, connectionGetExact
, buildPath
) where

import           Control.Exception (finally, onException)
import           Data.Char ( toUpper )
import qualified Data.ByteString.Char8 as B
import qualified Data.Text as T
import qualified Text.URI as URI
import           Data.Monoid ((<>))

import           Control.Exception ( Exception
                                   , throwIO )
import           Data.Pool (takeResource, putResource, destroyResource)
import           Network.Connection ( Connection
                                    , ConnectionContext
                                    , ConnectionParams(..)
                                    , ProxySettings(..)
                                    , TLSSettings(..)
                                    , connectionGet
                                    , connectTo )

import           Network.Mattermost.Types.Base
import           Network.Mattermost.Types.Internal
import           Network.Mattermost.Proxy

-- | This unwraps a 'Maybe' value, throwing a provided exception
--   if the value is 'Nothing'.
noteE :: Exception e => Maybe r -> e -> IO r
noteE Nothing  e  = throwIO e
noteE (Just r) _  = pure    r

-- | This unwraps an 'Either' value, throwing the contained exception
--   if the 'Either' was a 'Left' value.
hoistE :: Exception e => Either e r -> IO r
hoistE (Left e)  = throwIO e
hoistE (Right r) = pure    r

-- | This asserts that the provided 'Bool' is 'True', throwing a
--   provided exception is the argument was 'False'.
assertE :: Exception e => Bool -> e -> IO ()
assertE True  _ = pure    ()
assertE False e = throwIO e

-- | Case Insensitive string comparison
(~=) :: String -> String -> Bool
a ~= b = map toUpper a == map toUpper b

-- | Creates a new connection to 'Hostname' from an already initialized
-- 'ConnectionContext'.
withConnection :: ConnectionData -> (MMConn -> IO a) -> IO a
withConnection cd action = do
    (conn, lp) <- takeResource (cdConnectionPool cd)
    (action conn `onException` closeMMConn conn) `finally` do
        c <- isConnected conn
        if c then
             putResource lp conn else
             destroyResource (cdConnectionPool cd) lp conn

-- | Creates a connection from a 'ConnectionData' value, returning it. It
--   is the user's responsibility to close this appropriately.
--
-- This function respects ALL_PROXY, HTTP_PROXY, HTTPS_PROXY, and
-- NO_PROXY environment variables for controlling whether the resulting
-- connection uses a proxy. However, note:
--
-- * Only SOCKS version 4 and 5 proxies are supported using socks4://
--   and socks5:// URIs, and
-- * No proxy authentication is supported.
mkConnection :: ConnectionContext -> Hostname -> Port -> ConnectionType -> IO Connection
mkConnection ctx host port connTy = do
  proxy' <- case connTy of
     ConnectHTTPS _ -> proxyForScheme HTTPS
     ConnectHTTP -> return Nothing

  canUseProxy <- proxyHostPermitted (T.unpack host)
  let proxy = if canUseProxy then proxy' else Nothing
  connectTo ctx $ ConnectionParams
    { connectionHostname  = T.unpack host
    , connectionPort      = fromIntegral port
    , connectionUseSecure = case connTy of
        ConnectHTTP -> Nothing
        ConnectHTTPS requireTrustedCert ->
            -- The first argument to TLSSettingsSimple is whether to
            -- /disable/ cert validation. If requireTrustedCert is True,
            -- we want that argument to be False to force validation.
            Just (TLSSettingsSimple (not requireTrustedCert) False False)
    , connectionUseSocks  = do
        (ty, cHost, cPort) <- proxy
        case ty of
            Socks -> return $ SockSettingsSimple cHost (toEnum cPort)
    }

-- | Get exact count of bytes from a connection.
--
-- The size argument is the exact amount that must be returned to the user.
-- The call will wait until all data is available.  Hence, it behaves like
-- 'B.hGet'.
--
-- On end of input, 'connectionGetExact' will throw an 'E.isEOFError'
-- exception.
-- Taken from: https://github.com/vincenthz/hs-connection/issues/9
connectionGetExact :: Connection -> Int -> IO B.ByteString
connectionGetExact con n = loop B.empty 0
  where loop bs y
          | y == n = return bs
          | otherwise = do
            next <- connectionGet con (n - y)
            loop (B.append bs next) (y + (B.length next))

-- | Build a full URI path from the path of an API endpoint
buildPath :: ConnectionData -> T.Text -> IO T.Text
buildPath cd endpoint = do
  let rawPath = "/" <> (T.dropWhile (=='/') $ cdUrlPath cd <> "/api/v4/" <> endpoint)
  uri <- URI.mkURI rawPath
  return $ URI.render uri