module Funcons.Operations.Graphs where

import Funcons.Operations.Internal
import Funcons.Operations.Booleans (tobool)
import qualified Data.Map as M
import qualified Data.Set as S
import Data.List ((\\))

library :: (HasValues t, Ord t) => Library t
library = libFromList [
    ("is-cyclic", UnaryExpr is_cyclic)
  , ("topological-sort", UnaryExpr topological_sort)
  ]

is_cyclic_ :: (Ord t, HasValues t) => [OpExpr t] -> OpExpr t
is_cyclic_ = unaryOp is_cyclic
is_cyclic :: (Ord t, HasValues t) => OpExpr t -> OpExpr t
is_cyclic = vUnaryOp "is-cyclic" op
  where op mm | Just m <- toGraph mm = Normal $ inject $ tobool (cyclic m)
        op _ = SortErr "is-cyclic not applied to a graph"

topological_sort_ :: (Ord t, HasValues t) => [OpExpr t] -> OpExpr t
topological_sort_ = unaryOp topological_sort
topological_sort :: (Ord t, HasValues t) => OpExpr t -> OpExpr t
topological_sort = vUnaryOp "topological-sort" op
  where op mm | Just m <- toGraph mm = Normal $ inject $ multi $ map inject $ fst (schedule m)
        op _ = SortErr "topological-sort not applied to a graph"

toGraph :: (Ord t) => Values t -> Maybe (Graph (Values t))
toGraph (Map m) = M.foldrWithKey combine (Just M.empty) m
  where combine k [Set s] mm = fmap (M.insert k s) mm
        combine _ _ _ = Nothing
toGraph _ = Nothing

-- small graph library
type Graph e = M.Map e (S.Set e)

-- | Get the entry points of the graph
entries :: Eq e => Graph e -> [e]
entries m = M.keys m \\ withIncoming
  where withIncoming = concatMap S.toList (M.elems m)

-- | Delete a node from the graph
delete :: Ord e => e -> Graph e -> Graph e
delete n m = M.map (S.delete n) $ M.delete n m

-- | Return all nodes in the graph such that if `a -> b` in the graph
-- then `a` occurs before `b` in the result
-- Also returns a graph which, if cyclic, contains all the cycles in the 
-- original graph, corresponding to nodes not in the schedule.
schedule :: (Ord e) => Graph e -> ([e], Graph e)
schedule gr = schedule' gr (entries gr) []
  where schedule' gr []     uset = (uset, gr)
        schedule' gr (e:es) uset = schedule' gr' (entries gr') uset'
          where uset'       = uset ++ [e]
                gr'         = delete e gr

-- | Checks whether the given grammar contains cycles
cyclic :: (Ord e) => Graph e -> Bool
cyclic gr = not (is_empty (snd (schedule gr)))

-- | Checks whether the given graph is empty
is_empty gr = M.null gr