content: module Language.PureScript.Erl.Make where
  
  import Prelude
  
  import Control.Monad ( when )
  import           Control.Monad.Error.Class (MonadError(..))
  import           Control.Monad.Trans.Class (MonadTrans(..))
  import           Control.Monad.Writer.Class (MonadWriter(..))
  import qualified Language.PureScript.CoreFn as CF
  import Language.PureScript.Erl.Make.Monad
      ( Make,
        makeIO,
        getTimestampMaybe,
        readTextFile,
        writeTextFile,
        copyFile )
  import qualified Language.PureScript as P
  import Control.Monad.Supply ( SupplyT )
  import qualified Data.Map as M
  import           Data.List ((\\))
  import qualified Data.List.NonEmpty as NEL
  import           Language.PureScript.Erl.Parser (parseFile)
  import qualified Data.Text as T
  import qualified Data.Text.Encoding as TE
  import           System.FilePath ((</>))
  import           Data.Foldable (for_)
  import           System.Directory (getCurrentDirectory)
  import           Data.Version (showVersion)
  import           Data.Time.Clock (UTCTime)
  
  import qualified Paths_purerl as Paths
  
  import           Language.PureScript.Erl.CodeGen.Common (erlModuleName, erlModuleNameBase, atomModuleName, atom, ModuleType(..), runAtom)
  import           Language.PureScript.Erl.CodeGen (moduleToErl, CodegenEnvironment)
  import           Language.PureScript.Erl.CodeGen.Optimizer (optimize)
  import           Language.PureScript.Erl.Pretty (prettyPrintErl)
  import Language.PureScript.Erl.Errors ( errorMessage )
  import Language.PureScript.Erl.Errors.Types
      ( SimpleErrorMessage(MissingFFIModule, UnnecessaryFFIModule) )
  import Data.Either (fromRight)
  
  data MakeActions m = MakeActions
    { codegen :: CodegenEnvironment -> CF.Module CF.Ann -> SupplyT m ()
    -- ^ Run the code generator for the module and write any required output files.
    , ffiCodegen :: CF.Module CF.Ann -> m ()
    , getOutputTimestamp :: P.ModuleName -> m (Maybe UTCTime)
    -- ^ Get the timestamp for the output files for a module. This should be the
    -- timestamp for the oldest modified file, or 'Nothing' if any of the required
    -- output files are missing.
    }
  
  buildActions :: String  -> M.Map P.ModuleName FilePath -> Bool -> Bool -> MakeActions Make
  buildActions outputDir foreigns usePrefix generateChecked =
    MakeActions codegen ffiCodegen getOutputTimestamp
    where
  
    getOutputTimestamp :: P.ModuleName -> Make (Maybe UTCTime)
    getOutputTimestamp mn = do
      let outputPaths = [ outFile mn ]
      timestamps <- traverse getTimestampMaybe outputPaths
      pure $ fmap minimum . NEL.nonEmpty =<< sequence timestamps
  
    codegen :: CodegenEnvironment -> CF.Module CF.Ann -> SupplyT Make ()
    codegen env m = do
      let mn = CF.moduleName m
      foreignExports <- lift $ case mn `M.lookup` foreigns of
        Just path
          | not $ requiresForeign m ->
              return []
          | otherwise ->
              getForeigns path
        Nothing ->
          return []
  
      (exports, typeDecls, foreignSpecs, rawErl, checkedExports, checkedRawErl, memoizable) <- moduleToErl env m foreignExports
  
      optimized <- optimize exports memoizable rawErl
      checked <- optimize checkedExports memoizable checkedRawErl
  
      dir <- lift $ makeIO "get file info: ." getCurrentDirectory
      let makeAbsFile file = dir </> file
      let pretty = prettyPrintErl makeAbsFile optimized
          prettyChecked = prettyPrintErl makeAbsFile checked
          prettySpecs = prettyPrintErl makeAbsFile foreignSpecs
          prettyDecls = prettyPrintErl makeAbsFile typeDecls
          
      let 
          prefix :: [T.Text]
          prefix = ["Generated by purerl version " <> T.pack (showVersion Paths.version) | usePrefix]
          -- directives :: [(T.Text, Int)] -> ModuleType -> [T.Text]
          directives exports' moduleType = [
            "-module(" <> atom (atomModuleName mn moduleType) <> ").",
            "-export([" <> T.intercalate ", " (map (\(f, a) -> runAtom f <> "/" <> T.pack (show a)) exports') <> "]).",
            "-compile(nowarn_shadow_vars).",
            "-compile(nowarn_unused_vars).",
            "-compile(nowarn_unused_function).",
            "-compile(no_auto_import).",
            includeHrl,
            "-ifndef(PURERL_MEMOIZE).",
            "-define(MEMOIZE(X), X).",
            "-else.",
            "-define(MEMOIZE, memoize).",
            "memoize(X) -> X.",
            "-endif."
            ]
          includeHrl :: T.Text
          includeHrl = "-include(\"./" <> erlModuleNameBase mn <> ".hrl\").\n"
      let erl :: T.Text = T.unlines $ map ("% " <>) prefix ++ directives exports PureScriptModule ++  [ pretty ]
      lift $ writeTextFile (outFile mn) $ TE.encodeUtf8 erl
  
      when generateChecked $ do
        let erlchecked :: T.Text = T.unlines $ map ("% " <>) prefix ++ directives checkedExports PureScriptCheckedModule ++  [ prettyChecked ]
        lift $ writeTextFile (outFileChecked mn) $ TE.encodeUtf8 erlchecked
  
      let hrl :: T.Text = T.unlines $ map ("% " <>) prefix ++ [ prettyDecls ]
      lift $ writeTextFile (hrlFile mn) $ TE.encodeUtf8 hrl
      
      let foreignHrl :: T.Text = T.unlines $ map ("% " <>) prefix ++ [ includeHrl, prettySpecs ]
      lift $ writeTextFile (foreignHrlFile mn) $ TE.encodeUtf8 foreignHrl
  
    ffiCodegen :: CF.Module CF.Ann -> Make ()
    ffiCodegen m = do
      let mn = CF.moduleName m
          foreignFile = moduleDir mn </> T.unpack (erlModuleName mn ForeignModule) ++ ".erl"
      case mn `M.lookup` foreigns of
        Just path
          | not $ requiresForeign m ->
              tell $ errorMessage $ UnnecessaryFFIModule mn path
          | otherwise -> pure ()
        Nothing ->
          when (requiresForeign m) $ throwError . errorMessage $ MissingFFIModule mn
      for_ (mn `M.lookup` foreigns) $ \path ->
        copyFile path foreignFile
  
    outFile mn = moduleDir mn </> T.unpack (erlModuleName mn PureScriptModule) ++ ".erl"
    outFileChecked mn = moduleDir mn </> T.unpack (erlModuleName mn PureScriptCheckedModule) ++ ".erl"
    hrlFile mn = moduleDir mn </> T.unpack (erlModuleNameBase mn) ++ ".hrl"
    foreignHrlFile mn = moduleDir mn </> T.unpack (erlModuleName mn ForeignModule) ++ ".hrl"
    moduleDir mn = outputDir </> T.unpack (P.runModuleName mn)
        
  
    requiresForeign :: CF.Module a -> Bool
    requiresForeign = not . null . CF.moduleForeign
  
    getForeigns :: String -> Make [(T.Text, Int)]
    getForeigns path = do
      text <- readTextFile path
      let (exports, ignoreExports) = fromRight ([],[]) $ parseFile path text
      pure $ exports \\ ignoreExports