/-
  Topolei.Cubical.Face
  ====================
  The face lattice F: distributive lattice generated by (i=0) and (i=1)
  with the fundamental relation (i=0) ∧ (i=1) = 0_F.

  Reference: CCHM §4.1
  Grammar:  ϕ, ψ ::= 0_F | 1_F | (i=0) | (i=1) | ϕ∧ψ | ϕ∨ψ

  Face formulas describe which sub-face of a cube we are on.
  The key invariant: (i=0) and (i=1) are mutually exclusive and jointly exhaustive.
-/

import CubicalTransport.Interval

-- ── Face formulas ─────────────────────────────────────────────────────────────

inductive FaceFormula where
  | bot                        : FaceFormula  -- 0_F
  | top                        : FaceFormula  -- 1_F
  | eq0 (i : DimVar)           : FaceFormula  -- (i = 0)
  | eq1 (i : DimVar)           : FaceFormula  -- (i = 1)
  | meet (ϕ ψ : FaceFormula)  : FaceFormula  -- ϕ ∧ ψ
  | join (ϕ ψ : FaceFormula)  : FaceFormula  -- ϕ ∨ ψ
  deriving Repr, Inhabited

-- ── Semantic evaluation ───────────────────────────────────────────────────────

def FaceFormula.eval (env : DimVar → Bool) : FaceFormula → Bool
  | .bot       => false
  | .top       => true
  | .eq0 i     => !(env i)
  | .eq1 i     => env i
  | .meet ϕ ψ => ϕ.eval env && ψ.eval env
  | .join ϕ ψ => ϕ.eval env || ψ.eval env

-- ── Fundamental laws ──────────────────────────────────────────────────────────
-- Strategy: simp only [eval] to unfold, then Bool case analysis.
-- Note: `show` is avoided here because `&&`/`||` bind looser than `=` in Lean 4,
-- causing parse surprises. `simp only [eval]` is cleaner and consistent.

namespace FaceFormula

-- (i=0) and (i=1) are mutually exclusive: their meet is always false
theorem eq0_meet_eq1 (i : DimVar) (env : DimVar → Bool) :
    (meet (eq0 i) (eq1 i)).eval env = false := by
  simp only [eval]
  cases env i <;> rfl

-- They are jointly exhaustive: their join is always true
theorem eq0_join_eq1 (i : DimVar) (env : DimVar → Bool) :
    (join (eq0 i) (eq1 i)).eval env = true := by
  simp only [eval]
  cases env i <;> rfl

-- Commutativity
theorem meet_comm (ϕ ψ : FaceFormula) (env : DimVar → Bool) :
    (meet ϕ ψ).eval env = (meet ψ ϕ).eval env := by
  simp only [eval]
  cases ϕ.eval env <;> cases ψ.eval env <;> rfl

theorem join_comm (ϕ ψ : FaceFormula) (env : DimVar → Bool) :
    (join ϕ ψ).eval env = (join ψ ϕ).eval env := by
  simp only [eval]
  cases ϕ.eval env <;> cases ψ.eval env <;> rfl

-- Associativity
theorem meet_assoc (ϕ ψ χ : FaceFormula) (env : DimVar → Bool) :
    (meet ϕ (meet ψ χ)).eval env = (meet (meet ϕ ψ) χ).eval env := by
  simp only [eval]
  cases ϕ.eval env <;> cases ψ.eval env <;> cases χ.eval env <;> rfl

theorem join_assoc (ϕ ψ χ : FaceFormula) (env : DimVar → Bool) :
    (join ϕ (join ψ χ)).eval env = (join (join ϕ ψ) χ).eval env := by
  simp only [eval]
  cases ϕ.eval env <;> cases ψ.eval env <;> cases χ.eval env <;> rfl

-- Identity laws
theorem meet_top (ϕ : FaceFormula) (env : DimVar → Bool) :
    (meet ϕ top).eval env = ϕ.eval env := by
  simp only [eval]
  cases ϕ.eval env <;> rfl

