{-# LANGUAGE TupleSections #-}
module PureScript.Ide where

import           Control.Monad.Except
import           Control.Monad.State.Lazy (StateT (..), get, modify)
import           Control.Monad.Trans.Either
import qualified Data.Map.Lazy            as M
import           Data.Maybe               (mapMaybe, catMaybes)
import           Data.Monoid
import qualified Data.Text  as T
import           PureScript.Ide.Completion
import           PureScript.Ide.Externs
import           PureScript.Ide.Pursuit
import           PureScript.Ide.Error
import           PureScript.Ide.Types
import           PureScript.Ide.SourceFile
import           PureScript.Ide.Reexports
import           System.FilePath
import           System.Directory

type PscIde = StateT PscState IO

getPscIdeState :: PscIde (M.Map ModuleIdent [ExternDecl])
getPscIdeState = pscStateModules <$> get

getAllDecls :: PscIde [ExternDecl]
getAllDecls = concat <$> getPscIdeState

getAllModules :: PscIde [Module]
getAllModules = M.toList <$> getPscIdeState

getAllModulesWithReexports :: PscIde [Module]
getAllModulesWithReexports = do
  mis <- M.keys <$> getPscIdeState
  ms  <- traverse getModuleWithReexports mis
  return (catMaybes ms)

getModule :: ModuleIdent -> PscIde (Maybe Module)
getModule m = do
  modules <- getPscIdeState
  return ((m,) <$> M.lookup m modules)

getModuleWithReexports :: ModuleIdent -> PscIde (Maybe Module)
getModuleWithReexports mi = do
  m <- getModule mi
  db <- getPscIdeState
  case m of
    Just m' -> do
      resolved <- resolveReexports m' db
      return (Just resolved)
    Nothing -> return Nothing
  where
    resolveReexports m db = do
      let replaced = replaceReexports m db
      -- Maybe add logging for statements like these
      -- liftIO $ print (getReexports replaced)
      if null . getReexports $ replaced
        then return replaced
        else resolveReexports replaced db

insertModule :: Module -> PscIde ()
insertModule (name, decls) =
  modify (\x -> x { pscStateModules = M.insert name decls (pscStateModules x)})

findCompletions :: [Filter] -> Matcher -> PscIde Success
findCompletions filters matcher =
  CompletionResult <$> getCompletions filters matcher <$> getAllModulesWithReexports

findType :: DeclIdent -> [Filter] -> PscIde Success
findType search filters =
  CompletionResult <$> getExactMatches search filters <$> getAllModulesWithReexports

findPursuitCompletions :: PursuitQuery -> PscIde Success
findPursuitCompletions (PursuitQuery q) =
  PursuitResult <$> liftIO (searchPursuitForDeclarations q)

findPursuitPackages :: PursuitQuery -> PscIde Success
findPursuitPackages (PursuitQuery q) =
  PursuitResult <$> liftIO (findPackagesForModuleIdent q)

loadExtern :: FilePath -> PscIde (Either Error ())
loadExtern fp = runEitherT $ do
  m <- EitherT . liftIO $ readExternFile fp
  lift (insertModule m)

printModules :: PscIde Success
printModules = do
  modules <- M.keys <$> getPscIdeState
  return (ModuleList modules)

listAvailableModules :: PscIde Success
listAvailableModules = liftIO $ do
  cwd <- getCurrentDirectory
  modules <- getDirectoryContents (cwd </> "output")
  let cleanedModules = filter (`notElem` [".", ".."]) modules
  return (ModuleList (map T.pack cleanedModules))

importsForFile :: FilePath -> PscIde (Either Error Success)
importsForFile fp = do
  imports <- liftIO (getImportsForFile fp)
  return (ImportList <$> imports)

-- | The first argument is a set of modules to load. The second argument
--   denotes modules for which to load dependencies
loadModulesAndDeps :: [ModuleIdent] -> [ModuleIdent] -> PscIde (Either Error Success)
loadModulesAndDeps mods deps = do
  r1 <- mapM loadModule (mods ++ deps)
  r2 <- mapM loadModuleDependencies deps
  return $ do
    moduleResults <- fmap T.concat (sequence r1)
    dependencyResults <- fmap T.concat (sequence r2)
    return (TextResult (moduleResults <> ", " <> dependencyResults))

loadModuleDependencies :: ModuleIdent -> PscIde (Either Error T.Text)
loadModuleDependencies moduleName = do
  m <- getModule moduleName
  case getDependenciesForModule <$> m of
    Just deps -> do
      mapM_ loadModule deps
      -- We need to load the modules, that get reexported from the dependencies
      depModules <- catMaybes <$> mapM getModule deps
      -- What to do with errors here? This basically means a reexported dependency
      -- doesn't exist in the output/ folder
      _ <- traverse loadReexports depModules
      return (Right ("Dependencies for " <> moduleName <> " loaded."))
    Nothing -> return (Left (ModuleNotFound moduleName))

loadReexports :: Module -> PscIde (Either Error [ModuleIdent])
loadReexports m = case getReexports m of
  [] -> return (Right [])
  exportDeps -> runEitherT $ do
    -- I'm fine with this crashing on a failed pattern match.
    -- If this ever fails I'll need to look at GADTs
    let reexports = map (\(Export mn) -> mn) exportDeps
    -- liftIO $ print reexports
    _ <- traverse (EitherT . loadModule) reexports
    exportDepsModules <- lift $ catMaybes <$> traverse getModule reexports
    exportDepDeps <- traverse (EitherT . loadReexports) exportDepsModules
    return $ concat exportDepDeps

getDependenciesForModule :: Module -> [ModuleIdent]
getDependenciesForModule (_, decls) = mapMaybe getDependencyName decls
  where getDependencyName (Dependency dependencyName _) = Just dependencyName
        getDependencyName _ = Nothing

loadModule :: ModuleIdent -> PscIde (Either Error T.Text)
loadModule mn = runEitherT $ do
  path <- EitherT . liftIO $ filePathFromModule "json" mn
  EitherT (loadExtern path)
  return ("Loaded extern file at: " <> T.pack path)

filePathFromModule :: String -> ModuleIdent -> IO (Either Error FilePath)
filePathFromModule extension moduleName = do
  cwd <- getCurrentDirectory
  let path = cwd </> "output" </> T.unpack moduleName </> "externs." ++ extension
  ex <- doesFileExist path
  return $
    if ex
    then Right path
    else Left (ModuleFileNotFound moduleName)

-- | Taken from Data.Either.Utils
maybeToEither :: MonadError e m =>
                 e                      -- ^ (Left e) will be returned if the Maybe value is Nothing
              -> Maybe a                -- ^ (Right a) will be returned if this is (Just a)
              -> m a
maybeToEither errorval Nothing = throwError errorval
maybeToEither _ (Just normalval) = return normalval