{-# LANGUAGE DisambiguateRecordFields, RecordWildCards, NamedFieldPuns #-}
{-# LANGUAGE BangPatterns #-}
{-# LANGUAGE DeriveGeneric #-}
module Network.Data.OpenFlow.Match ( 
  Match (..)
  , MatchHeader(..)
  , MatchBody(..)
  , matchAny
  , matchAnyHeader
  , matchAnyBody
  , isExactMatch
  , exactMatchPacketNoPort
  , frameToExactMatchNoPort
  , ofpVlanNone
  , matches
  , overlap
  , matchIntersection
  , prettyPrintMatch
  ) where

import Network.Data.Ethernet.EthernetAddress
import Network.Data.Ethernet.EthernetFrame 
import Network.Data.Ethernet.AddressResolutionProtocol
import Network.Data.IPv4.IPAddress hiding (intersect)
import qualified Network.Data.IPv4.IPAddress as IPAddress
import qualified Network.Data.IPv4.IPPacket as IP
import Network.Data.IPv4.IPPacket (IPHeader(..))
import Network.Data.OpenFlow.Port
import Network.Data.OpenFlow.Packet

import Data.Aeson
import qualified Data.List as List
import Data.Maybe (isJust, catMaybes)
import GHC.Generics


-- | Each flow entry includes a match, which essentially defines packet-matching condition. 
-- Fields that are left Nothing are "wildcards".
data Match = Match !(Maybe PortID) !MatchHeader !MatchBody
           deriving (Show,Read,Eq,Ord,Generic)

instance ToJSON Match
instance FromJSON Match

data MatchHeader = MatchHeader { srcEthAddress, dstEthAddress       :: !(Maybe EthernetAddress), 
                                 vLANID                             :: !(Maybe VLANID), 
                                 vLANPriority                       :: !(Maybe VLANPriority), 
                                 ethFrameType                       :: !(Maybe EthernetTypeCode) }
                 deriving (Show,Read,Eq,Ord, Generic)
instance ToJSON MatchHeader
instance FromJSON MatchHeader

data MatchBody = MatchBody { ipTypeOfService                    :: !(Maybe IP.IPTypeOfService), 
                             matchIPProtocol                    :: !(Maybe IP.IPProtocol), 
                             srcIPAddress, dstIPAddress         :: !IPAddressPrefix,
                             srcTransportPort, dstTransportPort :: !(Maybe IP.TransportPort) 
                           }
               deriving (Show,Read,Eq,Ord, Generic)
instance ToJSON MatchBody
instance FromJSON MatchBody
                        
prettyPrintMatch :: Match -> String
prettyPrintMatch (Match ip (MatchHeader seth deth vid vpr etht) (MatchBody tos prot sip dip stp dtp)) = 
    let mshow _ Nothing = Nothing
        mshow prefix (Just v) = Just (prefix ++ show v)
        mshowIP _ (_, 0) = Nothing
        mshowIP prefix ipPref = Just (prefix ++ IPAddress.showPrefix ipPref)
        lst = [ mshow "inPort=" ip,
                fmap (\eth -> "srcEth=" ++ prettyPrintEthernetAddress eth) seth,
                fmap (\eth -> "dstEth=" ++ prettyPrintEthernetAddress eth) deth,                
                mshow "vid=" vid, 
                mshow "vpr=" vpr, 
                mshow "ethtyp=" etht,
                mshow "tos=" tos,
                mshow "prot=" prot,
                mshowIP "sip=" sip,
                mshowIP "dip=" dip,
                mshow "stp=" stp,
                mshow "dtp=" dtp ]
      in "<" ++ List.concat (List.intersperse "," (catMaybes lst)) ++ ">"
-- |A match that matches every packet.
matchAny :: Match
matchAny = Match Nothing matchAnyHeader matchAnyBody

matchAnyHeader :: MatchHeader
matchAnyHeader = MatchHeader { srcEthAddress    = Nothing, 
                               dstEthAddress    = Nothing, 
                               vLANID           = Nothing, 
                               vLANPriority     = Nothing, 
                               ethFrameType     = Nothing }

matchAnyBody :: MatchBody
matchAnyBody = MatchBody { ipTypeOfService  = Nothing, 
                           matchIPProtocol  = Nothing, 
                           srcIPAddress     = defaultIPPrefix,
                           dstIPAddress     = defaultIPPrefix, 
                           srcTransportPort = Nothing, 
                           dstTransportPort = Nothing
                         }

matchIntersection :: Match -> Match -> Maybe Match
matchIntersection
  (Match inp (MatchHeader seth deth vid vp etp) (MatchBody tos pr sh dh sp dp))
  (Match inp' (MatchHeader seth' deth' vid' vp' etp') (MatchBody tos' pr' sh' dh' sp' dp')) = do
  let inter lhs      Nothing  = Just lhs
      inter Nothing  rhs      = Just rhs
      inter (Just l) (Just r) 
        | l == r    = Just (Just l)
        | otherwise = Nothing
  inPort <- inter inp inp'
  srcEth <- inter seth seth'
  dstEth <- inter deth deth'
  vlanID <- inter vid vid'
  vlanPrio <- inter vp vp'
  ethTyp <- inter etp etp'
  ipTOS <- inter tos tos'
  ipProto <- inter pr pr'
  srcIP <- IPAddress.intersect sh sh'
  dstIP <- IPAddress.intersect dh dh'
  srcPort <- inter sp sp'
  dstPort <- inter dp dp'
  return (Match inPort (MatchHeader srcEth dstEth vlanID vlanPrio ethTyp) (MatchBody ipTOS ipProto srcIP dstIP srcPort dstPort))

overlap :: Match -> Match -> Bool
overlap (Match inp (MatchHeader seth deth vid vp etp) (MatchBody tos pr sh dh sp dp))
       (Match inp' (MatchHeader seth' deth' vid' vp' etp') (MatchBody tos' pr' sh' dh' sp' dp')) =
  let inm (Just l) (Just r) = l == r
      inm _ _ = True
      ip = IPAddress.prefixOverlaps
    in inm inp inp' && inm seth seth' && inm deth deth' && inm vid vid' &&
       inm vp vp' && inm etp etp' && inm tos tos' && inm pr pr' && ip sh sh' &&
       ip dh dh' && inm sp sp' && inm dp dp'

-- | Return True if given 'Match' represents an exact match, i.e. no
--   wildcards and the IP addresses' prefixes cover all bits.
isExactMatch :: Match -> Bool
isExactMatch (Match inPort (MatchHeader {..}) (MatchBody {..})) =
    (isJust inPort) &&
    (isJust srcEthAddress) &&
    (isJust dstEthAddress) &&
    (isJust vLANID) &&
    (isJust vLANPriority) &&
    (isJust ethFrameType) &&
    (isJust ipTypeOfService) &&
    (isJust matchIPProtocol) &&
    (prefixIsExact srcIPAddress) &&
    (prefixIsExact dstIPAddress) &&
    (isJust srcTransportPort) &&
    (isJust dstTransportPort)

ofpVlanNone :: Int
ofpVlanNone = 0xffff

exactMatchPacketNoPort :: PacketInfo -> Match
exactMatchPacketNoPort = frameToExactMatchNoPort . enclosedFrame 

frameToExactMatchNoPort :: EthernetFrame -> Match
frameToExactMatchNoPort (h, body) = 
  Match Nothing hdr bdy
  where
    hdr = MatchHeader { srcEthAddress    = Just srcEthAddress, 
                        dstEthAddress    = Just dstEthAddress, 
                        vLANID           = Just vLANID, 
                        vLANPriority     = Just vLANPriority, 
                        ethFrameType     = Just ethFrameType }
    bdy = MatchBody { ipTypeOfService  = Just iptos, 
                      matchIPProtocol  = Just ipproto, 
                      srcIPAddress     = srcip,
                      dstIPAddress     = dstip, 
                      srcTransportPort = Just tsrc, 
                      dstTransportPort = Just tdst
                    }

    ethFrameType = typeCode body
    (srcEthAddress, dstEthAddress, vLANID, vLANPriority) = 
          case h of 
            (EthernetHeader {..}) -> 
              (etherSrc, etherDst, fromIntegral ofpVlanNone, 0)
            (Ethernet8021Q {..})  -> 
              (etherSrc, etherDst, vlanId, priorityCodePoint)
        
    (iptos, ipproto, srcip, dstip, tsrc, tdst) = 
          case body of
            (IPInEthernet (IPHeader {dscp,ipSrcAddress,ipDstAddress}, ipbody)) -> 
              let (tsrc',tdst') = 
                    case ipbody of 
                      (IP.TCPInIP s d)           -> (s,d)
                      (IP.UDPInIP s d)           -> (s,d)
                      (IP.ICMPInIP (icmpType,_) _ _) -> (fromIntegral icmpType, 0)
                      (IP.UninterpretedIPBody _) -> (0,0)
              in (dscp, IP.ipProtocol ipbody, exactPrefix ipSrcAddress, exactPrefix ipDstAddress, tsrc', tdst')
              
            (ARPInEthernet arpPacket) -> 
              let (ipproto', srcip', dstip') = 
                    case arpPacket of 
                      (ARPQuery (ARPQueryPacket {..})) -> (1, exactPrefix querySenderIPAddress, exactPrefix queryTargetIPAddress)
                      (ARPReply (ARPReplyPacket {..})) -> (2, exactPrefix replySenderIPAddress, exactPrefix replyTargetIPAddress)
              in (0, ipproto', srcip', dstip', 0, 0)
    
            _ -> (0, 0, defaultIPPrefix, defaultIPPrefix, 0, 0)


-- | Models the match semantics of an OpenFlow switch.
matches :: (PortID, EthernetFrame) -> Match -> Bool
matches (inPort, (ethHeader,ethBody)) (Match mInPort (MatchHeader {..}) (MatchBody {..})) = 
  noneOrEq mInPort inPort 
  && noneOrEq srcEthAddress (etherSrc ethHeader) 
  && noneOrEq dstEthAddress (etherDst ethHeader)  
  && noneOrEq ethFrameType (typeCode ethBody)    
  && case ethHeader of
      {  EthernetHeader {}   ->  True;
         Ethernet8021Q {..}  ->  noneOrEq vLANID vlanId 
                                 && noneOrEq vLANPriority priorityCodePoint}
  && case ethBody of
       {  IPInEthernet (hdr,bdy) -> 
            noneOrEq matchIPProtocol (IP.ipProtocol bdy) 
            && noneOrEq ipTypeOfService (IP.dscp hdr)  
            && IP.ipSrcAddress hdr `elemOfPrefix` srcIPAddress 
            && IP.ipDstAddress hdr `elemOfPrefix` dstIPAddress 
            && case bdy of
                 { IP.TCPInIP srcPort dstPort ->
                      noneOrEq srcTransportPort srcPort
                      && noneOrEq dstTransportPort dstPort  ;
                   IP.UDPInIP srcPort dstPort ->
                      noneOrEq srcTransportPort srcPort 
                      && noneOrEq dstTransportPort dstPort  ;
                   _ -> True }; 
          _ -> True }    

noneOrEq :: Eq a => Maybe a -> a -> Bool
noneOrEq Nothing _   = True
noneOrEq (Just a) a' = a == a'
