/-
  QLF_Unsaturation.lean — "degree of unsaturation" is the closure count of a molecule.

  Chemistry.md gets molecules from one rule — a bond is a shared closure — and then admits a
  gap: it gets stoichiometry and formulas right "but not double bonds, bond angles, resonance."
  This module closes the first of those, and the way it closes is the point.

  Organic chemistry teaches a formula to be memorised,

      DoU = (2C + 2 + N − H − X) / 2

  with a rule attached: oxygen and sulphur are *left out*, and nobody says why. Read the molecule
  as a graph whose vertex degrees are the valences and the formula stops being a formula. The
  handshake lemma gives `2E = Σ vᵢ`, so the cycle rank of a connected molecular graph is

      b₁ = E − V + 1 = Σ (vᵢ − 2) / 2 + 1

  which is that formula, with each element's coefficient revealed as `(valence − 2)/2`:
  carbon `+1`, nitrogen `+½`, hydrogen and halogen `−½` — and **oxygen `0`, which is why it was
  missing.** Nothing was omitted by convention. A divalent atom adds one vertex and one edge, so
  it cannot change a cycle rank (`divalent_neutral`).

  What this buys QLF is not the arithmetic, which is easy, but the identification:

  * **b₁ counts independent closures.** A cycle in the molecular graph is a loop that returns —
    the same object as a contact loop in QLF_Folding and as any ZFA-closed twist history.
    "Unsaturation" is closure count; **"saturated" means zero closures**, i.e. a tree.
  * **A double bond and a ring are the same thing.** Both contribute exactly `1` to b₁. Chemistry
    files them as different phenomena in different chapters; on the substrate there is one
    phenomenon, a closure, and `C₆H₁₂` is one census class holding cyclohexane and every hexene
    together — as it must, since they have the same formula and therefore the same closure count.
  * **Valence 2 is the neutral element of closure counting** (`divalent_neutral`,
    `divalent_chain_neutral`). That is why a divalent monomer polymerises into a *chain* rather
    than a ring or a network (Chemistry.md §9): a backbone of divalent units carries no closure of
    its own, so every closure a polymer has is a contact — which is precisely the setting
    QLF_Folding assumes.

  Companion computation: hydrocarbon_census.py enumerates the carbon skeletons the valence rule
  admits and checks the closure count on every one. Honest scope: the theorems here are exact
  arithmetic over the valence bookkeeping; the graph itself is not formalised, so `b₁` enters as
  the definition `E − V + 1` rather than as a proved property of a cycle space. No axioms.
-/

import Mathlib

namespace QLF.Unsaturation

/-- **Twice the number of independent closures** a molecule carries: `2·b₁` where
    `b₁ = E − V + 1` is the cycle rank of the molecular graph. The list is the molecule's
    atoms, each given by its valence; `2E = Σ vᵢ` is the handshake lemma, so doubling keeps
    everything in `ℤ` with no division. -/
def doubledClosures (atoms : List ℕ) : ℤ :=
  (atoms.sum : ℤ) - 2 * (atoms.length : ℤ) + 2

/-- **The master lemma: an atom contributes `valence − 2`.** Every statement below is this
    one counted up. An atom brings one vertex and half of each of its `v` bond-ends, so it
    moves `2(E − V)` by exactly `v − 2`. -/
theorem doubledClosures_cons (v : ℕ) (atoms : List ℕ) :
    doubledClosures (v :: atoms) = doubledClosures atoms + ((v : ℤ) - 2) := by
  unfold doubledClosures
  rw [List.sum_cons, List.length_cons]
  push_cast
  ring

theorem doubledClosures_append (a b : List ℕ) :
    doubledClosures (a ++ b) = doubledClosures a + doubledClosures b - 2 := by
  unfold doubledClosures
  rw [List.sum_append, List.length_append]
  push_cast
  ring

theorem doubledClosures_replicate (k v : ℕ) :
    doubledClosures (List.replicate k v) = k * ((v : ℤ) - 2) + 2 := by
  induction k with
  | zero => simp [doubledClosures]
  | succ k ih =>
      rw [List.replicate_succ, doubledClosures_cons, ih]
      push_cast
      ring

/-! ## Valence 2 is the neutral element -/

/-- **A divalent atom cannot change the closure count.** One vertex, one edge, no cycle. This
    is the whole reason oxygen and sulphur are absent from the textbook formula — not a
    convention, a cancellation. -/
theorem divalent_neutral (atoms : List ℕ) :
    doubledClosures (2 :: atoms) = doubledClosures atoms := by
  rw [doubledClosures_cons]
  push_cast
  ring

/-- **A chain of divalent units is closure-neutral.** So a divalent monomer polymerises into a
    *chain*: the backbone carries no closure of its own, and every closure a polymer has is a
    contact (Chemistry.md §9, QLF_Folding). -/
theorem divalent_chain_neutral (k : ℕ) (atoms : List ℕ) :
    doubledClosures (List.replicate k 2 ++ atoms) = doubledClosures atoms := by
  rw [doubledClosures_append, doubledClosures_replicate]
  push_cast
  ring

/-- A monovalent atom **caps**: it decrements the closure count. Hydrogen and the halogens
    enter the textbook formula with a minus sign for this reason and no other. -/
theorem monovalent_caps (atoms : List ℕ) :
    doubledClosures (1 :: atoms) = doubledClosures atoms - 1 := by
  rw [doubledClosures_cons]
  push_cast
  ring

/-! ## The textbook formula, recovered -/