theorem join_bot (ϕ : FaceFormula) (env : DimVar → Bool) :
    (join ϕ bot).eval env = ϕ.eval env := by
  simp only [eval]
  cases ϕ.eval env <;> rfl

theorem meet_bot (ϕ : FaceFormula) (env : DimVar → Bool) :
    (meet ϕ bot).eval env = false := by
  simp only [eval]
  cases ϕ.eval env <;> rfl

theorem join_top (ϕ : FaceFormula) (env : DimVar → Bool) :
    (join ϕ top).eval env = true := by
  simp only [eval]
  cases ϕ.eval env <;> rfl

-- Distributivity
theorem distrib (ϕ ψ χ : FaceFormula) (env : DimVar → Bool) :
    (meet ϕ (join ψ χ)).eval env = (join (meet ϕ ψ) (meet ϕ χ)).eval env := by
  simp only [eval]
  cases ϕ.eval env <;> cases ψ.eval env <;> cases χ.eval env <;> rfl

-- ── Face restriction ──────────────────────────────────────────────────────────

/-- Restrict a face formula by fixing dimension i to value b.
    This is the substitution ϕ(i/b) from CCHM. -/
def restrict (i : DimVar) (b : Bool) : FaceFormula → FaceFormula
  | .bot       => .bot
  | .top       => .top
  | .eq0 j     => if j = i then (if b then .bot else .top) else .eq0 j
  | .eq1 j     => if j = i then (if b then .top else .bot) else .eq1 j
  | .meet ϕ ψ => .meet (restrict i b ϕ) (restrict i b ψ)
  | .join ϕ ψ => .join (restrict i b ϕ) (restrict i b ψ)

theorem eval_restrict (i : DimVar) (b : Bool) (ϕ : FaceFormula) (env : DimVar → Bool) :
    (restrict i b ϕ).eval env =
    ϕ.eval (fun j => if j = i then b else env j) := by
  induction ϕ with
  | bot         => rfl
  | top         => rfl
  | eq0 j       =>
      simp only [restrict, eval]
      by_cases h : j = i
      · subst h
        cases b <;> simp [eval]
      · simp only [if_neg h, eval]
  | eq1 j       =>
      simp only [restrict, eval]
      by_cases h : j = i
      · subst h
        cases b <;> simp [eval]
      · simp only [if_neg h, eval]
  | meet ϕ ψ ih_ϕ ih_ψ => simp only [restrict, eval, ih_ϕ, ih_ψ]
  | join ϕ ψ ih_ϕ ih_ψ => simp only [restrict, eval, ih_ϕ, ih_ψ]

-- Endpoint restriction lemmas (used in composition algorithm)
theorem restrict_eq0_false (i : DimVar) (env : DimVar → Bool) :
    (restrict i false (eq0 i)).eval env = true := by
  simp [restrict, eval]

theorem restrict_eq0_true (i : DimVar) (env : DimVar → Bool) :
    (restrict i true (eq0 i)).eval env = false := by
  simp [restrict, eval]

theorem restrict_eq1_true (i : DimVar) (env : DimVar → Bool) :
    (restrict i true (eq1 i)).eval env = true := by
  simp [restrict, eval]

theorem restrict_eq1_false (i : DimVar) (env : DimVar → Bool) :
    (restrict i false (eq1 i)).eval env = false := by
  simp [restrict, eval]

-- ── Entailment ────────────────────────────────────────────────────────────────

/-- ϕ entails ψ: whenever ϕ holds, ψ holds. -/
def Entails (ϕ ψ : FaceFormula) : Prop :=
  ∀ env : DimVar → Bool, ϕ.eval env = true → ψ.eval env = true

theorem entails_refl (ϕ : FaceFormula) : Entails ϕ ϕ :=
  fun _ h => h

theorem entails_trans (ϕ ψ χ : FaceFormula) :
    Entails ϕ ψ → Entails ψ χ → Entails ϕ χ :=
  fun h1 h2 env hϕ => h2 env (h1 env hϕ)

