{-# LINE 1 "Network/Hermes/Net.hsc" #-}
{-# LANGUAGE ViewPatterns, ForeignFunctionInterface, CPP #-}
{-# LINE 2 "Network/Hermes/Net.hsc" #-}
module Network.Hermes.Net(connectStream,streamServer,Address(..),resolve,reverseLookup) where

import Control.Concurrent
import Control.Exception
import Control.Monad
import Data.Maybe

import System.IO
import Network.Socket

import Foreign
import Foreign.C

import Network.Hermes.Protocol
import Network.Hermes.Misc

connectStream :: Address -> IO Handle
connectStream address = do
  ip <- resolve address
  s <- case ip of
    SockAddrInet _ _ -> socket AF_INET Stream defaultProtocol
    SockAddrInet6 _ _ _ _ -> socket AF_INET6 Stream defaultProtocol
    SockAddrUnix _ -> socket AF_UNIX Stream defaultProtocol
  connect s ip
  socketToHandle s ReadWriteMode

-- | Returns the /best/ fit only, or a DNSFailure exception
resolve :: Address -> IO SockAddr
resolve (Unix path) = return $ SockAddrUnix path
resolve address = do
  fits <- getAddrInfo (Just defaultHints{addrFlags=[AI_ADDRCONFIG,AI_NUMERICSERV]})
          (Just $ ghead address)
          (Just $ show (ghead address :: Int))
  let fits' = flip filter (map addrAddress fits) $ \fit ->
        case (fit,address) of
          (_,IP _ _) -> True
          (SockAddrInet _ _, IPv4 _ _) -> True
          (SockAddrInet6 _ _ _ _, IPv6 _ _) -> True
          _ -> False
  when (null fits') $ throwIO $ DNSFailure address
  return $ head fits'

-- | Creates a TCP server that will hand off incoming connections to
-- new threads.
--
-- Killing the server does not kill these forked threads.
--
-- The handle passed to your action will be automatically closed when
-- that action returns.
streamServer :: Address -- ^ Address we should bind to. Use "" for hostname to use all interfaces.
               -> (Handle -> Address -> IO ()) -- ^ Function called to handle connections
               -> IO (IO ()) -- ^ Returns an action you may use to kill the server
streamServer address act = block $ do
  socks <- listenAt address
  threads <- forM socks $ \server -> forkIO $ flip finally (sClose server) $ unblock $ forever $ do
    (sock,sockAddr) <- accept server
    address <- reverseLookup sockAddr
    handle <- socketToHandle sock ReadWriteMode
    trapForkIO "hermes.net.streamServer" $ act handle address `finally` hClose handle
  return $ mapM_ killThread threads

reverseLookup :: SockAddr -> IO Address
reverseLookup (SockAddrUnix path) = return $ Unix path
reverseLookup sockAddr = do
  (Just host, Just (read -> port)) <- getNameInfo [NI_NUMERICSERV] True True sockAddr
  return $ case sockAddr of
    SockAddrInet _ _ -> IPv4 host port
    SockAddrInet6 _ _ _ _ -> IPv6 host port

-- On linux, binding an IPv6 port by default also binds the corresponding IPv4 port. Disable that.

{-# LINE 73 "Network/Hermes/Net.hsc" #-}

{-# LINE 74 "Network/Hermes/Net.hsc" #-}

{-# LINE 75 "Network/Hermes/Net.hsc" #-}

type SockLen = Word32
{-# LINE 77 "Network/Hermes/Net.hsc" #-}
iPPROTO_IPV6 :: CInt
iPPROTO_IPV6 = 41
{-# LINE 79 "Network/Hermes/Net.hsc" #-}
iPV6_V6ONLY :: CInt
iPV6_V6ONLY  = 26
{-# LINE 81 "Network/Hermes/Net.hsc" #-}
foreign import ccall unsafe "setsockopt" _setsockopt :: CInt -> CInt -> CInt -> Ptr CInt -> SockLen -> IO ()


{-# LINE 84 "Network/Hermes/Net.hsc" #-}

tryListenAt :: Family -> HostName -> Int -> IO (Maybe Socket)
tryListenAt family host port = do
  addr <- getAddrInfo (Just defaultHints{addrFamily=family,addrFlags=[AI_PASSIVE,AI_ADDRCONFIG]})
                     (if null host then Nothing else Just host)
                     (Just (show port))
  case addr of
    [] -> return Nothing
    ((addrAddress -> address):_) -> do
      s <- socket family Stream defaultProtocol
      setSocketOption s ReuseAddr 1

{-# LINE 96 "Network/Hermes/Net.hsc" #-}
      when (family == AF_INET6) $ do
        alloca $ \intptr -> do
          poke intptr 1
          _setsockopt (fdSocket s) iPPROTO_IPV6 iPV6_V6ONLY intptr ((4))
{-# LINE 100 "Network/Hermes/Net.hsc" #-}

{-# LINE 101 "Network/Hermes/Net.hsc" #-}
      bindSocket s address
      listen s 3
      return $ Just s
  
listenAt :: Address -> IO [Socket]  
listenAt (IP host port) = do
  v4 <- tryListenAt AF_INET host port
  v6 <- tryListenAt AF_INET6 host port
  when (isNothing v4 && isNothing v6) (error "listenAt: No good exception, FIXME")
  return $ concat $ [maybeToList v4,maybeToList v6]
listenAt (IPv4 host port) = do
  Just s <- tryListenAt AF_INET host port -- FIXME
  return [s]
listenAt (IPv6 host port) = do
  Just s <- tryListenAt AF_INET6 host port -- FIXME
  return [s]