/-- A hydrocarbon `CₙHₘ`: `n` tetravalent atoms and `m` monovalent ones. -/
def hydrocarbon (n m : ℕ) : List ℕ := List.replicate n 4 ++ List.replicate m 1

/-- A `CcHhNnOo` molecule, right-nested so each append is one application. -/
def molecule (c h n o : ℕ) : List ℕ :=
  List.replicate c 4 ++ (List.replicate h 1 ++ (List.replicate n 3 ++ List.replicate o 2))

/-- **The degree of unsaturation of a hydrocarbon is its cycle rank**: `2·DoU = 2n + 2 − m`. -/
theorem dou_hydrocarbon (n m : ℕ) :
    doubledClosures (hydrocarbon n m) = 2 * n + 2 - m := by
  unfold hydrocarbon
  rw [doubledClosures_append, doubledClosures_replicate, doubledClosures_replicate]
  push_cast
  ring

/-- **And in general — with oxygen absent from the answer.** `2·DoU = 2C + 2 + N − H`, with no
    `O` on the right-hand side, because `divalent_neutral` says there cannot be one. -/
theorem dou_chno (c h n o : ℕ) :
    doubledClosures (molecule c h n o) = 2 * c + 2 + n - h := by
  unfold molecule
  rw [doubledClosures_append, doubledClosures_append, doubledClosures_append,
      doubledClosures_replicate, doubledClosures_replicate,
      doubledClosures_replicate, doubledClosures_replicate]
  push_cast
  ring

/-- **"Saturated" means zero closures** — and the alkane formula `CₙH₂ₙ₊₂` follows rather than
    being stipulated. A saturated hydrocarbon is a tree; every additional closure, whether it is
    drawn as a ring or as a double bond, costs two hydrogens. -/
theorem saturated_iff_alkane (n m : ℕ) :
    doubledClosures (hydrocarbon n m) = 0 ↔ m = 2 * n + 2 := by
  rw [dou_hydrocarbon]
  omega

/-- Benzene carries four closures — three double bonds and the ring — and the count does not
    care which is which. That is the module's claim in one number. -/
theorem benzene_four_closures : doubledClosures (hydrocarbon 6 6) = 8 := by
  rw [dou_hydrocarbon]
  norm_num

/-- Cyclohexane and every hexene share a formula, hence a closure count: **one census class**,
    though chemistry files them in different chapters. -/
theorem c6h12_one_closure : doubledClosures (hydrocarbon 6 12) = 2 := by
  rw [dou_hydrocarbon]
  norm_num

/-! ## Reactions: the closure count is the molecule count -/

/-- A **mixture**: a list of molecules, each given as the list of its atoms' valences. -/
def mixtureClosures (ms : List (List ℕ)) : ℤ := (ms.map doubledClosures).sum

theorem mixtureClosures_cons (m : List ℕ) (ms : List (List ℕ)) :
    mixtureClosures (m :: ms) = doubledClosures m + mixtureClosures ms := by
  unfold mixtureClosures
  rw [List.map_cons, List.sum_cons]

/-- For a mixture of `k` molecules the closure count is `E − V + k`: the atom inventory fixes
    `V` and `E = Σ vᵢ / 2`, and the only remaining freedom is **how many pieces the atoms are
    distributed into**. -/
theorem mixtureClosures_eq (ms : List (List ℕ)) :
    mixtureClosures ms
      = ((ms.map List.sum).sum : ℤ) - 2 * ((ms.map List.length).sum : ℤ)
        + 2 * (ms.length : ℤ) := by
  induction ms with
  | nil => simp [mixtureClosures]
  | cons m rest ih =>
      rw [mixtureClosures_cons, ih]
      simp only [List.map_cons, List.sum_cons, List.length_cons]
      unfold doubledClosures
      push_cast
      ring

/-- **A balanced reaction changes the closure count by exactly its change in molecule count.**

    Both hypotheses say the atom inventory is conserved — the same valences (`hE`, so the same
    edge count) over the same number of atoms (`hV`). Everything else in `E − V + k` is then
    pinned, so organic chemistry's reaction taxonomy is a count of *molecules*:

      * **addition** — two molecules become one, `Δ = −1`: one closure is destroyed;
      * **elimination** — one becomes two, `Δ = +1`: one closure is created;
      * **substitution / condensation** — same count, `Δ = 0`: closure-neutral. -/
theorem reaction_delta (lhs rhs : List (List ℕ))
    (hE : (lhs.map List.sum).sum = (rhs.map List.sum).sum)
    (hV : (lhs.map List.length).sum = (rhs.map List.length).sum) :
    mixtureClosures rhs - mixtureClosures lhs
      = 2 * ((rhs.length : ℤ) - (lhs.length : ℤ)) := by
  rw [mixtureClosures_eq, mixtureClosures_eq, hE, hV]
  ring

/-- **Condensation is closure-neutral.** The peptide bond is in this class — two amino acids in,
    a dipeptide and a water out — which is why a polypeptide backbone carries no closure of its
    own and every closure a folded chain has is a **contact** (QLF_Folding, Chemistry.md §9). -/
theorem equal_molecule_count_preserves_closures (lhs rhs : List (List ℕ))
    (hE : (lhs.map List.sum).sum = (rhs.map List.sum).sum)
    (hV : (lhs.map List.length).sum = (rhs.map List.length).sum)
    (hk : lhs.length = rhs.length) :
    mixtureClosures rhs = mixtureClosures lhs := by
  have h := reaction_delta lhs rhs hE hV
  rw [hk] at h
  linarith

end QLF.Unsaturation
