-- | Module that provides various functions for interaction with 'GraphEdge's,
-- 'EdgeList's and vertices themselves.
--
module Math.Grads.Algo.Interaction
  (
    -- * Vertex Functions
    --
    areAdjacent
  , getEnds
  , getOtherEnd
  , getSharedVertex
  , getVertexAdjacent
  , getVertexIncident
  , getVertexIncidentIdx
  , haveSharedVertex
  , isIncident
  , (~=)
  , (/~=)
    -- * Edge Functions
    --
  , matchEdges
  , getEdgeIncident
    -- * EdgeList Functions
    --
  , doubleEdgeList
  , edgeListToMap
  , haveSharedEdge
  , sortBondList
  , getIndices
  ) where

import           Data.List        (find, findIndices, intersect, sortOn)
import           Data.Map         (Map)
import qualified Data.Map         as M
import           Data.Maybe       (fromJust, isJust)

import           Math.Grads.Graph (EdgeList, GraphEdge, edgeType)
import           Math.Grads.Utils (nub)

-- | Equality operator for 'GraphEdge's.
--
(~=) :: GraphEdge e1 -> GraphEdge e2 -> Bool
(b, e, _) ~= (b', e', _) = (b == b' && e == e') || (b == e' && e == b')

-- | Inequality operator for 'GraphEdge's.
--
(/~=) :: GraphEdge e1 -> GraphEdge e2 -> Bool
b1 /~= b2 = not $ b1 ~= b2

-- | Checks that vertex with given index is incident to 'GraphEdge'.
--
isIncident :: GraphEdge e -> Int -> Bool
isIncident (b, e, _) n = b == n || e == n

-- | Find all edges in given 'EdgeList' that are incident to vertex with given index.
--
getVertexIncident :: EdgeList e -> Int -> EdgeList e
getVertexIncident bs n = filter (`isIncident` n) bs

-- | Returns indices of edges in 'EdgeList' that are incident to vertex with given index.
--
getVertexIncidentIdx :: EdgeList e -> Int -> [Int]
getVertexIncidentIdx bs n = findIndices (`isIncident` n) bs

-- | Returns index of vertex incident to given 'GraphEdge' and different from passed index.
--
getOtherEnd :: GraphEdge e -> Int -> Int
getOtherEnd (b, e, _) n | b == n = e
                        | e == n = b
                        | otherwise = error "There is no such index in edge."

-- | Finds in given 'EdgeList' all indices of vertices adjacent to given vertex.
--
getVertexAdjacent :: EdgeList e -> Int -> [Int]
getVertexAdjacent bs n = (`getOtherEnd` n) <$> getVertexIncident bs n

-- | Checks whether two vertices with given indices are adjacent in given 'EdgeList'.
--
areAdjacent :: EdgeList e -> Int -> Int -> Bool
areAdjacent bs n n' = n' `elem` getVertexAdjacent bs n

-- | Retrieves indices of vertices that are being connected by given 'GraphEdge'.
--
getEnds :: GraphEdge e -> [Int]
getEnds (b, e, _) = [b, e]

-- | Checks that two edges have common vertex.
--
haveSharedVertex :: GraphEdge e1 -> GraphEdge e2 -> Bool
haveSharedVertex b1 b2 = isJust $ getSharedVertex b1 b2

-- | Gets shared common vertex of two edges. If edges don't have common vertex,
-- returns Nothing.
--
getSharedVertex :: GraphEdge e1 -> GraphEdge e2 -> Maybe Int
getSharedVertex b1 b2 | null is        = Nothing
                      | length is == 2 = Nothing
                      | otherwise      = Just $ head is
  where
    is = getEnds b1 `intersect` getEnds b2

-- | Find edges in 'EdgeList' which ordered pairs of indices, that they are connecting,
-- are present in passed list of ordered pairs.
--
matchEdges :: EdgeList e -> [(Int, Int)] -> EdgeList e
matchEdges bonds = fmap (\(a, b) -> fromJust (find (~= (a, b, undefined)) bonds))

-- | Find all edges that are incident to edge in 'EdgeList' with given index.
--
getEdgeIncident :: Ord e => EdgeList e -> Int -> EdgeList e
getEdgeIncident bs n | n >= length bs = []
                     | otherwise = filter (/= (beg, end, typ)) $ getVertexIncident bs beg ++ getVertexIncident bs end
  where
    (beg, end, typ) = bs !! n

-- | For every edge in 'EdgeList' add to that list an edge in opposite direction.
--
doubleEdgeList :: EdgeList e -> EdgeList e
doubleEdgeList = concatMap (\(a, b, t) -> [(a, b, t), (b, a, t)])

-- | Transforms 'EdgeList' into 'Map' that corresponds to adjacency list of undirected
-- graph induced by these edges.
--
edgeListToMap :: EdgeList e -> Map Int [Int]
edgeListToMap bonds' = M.fromList (fmap (toVertex bonds) (getIndices bonds))
  where
    bonds = doubleEdgeList bonds'

    toVertex :: EdgeList e -> Int -> (Int, [Int])
    toVertex bs i = (i, concatMap (\(a, b, _) -> [a | b == i]) bs)

-- | Checks that two 'EdgeList's have common edge.
--
haveSharedEdge :: Eq e => EdgeList e -> EdgeList e -> Bool
haveSharedEdge b1 b2 = or $ fmap (`elem` b2) b1

-- | Sorting for 'EdgeList', that sorts edges on their type, then on index of their
-- to (right) vertex, then on index of their from (left) vertex.
--
sortBondList :: Ord e => EdgeList e -> EdgeList e
sortBondList = sortOn left . sortOn right . sortOn edgeType
  where
    left (a, _, _) = a
    right (_, b, _) = b

-- | Gets all vertices from 'EdgeList'.
--
getIndices :: EdgeList e -> [Int]
getIndices = nub . concatMap getEnds