module System.Win32.DHCP
    ( DhcpApi ()
    , loadDHCP
    -- * General Types
    , Context (..)
    , ClientType (..)
    , DATE_TIME (..)
    , HOST_INFO (..)
    , SEARCH_INFO (..)
    -- * Leases
    , Client (..)
    , enumClients
    , lookupClient
    , deleteClient
    -- * Reservations
    , Mapping (..)
    , Reservation(..)
    , addReservation
    , enumReservations
    , removeReservation
    ) where

import Control.Monad (unless)
import Foreign
import Foreign.C.Types

import System.Win32.Types

import Data.Ip

import System.Win32.DHCP.DhcpStructure
import System.Win32.DHCP.HOST_INFO
import System.Win32.DHCP.Internal
import System.Win32.DHCP.Client
import System.Win32.DHCP.LengthBuffer
import System.Win32.DHCP.Reservation
import System.Win32.DHCP.SEARCH_INFO
import System.Win32.DHCP.SUBNET_CLIENT_INFO_ARRAY_V4
import System.Win32.DHCP.SUBNET_ELEMENT_DATA_V4
import System.Win32.DHCP.SUBNET_ELEMENT_INFO_ARRAY_V4
import System.Win32.DHCP.Types

-- | A Context defines which server and scope within that server a command
-- refers to.  Microsoft's DHCP server supports multiple scopes. This allows
-- different configurations to be sent to devices based on their hardware
-- (MAC) address.  Scopes are identified by their network address.
data Context = Context
    { -- | The DHCP server management API uses an RPC mechanism to control the
      -- server.
      contextServer :: !String
    , -- | Scopes are identified by a subnet identifier. This is useful in
      -- cases where multiple scopes are defined, but still required in the
      -- common case of a single scope.
      contextSubnet :: !Ip
    } deriving (Eq, Ord)

-- | Delete an active DHCP lease from the server.
-- The `SEARCH_INFO` argument determines which search criteria
-- to use. Searching by name will delete all active leases
-- with that name. This action corresponds to MSDN's DhcpDeleteClientInfoV4
-- function.
deleteClient :: DhcpApi
    -> String
    -- ^ Unicode string that specifies the IP address or hostname of the DHCP
    -- server.
    -> SEARCH_INFO
    -- ^ Define how to lookup a client to delete. Only deleting based on
    -- IP addresses have been tested.
    -> IO ()
    -- ^ This action may throw exceptions. According to Microsoft's protocol
    -- specification an ERROR_DHCP_JET_ERROR (0x00004E2D) will be returned
    -- if no client was found on the server.
deleteClient api server si =
    withTString server $ \pserver ->
    withSearchInfo si $ \psi -> do
    failUnlessSuccess (unwords ["DeleteClientInfo", server, show si])
        $ c_DeleteClientInfo api pserver psi

-- | Perform a lookup operation for all client lease records within a
-- scope. This action corresponds to MSDN's DhcpEnumSubnetClientsV4 function.
enumClients :: DhcpApi
    -> Context
    -- ^ Specify which server and scope to search for client leases.
    -> IO [Client]
    -- ^ The empty list means that no client records exist for the provided
    -- subnet. An exception will be thrown if the provided server or subnet
    -- does not exist, or any other error occurs.
enumClients dhcp (Context server subnet) =
    enumSubnetClientsV4 dhcp server subnet

-- | Search the DHCP server for a lease matching the given search criteria.
-- `Nothing` is returned when no lease was found. This corresponds to MSDN's
-- DhcpGetClientInfoV4 function.
lookupClient :: DhcpApi
    -> String
    -- ^ According to MSDN this must specify the IP address of the server.
    -- Other functions (including this one) may or may not also accept
    -- a Unicode host name.
    -> SEARCH_INFO
    -- ^ Define how to lookup a client. Only searching based on an
    -- IP addresses has been tested.
    -> IO (Maybe Client)
    -- ^ A `Nothing` indicates that no client was found. An exception will
    -- be thrown if any internal error occurs. Though MSDN documents don't say
    -- so, an ERROR_DHCP_JET_ERROR (0x00004E2D) may be thrown if no client
    -- record is found. The reason I think this is that other functions are
    -- said to behave like this.
