/-
Copyright (c) 2019 Microsoft Corporation. All rights reserved.
Released under Apache 2.0 license as described in the file LICENSE.
Authors: Leonardo de Moura
-/
module

prelude
public import Std.Data.HashMap.Basic
public import Lean.Data.PersistentHashMap
public import Std.Data.HashMap.Iterator
public import Lean.Data.Iterators.Producers.PersistentHashMap
public import Init.Data.Iterators.Combinators.Append

public section
universe u v w w'

namespace Lean

/-- Staged map for implementing the Environment. The idea is to store
   imported entries into a hashtable and local entries into a persistent hashtable.

   Hypotheses:
   - The number of entries (i.e., declarations) coming from imported files is much bigger than
     the number of entries in the current file.
   - HashMap is faster than PersistentHashMap.
   - When we are reading imported files, we have exclusive access to the map, and efficient
     destructive updates are performed.

   Remarks:
   - We never remove declarations from the Environment. In principle, we could support
     deletion by using `(PHashMap α (Option β))` where the value `none` would indicate
     that an entry was "removed" from the hashtable.
   - We do not need additional bookkeeping for extracting the local entries.
-/
structure SMap (α : Type u) (β : Type v) [BEq α] [Hashable α] where
  stage₁ : Bool         := true
  /-- Imported constants. -/
  map₁   : Std.HashMap α β  := {}
  /-- Local constants defined in the current module. -/
  map₂   : PHashMap α β := {}

namespace SMap
variable {α : Type u} {β : Type v} [BEq α] [Hashable α]

instance : Inhabited (SMap α β) := ⟨{}⟩
def empty : SMap α β := {}

@[inline] def fromHashMap (m : Std.HashMap α β) (stage₁ := true) : SMap α β :=
  { map₁ := m, stage₁ := stage₁ }

@[specialize] def insert : SMap α β → α → β → SMap α β
  | ⟨true, m₁, m₂⟩, k, v  => ⟨true, m₁.insert k v, m₂⟩
  | ⟨false, m₁, m₂⟩, k, v => ⟨false, m₁, m₂.insert k v⟩

@[specialize] def insert' : SMap α β → α → β → SMap α β
  | ⟨true, m₁, m₂⟩, k, v  => ⟨true, m₁.insert k v, m₂⟩
  | ⟨false, m₁, m₂⟩, k, v => ⟨false, m₁, m₂.insert k v⟩

@[specialize] def find? : SMap α β → α → Option β
  | ⟨true, m₁, _⟩, k   => m₁[k]?
  | ⟨false, m₁, m₂⟩, k => (m₂.find? k).orElse fun _ => m₁[k]?

@[inline] def findD (m : SMap α β) (a : α) (b₀ : β) : β :=
  (m.find? a).getD b₀

@[inline] def find! [Inhabited β] (m : SMap α β) (a : α) : β :=
  match m.find? a with
  | some b => b
  | none   => panic! "key is not in the map"

@[specialize] def contains : SMap α β → α → Bool
  | ⟨true, m₁, _⟩, k   => m₁.contains k
  | ⟨false, m₁, m₂⟩, k => m₁.contains k || m₂.contains k

/-- Similar to `find?`, but searches for result in the hashmap first.
   So, the result is correct only if we never "overwrite" `map₁` entries using `map₂`. -/
@[specialize] def find?' : SMap α β → α → Option β
  | ⟨true, m₁, _⟩, k   => m₁[k]?
  | ⟨false, m₁, m₂⟩, k => m₁[k]?.orElse fun _ => m₂.find? k

def forM [Monad m] (s : SMap α β) (f : α → β → m PUnit) : m PUnit := do
  s.map₁.forM f
  s.map₂.forM f

instance [Monad m] : ForM m (SMap α β) (α × β) where
  forM s f := forM s fun x y => f (x, y)

instance [Monad m] : ForIn m (SMap α β) (α × β) where
  forIn := ForM.forIn

def iter (s : SMap α β) :=
  s.map₁.iter.append s.map₂.iter

/-- Move from stage 1 into stage 2. -/
def switch (m : SMap α β) : SMap α β :=
  if m.stage₁ then { m with stage₁ := false } else m

@[inline] def foldStage2 {σ : Type w} (f : σ → α → β → σ) (s : σ) (m : SMap α β) : σ :=
  m.map₂.foldl f s

/-- Monadic fold over a staged map. -/
def foldM {m : Type w → Type w'} [Monad m]
    (f : σ → α → β → m σ) (init : σ) (map : SMap α β) : m σ := do
  map.map₂.foldlM f (← map.map₁.foldM f init)

def fold {σ : Type w} (f : σ → α → β → σ) (init : σ) (m : SMap α β) : σ :=
  m.map₂.foldl f $ m.map₁.fold f init

def numBuckets (m : SMap α β) : Nat :=
  Std.HashMap.Internal.numBuckets m.map₁

def toList (m : SMap α β) : List (α × β) :=
  m.fold (init := []) fun es a b => (a, b)::es

end SMap

def _root_.List.toSMap [BEq α] [Hashable α] (es : List (α × β)) : SMap α β :=
  es.foldl (init := {}) fun s (a, b) => s.insert a b

instance {_ : BEq α} {_ : Hashable α} [Repr α] [Repr β] : Repr (SMap α β) where
  reprPrec v prec := Repr.addAppParen (reprArg v.toList ++ ".toSMap") prec

end Lean
