-- | A-little-bit-higher-level WebSocket client library.
--
--   Thanks to [http-client](https://hackage.haskell.org/package/http-client) and [http-client-tls](https://hackage.haskell.org/package/http-client-tls), functions in this module support @HTTP_PROXY@ environment variable and TLS.
--
--   __NOTE__: Currently, non-TLS connection via an HTTP proxy server
--             is NOT supported.

module Network.WebSockets.Client
  ( withConnection

    -- * Re-export from Network.WebSockets
  , WS.Connection

    -- ** Sending and receiving messages
  , WS.receive
  , WS.receiveDataMessage
  , WS.receiveData
  , WS.send
  , WS.sendDataMessage
  , WS.sendDataMessages
  , WS.sendTextData
  , WS.sendTextDatas
  , WS.sendBinaryData
  , WS.sendBinaryDatas
  , WS.sendClose
  , WS.sendCloseCode
  , WS.sendPing

    -- ** WebSocket message types
  , WS.Message (..)
  , WS.ControlMessage (..)
  , WS.DataMessage (..)
  , WS.WebSocketsData (..)

    -- ** Exceptions
  , WS.HandshakeException (..)
  , WS.ConnectionException (..)

    -- ** Utilities
  , WS.withPingThread
  ) where


import qualified Control.Exception            as E
import qualified Data.ByteString              as BS
import qualified Data.ByteString.Lazy         as BSL
import qualified Network.HTTP.Client          as Http
import qualified Network.HTTP.Client.Internal as Http
import           Network.HTTP.Client.TLS      (tlsManagerSettings)
import           Network.URI                  (URI (..), URIAuth (..), parseURI)
import qualified Network.WebSockets           as WS
import qualified Network.WebSockets.Stream    as WS

-- | The main entrypoint to connect by the WebSocket protocol.
--   This function automatically closes the created connection
--   after exiting the action.
--
--   If @HTTP_PROXY@ environment variable is set,
--   The connection is automatically made via the HTTP proxy server
--   specified by the variable.
--
--   __NOTE__: Currently, non-TLS connection via an HTTP proxy server
--             is NOT supported.
withConnection
  :: String -- ^ Endpoint URL (e.g. wss:\/\/example.com\/path).
  -> (WS.Connection -> IO a) -- ^ Action using the 'WS.Connection'
  -> IO a
withConnection url action = do
  man <- Http.newManager tlsManagerSettings
  withConnectionFromManager man url action


withConnectionFromManager
  :: Http.Manager -> String -> (WS.Connection -> IO a) -> IO a
withConnectionFromManager man rawUrl action = do
  (isSecure, host, port, path) <- parseWsUrl rawUrl

  let httpUrl = (if isSecure then "https://" else "http://") ++ host ++ port ++ path
  req <- Http.parseRequest $ "GET " ++ httpUrl

  Http.withConnection req man $ \httpConn -> do
    E.bracket
      ( do
        let r = do
              bs <- Http.connectionRead httpConn
              return $ if BS.null bs then Nothing else Just bs

            w = maybe (Http.connectionClose httpConn)
                      (Http.connectionWrite httpConn . BSL.toStrict)
        WS.makeStream r w
      )
      WS.close
      ( \stream -> do
        -- TODO: configure WS.ConnectionOptions
        WS.runClientWithStream stream
                               host
                               path
                               WS.defaultConnectionOptions
                               []
                               action
      )


parseWsUrl :: String -> IO (Bool, String, String, String)
parseWsUrl raw = do
  uri  <- noteInvalidUrl "Invalid URL given" $ parseURI raw
  auth <- noteInvalidUrl "No authroity specified" $ uriAuthority uri
  host <- dieWhenEmpty "No host specified" $ uriRegName auth
  let wss      = "wss:"
      scheme'  = uriScheme uri
      scheme   = if null scheme' then wss else scheme'
      isSecure = scheme == wss
      path     = uriPath uri ++ uriQuery uri ++ uriFragment uri
  return (isSecure, host, uriPort auth, if null path then "/" else path)
 where
  noteInvalidUrl :: String -> Maybe a -> IO a
  noteInvalidUrl msg =
    maybe (E.throwIO $ Http.InvalidUrlException raw msg) return

  dieWhenEmpty :: String -> String -> IO String
  dieWhenEmpty msg "" = E.throwIO $ Http.InvalidUrlException raw msg
  dieWhenEmpty _   s  = return s