module Main

import Prelude
import Data.List1
import Data.String
import Data.IORef
import Data.SortedMap
import Lib.Common
import Lib.Compile
import Lib.Parser
import Lib.Elab
import Lib.Util
import Lib.Parser.Impl
import Lib.Prettier
import Lib.ProcessDecl
import Lib.Token
import Lib.Tokenizer
import Lib.TopContext
import Lib.Types
import Lib.Syntax
import Node
import Serialize

-- For editors, dump some information about the context (fc, name, type)
jsonTopContext : M Json
jsonTopContext = do
  top <- getTop
  let defs = join $ map (\mod => listValues mod.modDefs) $ listValues top.modules
  pure $ JsonObj (("context", JsonArray (map jsonDef $ defs)) :: Nil)
  where
    jsonDef : TopEntry -> Json
    -- There is no FC here...
    jsonDef (MkEntry fc (QN ns name) type def _) = JsonObj
      (  ("fc", toJson fc)
      :: ("name", toJson name)
      :: ("type", toJson (render 80 $ pprint Nil type) )
      :: Nil)

dumpContext : TopContext -> M Unit
dumpContext top = do
    putStrLn "Context:"
    go $ listValues top.defs
    putStrLn "---"
  where
    go : List TopEntry -> M Unit
    go Nil = pure MkUnit
    go (x :: xs) = putStrLn "    \{show x}" >> go xs

writeSource : String -> M Unit
writeSource fn = do
  docs <- compile
  let src = unlines $
        ( "\"use strict\";"
        :: "const bouncer = (f,ini) => { let obj = ini; while (obj.tag !== 'return') obj = f(obj); return obj.h0 };"
        :: Nil)
        ++ map (render 90 ∘ noAlt) docs
  (Right _) <- liftIO {M} $ writeFile fn src
    | Left err => exitFailure (show err)
  -- (Right _) <- chmodRaw fn 493 | Left err => exitFailure (show err)
  pure MkUnit


parseDecls : String -> Operators -> TokenList -> SnocList Decl -> M (List Decl × Operators)
parseDecls fn ops Nil acc = pure (acc <>> Nil, ops)
parseDecls fn ops toks@(first :: _) acc =
  case partialParse fn (sameLevel parseDecl) ops toks of
    Left (err, toks) => do
      putStrLn $ showError "" err
      addError err
      parseDecls fn ops (recover toks) acc
    Right (decl,ops,toks) => parseDecls fn ops toks (acc :< decl)
  where
    recover : TokenList -> TokenList
    recover Nil = Nil
    -- skip to top token, but make sure there is progress
    recover (tok :: toks) = if tok.bounds.startCol == 0 && tok.bounds /= first.bounds
      then (tok :: toks)
      else recover toks

