/-
This is from Markus's blog post on monadic reasoning:
https://markushimmel.de/blog/my-first-verified-imperative-program/
-/

import Std.Data.HashSet.Lemmas
import Std.Tactic.Do
set_option backward.do.legacy false

open Std Do

/-!
# Missing standard library material that will be part of a future Lean release
-/

namespace List

/--
`l.ExistsPair P` asserts that there are indices `i` and `j` such that `i < j` and `P l[i] l[j]` is true.
-/
structure ExistsPair (P : α → α → Prop) (l : List α) where
  not_pairwise : ¬l.Pairwise (¬P · ·)

theorem existsPair_iff_not_pairwise {P : α → α → Prop} {l : List α} :
    l.ExistsPair P ↔ ¬l.Pairwise (fun x y => ¬P x y) := by
  grind [ExistsPair]

@[simp, grind]
theorem not_existsPair_nil {P : α → α → Prop} : ¬[].ExistsPair P := by
  simp [existsPair_iff_not_pairwise]

@[simp, grind]
theorem existsPair_cons {P : α → α → Prop} {x : α} {xs : List α} :
    (x::xs).ExistsPair P  ↔ (∃ y ∈ xs, P x y) ∨ xs.ExistsPair P := by
  grind [List.existsPair_iff_not_pairwise, pairwise_iff_forall_sublist]

@[simp, grind]
theorem existsPair_append {P : α → α → Prop} {xs ys : List α} :
    (xs ++ ys).ExistsPair P ↔ xs.ExistsPair P ∨ ys.ExistsPair P ∨ (∃ x ∈ xs, ∃ y ∈ ys, P x y) := by
  grind [List.existsPair_iff_not_pairwise]

end List

-- Tell Lean that it doesn't need to warn us that `mvcgen` is still a pre-release.
set_option mvcgen.warning false

/-!
# Imperative implementation
-/

def pairsSumToZero (l : List Int) : Bool := Id.run do
  let mut seen : HashSet Int := ∅

  for x in l do
    if -x ∈ seen then
      return true
    seen := seen.insert x

  return false

/-!
# Verification of imperative implementation
-/

theorem pairsSumToZero_correct (l : List Int) : pairsSumToZero l ↔ l.ExistsPair (fun a b => a + b = 0) := by
  generalize h : pairsSumToZero l = r
  apply Id.of_wp_run_eq h

  mvcgen

  case inv1 =>
    exact Invariant.withEarlyReturnNewDo
      (onReturn := fun r b => ⌜r = true ∧ l.ExistsPair (fun a b => a + b = 0)⌝)
      (onContinue := fun traversalState seen =>
        ⌜(∀ x, x ∈ seen ↔ x ∈ traversalState.prefix) ∧ ¬traversalState.prefix.ExistsPair (fun a b => a + b = 0)⌝)

  all_goals simp_all <;> grind

/--
trace: l : List Int
⊢ (match
          (forIn l (none, ∅) fun x __s =>
                if -x ∈ __s.snd then pure (ForInStep.done (some true, __s.snd))
                else pure (ForInStep.yield (none, __s.snd.insert x))).run.fst with
        | some r => pure r
        | none => pure false).run =
      true ↔
    List.ExistsPair (fun a b => a + b = 0) l
---
warning: declaration uses `sorry`
-/
#guard_msgs in
theorem pairsSumToZero_correct_directly (l : List Int) : pairsSumToZero l ↔ l.ExistsPair (fun a b => a + b = 0) := by
  simp [pairsSumToZero]
  trace_state
  -- give up; need lemma about `forIn`
  admit

namespace Functional

/-!
# Bonus: functional implementation
-/

def pairsSumToZero (l : List Int) : Bool :=
  go l ∅
where
  go (m : List Int) (seen : HashSet Int) : Bool :=
    match m with
    | [] => false
    | x::xs => if -x ∈ seen then true else go xs (seen.insert x)

/-!
# Bonus: verification of functional implementation
-/

theorem pairsSumToZero_go_iff (l : List Int) (seen : HashSet Int) :
    pairsSumToZero.go l seen = true ↔
      l.ExistsPair (fun a b => a + b = 0) ∨ ∃ a ∈ seen, ∃ b ∈ l, a + b = 0 := by
  fun_induction pairsSumToZero.go <;> simp_all <;> grind

theorem pairsSumToZero_iff (l : List Int) :
    pairsSumToZero l = true ↔ l.ExistsPair (fun a b => a + b = 0) := by
  simp [pairsSumToZero, pairsSumToZero_go_iff]