theorem entails_meet_left (ϕ ψ : FaceFormula) : Entails (meet ϕ ψ) ϕ :=
  fun env h => by
    simp only [eval] at h
    cases hϕ : ϕ.eval env
    · simp [hϕ] at h
    · rfl

theorem entails_join_left (ϕ ψ : FaceFormula) : Entails ϕ (join ϕ ψ) :=
  fun env h => by simp only [eval, h, Bool.true_or]

theorem entails_join_right (ϕ ψ : FaceFormula) : Entails ψ (join ϕ ψ) :=
  fun env h => by simp only [eval, h, Bool.or_true]

-- ── Dimension absence ─────────────────────────────────────────────────────────

/-- Syntactic check: `i` does not appear in the face formula. -/
def dimAbsent (i : DimVar) : FaceFormula → Bool
  | .bot       => true
  | .top       => true
  | .eq0 j     => j != i
  | .eq1 j     => j != i
  | .meet ϕ ψ => ϕ.dimAbsent i && ψ.dimAbsent i
  | .join ϕ ψ => ϕ.dimAbsent i && ψ.dimAbsent i

-- ── DimExpr → FaceFormula: "the DimExpr equals 0 / 1" ────────────────────────
-- These translate a DimExpr r into a FaceFormula saying "r = 0" (or "r = 1").
-- Used by FaceFormula.substDim below: when we substitute a dim variable j
-- that appears inside `eq0 j` with a DimExpr r, the result should encode
-- "r = 0" as a face, which — for composite r — is itself a composite face.
--
-- De Morgan at the DimExpr ↔ FaceFormula boundary:
--   (meet r s = 0) ↔ (r = 0 ∨ s = 0)       [meet zero: either factor is 0]
--   (join r s = 0) ↔ (r = 0 ∧ s = 0)       [join zero: both factors are 0]
--   (inv r = 0)    ↔ (r = 1)
-- Dually for "= 1".

mutual
  /-- Build a FaceFormula expressing `r = 0` for an arbitrary DimExpr `r`. -/
  def dimExprEq0 : DimExpr → FaceFormula
    | .zero      => .top
    | .one       => .bot
    | .var k     => .eq0 k
    | .inv r     => dimExprEq1 r
    | .meet r s  => .join (dimExprEq0 r) (dimExprEq0 s)
    | .join r s  => .meet (dimExprEq0 r) (dimExprEq0 s)

  /-- Build a FaceFormula expressing `r = 1` for an arbitrary DimExpr `r`. -/
  def dimExprEq1 : DimExpr → FaceFormula
    | .zero      => .bot
    | .one       => .top
    | .var k     => .eq1 k
    | .inv r     => dimExprEq0 r
    | .meet r s  => .meet (dimExprEq1 r) (dimExprEq1 s)
    | .join r s  => .join (dimExprEq1 r) (dimExprEq1 s)
end

-- ── Face formula substitution by DimExpr ─────────────────────────────────────

/-- Substitute a dimension variable `i` with an arbitrary `DimExpr r`
    throughout a face formula.  Uses `dimExprEq0`/`dimExprEq1` to encode
    the composite faces arising from substitution into `eq0 j`/`eq1 j`. -/
def substDim (i : DimVar) (r : DimExpr) : FaceFormula → FaceFormula
  | .bot       => .bot
  | .top       => .top
  | .eq0 j     => if j = i then dimExprEq0 r else .eq0 j
  | .eq1 j     => if j = i then dimExprEq1 r else .eq1 j
  | .meet ϕ ψ => .meet (ϕ.substDim i r) (ψ.substDim i r)
  | .join ϕ ψ => .join (ϕ.substDim i r) (ψ.substDim i r)

-- ── Correctness: dimExprEq0/1 compute the right predicate ───────────────────
-- The semantic content: `(dimExprEq0 r).eval env = !(r.eval env)`, i.e. the
-- face holds iff r evaluates to 0 (= false in Bool).