moduleHash : String → List (List String) → M String
moduleHash src imports = do
  srcHash <- liftIO $ checksum src
  top <- getTop
  let mods = mapMaybe (\x => lookupMap' x top.modules) imports
  let modHashes = map (\x => x.csum) mods
  liftIO $ checksum $ fastConcat $ srcHash :: modHashes

importHints : List TopEntry → M Unit
importHints Nil = pure MkUnit
importHints (entry :: entries) = do
  when (elem Hint entry.eflags) $ \ _ => addHint entry.name
  importHints entries

-- New style loader, one def at a time
processModule : FC -> String -> List String -> QName -> M String
processModule importFC base stk qn@(QN ns nm) = do
  top <- getTop
  -- TODO make top.loaded a List QName
  let modns = (snoc ns nm)
  let name = joinBy "." modns
  let (Nothing) = lookupMap modns top.modules | _ => pure ""
  -- dummy entry for processing
  modifyTop [modules := updateMap modns (emptyModCtx "") top.modules]
  let fn = (joinBy "/" (base :: ns)) ++ "/" ++ nm ++ ".newt"
  (Right src) <- liftIO {M} $ readFile fn
    | Left err => exitFailure "ERROR at \{show importFC}: error reading \{fn}: \{show err}"
  let (Right toks) = tokenise fn src
    | Left err => exitFailure (showError src err)

  let (Right ((nameFC, modName), ops, toks)) = partialParse fn parseModHeader top.ops toks
    | Left (err, toks) => exitFailure (showError src err)

  log 1 $ \ _ => "scan imports for module \{modName}"
  let ns = split modName "."
  let (path, modName') = unsnoc $ split1 modName "."
  -- let bparts = split base "/"
  let (True) = qn == QN path modName'
    | _ => exitFailure "ERROR at \{show nameFC}: module name \{show modName} doesn't match file name \{show fn}"

  let (Right (imports, ops, toks)) = partialParse fn parseImports ops toks
    | Left (err, toks) => exitFailure (showError src err)

  imported <- for imports $ \case
    MkImport fc name' => do
      let (a,b) = unsnoc $ split1 name' "."
      let qname = QN a b
      -- we could use `fc` if it had a filename in it
      when (elem name' stk) $ \ _ => error emptyFC "import loop \{show name} -> \{show name'}"

      processModule fc base (name :: stk) qname
      pure $ split name' "."
  let imported = snoc imported primNS
  srcSum <- liftIO $ checksum src
  csum <- moduleHash srcSum imported

  putStrLn "module \{modName}"
  top <- getTop
  -- TODO we need a flag on this so `make newt3.js` properly tests self-compile
  (Nothing) <- loadModule qn csum
    | Just mod => do
      let modules = updateMap modns mod top.modules

      -- FIXME - we don't want stray operators in a module.
      -- inject module ops into top
      let ops = foldMap const top.ops $ toList mod.ctxOps
      modifyTop [modules := modules; ops := ops ]
      pure src  -- why am I returning this?

  log 1 $ \ _ => "MODNS " ++ show modns
  top <- getTop
  (decls, ops) <- parseDecls fn top.ops toks Lin

  top <- getTop
  let freshMC = MC emptyMap Nil 0 CheckAll
  -- set imported, mod, freshMC, ops  before processing
  modifyTop (\ top => MkTop top.modules imported emptyMap modns emptyMap freshMC top.verbose top.errors ops)
  for imported $ \ ns => do
    let (Just mod) = lookupMap' ns top.modules | _ => error emptyFC "namespace \{show ns} missing"
    importHints (listValues mod.modDefs)

  log 1 $ \ _ => "process Decls"
  traverse (tryProcessDecl src ns) (collectDecl decls)

  -- update modules with result, leave the rest of context in case this is top file
  top <- getTop

  let mod = MkModCtx csum top.defs top.metaCtx top.ops
  errors <- liftIO {M} $ readIORef top.errors
  if stk /= Nil && length' errors == 0
    then dumpModule qn src mod
    else pure MkUnit

  let modules = updateMap modns mod top.modules
  modifyTop [modules := modules]

  (Nil) <- liftIO {M} $ readIORef top.errors
    | errors => do
        -- we're now showing errors when they occur, so they're next to debug messages
        -- traverse (putStrLn ∘ showError src) errors
        exitFailure "Compile failed"
  logMetas $ reverse $ listValues top.metaCtx.metas
  pure src
  where
    tryProcessDecl : String -> List String → Decl -> M Unit
    tryProcessDecl src ns decl = do
      (Left err) <- tryError $ processDecl ns decl | _ => pure MkUnit
      putStrLn $ showError src err
      addError err

-- unwind the module part of the path name
baseDir : SnocList String -> SnocList String -> Either String String
baseDir dirs Lin = Right $ joinBy "/" (dirs <>> Nil)
baseDir (dirs :< d) (ns :< n) = if d == n
  then baseDir dirs ns
  else Left "module path doesn't match directory"
baseDir Lin _ = Left "module path doesn't match directory"

showErrors : String -> String -> M Unit
showErrors fn src = do
  top <- getTop
  -- TODO {M} needed to sort out scrutinee
  (Nil) <- liftIO {M} $ readIORef top.errors
    | errors => do
        traverse (putStrLn ∘ showError src) errors
        exitFailure "Compile failed"
  pure MkUnit

-- processFile called on the top level file
-- it sets up everything and then recurses into processModule
processFile : String -> M Unit
processFile fn = do
  putStrLn "*** Process \{fn}"
  let parts = split1 fn "/"
  let (dirs,file) = unsnoc parts
  let dir = if dirs == Nil then "." else joinBy "/" dirs
  let (name, ext) = splitFileName file
  log 1 $ \ _ => "\{show dir} \{show name} \{show ext}"

  (Right src) <- liftIO {M} $ readFile fn
    | Left err => error (MkFC fn (0,0)) "error reading \{fn}: \{show err}"
  let (Right toks) = tokenise fn src
    | Left err => throwError err
  let (Right ((nameFC, modName), _, _)) = partialParse fn parseModHeader emptyMap toks
    | Left (err,toks) => throwError err

  (base,qn) <- getBaseDir fn modName

  -- declare internal primitives

  processDecl primNS (PType emptyFC "Int" Nothing)
  processDecl primNS (PType emptyFC "String" Nothing)
  processDecl primNS (PType emptyFC "Char" Nothing)
  setDef (QN primNS "PiType") emptyFC (Erased emptyFC) (PrimFn "(h0, h1) => ({ tag: \"PiType\", h0, h1 });" (S (S Z)) Nil) Nil

  top <- getTop
  let modules = updateMap primNS (MkModCtx "" top.defs (MC emptyMap Nil 0 CheckAll) top.ops) top.modules
  modifyTop (\ top => MkTop modules (primNS :: Nil) emptyMap Nil emptyMap top.metaCtx top.verbose top.errors top.ops)

  src <- processModule emptyFC base Nil qn
  top <- getTop

  showErrors fn src
  pure MkUnit

cmdLine : List String -> M (Maybe String × List String)
cmdLine Nil = pure (Nothing, Nil)
cmdLine ("--top" :: args) = cmdLine args -- handled later
cmdLine ("-v" :: args) = do
  modifyTop [ verbose $= _+_ 1 ]
  cmdLine args
cmdLine ("-o" :: fn :: args) = do
  (out, files) <- cmdLine args
  pure ((out <|> Just fn), files)

cmdLine (fn :: args) = do
  let (True) = isSuffixOf ".newt" fn
    | _ => error emptyFC "Bad argument \{show fn}"
  (out, files) <- cmdLine args
  pure $ (out, fn :: files)

main' : M Unit
main' = do
  (arg0 :: args) <- liftIO {M} $ getArgs
    | _ => error emptyFC "error reading args"
  (out, files) <- cmdLine args
  traverse processFile files

  when (elem "--top" args) $ \ _ => do
    json <- jsonTopContext
    putStrLn "TOP:\{renderJson json}"

  case out of
    Nothing => pure MkUnit
    Just name => writeSource name


main : IO Unit
main = do
  -- we'll need to reset for each file, etc.
  ctx <- emptyTop
  (Right _)  <- .runM main' ctx
    | Left err => exitFailure "ERROR at \{show $ getFC err}: \{errorMsg err}"
  putStrLn "done"
