{-# LANGUAGE CPP #-}

-- | Convenience methods for constructing backend-agnostic applications
module Database.Beam.Backend.URI where

import           Control.Exception

import qualified Data.Map as M
#if !MIN_VERSION_base(4, 11, 0)
import           Data.Semigroup
#endif

import           Network.URI

data BeamResourceNotFound = BeamResourceNotFound deriving Show
instance Exception BeamResourceNotFound

data BeamOpenURIInvalid = BeamOpenURIInvalid deriving Show
instance Exception BeamOpenURIInvalid

data BeamOpenURIUnsupportedScheme = BeamOpenURIUnsupportedScheme String deriving Show
instance Exception BeamOpenURIUnsupportedScheme

data BeamURIOpener c where
  BeamURIOpener :: c be hdl m
                -> (forall a. hdl -> m a -> IO a)
                -> (URI -> IO (hdl, IO ()))
                -> BeamURIOpener c
newtype BeamURIOpeners c where
  BeamURIOpeners :: M.Map String (BeamURIOpener c) -> BeamURIOpeners c

instance Semigroup (BeamURIOpeners c) where
  (<>) = mappend

instance Monoid (BeamURIOpeners c) where
  mempty = BeamURIOpeners mempty
  mappend (BeamURIOpeners a) (BeamURIOpeners b) =
    BeamURIOpeners (mappend a b)

data OpenedBeamConnection c where
  OpenedBeamConnection
    :: { beamRunner          :: (forall a. hdl -> m a -> IO a)
       , openedBeamDatabase  :: c be hdl m
       , openedBeamHandle    :: hdl
       , closeBeamConnection :: IO ()
     } -> OpenedBeamConnection c

mkUriOpener :: (forall a. hdl -> m a -> IO a)
            -> String
            -> (URI -> IO (hdl, IO ()))
            -> c be hdl m
            -> BeamURIOpeners c
mkUriOpener runner schemeNm opener c = BeamURIOpeners (M.singleton schemeNm (BeamURIOpener c runner opener))

withDbFromUri :: forall c a
               . BeamURIOpeners c
              -> String
              -> (forall be hdl m. (forall r. hdl -> m r -> IO r) -> c be hdl m -> m a)
              -> IO a
withDbFromUri protos uri actionWithDb =
  withDbConnection protos uri (\runner c hdl -> runner hdl (actionWithDb runner c))

withDbConnection :: forall c a
                  . BeamURIOpeners c
                 -> String
                 -> (forall be hdl m. (forall r. hdl -> m r -> IO r) ->
                      c be hdl m -> hdl -> IO a)
                 -> IO a
withDbConnection protos uri actionWithDb =
  bracket (openDbConnection protos uri) closeBeamConnection $
  \(OpenedBeamConnection runner c hdl _) -> actionWithDb runner c hdl

openDbConnection :: forall c
                  . BeamURIOpeners c
                 -> String
                 -> IO (OpenedBeamConnection c)
openDbConnection protos uri = do
  (parsedUri, BeamURIOpener c runner openURI) <- findURIOpener protos uri
  (hdl, closeHdl) <- openURI parsedUri
  pure (OpenedBeamConnection runner c hdl closeHdl)

findURIOpener :: BeamURIOpeners c -> String -> IO (URI, BeamURIOpener c)
findURIOpener (BeamURIOpeners protos) uri =
  case parseURI uri of
    Nothing -> throwIO BeamOpenURIInvalid
    Just parsedUri ->
      case M.lookup (uriScheme parsedUri) protos of
        Nothing -> throwIO (BeamOpenURIUnsupportedScheme (uriScheme parsedUri))
        Just opener -> pure (parsedUri, opener)