import Pantograph.Goal
import Pantograph.Environment

import Lean.Environment
import Lean.Replay
import Lean.Util.ShareCommon
import Std.Data.HashMap

open Lean

/-!
Input/Output functions borrowed from REPL

# Pickling and unpickling objects

By abusing `saveModuleData` and `readModuleData` we can pickle and unpickle objects to disk.
-/

namespace Pantograph

/--
Save an object to disk.
If you need to write multiple objects from within a single declaration,
you will need to provide a unique `key` for each.
-/
def pickle {α : Type} (path : System.FilePath) (x : α) (key : Name := by exact decl_name%) : IO Unit :=
  saveModuleData path key (unsafe unsafeCast x)

/--
Load an object from disk.
Note: The returned `CompactedRegion` can be used to free the memory behind the value
of type `α`, using `CompactedRegion.free` (which is only safe once all references to the `α` are
released). Ignoring the `CompactedRegion` results in the data being leaked.
Use `withUnpickle` to call `CompactedRegion.free` automatically.

This function is unsafe because the data being loaded may not actually have type `α`, and this
may cause crashes or other bad behavior.
-/
unsafe def unpickle (α : Type) (path : System.FilePath) : IO (α × CompactedRegion) := do
  let (x, region) ← readModuleData path
  pure (unsafeCast x, region)

/-- Load an object from disk and run some continuation on it, freeing memory afterwards. -/
unsafe def withUnpickle [Monad m] [MonadLiftT IO m] {α β : Type}
    (path : System.FilePath) (f : α → m β) : m β := do
  let (x, region) ← unpickle α path
  let r ← f x
  region.free
  pure r

/--
Pickle an `Environment` to disk relative to a background.

We only store:
* the list of imports
* the new constants from `Environment.constants`
and when unpickling, we build a fresh `Environment` from the imports,
and then add the new constants.
-/
@[export pantograph_env_pickle_m]
def environmentPickle (env : Environment) (path : System.FilePath) (background? : Option Environment := .none)
  : IO Unit :=
  let distilled := distilEnvironment env background?
  pickle path (ShareCommon.shareCommon distilled)

def resurrectEnvironment
  (distilled : DistilledEnvironment)
  (background? : Option Environment := .none)
  : IO Environment := do
  let (imports, constArray) := distilled
  let env ← match background? with
    | .none => importModules imports {} 0 (loadExts := true)
    | .some env =>
      assert! env.imports == imports
      pure env
  env.replay (Std.HashMap.ofList constArray.toList)

/--
Unpickle an `Environment` from disk.

We construct a fresh `Environment` with the relevant imports,
and then replace the new constants.

The `CompactedRegion` must be free'd after using the environment. Otherwise
there could be memory leaks.
-/
@[export pantograph_env_unpickle_m]
def environmentUnpickle (path : System.FilePath) (background? : Option Environment := .none)
  : IO (Environment × CompactedRegion) := unsafe do
  let (distilled, region) ← unpickle (Array Import × ConstArray) path
  return (← resurrectEnvironment distilled background?, region)

/-- `CoreM`'s state, with information irrelevant to pickling masked out -/
structure CompactCoreState where
  -- env             : Environment
  nextMacroScope  : MacroScope     := firstFrontendMacroScope + 1
  ngen            : NameGenerator  := {}
  auxDeclNGen     : DeclNameGenerator := {}
  -- traceState      : TraceState     := {}
  -- cache           : Cache     := {}
  -- messages        : MessageLog     := {}
  -- infoState       : Elab.InfoState := {}

structure CompactGoalState where
  env : DistilledEnvironment

  core : CompactCoreState
  «meta» : Meta.State
  «elab» : Elab.Term.State
  tactic : Elab.Tactic.State
  root : MVarId
  parentMVars : List MVarId
  fragments : FragmentMap

/-- Pickles a goal state by taking its diff relative to a background
environment. This function eliminates all `MessageData` from synthetic
metavariables, because these `MessageData` objects frequently carry closures,
which cannot be pickled. If there is no synthetic metavariable, this would not
cause a difference. -/
@[export pantograph_goal_state_pickle_m]
def goalStatePickle (goalState : GoalState) (path : System.FilePath) (background? : Option Environment := .none)
  : IO Unit :=
  let {
    savedState := {
      term := {
        «meta» := {
          core := {
            env, nextMacroScope, ngen, auxDeclNGen, ..
          },
          «meta»,
        }
        «elab» := «elab»@{
          syntheticMVars, ..
        },
      },
      tactic
    }
    root,
    parentMVars,
    fragments,
  } := goalState
  -- Delete `MessageData`s
  let syntheticMVars : MVarIdMap _ := syntheticMVars.foldl (init := .empty) λ acc key val =>
    let kind := match val.kind with
      | .typeClass _ => .typeClass .none
      | .coe header? expectedType e f? _ => .coe header? expectedType e f? .none
      | k => k
    acc.insert key { val with kind }
  let compacted : CompactGoalState := {
    env := distilEnvironment env background?,

    core := { nextMacroScope, ngen, auxDeclNGen },
    «meta»,
    «elab» := { «elab» with syntheticMVars },
    tactic,

    root,
    parentMVars,
    fragments,
  }
  pickle path (ShareCommon.shareCommon compacted)

/--
Loads a goal state  from disk. The background is the environment which the goal
state was cached relative to.

The `CompactedRegion` must be free'd after using the goal state. Otherwise there
will be memory leaks.
-/
@[export pantograph_goal_state_unpickle_m]
def goalStateUnpickle (path : System.FilePath) (background? : Option Environment := .none)
    : IO (GoalState × CompactedRegion) := unsafe do
  let ({
    env,

    core,
    «meta»,
    «elab»,
    tactic,

    root,
    parentMVars,
    fragments,
  }, region) ← Pantograph.unpickle CompactGoalState path
  let env ← resurrectEnvironment env background?
  let goalState := {
    savedState := {
      term := {
        «meta» := {
          core := {
            core with
            passedHeartbeats := 0,
            env,
          },
          «meta»,
        },
        «elab»,
      },
      tactic,
    },
    root,
    parentMVars,
    fragments,
  }
  return (goalState, region)

end Pantograph
