{-# LANGUAGE TemplateHaskell, NoMonomorphismRestriction, TupleSections #-}
module Graphene.Algorithms (
  kruskal,
  dfs,
  bfs,
  dijkstra,
  DijkstraState,
  underlyingGraph,
  distancePairings,
  prevs,
  unvisited,
  visited,
  from
) where

import Data.List
import qualified Data.Map as M
import Graphene.Graph
import Lens.Family2
import Lens.Family2.State
import Control.Monad.Writer
import Control.Monad.Trans.State
import Data.Ord
import Data.Bifunctor
import Data.Maybe

_3 :: Lens' (a, b, c) c
_3 k (a, b, c) = fmap (\c' -> (a, b, c')) (k c) 

_2 :: Lens' (a, b, c) b
_2 k (a, b, c) = fmap (\b' -> (a, b', c)) (k b) 

_1 :: Lens' (a, b, c) a
_1 k (a, b, c) = fmap (\a' -> (a', b, c)) (k a) 

-- | Kruskal's minimum spanning tree algorithm
kruskal :: (Ord v, Ord e) => Graph v e -> Graph v e
kruskal g = view _3 $ execState go (vertexSets, sortedEdges, emptyGraph)
  where vertexSets = map (:[]) $ g^.vertices            -- list of singletons for each vertex
        sortedEdges = sortBy (comparing fst) $ g^.edges -- edges sorted by weight
        go :: (Eq v, Ord v, Ord e) => State ([[v]], [(e, (v, v))], Graph e v) ()
        go = do
          (vs, es, _) <- get
          unless (null es) $ do                         -- break if no edges left
            let e@(w, (v1, v2)) = head es               -- find edge with least weight
                ss = filter (\s -> any (`elem` s) [v1, v2]) vs -- find vertices connected to e
            case ss of 
              [s1, s2] -> do                    -- if we find two separate sets for v1 and v2,
                _3.vertices %= union ([v1, v2]) -- union the sets
                _3.edges    %= insert e         -- insert e into the resulting tree
                _1          %= delete s1 . delete s2 . insert (s1 `union` s2) -- merge s1 and s2
              _        -> return () -- otherwise, continue
            _2 %= tail              -- remove first element of sortedEdges
            go                      -- recursively call algorithm

-- | Depth first search for connections of `v`
dfs :: Eq v => v -> Graph e v -> [v]
dfs v g = case ns of 
  [] -> [v] -- only v if empty
  _  -> v : concatMap (\w -> dfs w (g' w)) ns     -- dfs each graph with w and neighbors removed
  where ns   = neighbors v g                      -- neighbor vertices of v
        g' w = removeVertices (v : delete w ns) g -- remove neighbors (except next hop)

-- | Breadth first search for connections of `v`
bfs :: Eq v => v -> Graph e v -> [v]
bfs v g = go [v] g
  where go []     _ = []
        go (x:xs) g = x : go (xs ++ ns) (removeVertex x g) -- add neighbors to queue
         where ns = neighbors x g

-- test
sg :: Graph Int Char
sg = fromLists ['a'..'e'] (zip3 [1..5] ['a'..'d'] ['b'..'e'])

infinity :: Int
infinity = maxBound -- you get the idea

-- | Container for Dijkstra's algorithm information
data DijkstraState e v = DijkstraState{
      _underlyingGraph :: Graph e v     -- |Graph to run algorithm on 
    , _distancePairings :: M.Map v Int  -- |Mapping from Vertices to Distances
    , _prevs :: M.Map v (Maybe v)       -- |Mapping from Vertices to previous vertices
    , _unvisited :: [v]                 -- |Set of unvisited vertices
    , _visited :: [v]                   -- |Set to visited vertices
    , _from :: v                        
      -- ^Vertex to generate dijkstra paths from
    } deriving (Show, Eq)

underlyingGraph :: Lens' (DijkstraState e v) (Graph e v)
underlyingGraph k (DijkstraState u d p un v f) = fmap (\u' -> DijkstraState u' d p un v f) (k u)

distancePairings :: Lens' (DijkstraState e v) (M.Map v Int)
distancePairings k (DijkstraState u d p un v f) = fmap (\d' -> DijkstraState u d' p un v f) (k d)

prevs :: Lens' (DijkstraState e v) (M.Map v (Maybe v))
prevs k (DijkstraState u d p un v f) = fmap (\p' -> DijkstraState u d p' un v f) (k p)

unvisited :: Lens' (DijkstraState e v) [v]
unvisited k (DijkstraState u d p un v f) = fmap (\un' -> DijkstraState u d p un' v f) (k un)

visited :: Lens' (DijkstraState e v) [v]
visited k (DijkstraState u d p un v f) = fmap (\v' -> DijkstraState u d p un v' f) (k v)

from :: Lens' (DijkstraState e v) v
from k (DijkstraState u d p un v f) = fmap (\f' -> DijkstraState u d p un v f') (k f)

-- | smart constructor for dijkstra state
-- | Initialize dist(v) to 0, the rest to inifinity
-- | Initialize previous vertices to nothing
mkDijkstra :: (Eq v, Ord v) => Graph e v -> v -> DijkstraState e v
mkDijkstra g@(Graph vs es) v = DijkstraState g dists prevs vs [] v
  where dists = M.fromList ( (v, 0) : (map (, infinity) $ delete v vs) )
        prevs = M.fromList $ zip vs (repeat Nothing) 

-- | Run dijkstra's algorithm on a graph starting at vertex v
dijkstra :: (Eq v, Ord v) => Graph Int v -> v -> DijkstraState Int v
dijkstra g = execState go . mkDijkstra g
 where go :: (Eq v, Ord v) => State (DijkstraState Int v) ()
       go = do
         q   <- use unvisited
         unless (null q) $ do  -- if unvisited set is empty, complete.
           dists <- use distancePairings
           g     <- use underlyingGraph
           let u  = minimumBy (comparing (flip M.lookup dists)) q -- find vertex with min. weight
               (Just uWeight) = M.lookup u dists                  -- u's weight
               -- list of edges (weights) and the vertices they point to
               conns          = connections u g
           unvisited %= (delete u) -- remove u from q
           -- if current weight is infinity, the graph is disconnected, so end.
           unless ( uWeight == infinity ) $ do
             forM_ conns $ \(eWeight, v) -> do -- update distances
               let (Just vWeight) = M.lookup v dists
                   newWeight      = eWeight + uWeight
               -- only update previous vertex and distance a smaller distance was found
               unless (newWeight > vWeight) $ do
                 distancePairings %= (M.insert v newWeight)
                 prevs            %= (M.insert v (Just u) )
             visited %= (u:) -- set u to visited
             go              -- recursively run the algorithm
