{-# LANGUAGE Arrows, CPP #-}

-- | This module defines type classes for input types carrying
-- switch messages and output types carrying switch commands. It defines
-- a few basic instances of these classes. In addition it defines
-- some signal functions that implement the most basic functionality to maintain  
-- switch-controller connections.
module Nettle.FRPControl.SwitchInterface
    (
     -- * Switch input 
     SwitchMessage 
     , SockAddr
     , switchJoinE
     , switchLeaveE
     , switchHelloE            
     , switchEchoRequestE
     , switchEchoReplyE
     , packetInE 
     , switchFeaturesE
     , portUpdateE      
     , switchErrorE
     , flowRemovalE
     , statsReplyE
     , portStatsReplyE
     , flowStatsReplyE
#if OPENFLOW_VERSION==152 || OPENFLOW_VERSION==1
     , barrierReplyE
#endif
     , filterPacketIns

     -- * Switch output
     , SwitchOutput
     , switchCommand
     , noOp
     , (<+>)

     -- * Driver
     , switchInterfaceDriver

     -- * Basic OpenFlow controller
     , openFlowController
    ) where

import Nettle.Ethernet.EthernetAddress
import Nettle.OpenFlow.Messages as M
import Nettle.OpenFlow.Switch
import Nettle.OpenFlow.Port
import Nettle.OpenFlow.Action
import Nettle.OpenFlow.Match
import Nettle.OpenFlow.FlowTable
import Nettle.OpenFlow.Statistics
import Nettle.OpenFlow.Error
import Nettle.OpenFlow.Packet
import Nettle.OpenFlow.MessagesBinary as M
import Nettle.Servers.TCPServer
import Nettle.Servers.MultiplexedTCPServer 
import Nettle.FRPControl.AFRP 
import Data.Word
import Data.Monoid 
import Control.Exception 

-- | A switch message is a TCP message where normal messages
-- contain a @TransactionID@ and an @SCMessage@
type SwitchMessage = TCPMessage (TransactionID, SCMessage)

-- | Projects out the @SockAddr@ associated with the switch from a @ConnectionEstablished@ @TCPMessage@.
switchJoinE :: Event SwitchMessage -> Event SockAddr
switchJoinE = mapFilterE f 
    where f (ConnectionEstablished addr) = Just addr
          f _ = Nothing

-- | Outputs an event whenever the switch disconnects from the controller. 
-- The event carries the @SockAddr@ used to communicate with the switch and 
-- an @IOException@ value indicating the reason for the disconnection.
switchLeaveE :: Event SwitchMessage -> Event (SockAddr, IOException)
switchLeaveE = mapFilterE f
    where f (ConnectionTerminated addr e) = Just (addr, e)
          f _ = Nothing

-- | Outputs an event whenever a switch sends a hello message.
switchHelloE :: Event SwitchMessage -> Event (SockAddr, TransactionID)
switchHelloE = mapFilterE f 
    where f (PeerMessage sw (msgID, SCHello)) = Just (sw, msgID)
          f _             = Nothing

-- | Outputs an event whenever a switch sends an echo request; carries 
-- the @SockAddr@ of the switch, the @TransactionID@ of the echo request, 
-- and the data included in the echo request.
switchEchoRequestE :: Event SwitchMessage -> Event (SockAddr, TransactionID, [Word8])
switchEchoRequestE = mapFilterE f
    where f (PeerMessage sw (msgID, SCEchoRequest bytes)) = Just (sw, msgID, bytes)
          f _                                                = Nothing

-- | Outputs an event whenever a switch sends an echo reply; carries 
-- the @SockAddr@ of the switch, the @TransactionID@ of the echo reply, 
-- and the data included in the echo reply.
switchEchoReplyE :: Event SwitchMessage -> Event (SockAddr, TransactionID, [Word8])
switchEchoReplyE = mapFilterE f
    where f (PeerMessage sw (msgID, SCEchoReply bytes)) = Just (sw, msgID, bytes)
          f _                                              = Nothing

-- | Outputs an event whenever a switch sends a packet in message.
-- The event carries the @SockAddr@ of the switch, the @TransactionID@ of the
-- message, and the @PacketInfo@ message itself.
packetInE ::  Event SwitchMessage -> Event (SockAddr, TransactionID, PacketInfo)
packetInE = mapFilterE f
    where f (PeerMessage sw (msgID, PacketIn pktRecord)) = Just (sw, msgID, pktRecord)
          f _ = Nothing

-- | Outputs an event whenever a switch sends a switch features message.
-- The event carries the @SockAddr@ of the switch, the @TransactionID@ of the
-- message, and the @SwitchFeatures@ message itself.
switchFeaturesE :: Event SwitchMessage -> Event (SockAddr, TransactionID, SwitchFeatures)
switchFeaturesE = mapFilterE f 
    where f (PeerMessage sw (msgID, Features x)) = Just (sw, msgID, x)
          f _ = Nothing

-- | Outputs an event whenever a switch sends a port update message.
-- The event carries the @SockAddr@ of the switch, the @TransactionID@ of the
-- message, and the @PortStatus@ message itself.
portUpdateE :: Event SwitchMessage -> Event (SockAddr, TransactionID, PortStatus)
portUpdateE = mapFilterE f
    where f (PeerMessage sw (msgID, M.PortStatus x)) = Just (sw, msgID, x)
          f _ = Nothing

-- | Outputs an event whenever a switch sends an error message.
-- The event carries the @SockAddr@ of the switch, the @TransactionID@ of the
-- message, and the @SwitchError@ message itself.
switchErrorE :: Event SwitchMessage -> Event (SockAddr, TransactionID, SwitchError)
switchErrorE = mapFilterE f
    where f (PeerMessage sw (msgID, Error x)) = Just (sw, msgID, x)
          f _ = Nothing

-- | Outputs an event whenever a switch sends an flow removed message.
-- The event carries the @SockAddr@ of the switch, the @TransactionID@ of the
-- message, and the @FlowRemoved@ message itself.
flowRemovalE :: Event SwitchMessage -> Event (SockAddr, TransactionID, FlowRemoved)
flowRemovalE = mapFilterE f
    where f (PeerMessage sw (msgID, M.FlowRemoved x)) = Just (sw, msgID, x)
          f _ = Nothing

#if OPENFLOW_VERSION==152 || OPENFLOW_VERSION==1
-- | Outputs an event whenever a switch sends a barrier reply.
-- The event carries the @SockAddr@ of the switch and the @TransactionID@ of the
-- message.
barrierReplyE :: Event SwitchMessage -> Event (SockAddr, TransactionID)
barrierReplyE = mapFilterE f
    where f (PeerMessage sw (msgID, BarrierReply)) = Just (sw, msgID)
          f _                      = Nothing
#endif

-- | Outputs an event whenever a switch sends statistics reply.
-- The event carries the @SockAddr@ of the switch, the @TransactionID@ of the
-- message, and the @StatsReply@ message itself.
statsReplyE ::Event SwitchMessage -> Event (SockAddr, TransactionID, StatsReply)
statsReplyE = mapFilterE f
    where f (PeerMessage sw (msgID, StatsReply reply)) = Just (sw, msgID, reply)
          f _ = Nothing

-- | Outputs an event whenever a switch sends port statistics reply.
-- The event carries the @SockAddr@ of the switch, the @TransactionID@ of the
-- message, a flag indicating whether more statistics for this reply will follow 
-- in a separate message and the port statistics included in this message itself.
portStatsReplyE :: Event SwitchMessage -> Event (SockAddr, TransactionID, MoreToFollowFlag, [(PortID, PortStats)])
portStatsReplyE = statsReplyE >>> arr (mapFilterE f)
  where f (addr, xid, PortStatsReply moreComing ports) = Just (addr, xid, moreComing, ports)
        f _ = Nothing

-- | Outputs an event whenever a switch sends flow statistics reply.
-- The event carries the @SockAddr@ of the switch, the @TransactionID@ of the
-- message, a flag indicating whether more statistics for this reply will follow 
-- in a separate message and the flow statistics included in this message itself.
flowStatsReplyE :: Event SwitchMessage -> Event (SockAddr, TransactionID, MoreToFollowFlag, [FlowStats])
flowStatsReplyE = statsReplyE >>> arr (mapFilterE f)
  where f (addr, xid, FlowStatsReply moreComing stats) = Just (addr, xid, moreComing, stats)
        f _ = Nothing

-- | Remove packet in messages not satisfying the given predicate.
filterPacketIns :: (PacketInfo -> Bool) -> Event SwitchMessage -> Event SwitchMessage
filterPacketIns p = mapFilterE p'
  where p' m = case m of 
          PeerMessage switch (xid, PacketIn pktIn) -> if p pktIn then Just m else Nothing
          _ -> Just m

-----------------------------------------------------
-- Switch output commands
-----------------------------------------------------
-- An implementation of HasSwitchCommands
type SwitchOutput       = [(SockAddr, (TransactionID, CSMessage))]
                          
switchCommand :: SockAddr -> TransactionID -> CSMessage -> SwitchOutput 
switchCommand addr xid msg = [(addr, (xid, msg))]

-- | Another name for the unit element of a Monoid, specialized to the HasSwitchCommands class.
noOp :: Monoid o => o
noOp = mempty

-- | Infix binary operator for mappend.
(<+>) :: Monoid o => o -> o -> o 
(<+>) = mappend

-- | Monoid instance for a Monoid embedded inside an Event.
instance Monoid a => Monoid (Event a) where
    mempty  = NoEvent
    mappend = mergeBy mappend

-------------------------------------

-- | Runs an OpenFlow server at the specified port number, and return the driver IO actions.
switchInterfaceDriver :: ServerPortNumber -> IO (IO SwitchMessage, SwitchOutput -> IO ())
switchInterfaceDriver portNumber = 
    do process <- openFlowServer portNumber
       let actuator csMsgs = writeAll process csMsgs
       return (readP process, actuator)

-- | @openFlowController@ implements the basic aspects of the OpenFlow
-- protocol, such as the initial version negotiation and responding to 
-- echo requests.
openFlowController :: SF (Event SwitchMessage) (Event SwitchOutput)
openFlowController = 
    proc i -> do 
      o1E <- greeter     -< i
      o2E <- echoHandler -< i
      returnA -< mergeBy (<+>) o1E o2E

echoHandler :: SF (Event SwitchMessage) (Event SwitchOutput)
echoHandler = arr (fmap (\(addr, xid, bytes) -> switchCommand addr xid (CSEchoReply bytes)) . switchEchoRequestE)

greeter :: SF (Event SwitchMessage) (Event SwitchOutput)
greeter = 
    proc i -> do
      returnA -< liftE (\(addr, msgID) -> switchCommand addr msgID CSHello) (switchHelloE i)