module Data.Dependency
    ( -- * Functions
      resolveDependencies
    , buildSequence
    , check
    -- * Types
    , Dependency (..)
    , PackageSet (..)
    , Version (..)
    , DepM
    , ResolveError (..)
    , Constraint (..)
    ) where

import           Control.Arrow
import           Control.Monad
import           Data.Dependency.Error
import           Data.Dependency.Sort
import           Data.Dependency.Type
import           Data.Foldable         (toList)
import           Data.List             (groupBy)
import qualified Data.Map              as M
import qualified Data.Set              as S

lookupMap :: String -> M.Map String a -> DepM a
lookupMap k ps = case M.lookup k ps of
    Just x  -> Right x
    Nothing -> Left (NotPresent k)

-- | This function checks a package against currently in-scope packages,
-- downgrading versions as necessary until we reach something amenable or run out
-- of options.
checkWith :: Dependency -- ^ Dependency we want
          -> [Dependency] -- ^ Dependencies already in scope.
          -> S.Set Dependency -- ^ Set of available versions
          -> DepM Dependency
checkWith x ds s
    | check x ds = Right x
    | otherwise = lookupSet (Just x) ds (S.deleteMax s)

lookupSet :: Maybe Dependency -- ^ Optional dependency we are looking for.
          -> [Dependency] -- ^ Dependencies already added
          -> S.Set Dependency -- ^ Set of available versions of given dependency
          -> DepM Dependency
lookupSet x ds s = case S.lookupMax s of
    Just x' -> checkWith x' ds s
    Nothing -> g x

    where g Nothing   = Left InternalError
          g (Just x') = Left (Conflicts (_libName <$> ds') (_libName x'))
            where ds' = filter (\d -> not (check x' [d])) ds

-- This does check for compatibility with past packages, but doesn't do the
-- fancy tardis shenanigans it's supposed to when package resolution fails.
latest :: PackageSet Dependency -> [Dependency] -> Dependency -> DepM (String, Dependency)
latest (PackageSet ps) ds d@(Dependency ln _ _) = do
    s <- lookupMap ln ps
    finish ln (lookupSet (Just d) ds s)

finish :: String -> DepM Dependency -> DepM (String, Dependency)
finish ln dep' =
    case dep' of

        Right dep ->
            pure (ln, dep)

        Left err ->
            Left err

-- | This splits dependencies into phases
buildSequence :: [Dependency] -> [[Dependency]]
buildSequence = reverse . groupBy independent . sortDeps
    where independent (Dependency ln ls _) (Dependency ln' ls' _) =
            ln' `notElem` (fst <$> ls) && ln `notElem` (fst <$> ls')

iterateM :: (Monad m) => Int -> (a -> m a) -> a -> m [a]
iterateM 0 _ _ = pure []
iterateM n f x = (x:) <$> (iterateM (n-1) f =<< f x)

saturateDeps :: PackageSet Dependency -> Dependency -> DepM (S.Set Dependency)
saturateDeps ps = resolve <=< saturateDeps' ps
    where resolve set = last <$> iterateM n next set
          next depSet = S.unions <$> sequence (saturateDeps' ps <$> toList depSet)
          n = length (toList ps)

saturateDeps' :: PackageSet Dependency -> Dependency -> DepM (S.Set Dependency)
saturateDeps' (PackageSet ps) dep = S.fromList <$> list
    where list = (:) dep <$> (traverse (lookupSet Nothing (crunch ps)) =<< deps)
          deps = sequence [ lookupMap lib ps | lib <- fst <$> _libDependencies dep ]
          crunch = mconcat . fmap toList . toList

-- | Dependency resolution is guided by the following:
--
-- 1. Always use a newer version when possible
--
-- 2. Obey constraints
--
-- 3. Specify an error for circular dependencies
--
-- 4. Specify an error for overconstrained builds
--
-- 5. Specify an error if a package is not present
--
-- 6. Present a solution whenever one exists.
--
-- This doesn't do any package resolution beyond versioning.
resolveDependencies :: PackageSet Dependency -> [Dependency] -> DepM [[Dependency]]
resolveDependencies ps = select . getLatest <=< fmap ((buildSequence &&& id) . toList) . saturate
    where select = fmap (fmap (fmap snd))
          saturate = fmap S.unions . traverse (saturateDeps ps)
          getLatest (p, q) = traverse (traverse (latest ps q)) p