{-# LANGUAGE TupleSections #-}

module Language.PureScript.Docs.ParseAndDesugar
  ( parseAndDesugar
  , ParseDesugarError(..)
  ) where

import qualified Data.Map as M
import Control.Arrow (first)
import Control.Monad
import Control.Applicative

import Control.Monad.Trans.Except
import Control.Monad.Error.Class (MonadError(..))
import Control.Monad.IO.Class (MonadIO(..))

import Web.Bower.PackageMeta (PackageName)

import qualified Language.PureScript as P
import qualified Language.PureScript.Constants as C
import Language.PureScript.Docs.Types
import Language.PureScript.Docs.Convert (collectBookmarks)

data ParseDesugarError
  = ParseError P.MultipleErrors
  | SortModulesError P.MultipleErrors
  | DesugarError P.MultipleErrors
  deriving (Show)

-- |
-- Given:
--
--    * A list of local source files
--    * A list of source files from external dependencies, together with their
--      package names
--    * A callback, taking a list of bookmarks, and a list of desugared modules
--
-- This function does the following:
--
--    * Parse all of the input and dependency source files
--    * Partially desugar all of the resulting modules
--    * Collect a list of bookmarks from the whole set of source files
--    * Collect a list of desugared modules from just the input source files (not
--      dependencies)
--    * Call the callback with the bookmarks and desugared module list.
parseAndDesugar ::
  [FilePath]
  -> [(PackageName, FilePath)]
  -> ([Bookmark] -> [P.Module] -> IO a)
  -> IO (Either ParseDesugarError a)
parseAndDesugar inputFiles depsFiles callback = do
  inputFiles' <- mapM (parseAs Local) inputFiles
  depsFiles'  <- mapM (\(pkgName, f) -> parseAs (FromDep pkgName) f) depsFiles

  runExceptT $ do
    ms         <- parseFiles (inputFiles' ++ depsFiles')
    ms'        <- sortModules (map snd ms)
    (bs, ms'') <- desugarWithBookmarks ms ms'
    liftIO $ callback bs ms''

parseFiles ::
  [(FileInfo, FilePath)]
  -> ExceptT ParseDesugarError IO [(FileInfo, P.Module)]
parseFiles =
  throwLeft ParseError . P.parseModulesFromFiles fileInfoToString

sortModules ::
  [P.Module]
  -> ExceptT ParseDesugarError IO [P.Module]
sortModules =
  fmap fst . throwLeft SortModulesError . sortModules' . map importPrim
  where
  sortModules' :: [P.Module] -> Either P.MultipleErrors ([P.Module], P.ModuleGraph)
  sortModules' = P.sortModules

desugarWithBookmarks ::
  [(FileInfo, P.Module)]
  -> [P.Module]
  -> ExceptT ParseDesugarError IO ([Bookmark], [P.Module])
desugarWithBookmarks msInfo msSorted =  do
  msDesugared <- throwLeft DesugarError (desugar msSorted)

  let msDeps = getDepsModuleNames (map (\(fp, m) -> (,m) <$> fp) msInfo)
      msPackages = map (addPackage msDeps) msDesugared
      bookmarks = concatMap collectBookmarks msPackages

  return (bookmarks, takeLocals msPackages)

throwLeft :: (MonadError e m) => (l -> e) -> Either l r -> m r
throwLeft f = either (throwError . f) return

-- | Specifies whether a PureScript source file is considered as:
--
-- 1) with the `Local` constructor, a target source file, i.e., we want to see
--    its modules in the output
-- 2) with the `FromDep` constructor, a dependencies source file, i.e. we do
--    not want its modules in the output; it is there to enable desugaring, and
--    to ensure that links between modules are constructed correctly.
type FileInfo = InPackage FilePath

fileInfoToString :: FileInfo -> FilePath
fileInfoToString (Local fn) = fn
fileInfoToString (FromDep _ fn) = fn

addDefaultImport :: P.ModuleName -> P.Module -> P.Module
addDefaultImport toImport m@(P.Module coms mn decls exps)  =
  if isExistingImport `any` decls || mn == toImport then m
  else P.Module coms mn (P.ImportDeclaration toImport P.Implicit Nothing : decls) exps
  where
  isExistingImport (P.ImportDeclaration mn' _ _) | mn' == toImport = True
  isExistingImport (P.PositionedDeclaration _ _ d) = isExistingImport d
  isExistingImport _ = False

importPrim :: P.Module -> P.Module
importPrim = addDefaultImport (P.ModuleName [P.ProperName C.prim])

desugar :: [P.Module] -> Either P.MultipleErrors [P.Module]
desugar = P.evalSupplyT 0 . desugar'
  where
  desugar' :: [P.Module] -> P.SupplyT (Either P.MultipleErrors) [P.Module]
  desugar' = mapM P.desugarDoModule >=> P.desugarCasesModule >=> P.desugarImports

parseFile :: FilePath -> IO (FilePath, String)
parseFile input' = (,) input' <$> readFile input'

parseAs :: (FilePath -> a) -> FilePath -> IO (a, String)
parseAs g = fmap (first g) . parseFile

getDepsModuleNames :: [InPackage (FilePath, P.Module)] -> M.Map P.ModuleName PackageName
getDepsModuleNames = foldl go M.empty
  where
  go deps p = deps # case p of
    Local _ -> id
    FromDep pkgName (_, m) -> M.insert (P.getModuleName m) pkgName
  (#) = flip ($)

addPackage :: M.Map P.ModuleName PackageName -> P.Module -> InPackage P.Module
addPackage depsModules m =
  case M.lookup (P.getModuleName m) depsModules of
    Just pkgName -> FromDep pkgName m
    Nothing -> Local m