mutual
  theorem dimExprEq0_eval (r : DimExpr) (env : DimVar → Bool) :
      (dimExprEq0 r).eval env = !(r.eval env) := by
    cases r with
    | zero => rfl
    | one  => rfl
    | var k => rfl
    | inv s =>
        show (dimExprEq1 s).eval env = _
        rw [dimExprEq1_eval s env]
        simp [DimExpr.eval]
    | meet s t =>
        show ((dimExprEq0 s).join (dimExprEq0 t)).eval env = _
        simp only [eval]
        rw [dimExprEq0_eval s env, dimExprEq0_eval t env]
        simp [DimExpr.eval, Bool.not_and]
    | join s t =>
        show ((dimExprEq0 s).meet (dimExprEq0 t)).eval env = _
        simp only [eval]
        rw [dimExprEq0_eval s env, dimExprEq0_eval t env]
        simp [DimExpr.eval, Bool.not_or]

  theorem dimExprEq1_eval (r : DimExpr) (env : DimVar → Bool) :
      (dimExprEq1 r).eval env = r.eval env := by
    cases r with
    | zero => rfl
    | one  => rfl
    | var k => rfl
    | inv s =>
        show (dimExprEq0 s).eval env = _
        rw [dimExprEq0_eval s env]
        simp [DimExpr.eval]
    | meet s t =>
        show ((dimExprEq1 s).meet (dimExprEq1 t)).eval env = _
        simp only [eval]
        rw [dimExprEq1_eval s env, dimExprEq1_eval t env]
        simp [DimExpr.eval]
    | join s t =>
        show ((dimExprEq1 s).join (dimExprEq1 t)).eval env = _
        simp only [eval]
        rw [dimExprEq1_eval s env, dimExprEq1_eval t env]
        simp [DimExpr.eval]
end

-- ── Endpoint specialisations: substDim at .zero / .one agrees with restrict ──

theorem substDim_zero (i : DimVar) (ϕ : FaceFormula) :
    ϕ.substDim i .zero = ϕ.restrict i false := by
  induction ϕ with
  | bot => rfl
  | top => rfl
  | eq0 j => by_cases h : j = i <;> simp [substDim, restrict, h, dimExprEq0]
  | eq1 j => by_cases h : j = i <;> simp [substDim, restrict, h, dimExprEq1]
  | meet ϕ ψ ihϕ ihψ => simp [substDim, restrict, ihϕ, ihψ]
  | join ϕ ψ ihϕ ihψ => simp [substDim, restrict, ihϕ, ihψ]

theorem substDim_one (i : DimVar) (ϕ : FaceFormula) :
    ϕ.substDim i .one = ϕ.restrict i true := by
  induction ϕ with
  | bot => rfl
  | top => rfl
  | eq0 j => by_cases h : j = i <;> simp [substDim, restrict, h, dimExprEq0]
  | eq1 j => by_cases h : j = i <;> simp [substDim, restrict, h, dimExprEq1]
  | meet ϕ ψ ihϕ ihψ => simp [substDim, restrict, ihϕ, ihψ]
  | join ϕ ψ ihϕ ihψ => simp [substDim, restrict, ihϕ, ihψ]

-- ── substDim is identity when dim is absent ──────────────────────────────────

theorem substDim_of_absent (i : DimVar) (r : DimExpr)
    (ϕ : FaceFormula) (h : ϕ.dimAbsent i = true) :
    ϕ.substDim i r = ϕ := by
  induction ϕ with
  | bot => rfl
  | top => rfl
  | eq0 j =>
      simp only [dimAbsent, bne_iff_ne] at h
      simp [substDim, if_neg h]
  | eq1 j =>
      simp only [dimAbsent, bne_iff_ne] at h
      simp [substDim, if_neg h]
  | meet ϕ ψ ihϕ ihψ =>
      simp only [dimAbsent, Bool.and_eq_true] at h
      simp [substDim, ihϕ h.1, ihψ h.2]
  | join ϕ ψ ihϕ ihψ =>
      simp only [dimAbsent, Bool.and_eq_true] at h
      simp [substDim, ihϕ h.1, ihψ h.2]

