content: module IHP.IDE.PortConfig
  ( PortConfig (..)
  , defaultAppPort
  , findAvailablePortConfig
  , isPortAvailable
  )
  where
  
  import ClassyPrelude
  import qualified Network.Socket as Socket
  import qualified UnliftIO.Exception as Exception
  import Foreign.C.Error (Errno (..), eCONNREFUSED)
  import Control.Exception.Safe (IOException(..))
  import GHC.IO.Exception (ioe_errno)
  import IHP.FrameworkConfig (defaultPort)
  
  -- | Port configuration used for starting the different app services
  data PortConfig = PortConfig
      { appPort :: !Socket.PortNumber
      , toolServerPort :: !Socket.PortNumber
      } deriving (Show, Eq)
  
  defaultAppPort :: Socket.PortNumber
  defaultAppPort = fromIntegral defaultPort
  
  allPorts :: PortConfig -> [Socket.PortNumber]
  allPorts PortConfig { .. } = [appPort, toolServerPort]
  
  instance Enum PortConfig where
      fromEnum PortConfig { .. } = fromIntegral $ toInteger (appPort - defaultAppPort)
      toEnum i = PortConfig { .. }
          where
              port = fromIntegral i
              appPort = port + defaultAppPort
              toolServerPort = port + defaultAppPort + 1
  
  -- | Returns True when the given port looks to be free.
  -- Used to e.g. detect which port the dev server should use.
  isPortAvailable :: Socket.PortNumber -> IO Bool
  isPortAvailable port = do
      let address = Socket.SockAddrInet port (Socket.tupleToHostAddress (127, 0, 0, 1))
      Exception.bracket (Socket.socket Socket.AF_INET Socket.Stream 6) Socket.close' $ \socket -> do
          res <- Exception.try (Socket.connect socket address)
          case res of
              Left e -> if (Errno <$> ioe_errno e) == Just eCONNREFUSED
                      then pure True
                      else throwIO e
              Right _ -> pure False
  
  -- | Returns True when all ports in port config are available.
  -- 
  -- Example:
  --
  -- >>> let portConfig = PortConfig { appPort = 8000, toolServerPort = 8001 }
  -- >>> isPortConfigAvailable portConfig
  -- True
  isPortConfigAvailable :: PortConfig -> IO Bool
  isPortConfigAvailable portConfig = do
      available <- mapM isPortAvailable (allPorts portConfig)
      pure (and available)
  
  -- | Returns a port config where all ports are available
  --
  -- When e.g. port 8000 and 8001 are not used:
  --
  -- >>> portConfig <- findAvailablePortConfig
  -- PortConfig { appPort = 8000, toolServerPort = 8001 }
  findAvailablePortConfig :: IO PortConfig
  findAvailablePortConfig = do
          let portConfigs :: [PortConfig] = take 100 (map toEnum [0..])
          go portConfigs
      where
          go (portConfig : rest) = do
              available <- isPortConfigAvailable portConfig
              if available
                  then pure portConfig
                  else go rest
          go [] = error "findAvailablePortConfig: No port configuration found"
  