{-# LANGUAGE ConstraintKinds, DeriveAnyClass, DeriveFunctor, DeriveGeneric,
             FlexibleContexts, FlexibleInstances, MultiParamTypeClasses,
             StandaloneDeriving, TupleSections, TypeFamilies #-}

{- |
   Module      : Data.Graph.Unordered
   Description : Graphs with Hashable nodes
   Copyright   : (c) Ivan Lazar Miljenovic
   License     : MIT
   Maintainer  : Ivan.Miljenovic@gmail.com

Known limitations:

* Adding edges might not be the same depending on graph construction
  (if you add then delete a lot of edges, then the next new edge might
  be higher than expected).

 -}
module Data.Graph.Unordered
  ( -- * Graph datatype
    Graph
  , DirGraph
  , UndirGraph
  , ValidGraph
    -- ** Edge types
  , Edge (..)
  , DirEdge (..)
  , UndirEdge (..)
  , EdgeType (..)
  , NodeFrom (..)
  , DirAdj (..)
  , Identity (..)
    -- ** Graph Context
  , Context (..)
  , AdjLookup
  , Contextual (..)
  , ValidContext
  , FromContext (..)
  , ToContext (..)

    -- * Graph functions
    -- ** Graph Information
  , isEmpty

    -- *** Node information
  , order
  , hasNode
  , ninfo
  , nodes
  , nodeDetails
  , lnodes
  , nlab
  , neighbours

    -- *** Edge information
  , size
  , hasEdge
  , einfo
  , edges
  , edgeDetails
  , ledges
  , elab
  , edgePairs
  , ledgePairs

    -- ** Graph construction
  , empty
  , mkGraph
  , buildGr
  , insNode
  , insEdge
    -- *** Merging
  , Mergeable
  , merge
  , mergeAs

    -- ** Graph deconstruction
  , delNode
  , delEdge
  , delEdgeLabel
  , delEdgesBetween
    -- *** Matching
  , Matchable
  , match
  , matchAs
  , matchAny
  , matchAnyAs

    -- ** Manipulation
  , nmap
  , nmapFor
  , emap
  , emapFor
  ) where

import Data.Graph.Unordered.Internal

import           Control.Arrow         (first, second)
import           Control.DeepSeq       (NFData)
import           Data.Function         (on)
import           Data.Functor.Identity
import           Data.HashMap.Strict   (HashMap)
import qualified Data.HashMap.Strict   as HM
import           Data.List             (delete, foldl', groupBy, sortBy)
import           Data.Maybe            (listToMaybe)
import           GHC.Generics          (Generic)

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

type DirGraph = Graph DirEdge

type UndirGraph = Graph UndirEdge

type AdjLookup n el = HashMap Edge (n,el)

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

data DirEdge n = DE { fromNode :: !n
                    , toNode   :: !n
                    }
               deriving (Eq, Ord, Show, Read, Functor, Generic, NFData)

-- 2-element set
-- INVARIANT: always has length == 2.
-- TODO: compare against using a simple tuple.
newtype UndirEdge n = UE { ueElem :: [n] }
                    deriving (Eq, Ord, Show, Read, Functor, Generic, NFData)

data DirAdj n = ToNode   n
              | FromNode n
              deriving (Eq, Ord, Show, Read, Generic, NFData)

instance NodeFrom DirAdj where
  getNode (ToNode   n) = n
  getNode (FromNode n) = n

-- | Note that for loops, the result of 'otherN' will always be a
-- 'ToNode'.
instance EdgeType DirEdge where
  type AdjType DirEdge = DirAdj

  mkEdge = DE

  otherN n (DE u v)
    | n == u    = ToNode v
    | otherwise = FromNode u

  toEdge u (ToNode   v) = DE u v
  toEdge v (FromNode u) = DE u v

  edgeNodes (DE u v) = [u,v]

  edgeTriple (DE u v, el) = (u,v,el)

instance EdgeType UndirEdge where
  type AdjType UndirEdge = Identity

  mkEdge u v = UE [u,v]

  otherN n (UE vs) = Identity $ head (delete n vs)

  toEdge u (Identity v) = UE [u,v]

  edgeNodes = ueElem

  edgeTriple (UE vs,el) = let [u,v] = vs
                          in (u,v,el)

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

data Context at n nl el = Ctxt { cNode  :: !n
                               , cLabel :: !nl
                               , cAdj   :: !(AdjLookup (at n) el)
                               }
                        deriving (Eq, Show, Read, Generic, NFData)

class Contextual ctxt where
  type CNode   ctxt :: *
  type CAType  ctxt :: * -> *
  type CNLabel ctxt :: *
  type CELabel ctxt :: *

type ValidContext et n nl el ctxt = (Contextual ctxt
                                    ,n ~ CNode ctxt
                                    ,AdjType et ~ CAType ctxt
                                    ,nl ~ CNLabel ctxt
                                    ,el ~ CELabel ctxt
                                    )

instance Contextual (Context at n nl el) where
  type CNode   (Context at n nl el) = n
  type CAType  (Context at n nl el) = at
  type CNLabel (Context at n nl el) = nl
  type CELabel (Context at n nl el) = el

class (Contextual ctxt) => FromContext ctxt where

  fromContext :: Context (CAType ctxt) (CNode ctxt) (CNLabel ctxt) (CELabel ctxt)
                 -> ctxt

-- This isn't quite right: have to work out what to do with Edge identifiers.
class (Contextual ctxt) => ToContext ctxt where

  toContext :: ctxt -> Context (CAType ctxt) (CNode ctxt) (CNLabel ctxt) (CELabel ctxt)

instance FromContext (Context at n nl el) where
  fromContext = id

instance ToContext (Context at n nl el) where
  toContext   = id

instance Contextual (n, nl, AdjLookup (at n) el) where
  type CNode   (n, nl, AdjLookup (at n) el) = n
  type CAType  (n, nl, AdjLookup (at n) el) = at
  type CNLabel (n, nl, AdjLookup (at n) el) = nl
  type CELabel (n, nl, AdjLookup (at n) el) = el

instance FromContext (n, nl, AdjLookup (at n) el) where
  fromContext (Ctxt n nl adj) = (n,nl,adj)

instance ToContext (n, nl, AdjLookup (at n) el) where
  toContext (n,nl,adj) = Ctxt n nl adj

instance Contextual (n, nl, [(n,[el])]) where
  type CNode   (n, nl, [(n,[el])]) = n
  type CAType  (n, nl, [(n,[el])]) = AdjType UndirEdge
  type CNLabel (n, nl, [(n,[el])]) = nl
  type CELabel (n, nl, [(n,[el])]) = el

instance (Ord n) => FromContext (n, nl, [(n,[el])]) where
  fromContext ctxt = (cNode ctxt
                     ,cLabel ctxt
                     ,toLookup (cAdj ctxt))
    where
      toLookup = map (\cels -> (fst (head cels), map snd cels))
                 . groupBy ((==) `on` fst)
                 . sortBy (compare `on` fst)
                 . map (first runIdentity)
                 . HM.elems

-- Can't have a ToContext for (n, nl, [(n,[el])]) as we threw out the
-- Edge values.

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

empty :: Graph et n nl el
empty = Gr HM.empty HM.empty minBound

isEmpty :: Graph et n nl el -> Bool
isEmpty = HM.null . nodeMap

-- | Number of nodes
order :: Graph et n nl el -> Int
order = HM.size . nodeMap

-- | Number of edges
size :: Graph et n nl el -> Int
size = HM.size . edgeMap

-- | Assumes the Contexts describe a graph in total, with the
-- outermost one first (i.e. @buildGr (c:cs) == c `merge` buildGr
-- cs@).
buildGr :: (ValidGraph et n) => [Context (AdjType et) n nl el] -> Graph et n nl el
buildGr = foldr merge empty

ninfo :: (ValidGraph et n) => Graph et n nl el -> n -> Maybe ([Edge], nl)
ninfo g = fmap (first HM.keys) . (`HM.lookup` nodeMap g)

hasNode :: (ValidGraph et n) => Graph et n nl el -> n -> Bool
hasNode g n = HM.member n (nodeMap g)

nlab :: (ValidGraph et n) => Graph et n nl el -> n -> Maybe nl
nlab g = fmap snd . (`HM.lookup` nodeMap g)

neighbours :: (ValidGraph et n) => Graph et n nl el -> n -> [n]
neighbours g n = maybe [] (map (getNode . otherN n . fst . (edgeMap g HM.!)) . fst)
                 $ ninfo g n

hasEdge :: (ValidGraph et n) => Graph et n nl el -> Edge -> Bool
hasEdge g e = HM.member e (edgeMap g)

einfo :: (ValidGraph et n) => Graph et n nl el -> Edge -> Maybe (et n, el)
einfo g = (`HM.lookup` edgeMap g)

elab :: (ValidGraph et n) => Graph et n nl el -> Edge -> Maybe el
elab g = fmap snd . einfo g

nodes :: Graph et n nl el -> [n]
nodes = HM.keys . nodeMap

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

type Matchable et n nl el ctxt = (ValidGraph et n
                                 ,FromContext ctxt
                                 ,ValidContext et n nl el ctxt
                                 )

-- | Note that for any loops, the resultant edge will only appear once
-- in the output 'cAdj' field.
match :: (ValidGraph et n) => n -> Graph et n nl el
         -> Maybe (Context (AdjType et) n nl el, Graph et n nl el)
match n g = getCtxt <$> HM.lookup n nm
  where
    nm = nodeMap g
    em = edgeMap g

    getCtxt (adj,nl) = (ctxt, g')
      where
        ctxt = Ctxt n nl adjM

        -- Note that loops will only appear once here.
        adjM = HM.map (first $ otherN n) (HM.intersection em adj)

        g' = g { nodeMap = nm'
               , edgeMap = em'
               }

        em' = em `HM.difference` adj

        adjNs = filter (/=n) -- take care of loops
                . map (getNode . fst)
                $ HM.elems adjM
        nm' = foldl' (flip $ HM.adjust (first (`HM.difference`adj)))
                     (HM.delete n nm)
                     adjNs

matchAs :: (Matchable et n nl el ctxt) => n -> Graph et n nl el
           -> Maybe (ctxt, Graph et n nl el)
matchAs n = fmap (first fromContext) . match n

matchAny :: (ValidGraph et n) => Graph et n nl el
            -> Maybe (Context (AdjType et) n nl el, Graph et n nl el)
matchAny g
  | isEmpty g = Nothing
  | otherwise = flip match g . head . HM.keys $ nodeMap g

matchAnyAs :: (Matchable et n nl el ctxt) => Graph et n nl el
              -> Maybe (ctxt, Graph et n nl el)
matchAnyAs = fmap (first fromContext) . matchAny

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

type Mergeable et n nl el ctxt = (ValidGraph et n
                                 ,ToContext ctxt
                                 ,ValidContext et n nl el ctxt
                                 )

-- Assumes edge identifiers are valid
merge :: (ValidGraph et n) => Context (AdjType et) n nl el
         -> Graph et n nl el -> Graph et n nl el
merge ctxt g = Gr nm' em' nextE'
  where
    n = cNode ctxt

    adjM = cAdj ctxt

    adj = HM.map (adjCount n . getNode . fst) adjM

    nAdj = HM.toList
           . foldl' (HM.unionWith HM.union) HM.empty
           . map (uncurry toNAdj)
           . HM.toList
           $ adjM

    toNAdj e (av,_) = let v = getNode av
                      in HM.singleton v (HM.singleton e (adjCount n v))

    -- Can we blindly assume that max == last ?
    maxCE = fmap succ . listToMaybe . sortBy (flip compare) . HM.keys $ adjM

    nextE = nextEdge g
    nextE' = maybe nextE (max nextE) maxCE

    em = edgeMap g
    em' = em `HM.union` HM.map (first $ toEdge n) adjM

    nm = nodeMap g
    nm' = foldl' (\m (v,es) -> HM.adjust (first (`HM.union`es)) v m)
                 (HM.insert n (adj,cLabel ctxt) nm)
                 nAdj

mergeAs :: (Mergeable et n nl el ctxt) => ctxt -> Graph et n nl el
           -> Graph et n nl el
mergeAs = merge . toContext

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

insNode :: (ValidGraph et n) => n -> nl -> Graph et n nl el -> Graph et n nl el
insNode n l g = g { nodeMap = HM.insert n (HM.empty, l) (nodeMap g) }

insEdge :: (ValidGraph et n) => (n,n,el) -> Graph et n nl el
           -> (Edge, Graph et n nl el)
insEdge (u,v,l) g = (e, Gr nm' em' (succ e))
  where
    e = nextEdge g

    nm' = addE u . addE v $ nodeMap g

    addE = HM.adjust (first $ HM.insert e (adjCount u v))

    em' = HM.insert e (mkEdge u v, l) (edgeMap g)

delNode :: (ValidGraph et n) => n -> Graph et n nl el -> Graph et n nl el
delNode n g = maybe g snd $ match n g

delEdge :: (ValidGraph et n) => Edge -> Graph et n nl el -> Graph et n nl el
delEdge e g = g { nodeMap = foldl' (flip delE) (nodeMap g) ens
                , edgeMap = HM.delete e (edgeMap g)
                }
  where
    ens = maybe [] (edgeNodes . fst) (HM.lookup e (edgeMap g))

    delE = HM.adjust (first $ HM.delete e)

-- TODO: care about directionality of edge.
delEdgeLabel :: (ValidGraph et n, Eq el) => (n,n,el) -> Graph et n nl el
                -> Graph et n nl el
delEdgeLabel (u,v,l) g
  | HM.null es = g
  | otherwise = g { nodeMap = delEs u . delEs v $ nm
                  , edgeMap = em `HM.difference` es
                  }
  where
    nm = nodeMap g

    em = edgeMap g

    es = maybe HM.empty (HM.filter isE . HM.intersection em . fst) $ HM.lookup u nm

    isE (et,el) = getNode (otherN u et) == v && el == l

    delEs = HM.adjust (first (`HM.difference`es))

delEdgesBetween :: (ValidGraph et n) => n -> n -> Graph et n nl el
                   -> Graph et n nl el
delEdgesBetween u v g
  | HM.null es = g
  | otherwise = g { nodeMap = delEs u . delEs v $ nm
                  , edgeMap = em `HM.difference` es
                  }
  where
    nm = nodeMap g
    em = edgeMap g
    es = maybe HM.empty (HM.filter isE . HM.intersection em . fst) $ HM.lookup u nm

    isE (et,_) = getNode (otherN u et) == v

    delEs = HM.adjust (first (`HM.difference`es))

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

nmap :: (ValidGraph et n) => (nl -> nl') -> Graph et n nl el -> Graph et n nl' el
nmap f = withNodeMap (HM.map (second f))

nmapFor :: (ValidGraph et n) => (nl -> nl) -> Graph et n nl el -> n
           -> Graph et n nl el
nmapFor f g n = withNodeMap (HM.adjust (second f) n) g

emap :: (ValidGraph et n) => (el -> el') -> Graph et n nl el -> Graph et n nl el'
emap f = withEdgeMap (HM.map (second f))

emapFor :: (ValidGraph et n) => (el -> el) -> Graph et n nl el -> Edge
           -> Graph et n nl el
emapFor f g e = withEdgeMap (HM.adjust (second f) e) g
