-- |
-- Provides the ability to sort modules based on module dependencies
--
module Language.PureScript.ModuleDependencies
  ( sortModules
  , ModuleGraph
  ) where

import Prelude.Compat

import Control.Monad.Error.Class (MonadError(..))

import Data.Graph
import Data.List (nub)
import Data.Maybe (fromMaybe)

import Language.PureScript.AST
import Language.PureScript.Crash
import Language.PureScript.Errors
import Language.PureScript.Names
import Language.PureScript.Types

-- | A list of modules with their transitive dependencies
type ModuleGraph = [(ModuleName, [ModuleName])]

-- | Sort a collection of modules based on module dependencies.
--
-- Reports an error if the module graph contains a cycle.
--
sortModules :: (MonadError MultipleErrors m) => [Module] -> m ([Module], ModuleGraph)
sortModules ms = do
  let verts = map goModule ms
  ms' <- mapM toModule $ stronglyConnComp verts
  let (graph, fromVertex, toVertex) = graphFromEdges verts
      moduleGraph = do (_, mn, _) <- verts
                       let v       = fromMaybe (internalError "sortModules: vertex not found") (toVertex mn)
                           deps    = reachable graph v
                           toKey i = case fromVertex i of (_, key, _) -> key
                       return (mn, filter (/= mn) (map toKey deps))
  return (ms', moduleGraph)
  where
  goModule :: Module -> (Module, ModuleName, [ModuleName])
  goModule m@(Module _ _ _ ds _) =
    let ams = concatMap extractQualAs ds
    in (m, getModuleName m, nub (concatMap (usedModules ams) ds))

  -- Extract module names that have been brought into scope by an `as` import.
  extractQualAs :: Declaration -> [ModuleName]
  extractQualAs (PositionedDeclaration _ _ d) = extractQualAs d
  extractQualAs (ImportDeclaration _ _ (Just am)) = [am]
  extractQualAs _ = []

-- |
-- Calculate a list of used modules based on explicit imports and qualified
-- names. `ams` is a list of `ModuleNames` that refer to names brought into
-- scope by importing with `as` - this ensures that when building the list we
-- don't inadvertantly assume a dependency on an actual module, if there is a
-- module that has the same name as the qualified import.
--
usedModules :: [ModuleName] -> Declaration -> [ModuleName]
usedModules ams d =
  let (f, _, _, _, _) = everythingOnValues (++) forDecls forValues (const []) (const []) (const [])
      (g, _, _, _, _) = accumTypes (everythingOnTypes (++) forTypes)
  in nub (f d ++ g d)
  where

  forDecls :: Declaration -> [ModuleName]
  forDecls (ImportDeclaration mn _ _) =
    -- Regardless of whether an imported module is qualified we still need to
    -- take into account its import to build an accurate list of dependencies.
    [mn]
  forDecls (FixityDeclaration fd)
    | Just mn <- extractQualFixity fd, mn `notElem` ams = [mn]
  forDecls (TypeInstanceDeclaration _ _ (Qualified (Just mn) _) _ _)
    | mn `notElem` ams = [mn]
  forDecls _ = []

  forValues :: Expr -> [ModuleName]
  forValues (Var (Qualified (Just mn) _))
    | mn `notElem` ams = [mn]
  forValues (Constructor (Qualified (Just mn) _))
    | mn `notElem` ams = [mn]
  forValues _ = []

  forTypes :: Type -> [ModuleName]
  forTypes (TypeConstructor (Qualified (Just mn) _))
    | mn `notElem` ams = [mn]
  forTypes _ = []

  extractQualFixity :: Either ValueFixity TypeFixity -> Maybe ModuleName
  extractQualFixity (Left (ValueFixity _ (Qualified mn _) _)) = mn
  extractQualFixity (Right (TypeFixity _ (Qualified mn _) _)) = mn

-- |
-- Convert a strongly connected component of the module graph to a module
--
toModule :: (MonadError MultipleErrors m) => SCC Module -> m Module
toModule (AcyclicSCC m) = return m
toModule (CyclicSCC [m]) = return m
toModule (CyclicSCC ms) = throwError . errorMessage $ CycleInModules (map getModuleName ms)