{-# LANGUAGE GADTs #-}

module Network.Data.OpenFlow.MatchBuilder
       (
         MatchBuilder
       , Error
       , Attribute
       , match
       , matchOrError
       , (.&&.)
       , (.==.)
       , true
       , in_port
       , vlan_id
       , vlan_priority
       , eth_src
       , eth_dst
       , eth_type
       , arp
       , arp_ip_src
       , arp_ip_dst         
       , ip
       , ip_dst
       , ip_src
       , ip_prot
       , ip_dscp
       , icmp
       , tcp
       , tcp_src
       , tcp_dst
       , udp
       , udp_src
       , udp_dst
       ) where

import Network.Data.Ethernet.EthernetAddress
import Network.Data.Ethernet.EthernetFrame
import Network.Data.IPv4.IPAddress
import Network.Data.IPv4.IPPacket
import Network.Data.OpenFlow.Match
import Network.Data.OpenFlow.Port
import Data.Bits
import Data.Monoid
import Text.Printf (printf)

{-
data Match = Match !(Maybe PortID) !MatchHeader !MatchBody

data MatchHeader = MatchHeader { srcEthAddress, dstEthAddress       :: !(Maybe EthernetAddress), 
                                 vLANID                             :: !(Maybe VLANID), 
                                 vLANPriority                       :: !(Maybe VLANPriority), 
                                 ethFrameType                       :: !(Maybe EthernetTypeCode) }

data MatchBody = MatchBody { ipTypeOfService                    :: !(Maybe IP.IPTypeOfService), 
                             matchIPProtocol                    :: !(Maybe IP.IPProtocol), 
                             srcIPAddress, dstIPAddress         :: !IPAddressPrefix,
                             srcTransportPort, dstTransportPort :: !(Maybe IP.TransportPort) 
                           }
-}

updSrcEthAddress :: MatchHeader -> Maybe EthernetAddress -> MatchHeader
updSrcEthAddress h ma = h { srcEthAddress = ma }

updDstEthAddress :: MatchHeader -> Maybe EthernetAddress -> MatchHeader
updDstEthAddress h ma = h { dstEthAddress = ma }

emptyMatch :: Match
emptyMatch = Match Nothing matchAnyHeader matchAnyBody

true :: MatchBuilder
true = liftBuilder Right

in_port_ :: PortID -> MatchBuilder
in_port_ pid = liftBuilder $ \m@(Match mInPort hdr bdy) ->
  case mInPort of
    Nothing   -> Right (Match (Just pid) hdr bdy)
    Just pid' -> if pid==pid' then Right m else Left "conflicing in_port ids"

eth_addr_set :: EthernetAddress
                -> String
                -> (MatchHeader -> Maybe EthernetAddress -> MatchHeader)
                -> (MatchHeader -> Maybe EthernetAddress)
                -> MatchBuilder
eth_addr_set addr name set get =
  liftBuilder $ \m@(Match mInPort hdr bdy) ->
  case get hdr of
    Nothing    -> Right (Match mInPort (set hdr (Just addr)) bdy)
    Just addr' -> if addr==addr' then Right m else Left (printf "conflicing %s addresses" name)

eth_src_ :: EthernetAddress -> MatchBuilder
eth_src_ addr = eth_addr_set addr "eth_src" updSrcEthAddress srcEthAddress

eth_dst_ :: EthernetAddress -> MatchBuilder
eth_dst_ addr = eth_addr_set addr "eth_dst" updDstEthAddress dstEthAddress

updateSrcIPAddress, updateDstIPAddress :: IPAddressPrefix -> MatchBody -> MatchBody
updateSrcIPAddress prefix body = body { srcIPAddress = prefix}
updateDstIPAddress prefix body = body { dstIPAddress = prefix}

vlan_id_ :: VLANID -> MatchBuilder
vlan_id_ vid = liftBuilder $ \m@(Match mInPort hdr bdy) ->
  case vLANID hdr of
    Nothing -> Right (Match mInPort (hdr { vLANID = Just vid }) bdy)
    Just vid' | vid' == vid  -> Right m
              | otherwise    -> Left $ printf "conflicting vlan ids. Was %d, expecting %d" vid' vid

vlan_priority_ :: VLANPriority -> MatchBuilder
vlan_priority_ pcp = liftBuilder $ \m@(Match mInPort hdr bdy) ->
  case vLANPriority hdr of
    Nothing -> Right (Match mInPort (hdr { vLANPriority = Just pcp }) bdy)
    Just pcp' | pcp' == pcp  -> Right m
              | otherwise    -> Left $ printf "conflicting vlan priorities. Was %d, expecting %d" pcp' pcp

eth_type_ :: EthernetTypeCode -> MatchBuilder
eth_type_ c = liftBuilder $ \m@(Match mInPort hdr bdy) ->
  case ethFrameType hdr of
    Nothing -> Right (Match mInPort (hdr { ethFrameType = Just c }) bdy)
    Just typ | typ == c  -> Right m
             | otherwise -> Left $ printf "conflicting eth types. Was %d, expecting %d" typ c

arp :: MatchBuilder
arp = liftBuilder $ \(Match mInPort hdr bdy) ->
  case ethFrameType hdr of
    Nothing -> Right (Match mInPort (hdr { ethFrameType = Just ethTypeARP }) bdy)
    Just typ | typ == ethTypeARP -> Right (Match mInPort hdr bdy)
             | otherwise        -> Left "ACK conflicting L3 packet types"

arp_ip_src_ :: IPAddressPrefix -> MatchBuilder
arp_ip_src_ prefix = liftBuilder $ \(Match mInPort hdr bdy) ->
  let bdy' = bdy { srcIPAddress = prefix} in
  case ethFrameType hdr of
    Nothing -> Right (Match mInPort (hdr { ethFrameType = Just ethTypeARP }) bdy')
    Just typ | typ == ethTypeARP -> Right (Match mInPort hdr bdy')
             | otherwise        -> Left "ACK conflicting L3 packet types"

arp_ip_dst_ :: IPAddressPrefix -> MatchBuilder
arp_ip_dst_ prefix = liftBuilder $ \(Match mInPort hdr bdy) ->
  let bdy' = bdy { dstIPAddress = prefix} in
  case ethFrameType hdr of
    Nothing -> Right (Match mInPort (hdr { ethFrameType = Just ethTypeARP }) bdy')
    Just typ | typ == ethTypeARP -> Right (Match mInPort hdr bdy')
             | otherwise        -> Left "ACK conflicting L3 packet types"

ip :: MatchBuilder
ip = liftBuilder $ \(Match mInPort hdr bdy) ->
  case ethFrameType hdr of
    Nothing -> Right (Match mInPort (hdr { ethFrameType = Just ethTypeIP }) bdy)
    Just typ | typ == ethTypeIP -> Right (Match mInPort hdr bdy)
             | otherwise        -> Left "conflicting L3 packet types"

set_ip_field :: (MatchBody -> MatchBody) -> MatchBuilder
set_ip_field set = liftBuilder $ \(Match mInPort hdr bdy) ->
  case ethFrameType hdr of
    Nothing -> Right (Match mInPort (hdr { ethFrameType = Just ethTypeIP }) (set bdy))
    Just typ | typ == ethTypeIP -> Right (Match mInPort hdr (set bdy))
             | otherwise        -> Left "conflicting L3 packet types"
                                   
ip_dst_ :: IPAddressPrefix -> MatchBuilder
ip_dst_ = set_ip_field . updateDstIPAddress

ip_src_ :: IPAddressPrefix -> MatchBuilder
ip_src_ = set_ip_field . updateSrcIPAddress

dscp_ :: DifferentiatedServicesCodePoint -> MatchBuilder
dscp_ x = set_ip_field (\bdy -> bdy { ipTypeOfService = Just (shiftL x 2) })

icmp :: MatchBuilder
icmp = set_ip_field (\bdy -> bdy { matchIPProtocol = Just ipTypeIcmp })

tcp :: MatchBuilder
tcp  = set_ip_field (\bdy -> bdy { matchIPProtocol = Just ipTypeTcp })

udp :: MatchBuilder
udp  = set_ip_field (\bdy -> bdy { matchIPProtocol = Just ipTypeUdp })

ip_prot_ :: IPProtocol -> MatchBuilder
ip_prot_ p = set_ip_field (\bdy -> bdy { matchIPProtocol = Just p })

tcp_src_ :: TCPPortNumber -> MatchBuilder
tcp_src_ = set_tcp_field . updateTransportSrc

tcp_dst_ :: TCPPortNumber -> MatchBuilder
tcp_dst_ = set_tcp_field . updateTransportDst

udp_src_ :: TCPPortNumber -> MatchBuilder
udp_src_ = set_udp_field . updateTransportSrc

udp_dst_ :: TCPPortNumber -> MatchBuilder
udp_dst_ = set_udp_field . updateTransportDst

updateTransportSrc :: TCPPortNumber -> MatchBody -> MatchBody
updateTransportSrc s bdy = bdy { srcTransportPort = Just s }

updateTransportDst :: TCPPortNumber -> MatchBody -> MatchBody
updateTransportDst s bdy = bdy { dstTransportPort = Just s }

set_tcp_field :: (MatchBody -> MatchBody) -> MatchBuilder
set_tcp_field = set_transport_field ipTypeTcp 

set_udp_field :: (MatchBody -> MatchBody) -> MatchBuilder
set_udp_field = set_transport_field ipTypeUdp

set_transport_field :: IPProtocol -> (MatchBody -> MatchBody) -> MatchBuilder
set_transport_field prot' set = liftBuilder $ \(Match mInPort hdr bdy) ->
  case ethFrameType hdr of
    Nothing ->
      let hdr' = hdr { ethFrameType = Just ethTypeIP }
          bdy' = set (bdy { matchIPProtocol = Just prot' })
      in Right $ Match mInPort hdr' bdy'
    Just typ
      | typ == ethTypeIP ->
      case matchIPProtocol bdy of
        Nothing -> let bdy' = set (bdy { matchIPProtocol = Just prot' })
                   in Right $ Match mInPort hdr bdy'
        Just prot | prot == prot' -> Right $ Match mInPort hdr (set bdy)
                  | otherwise     -> Left "conflicting transport layer types"
      | otherwise -> Left "Cannot set transport field of non-IP frame"

newtype MatchBuilder = MatchBuilder (Either Error Match -> Either Error Match)
type Error = String

instance Monoid MatchBuilder where
  mempty = MatchBuilder id
  mappend (MatchBuilder f) (MatchBuilder g) = MatchBuilder (f.g)

(.&&.) :: MatchBuilder -> MatchBuilder -> MatchBuilder
(.&&.) = mappend

data Attribute a where
  ETH_SRC :: Attribute EthernetAddress
  ETH_DST :: Attribute EthernetAddress
  ETH_TYP :: Attribute EthernetTypeCode
  VLAN_ID :: Attribute VLANID
  VLAN_PRIORITY :: Attribute VLANPriority
  ARP_IP_SRC :: Attribute IPAddressPrefix
  ARP_IP_DST :: Attribute IPAddressPrefix    
  IP_SRC :: Attribute IPAddressPrefix
  IP_DST :: Attribute IPAddressPrefix
  IP_PROT :: Attribute IPProtocol
  IP_DSCP :: Attribute DifferentiatedServicesCodePoint
  TCP_SRC :: Attribute TCPPortNumber
  TCP_DST :: Attribute TCPPortNumber  
  UDP_SRC :: Attribute TCPPortNumber
  UDP_DST :: Attribute TCPPortNumber  
  IN_PORT :: Attribute PortID

in_port :: Attribute PortID
in_port = IN_PORT

vlan_id :: Attribute VLANID
vlan_id = VLAN_ID

vlan_priority :: Attribute VLANPriority
vlan_priority = VLAN_PRIORITY

eth_src :: Attribute EthernetAddress
eth_src = ETH_SRC

eth_dst :: Attribute EthernetAddress
eth_dst = ETH_DST

eth_type :: Attribute EthernetTypeCode
eth_type = ETH_TYP

arp_ip_src :: Attribute IPAddressPrefix
arp_ip_src  = ARP_IP_SRC

arp_ip_dst :: Attribute IPAddressPrefix
arp_ip_dst  = ARP_IP_DST

ip_src :: Attribute IPAddressPrefix
ip_src  = IP_SRC

ip_dst :: Attribute IPAddressPrefix
ip_dst  = IP_DST

ip_prot :: Attribute IPProtocol
ip_prot = IP_PROT

ip_dscp :: Attribute DifferentiatedServicesCodePoint
ip_dscp = IP_DSCP

tcp_src :: Attribute TCPPortNumber
tcp_src = TCP_SRC

tcp_dst :: Attribute TCPPortNumber
tcp_dst = TCP_DST

udp_src :: Attribute TCPPortNumber
udp_src = UDP_SRC

udp_dst :: Attribute TCPPortNumber
udp_dst = UDP_DST

(.==.) :: Attribute a -> a -> MatchBuilder
(.==.) VLAN_ID vid = vlan_id_ vid
(.==.) VLAN_PRIORITY pcp = vlan_priority_ pcp
(.==.) IN_PORT p = in_port_ p
(.==.) ETH_DST addr = eth_dst_ addr
(.==.) ETH_SRC addr = eth_src_ addr
(.==.) ETH_TYP c    = eth_type_ c
(.==.) ARP_IP_SRC addr = arp_ip_src_ addr
(.==.) ARP_IP_DST addr = arp_ip_dst_ addr
(.==.) IP_DST addr = ip_dst_ addr
(.==.) IP_SRC addr = ip_src_ addr
(.==.) IP_PROT p   = ip_prot_ p
(.==.) IP_DSCP x   = dscp_ x
(.==.) TCP_DST s   = tcp_dst_ s
(.==.) TCP_SRC s   = tcp_src_ s
(.==.) UDP_DST s   = udp_dst_ s
(.==.) UDP_SRC s   = udp_src_ s

infixr 4 .&&.
infix 6 .==.


liftBuilder :: (Match -> Either Error Match) -> MatchBuilder
liftBuilder f = MatchBuilder $ \x ->
  case x of
    Left e -> Left e
    Right m -> f m

match :: MatchBuilder -> Match
match b =
  case matchOrError b of
    Left e -> error e
    Right m -> m

matchOrError :: MatchBuilder -> Either Error Match
matchOrError (MatchBuilder f) = f (Right emptyMatch)

        