-- ── dimExprEq{0,1} dim-absence preservation ────────────────────────────────
-- Mutual recursion because dimExprEq0 and dimExprEq1 refer to each other
-- on `.inv`.

mutual
  /-- When `r` doesn't mention `j`, neither does `dimExprEq0 r`. -/
  theorem dimExprEq0_dimAbsent (j : DimVar) : (r : DimExpr) →
      r.dimAbsent j = true → (dimExprEq0 r).dimAbsent j = true
    | .zero, _ => rfl
    | .one,  _ => rfl
    | .var k, h => by
        simp only [DimExpr.dimAbsent, bne_iff_ne] at h
        simp [dimExprEq0, dimAbsent, bne_iff_ne, h]
    | .inv r, h => by
        simp only [DimExpr.dimAbsent] at h
        show (dimExprEq1 r).dimAbsent j = true
        exact dimExprEq1_dimAbsent j r h
    | .meet r s, h => by
        simp only [DimExpr.dimAbsent, Bool.and_eq_true] at h
        show ((dimExprEq0 r).join (dimExprEq0 s)).dimAbsent j = true
        simp only [dimAbsent, Bool.and_eq_true]
        exact ⟨dimExprEq0_dimAbsent j r h.1, dimExprEq0_dimAbsent j s h.2⟩
    | .join r s, h => by
        simp only [DimExpr.dimAbsent, Bool.and_eq_true] at h
        show ((dimExprEq0 r).meet (dimExprEq0 s)).dimAbsent j = true
        simp only [dimAbsent, Bool.and_eq_true]
        exact ⟨dimExprEq0_dimAbsent j r h.1, dimExprEq0_dimAbsent j s h.2⟩

  /-- When `r` doesn't mention `j`, neither does `dimExprEq1 r`. -/
  theorem dimExprEq1_dimAbsent (j : DimVar) : (r : DimExpr) →
      r.dimAbsent j = true → (dimExprEq1 r).dimAbsent j = true
    | .zero, _ => rfl
    | .one,  _ => rfl
    | .var k, h => by
        simp only [DimExpr.dimAbsent, bne_iff_ne] at h
        simp [dimExprEq1, dimAbsent, bne_iff_ne, h]
    | .inv r, h => by
        simp only [DimExpr.dimAbsent] at h
        show (dimExprEq0 r).dimAbsent j = true
        exact dimExprEq0_dimAbsent j r h
    | .meet r s, h => by
        simp only [DimExpr.dimAbsent, Bool.and_eq_true] at h
        show ((dimExprEq1 r).meet (dimExprEq1 s)).dimAbsent j = true
        simp only [dimAbsent, Bool.and_eq_true]
        exact ⟨dimExprEq1_dimAbsent j r h.1, dimExprEq1_dimAbsent j s h.2⟩
    | .join r s, h => by
        simp only [DimExpr.dimAbsent, Bool.and_eq_true] at h
        show ((dimExprEq1 r).join (dimExprEq1 s)).dimAbsent j = true
        simp only [dimAbsent, Bool.and_eq_true]
        exact ⟨dimExprEq1_dimAbsent j r h.1, dimExprEq1_dimAbsent j s h.2⟩
end

/-- `substDim j s` on `dimExprEq0 r` is identity when `r` doesn't mention `j`. -/
theorem dimExprEq0_substDim_of_absent (j : DimVar) (s : DimExpr)
    (r : DimExpr) (h : r.dimAbsent j = true) :
    (dimExprEq0 r).substDim j s = dimExprEq0 r :=
  substDim_of_absent j s _ (dimExprEq0_dimAbsent j r h)

/-- `substDim j s` on `dimExprEq1 r` is identity when `r` doesn't mention `j`. -/
theorem dimExprEq1_substDim_of_absent (j : DimVar) (s : DimExpr)
    (r : DimExpr) (h : r.dimAbsent j = true) :
    (dimExprEq1 r).substDim j s = dimExprEq1 r :=
  substDim_of_absent j s _ (dimExprEq1_dimAbsent j r h)

