module Frenetic.Slices.Compile
  ( -- * Compilation
  , transformEdge
  , dynTransform
  , compileSlice
  , edgeCompileSlice
  -- * Internal tools
  , modifyVlan
  , setVlan
  , matchesSwitch
  ) where

import Control.Monad
import Frenetic.Common
import qualified Data.Map as Map
import qualified Data.MultiSet as MS
import qualified Data.Set as Set
import Frenetic.NetCore.Types
import Frenetic.NetCore.Short
import Frenetic.Pattern
import Frenetic.NetCore.Reduce
import Frenetic.NetCore.Pretty
import Frenetic.Slices.Slice
import Frenetic.Slices.VlanAssignment
import Frenetic.Topo

-- |Match a specific vlan tag
vlanMatch :: Vlan -> Predicate
vlanMatch vlan = dlVlan vlan

-- TODO(astory): also take in the packet channel and handle that
-- |Compile a list of slices and dynamic policies as they change.
dynTransform :: [(Slice, Chan Policy)] -> IO (Chan Policy)
dynTransform combined = do
  updateChan <- newChan :: IO (Chan (Vlan, Policy))
  outputChan <- newChan :: IO (Chan Policy)
  let tagged = zip [1..] combined
  -- Fork off threads to poll the input policy channels, compile them, and write
  -- them into the unified output channel.
  let poll (vlan, (slice, policyChan)) = do
      let loop = do
          update <- readChan policyChan
          let compiled = compileSlice slice vlan update
          writeChan updateChan (vlan, compiled)
      forkIO $ forever $ loop
  mapM_ poll tagged
  -- Poll from the unified channel, and update a map containing the most recent
  -- compiled version of each slice.  After each update, take the union and send
  -- the result down the pipe.
  let loop map = do
      (vlan, compiled) <- readChan updateChan
      let map' = Map.insert vlan compiled map
      writeChan outputChan $ mconcat (Map.elems map')
      loop map'
  forkIO $ loop Map.empty
  return outputChan

-- |Produce the combined policy by compiling a list of slices and policies with
-- the vanilla compiler
transform :: [(Slice, Policy)] -> Policy
transform combined = mconcat policies
    tagged = sequential combined
    policies = map (\(vlan, (slice, policy)) -> compileSlice slice vlan policy)

-- |Produce the combined policy by compiling a list of slices and policies with
-- the edge compiler
transformEdge :: Topo -> [(Slice, Policy)] -> Policy
transformEdge topo combined = mconcat policies where
  tagged = edge topo combined
  policies = map (\(assignment, (slice, policy)) ->
                   edgeCompileSlice slice assignment policy)

-- TODO(astory): egress predicates
-- |Compile a slice with a vlan key
compileSlice :: Slice -> Vlan -> Policy -> Policy
compileSlice slice vlan policy =
  if poUsesVlans policy then error "input policy uses VLANs." else
  let localPolicy = localize slice policy in
  -- A postcondition of localize is that all the forwarding actions of the
  -- policy make sense wrt the slice, and that every PoBasic matches at most one
  -- switch.  This is a precondition for outport
  let safePolicy = isolate slice vlan localPolicy in
  let inportPolicy = inportPo slice vlan localPolicy in
  let safeInportPolicy = PoUnion safePolicy inportPolicy in
  reduce $ outport slice safeInportPolicy

-- | Compile a slice with an assignment of VLAN tags to ports.  For this to work
-- properly, the assignment of tags to both ends of an edge must be the same
edgeCompileSlice :: Slice -> Map.Map Loc Vlan -> Policy -> Policy
edgeCompileSlice slice assignment policy = mconcat (queryPols : forwardPols)
    localPolicy = localize slice policy
    -- separate out queries to avoid multiplying them during the fracturing that
    -- goes on in creating the internal and external policies.
    queryPols = queryOnly slice assignment localPolicy
    forwardPols = forwardEdges slice assignment localPolicy

-- |Produce a list of policies that together implement just the query portion of
-- the policy running on the slice
queryOnly :: Slice -> Map.Map Loc Vlan -> Policy -> Policy
queryOnly slice assignment policy = justQueries <%> (onSlice <||> inBound) where
  onSlice = prOr . map onPort . Set.toList $ internal slice
  inBound = ingressPredicate slice <&&> dlNoVlan
  justQueries = removeForwards policy
  onPort l@(Loc s p) = inport s p <&&> dlVlan vlan <&&>
                       Map.findWithDefault top l (ingress slice) where
    vlan = case Map.lookup l assignment of
           Just v -> v
           Nothing -> error $
                      "assignment map incomplete at " ++ show l ++
                      "\nmap:   " ++ show assignment ++
                      "\nslice: " ++ show (internal slice)

-- |Remove forwarding actions from policy leaving only queries
removeForwards :: Policy -> Policy
removeForwards PoBottom = PoBottom
removeForwards (PoBasic pred (Action _ qs)) = PoBasic pred (Action MS.empty qs)
removeForwards (PoUnion p1 p2) = PoUnion p1' p2' where
  p1' = removeForwards p1
  p2' = removeForwards p2

-- |Remove queries from policy leaving only forwarding actions
removeQueries :: Policy -> Policy
removeQueries PoBottom = PoBottom
removeQueries (PoBasic pred (Action fs _)) = PoBasic pred (Action fs MS.empty)
removeQueries (PoUnion p1 p2) = PoUnion p1' p2' where
  p1' = removeQueries p1
  p2' = removeQueries p2

-- |Remove forwarding actions to ports other than p
justTo :: Port -> Policy -> Policy
justTo _ PoBottom = PoBottom
justTo p (PoBasic pred (Action fs qs)) = PoBasic pred (Action fs' qs) where
  fs' = MS.filter matches fs
  matches (Physical p', _) = p == p'
  matches (AllPorts, _) = error "AllPorts found while compiling."
justTo p (PoUnion p1 p2) = PoUnion p1' p2' where
  p1' = justTo p p1
  p2' = justTo p p2

-- TODO(astory): egress predicates
-- |Produce a list of policies that together instrument the edge-compiled
-- internal policy.  This only considers internal -> internal and internal ->
-- external forwarding.
forwardEdges :: Slice -> Map.Map Loc Vlan -> Policy -> [Policy]
forwardEdges slice assignment policy = concatMap buildPort locs where
  int = internal slice
  ing = Map.keysSet (ingress slice)
  egr = Map.keysSet (egress slice)
  portLookup = portsOfSet (Set.union int (Set.union ing egr))
  locs = Set.toList (Set.union int ing)
  buildPort :: Loc -> [Policy] -- Get the policies for packets to one location
  buildPort l@(Loc s p) = map hop $ Set.toList destinations where
    destinations = case Map.lookup s portLookup of
                     Just dests -> dests
                     Nothing -> error "Port lookup malformed."
    ourVlan = if Set.member l ing then dlNoVlan
              else case Map.lookup l assignment of
                     Just v -> dlVlan v
                     Nothing -> error "Vlan assignment malformed."
    restriction = inport s p <&&>
                  ourVlan <&&>
                  Map.findWithDefault top l (ingress slice)
    policy' = policy <%> restriction
    hop :: Port -> Policy -- Get the policies for one switch forwarding
    hop port = policy''' where
      loc = Loc s port
      targetVlan = if Set.member loc egr then Nothing
                   else case Map.lookup loc assignment of
                          Just v -> Just v
                          Nothing -> error "Vlan assignment malformed."
      policy'' = justTo port policy'
      -- It's safe to use modifyVlan because we only have actions on this one
      -- forwarding hop.
      policy''' = modifyVlan targetVlan policy''

portsOfSet :: Set.Set Loc -> Map.Map Switch (Set.Set Port)
portsOfSet = Map.fromListWith Set.union .
             map (\(Loc s p) -> (s, Set.singleton p)) .

-- |Produce a policy that only considers traffic on this vlan and on internal
-- ports.  Note that if the policy does not modify vlans, then it also only
-- emits traffic on this vlan.
isolate :: Slice -> Vlan -> Policy -> Policy
isolate slice vlan policy = policy <%> (vlPred <&&> intern)
    vlPred = vlanMatch vlan
    intern = prOr . map (\(Loc s p) -> inport s p) . Set.toList $
             internal slice

locToPred :: Loc -> Predicate
locToPred (Loc switch port) = inport switch port

-- |Produce a policy that moves packets into the vlan as defined by the slice's
-- input policy.
inportPo :: Slice -> Vlan -> Policy -> Policy
inportPo slice vlan policy =
  let incoming = ingressPredicate slice in
  let policyIntoVlan = modifyVlan (Just vlan) policy in
  policyIntoVlan <%> (incoming <&&> dlNoVlan)

-- |Produce a new policy the same as the old, but wherever a packet leaves an
-- outgoing edge, unset its VLAN.  Precondition:  every PoBasic must match at
-- most one switch.
outport :: Slice -> Policy -> Policy
outport slice policy = foldr stripVlan policy locs
  where locs = Map.keys (egress slice)

-- |Produce a predicate matching any of the inports (and their predicate)
-- specified
ingressPredicate :: Slice -> Predicate
ingressPredicate slice =
  prOr . map ingressSpecToPred . Map.assocs $ ingress slice

-- |Produce a predicate matching the ingress predicate at a particular location
ingressSpecToPred :: (Loc, Predicate) -> Predicate
ingressSpecToPred (loc, pred) = PrIntersect pred (locToPred loc)

-- |Walk through the policy and globally set VLAN to vlan at each forwarding
-- action
modifyVlan :: Maybe Vlan -> Policy -> Policy
modifyVlan _ PoBottom = PoBottom
modifyVlan vlan (PoBasic pred (Action m obs)) = PoBasic pred (Action m' obs)
    m' = setVlans m
    setVlans (p, mod) = (p, mod {modifyDlVlan = Just vlan})
modifyVlan vlan (PoUnion p1 p2) = PoUnion (modifyVlan vlan p1)
                                          (modifyVlan vlan p2)

-- |Unset vlan for packets forwarded to location (without link transfer) and
-- leave rest of policy unchanged.  Note that this assumes that each PoBasic
-- matches at most one switch.
stripVlan :: Loc -> Policy -> Policy
stripVlan = setVlan Nothing

-- |Set vlan tag for packets forwarded to location (without link transfer) and
-- leave rest of policy unchanged.  Note that this assumes that each PoBasic
-- matches at most one switch.
setVlan :: Maybe Vlan -> Loc -> Policy -> Policy
setVlan _ _ PoBottom = PoBottom
setVlan vlan loc (PoUnion p1 p2) = PoUnion (setVlan vlan loc p1)
                                           (setVlan vlan loc p2)
setVlan vlan (Loc switch port) pol@(PoBasic pred (Action m obs)) =
  if matchesSwitch switch pred then PoBasic pred (Action m' obs)
                               else pol
    m' = setVlanOnPort m
    setVlanOnPort (Physical p, mod) =
      if p == port then (Physical p, mod {modifyDlVlan = Just vlan})
                   else (Physical p, mod)
    setVlanOnPort (AllPorts, mod) =
      error "AllPorts encountered in slice compilation.  Did you first localize?"

-- |Determine if a predicate can match any packets on a switch (overapproximate)
matchesSwitch :: Switch -> Predicate -> Bool
matchesSwitch _ (PrPattern _)       = True
matchesSwitch s1 (PrTo s2)          = s1 == s2
matchesSwitch s (PrUnion p1 p2)     = matchesSwitch s p1 || matchesSwitch s p2
matchesSwitch s (PrIntersect p1 p2) = matchesSwitch s p1 && matchesSwitch s p2
matchesSwitch s (PrNegate _)        = True