module Build.Dependencies (getSortedDependencies) where

import Data.Data
import Control.Applicative
import Control.Monad.Error
import qualified Control.Monad.State as State
import qualified Data.Aeson as Json
import qualified Data.ByteString.Lazy.Char8 as BSC
import qualified Data.Char as Char
import qualified Data.Graph as Graph
import qualified Data.List as List
import qualified Data.Map as Map
import qualified Data.Maybe as Maybe
import qualified Data.Set as Set
import System.Directory
import System.Exit
import System.FilePath as FP
import System.IO
import Text.PrettyPrint (Doc)

import qualified SourceSyntax.Module as Module
import qualified SourceSyntax.Type as Type
import qualified Parse.Parse as Parse
import qualified Metadata.Prelude as Prelude
import qualified Transform.Check as Check
import qualified Transform.SortDefinitions as SD
import qualified Type.Inference as TI
import qualified Type.Constrain.Declaration as TcDecl
import qualified Transform.Canonicalize as Canonical
import qualified Elm.Internal.Paths as Path
import qualified Elm.Internal.Name as N
import qualified Elm.Internal.Version as V
import qualified Elm.Internal.Dependencies as Deps

getSortedDependencies :: [FilePath] -> Module.Interfaces -> FilePath -> IO [String]
getSortedDependencies srcDirs builtIns root =
    do extras <- extraDependencies
       let allSrcDirs = srcDirs ++ Maybe.fromMaybe [] extras
       result <- runErrorT $ readDeps allSrcDirs builtIns root
       case result of
         Right deps -> sortDeps deps
         Left err -> failure $ err ++ if Maybe.isJust extras then "" else msg
             where msg = "\nYou may need to create a " ++
                         Path.dependencyFile ++
                         " file if you\nare trying to use a 3rd party library."

failure msg = hPutStrLn stderr msg >> exitFailure

extraDependencies :: IO (Maybe [FilePath])
extraDependencies =
    do exists <- doesFileExist Path.dependencyFile
       if not exists then return Nothing else Just <$> getPaths
    where
      getPaths = do
        raw <- BSC.readFile Path.dependencyFile
        case Json.eitherDecode raw of
          Right (Deps.Mini deps) -> mapM validate deps
          Left err ->
              failure $ "Error reading the " ++ Path.dependencyFile ++ " file:\n" ++ err

      validate (name,version) = do
        let path = Path.dependencyDirectory </> toPath name version
        exists <- doesDirectoryExist path
        if exists then return path else failure (notFound name version)

      toPath name version = N.toFilePath name </> show version

      notFound name version =
          unlines
          [ "Your " ++ Path.dependencyFile ++ " file says you depend on library"
          , show name ++ " " ++ show version ++ " but it was not found."
          , "You may need to install it with:"
          , ""
          , "    elm-get install " ++ show name ++ " " ++ show version ]

type Deps = (FilePath, String, [String])

sortDeps :: [Deps] -> IO [String]
sortDeps depends =
    if null mistakes
      then return (concat sccs)
      else failure $ msg ++ unlines (map show mistakes)
  where
    sccs = map Graph.flattenSCC $ Graph.stronglyConnComp depends

    mistakes = filter (\scc -> length scc > 1) sccs
    msg = "A cyclical module dependency or was detected in:\n"

readDeps :: [FilePath] -> Module.Interfaces -> FilePath -> ErrorT String IO [Deps]
readDeps srcDirs builtIns root = do
  let ifaces = (Set.fromList . Map.keys) builtIns
  State.evalStateT (go ifaces root) Set.empty
  where
    go :: Set.Set String -> FilePath -> State.StateT (Set.Set String) (ErrorT String IO) [Deps]
    go builtIns root = do
      (root', txt) <- lift $ getFile srcDirs root
      case Parse.dependencies txt of
        Left err -> throwError $ msg ++ show err
            where msg = "Error resolving dependencies in " ++ root' ++ ":\n"

        Right (name,deps) ->
            do seen <- State.get
               let realDeps = Set.difference (Set.fromList deps) builtIns
                   newDeps = Set.difference (Set.filter (not . isNative) realDeps) seen
               State.put (Set.insert name (Set.union newDeps seen))
               rest <- mapM (go builtIns . toFilePath) (Set.toList newDeps)
               return ((makeRelative "." root', name, Set.toList realDeps) : concat rest)

getFile :: [FilePath] -> FilePath -> ErrorT String IO (FilePath,String)
getFile [] path =
    throwError $ unlines
    [ "Could not find file: " ++ path
    , "    If it is not in the root directory of your project, use"
    , "    --src-dir to declare additional locations for source files."
    , "    If it is part of a 3rd party library, it needs to be declared"
    , "    as a dependency in the " ++ Path.dependencyFile ++ " file." ]

getFile (dir:dirs) path = do
  let path' = dir </> path
  exists <- liftIO $ doesFileExist path'
  case exists of
    True  -> (,) path' `fmap` liftIO (readFile path')
    False -> getFile dirs path

isNative name = List.isPrefixOf "Native." name

toFilePath :: String -> FilePath
toFilePath name = map swapDots name ++ ext
    where swapDots '.' = '/'
          swapDots  c  =  c
          ext = if isNative name then ".js" else ".elm"