-- ── dimAbsent after substDim (i becomes absent after substituting i → r) ──

/-- After `substDim i r ϕ`, `i` is absent from the result — provided `r`
    itself doesn't mention `i`.  Needed downstream in `CTerm.dimAbsent_after_substDim`. -/
theorem dimAbsent_after_substDim (i : DimVar) (r : DimExpr)
    (hr : r.dimAbsent i = true) (ϕ : FaceFormula) :
    (ϕ.substDim i r).dimAbsent i = true := by
  induction ϕ with
  | bot => rfl
  | top => rfl
  | eq0 j =>
      simp only [substDim]
      by_cases hji : j = i
      · rw [hji, if_pos rfl]; exact dimExprEq0_dimAbsent i r hr
      · simp only [if_neg hji, dimAbsent, bne_iff_ne]; exact hji
  | eq1 j =>
      simp only [substDim]
      by_cases hji : j = i
      · rw [hji, if_pos rfl]; exact dimExprEq1_dimAbsent i r hr
      · simp only [if_neg hji, dimAbsent, bne_iff_ne]; exact hji
  | meet ϕ ψ ihϕ ihψ =>
      simp only [substDim, dimAbsent, Bool.and_eq_true]
      exact ⟨ihϕ, ihψ⟩
  | join ϕ ψ ihϕ ihψ =>
      simp only [substDim, dimAbsent, Bool.and_eq_true]
      exact ⟨ihϕ, ihψ⟩

-- ── substDim commutes with itself on disjoint dim variables ─────────────────

