-- Frap/Propositional.lean

import Mathlib.Computability.NFA
import Mathlib.Data.FinEnum
import Mathlib.Data.Rel
import Mathlib.Data.Vector.Basic
import Blase.AutoStructs.ForLean

open Set
open Mathlib
open SetRel

-- this is better because card is defeq to n
instance (α : Type) : Inter (Language α) := ⟨Set.inter⟩
instance (α : Type) : Union (Language α) := ⟨Set.union⟩

/--
The set of `n`-tuples of bit vectors of an arbitrary width.
-/
structure BitVecs (n : Nat) where
  w : Nat
  bvs : List.Vector (BitVec w) n

def BitVecs.cast (bvs : BitVecs n) (h : n = n') : BitVecs n' :=
  { w := bvs.w, bvs := h ▸ bvs.bvs }

abbrev BitVecs.empty : BitVecs n := ⟨0, List.Vector.replicate n .nil⟩
abbrev BitVecs.singleton {w : Nat} (bv : BitVec w) : BitVecs 1 := ⟨w, bv ::ᵥ .nil⟩
abbrev BitVecs.pair {w : Nat} (bv1 bv2 : BitVec w) : BitVecs 2 := ⟨w, bv1 ::ᵥ bv2 ::ᵥ .nil⟩

/--
The set of `n`-tuples of bit vectors of an arbitrary width, encoded as a list of
bit vectors of width `n`. The width of the encoded bit vectors is the length of
the list.
-/
abbrev BitVecs' (n : Nat) := List (BitVec n)

@[simps]
def dec (bvs' : BitVecs' n) : BitVecs n where
  w := bvs'.length
  bvs := List.Vector.ofFn fun k => BitVec.ofFn fun i => bvs'[i].getLsbD k

@[simp]

-- Repository definitions used:
content: inductive Iff : Prop → Prop → Prop where
  | intro : (a → b) → (b → a) → Iff a b

-- Repository lemmas used:
content: theorem List.getElem_append_left{α : Type u_1} {l₁ l₂ : List α} {i : Nat} (hn : i < l₁.length) :\n(l₁ ++ l₂)[i] = l₁[i]

-- Local definitions used:

-- Local lemmas used:
content: theorem and_commutative (p q : Prop) : p ∧ q ↔ q ∧ p

-- Target theorem and proof:
theorem and_associative (p q r : Prop) : (p ∧ q) ∧ r ↔ p ∧ (q ∧ r)
:= by
  constructor
  . intro h
    cases h with
    | intro hpq hr =>
      cases hpq with
      | intro hp hq =>
        apply And.intro
        . exact hp
        . apply And.intro
          . exact hq
          . exact hr
  . intro h
    cases h with
    | intro hp hqr =>
      cases hqr with
      | intro hq hr =>
        apply And.intro
        . apply And.intro
          . exact hp
          . exact hq
        . exact hr

/-
## Rewriting proof terms using previously proven results

Often in our proof, we'll need to use the same tactic sequence as in a prior proof.
Like in software development, duplicating the sequence results in duplicate code and can lead to maintenance problems.
We can refer to an earlier propeosition by simply writing the name of the theorem or lemma of interest.
This is similar to breaking down a large program into functions.

First, if we have a previously proven result, we can apply that result directly in the `apply` tactic.
-/
example (p q : Prop) : p ∧ q ↔ q ∧ p := by
  apply and_commutative

/-
We can even refer to asserted but unproven propositions.
-/
example (p q r : Prop) : (p ∧ q) ∧ r ↔ p ∧ (q ∧ r) := by
  apply and_associative

/-
For nested proof terms, we can use the `rewrite` tactic, along with a list of identities (equivalences) to apply to the proof goal or a hypothesis.

The syntax here is a bit complicated.
We need to pass a configuration stating which occurrence(s) of the proof term we want to rewrite.
An occurrence is a preorder position within the AST of the proof term, with 1 as the root (the entire proof term).
You might need to do some trial-and-error to get the desired occurrence.
Refer to the pop-up contextual guide for more information.
-/
example (p q r : Prop) : (p ∧ q) ∧ r ↔ (q ∧ p) ∧ r := by
  apply Iff.intro
  . intro h
    rewrite (config := {occs := .pos [2]}) [and_commutative] at h
    assumption
  . intro h
    rewrite (config := {occs := .pos [2]}) [and_commutative] at h
    assumption