lookupClient api serverip si =
    withTString serverip $ \pserverip ->
    withSearchInfo si $ \psi ->
    with nullPtr $ \ppclientinfo -> do
    -- DhcpGetClientInfo takes a structure on the stack.
    siType <- peek (castPtr psi :: Ptr CInt)
    siPayload <- peekElemOff (castPtr psi :: Ptr (Ptr ())) 4
    failUnlessSuccess (unwords ["GetClientInfoV4", serverip, show si])
        $ c_GetClientInfoV4  api pserverip siType siPayload ppclientinfo
    pclientinfo <- peek ppclientinfo
    if pclientinfo == nullPtr
      then return Nothing
      else do
        clientinfo <- peekDhcp clientInfo pclientinfo
        freeDhcp clientInfo (rpcFreeMemory api) pclientinfo
        poke (castPtr pclientinfo) nullPtr
        return $ Just clientinfo

addReservation :: DhcpApi -> Context -> Mapping -> IO ()
addReservation dhcp (Context server subnet) mapping =
    addSubnetElementV4 dhcp server subnet
    $ ReservedIps (Reservation mapping Both)

enumReservations :: DhcpApi -> Context -> IO [Reservation]
enumReservations dhcp (Context server subnet) = do
    ex <- enumSubnetElementsV4 dhcp server subnet 2
    res <- flip mapM ex $ \element -> do
        case element of
          ReservedIps res -> return res
          _ -> error "bug in Win32 API."
    return res

-- | Remove a reservation from the server
removeReservation :: DhcpApi -> Context -> Mapping
    -> ClientType
    -- ^ Specify a DHCP reservation, BOOTP reservation, or both. This is untested.
    -> Bool
    -- ^ Specify whether any active leases for the reservation should be
    -- removed as well.
    -> IO ()
removeReservation dhcp (Context server subnet) mapping ct force =
    removeSubnetElementV4 dhcp server subnet
    (ReservedIps $ Reservation mapping ct) fForce
 where
   fForce = if force then FullForce else NoForce

enumSubnetClientsV4 :: DhcpApi -> String -> Ip -> IO [Client]
enumSubnetClientsV4 dhcp server subnet =
    withTString server $ \pServer ->
    -- inout parameter. Supply on successive calls to retreive more elements
    with 0 $ \pResumeHandle ->
    alloca $ \pElementsRead ->
    alloca $ \pElementsTotal ->
    with nullPtr $ \ppInfoArray -> do
    -- We have to call enumSubnetElementsV4 at least twice for a sucessfull run.
    -- Failure to use the returned resumeHandle may result in an internal access
    -- violation within RPCRT4.dll.
    let loop acc = do
            ret <- c_EnumSubnetClientsV4 dhcp pServer (toWord32 subnet)
                       pResumeHandle 0xFFFFFFFF ppInfoArray
                       pElementsRead pElementsTotal
            unless (elem ret [eRROR_SUCCESS, eRROR_MORE_DATA, eRROR_NO_MORE_ITEMS]) $ do
                failWith (unwords ["EnumSubnetClientsV4", server, showIp subnet]) ret
            pInfoArray <- peek ppInfoArray
            elems <- if pInfoArray == nullPtr
                     then return []
                     else do
                       SUBNET_CLIENT_INFO_ARRAY_V4 (LengthBuffer _ elems) <- peekDhcp clientInfoArray pInfoArray
                       freeDhcp clientInfoArray (rpcFreeMemory dhcp) pInfoArray
                       poke ppInfoArray nullPtr
                       return elems
            elementsTotal <- peek pElementsTotal
            elementsRead <- peek pElementsRead
            if (ret == eRROR_NO_MORE_ITEMS || (ret == eRROR_SUCCESS && elementsTotal == elementsRead))
              then return (eRROR_SUCCESS, elems:acc)
              else loop (elems:acc)

    (ret, revelemss) <- loop []
    failUnlessSuccess (unwords ["EnumSubnetClientsV4", server, showIp subnet])
        $ return ret
    return $ concat $ reverse revelemss
  where
    eRROR_SUCCESS       = 0x00000000
    eRROR_NO_MORE_ITEMS = 0x00000103
    eRROR_MORE_DATA     = 0x000000ea

enumSubnetElementsV4 :: DhcpApi -> String -> Ip -> CInt
    -> IO [SUBNET_ELEMENT_DATA_V4]