/-- Substituting disjoint dims `i` and `j` (with the RHS DimExprs not
    mentioning the opposite side's binder) commutes.  Required for the
    `CTerm.substDim_comm_aux` mutual induction through the new `transp`/
    `comp` arms that now substitute in the face formula. -/
theorem substDim_comm (i j : DimVar) (r s : DimExpr) (hij : i ≠ j)
    (hrj : r.dimAbsent j = true) (hsi : s.dimAbsent i = true)
    (ϕ : FaceFormula) :
    (ϕ.substDim i r).substDim j s = (ϕ.substDim j s).substDim i r := by
  induction ϕ with
  | bot => rfl
  | top => rfl
  | eq0 k =>
      by_cases hki : k = i
      · subst hki
        simp only [substDim, ite_true, if_neg hij]
        exact dimExprEq0_substDim_of_absent j s r hrj
      · by_cases hkj : k = j
        · subst hkj
          simp only [substDim, if_neg hki, ite_true]
          exact (dimExprEq0_substDim_of_absent i r s hsi).symm
        · simp [substDim, if_neg hki, if_neg hkj]
  | eq1 k =>
      by_cases hki : k = i
      · subst hki
        simp only [substDim, ite_true, if_neg hij]
        exact dimExprEq1_substDim_of_absent j s r hrj
      · by_cases hkj : k = j
        · subst hkj
          simp only [substDim, if_neg hki, ite_true]
          exact (dimExprEq1_substDim_of_absent i r s hsi).symm
        · simp [substDim, if_neg hki, if_neg hkj]
  | meet ϕ ψ ihϕ ihψ => simp [substDim, ihϕ, ihψ]
  | join ϕ ψ ihϕ ihψ => simp [substDim, ihϕ, ihψ]

-- ── Normalisation (Stage 4.3, 2026-04-23) ────────────────────────────────────
--
-- After `substDim`, face formulas can contain terms like `.meet .top φ`
-- or `.join .bot φ` (e.g. `.substDim i .zero` on `.eq0 i` produces
-- `.top` as a sub-term which may need propagation).  Rust's cubical
-- evaluator needs canonicalised faces to make dispatch deterministic:
-- the existing Glue-transport axioms hinge on `φ.substDim i .one = .bot`
-- or `= .top` literal equalities, which only hold after normalisation.
--
-- Reductions implemented (mirrors the semantic laws already in this
-- file):
--   · `.meet .top φ → φ`,   `.meet φ .top → φ`
--   · `.meet .bot _ → .bot`, `.meet _ .bot → .bot`
--   · `.join .bot φ → φ`,   `.join φ .bot → φ`
--   · `.join .top _ → .top`, `.join _ .top → .top`
--
-- Mutually-exclusive literal reductions (`.meet (.eq0 i) (.eq1 i) → .bot`,
-- etc.) are deliberately skipped — they require decidable equality on
-- `DimVar` in the reduction step and don't affect any current axiom.

@[extern "topolei_cubical_face_normalize"]
private opaque normalizeRust : FaceFormula → FaceFormula

/-- Canonical form under literal-identity / absorption reductions. -/
@[implemented_by normalizeRust]
def normalize : FaceFormula → FaceFormula
  | .bot     => .bot
  | .top     => .top
  | .eq0 i   => .eq0 i
  | .eq1 i   => .eq1 i
  | .meet ϕ ψ =>
      match normalize ϕ, normalize ψ with
      | .bot, _    => .bot
      | _, .bot    => .bot
      | .top, nψ   => nψ
      | nϕ, .top   => nϕ
      | nϕ, nψ     => .meet nϕ nψ
  | .join ϕ ψ =>
      match normalize ϕ, normalize ψ with
      | .top, _    => .top
      | _, .top    => .top
      | .bot, nψ   => nψ
      | nϕ, .bot   => nϕ
      | nϕ, nψ     => .join nϕ nψ

-- ── Preservation of evaluation ──────────────────────────────────────────────

private theorem normalize_meet_aux (ϕ ψ : FaceFormula) (env : DimVar → Bool) :
    ((normalize (FaceFormula.meet ϕ ψ)).eval env) =
    (((normalize ϕ).eval env) && ((normalize ψ).eval env)) := by
  show (match normalize ϕ, normalize ψ with
        | FaceFormula.bot, _ => FaceFormula.bot
        | _, FaceFormula.bot => FaceFormula.bot
        | FaceFormula.top, nψ => nψ
        | nϕ, FaceFormula.top => nϕ
        | nϕ, nψ => FaceFormula.meet nϕ nψ).eval env =
      (((normalize ϕ).eval env) && ((normalize ψ).eval env))
  cases hϕ : normalize ϕ <;> cases hψ : normalize ψ <;> simp [eval]

private theorem normalize_join_aux (ϕ ψ : FaceFormula) (env : DimVar → Bool) :
    ((normalize (FaceFormula.join ϕ ψ)).eval env) =
    (((normalize ϕ).eval env) || ((normalize ψ).eval env)) := by
  show (match normalize ϕ, normalize ψ with
        | FaceFormula.top, _ => FaceFormula.top
        | _, FaceFormula.top => FaceFormula.top
        | FaceFormula.bot, nψ => nψ
        | nϕ, FaceFormula.bot => nϕ
        | nϕ, nψ => FaceFormula.join nϕ nψ).eval env =
      (((normalize ϕ).eval env) || ((normalize ψ).eval env))
  cases hϕ : normalize ϕ <;> cases hψ : normalize ψ <;> simp [eval]

/-- Normalisation is semantically transparent. -/
theorem normalize_preserves_eval (ϕ : FaceFormula) (env : DimVar → Bool) :
    (normalize ϕ).eval env = ϕ.eval env := by
  induction ϕ with
  | bot     => rfl
  | top     => rfl
  | eq0 i   => rfl
  | eq1 i   => rfl
  | meet ϕ ψ ihϕ ihψ =>
      have h := normalize_meet_aux ϕ ψ env
      rw [h, ihϕ, ihψ]
      rfl
  | join ϕ ψ ihϕ ihψ =>
      have h := normalize_join_aux ϕ ψ env
      rw [h, ihϕ, ihψ]
      rfl

end FaceFormula
