{-# LANGUAGE FlexibleContexts          #-}
{-# LANGUAGE FlexibleInstances         #-}
{-# LANGUAGE MultiParamTypeClasses     #-}
{-# LANGUAGE NoMonomorphismRestriction #-}
{-# LANGUAGE TypeSynonymInstances      #-}
module Data.Model.Util
  ( -- * Dependencies
    properMutualGroups
  , mutualGroups
  , transitiveClosure
  -- * Error utilities
  , Errors
  , toErrors
  , noErrors
  , errsInContext
  , inContext
  , errorToConvertResult
  , errorsToConvertResult
  , convertResultToError
  , convertResultToErrors
  , convertOrError
  -- * Convertible re-exports
  , Convertible(..)
  , convert
  , ConvertResult
  , ConvertError(..)
  -- * Formatting utilities
  , dotted
  ) where

import           Control.Monad
import           Control.Monad.Trans.State
import           Data.Bifunctor
import           Data.Convertible
import           Data.Foldable                  (toList)
import           Data.List
import qualified Data.Map.Lazy                  as M
import           Data.Maybe
import           Data.Typeable
import           Text.PrettyPrint.HughesPJClass (Pretty, prettyShow)

{-| Return the groups of mutually dependent entities, with more than one component

>>> properMutualGroups Just (M.fromList [("a",["b","c"]),("b",["a","c"]),("c",[])])
Right [["b","a"]]

-}
properMutualGroups :: (Ord r, Pretty r, Foldable t) => (a -> Maybe r) -> M.Map r (t a) -> Either [String] [[r]]
properMutualGroups getRef env = filter ((> 1) . length) <$> mutualGroups getRef env

{-| Return the groups of mutually dependent entities

>>> mutualGroups Just (M.fromList [("a",["b","c"]),("b",["a","c"]),("c",[])])
Right [["c"],["b","a"]]

-}
mutualGroups :: (Ord r, Pretty r, Foldable t) => (a -> Maybe r) -> M.Map r (t a) -> Either [String] [[r]]
mutualGroups getRef env = recs [] (M.keys env)
  where
    deps = transitiveClosure getRef env
    recs gs [] = return gs
    recs gs (n:ns) = do
      ds <- deps n
      mutual <- filterM (((n `elem`) <$>) . deps) ds
      recs (mutual:gs) (ns \\ mutual)

{-| Return the transitive closure of an element in a graph of dependencies specified as an adjacency list

>>> transitiveClosure Just (M.fromList [("a",["b","c"]),("b",["b","d","d","c"]),("c",[]),("d",["a"])]) "b"
Right ["c","a","d","b"]

>>> transitiveClosure Just (M.fromList [("a",["b","c"]),("b",["b","d","d","c"]),("c",[]),("d",["a"])]) "c"
Right ["c"]

-}
transitiveClosure :: (Foldable t, Pretty r, Ord r) => (a -> Maybe r) -> M.Map r (t a) -> r -> Either [String] [r]
transitiveClosure getRef env = execRec . deps
    where
      deps n = do
         present <- (n `elem`) <$> gets seen
         unless present $ do
           modify (\st -> st {seen=n:seen st})
           case M.lookup n env of
             Nothing -> modify (\st -> st {errors=unwords ["transitiveClosure:Unknown reference to",prettyShow n]:errors st})
             Just v  -> mapM_ deps (mapMaybe getRef . toList $ v)

execRec :: State (RecState r) a -> Either [String] [r]
execRec op = (\st -> if null (errors st) then Right (seen st) else Left (errors st)) $ execState op (RecState [] [])

data RecState r = RecState {seen::[r],errors::Errors} deriving Show

-- |A list of error messages
type Errors = [Error]

type Error = String

toErrors :: Bifunctor p => p a c -> p [a] c
toErrors = first (:[])

noErrors :: Errors -> Bool
noErrors = null

errorToConvertResult :: (Typeable b, Typeable a, Show a) => (a -> Either Error b) -> a -> ConvertResult b
errorToConvertResult conv a = either (\err -> convError err a) Right $ conv a

{-|
>>> errorsToConvertResult (const (Left ["Bad format","Invalid value"])) ".." :: ConvertResult Int
Left (ConvertError {convSourceValue = "\"..\"", convSourceType = "[Char]", convDestType = "Int", convErrorMessage = "Bad format, Invalid value"})
-}
errorsToConvertResult :: (Typeable b, Typeable t, Show t) => (t -> Either Errors b) -> t -> ConvertResult b
errorsToConvertResult conv a = either (\errs -> convError (intercalate ", " errs) a) Right $ conv a

{-|
>>> convertOrError 'a' :: Either Error Word
Right 97

>>> convertOrError (1E50::Double) :: Either Error Word
Left "Convertible: error converting source data 1.0e50 of type Double to type Word: Input value outside of bounds: (0,18446744073709551615)"
-}
convertOrError :: Convertible a c => a -> Either String c
convertOrError = convertResultToError . safeConvert

convertResultToError :: Bifunctor p => p ConvertError c -> p String c
convertResultToError = first prettyConvertError

convertResultToErrors :: Bifunctor p => p ConvertError c -> p [String] c
convertResultToErrors = toErrors . convertResultToError

instance Convertible String String where safeConvert = Right . id

-- |Prefix errors with a contextual note
errsInContext :: (Convertible ctx String, Bifunctor p) => ctx -> p [String] c -> p [String] c
errsInContext ctx = first (inContext ctx)

{-|Prefix a list of strings with a contextual note

>>> inContext "0/0" ["Zero denominator"]
["In 0/0: Zero denominator"]
-}
inContext :: Convertible ctx String => ctx -> [String] -> [String]
inContext ctx = map (\msg -> unwords ["In",convert ctx++":",msg])

{-| Intercalate a dot between the non empty elements of a list of strings.

>>> dotted []
""

>>> dotted ["","bc","de"]
"bc.de"

>>> dotted ["bc","","de"]
"bc.de"
-}
dotted :: [String] -> String
-- dotted = intercalate "." . filter (not . null)
dotted [] = ""
dotted [s] = s
dotted (h:t) = post h ++ dotted t
    where post s | null s = ""
                 | otherwise = s ++ "."