enumSubnetElementsV4 dhcp server subnet elementType =
    withTString server $ \pServer ->
    -- inout parameter. Supply on successive calls to retreive more elements
    with 0 $ \pResumeHandle ->
    alloca $ \pElementsRead ->
    alloca $ \pElementsTotal ->
    with nullPtr $ \ppEnumElementInfoArray -> do
    -- We have to call enumSubnetElementsV4 at least twice for a sucessfull run.
    -- Failure to use the returned resumeHandle may result in an internal access
    -- violation within RPCRT4.dll.
    let loop acc = do
            ret <- c_EnumSubnetElementsV4 dhcp pServer (toWord32 subnet) elementType
                       pResumeHandle 0xFFFFFFFF ppEnumElementInfoArray
                       pElementsRead pElementsTotal
            unless (elem ret [eRROR_SUCCESS, eRROR_MORE_DATA, eRROR_NO_MORE_ITEMS]) $ do
                failWith (unwords ["EnumSubnetElementsV4", server
                                  , showIp subnet, show elementType]) ret
            pEnumElementInfoArray <- peek ppEnumElementInfoArray
            elems <- if pEnumElementInfoArray == nullPtr
                     then return []
                     else do
                       SUBNET_ELEMENT_INFO_ARRAY_V4 (LengthBuffer _ elems) <- peekDhcp infoArray pEnumElementInfoArray
                       freeDhcp infoArray (rpcFreeMemory dhcp) pEnumElementInfoArray
                       poke ppEnumElementInfoArray nullPtr
                       return elems
            elementsTotal <- peek pElementsTotal
            if (ret == eRROR_NO_MORE_ITEMS || (ret == eRROR_SUCCESS && elementsTotal == 0))
              then return (eRROR_SUCCESS, elems:acc)
              else loop (elems:acc)

    (ret, revelemss) <- loop []
    failUnlessSuccess (unwords ["EnumSubnetElementsV4", server
                               , showIp subnet, show elementType])
        $ return ret
    return $ concat $ reverse revelemss
  where
    eRROR_SUCCESS       = 0x00000000
    eRROR_NO_MORE_ITEMS = 0x00000103
    eRROR_MORE_DATA     = 0x000000ea


addSubnetElementV4 :: DhcpApi -> String -> Ip -> SUBNET_ELEMENT_DATA_V4 -> IO ()
addSubnetElementV4 dhcp server subnet elementData =
    withTString server $ \pServer ->
    withDhcp subnetElementData elementData $ \pElementData ->
    failUnlessSuccess (unwords ["AddSubnetElementsV4", server, showIp subnet])
    $ c_AddSubnetElementV4 dhcp pServer (toWord32 subnet) pElementData

-- | Remove an IPv4 subnet element from an IPv4 subnet defined on the DHCPv4
--   server.
removeSubnetElementV4
    :: DhcpApi
    -> String -- ^ Unicode string that specifies the IP address, such as
              --   \"123.456.789.012\", or hostname, such as \"server\",
              --   of the DHCP server.
    -> Ip -- ^ 'DWORD' value that specifies the IP address of the
                  --   subnet gateway and uniquely identifies it. As an
                  --   example, with the above IP address and a subnet mask
                  --   of 255.255.255.0, the subnet would be 123.456.789.0.
                  --   This must then be converted to a 'DWORD' value of
                  --   2,093,683,968.
    -> SUBNET_ELEMENT_DATA_V4 -- ^ 'DHCP_SUBNET_ELEMENT_DATA_V4' structure
                              --   that contains information used to find the
                              --   element that will be removed from subnet
                              --   specified in SubnetAddress.
    -> FORCE_FLAG -- ^ DHCP_FORCE_FLAG enumeration value that indicates
                  --   whether or not the clients affected by the removal of
                  --   the subnet element should also be deleted.
                  -- 
                  --   Note If the flag is set to DhcpNoForce and this subnet
                  --   has served an IPv4 address to DHCPv4/BOOTP clients, the
                  --   IPv4 range is not deleted; conversely, if the flag is
                  --   set to DhcpFullForce, the IPv4 range is deleted along
                  --   with the DHCPv4 client lease record on the DHCPv4
                  --   server.
    -> IO () -- ^ Following the convention of the Win32 package, an exeption
             --   indicating the win32 error code will be raised if the
             --   underlying API call returns anything other than
             --   ERROR_SUCCESS.
removeSubnetElementV4 dhcp server subnet elementData forceFlag =
    withTString server $ \pServer ->
    withDhcp subnetElementData elementData $ \pElementData ->
    failUnlessSuccess (unwords ["RemoveSubnetElementV4", server, showIp subnet
                               , (show . elementTypeOf) elementData])
    $ c_RemoveSubnetElementV4 dhcp pServer (toWord32 subnet) pElementData
          (fromIntegral . fromEnum $ forceFlag)