/-
Copyright (c) 2023 Lean FRO, LLC. All rights reserved.
Released under Apache 2.0 license as described in the file LICENSE.
Authors: Scott Morrison
-/
prelude
import Init.Omega.Coeffs
import Init.Data.ToString.Macro

/-!
# Linear combinations

We use this data structure while processing hypotheses.

-/

namespace Lean.Omega

/-- Internal representation of a linear combination of atoms, and a constant term. -/
structure LinearCombo where
  /-- Constant term. -/
  const : Int := 0
  /-- Coefficients of the atoms. -/
  coeffs : Coeffs := []
deriving DecidableEq, Repr

namespace LinearCombo

instance : ToString LinearCombo where
  toString lc :=
    s!"{lc.const}{String.join <| lc.coeffs.toList.enum.map fun ⟨i, c⟩ => s!" + {c} * x{i+1}"}"

instance : Inhabited LinearCombo := ⟨{const := 1}⟩

theorem ext {a b : LinearCombo} (w₁ : a.const = b.const) (w₂ : a.coeffs = b.coeffs) :
    a = b := by
  cases a; cases b
  subst w₁; subst w₂
  congr

/--
Evaluate a linear combination `⟨r, [c_1, …, c_k]⟩` at values `[v_1, …, v_k]` to obtain
`r + (c_1 * x_1 + (c_2 * x_2 + ... (c_k * x_k + 0))))`.
-/
def eval (lc : LinearCombo) (values : Coeffs) : Int :=
  lc.const + lc.coeffs.dot values

@[simp] theorem eval_nil : (lc : LinearCombo).eval .nil = lc.const := by
  simp [eval]

/-- The `i`-th coordinate function. -/
def coordinate (i : Nat) : LinearCombo where
  const := 0
  coeffs := Coeffs.set .nil i 1

@[simp] theorem coordinate_eval (i : Nat) (v : Coeffs) :
    (coordinate i).eval v = v.get i := by
  simp [eval, coordinate]

theorem coordinate_eval_0 : (coordinate 0).eval (.ofList (a0 :: t)) = a0 := by simp
theorem coordinate_eval_1 : (coordinate 1).eval (.ofList (a0 :: a1 :: t)) = a1 := by simp
theorem coordinate_eval_2 : (coordinate 2).eval (.ofList (a0 :: a1 :: a2 :: t)) = a2 := by simp
theorem coordinate_eval_3 :
    (coordinate 3).eval (.ofList (a0 :: a1 :: a2 :: a3 :: t)) = a3 := by simp
theorem coordinate_eval_4 :
    (coordinate 4).eval (.ofList (a0 :: a1 :: a2 :: a3 :: a4 :: t)) = a4 := by simp
theorem coordinate_eval_5 :
    (coordinate 5).eval (.ofList (a0 :: a1 :: a2 :: a3 :: a4 :: a5 :: t)) = a5 := by simp
theorem coordinate_eval_6 :
    (coordinate 6).eval (.ofList (a0 :: a1 :: a2 :: a3 :: a4 :: a5 :: a6 :: t)) = a6 := by simp
theorem coordinate_eval_7 :
    (coordinate 7).eval
      (.ofList (a0 :: a1 :: a2 :: a3 :: a4 :: a5 :: a6 :: a7 :: t)) = a7 := by simp
theorem coordinate_eval_8 :
    (coordinate 8).eval
      (.ofList (a0 :: a1 :: a2 :: a3 :: a4 :: a5 :: a6 :: a7 :: a8 :: t)) = a8 := by simp
theorem coordinate_eval_9 :
    (coordinate 9).eval
      (.ofList (a0 :: a1 :: a2 :: a3 :: a4 :: a5 :: a6 :: a7 :: a8 :: a9 :: t)) = a9 := by simp

/-- Implementation of addition on `LinearCombo`. -/
def add (l₁ l₂ : LinearCombo) : LinearCombo where
  const := l₁.const + l₂.const
  coeffs := l₁.coeffs + l₂.coeffs

instance : Add LinearCombo := ⟨add⟩

@[simp] theorem add_const {l₁ l₂ : LinearCombo} : (l₁ + l₂).const = l₁.const + l₂.const := rfl
@[simp] theorem add_coeffs {l₁ l₂ : LinearCombo} : (l₁ + l₂).coeffs = l₁.coeffs + l₂.coeffs := rfl

/-- Implementation of subtraction on `LinearCombo`. -/
def sub (l₁ l₂ : LinearCombo) : LinearCombo where
  const := l₁.const - l₂.const
  coeffs := l₁.coeffs - l₂.coeffs

instance : Sub LinearCombo := ⟨sub⟩

@[simp] theorem sub_const {l₁ l₂ : LinearCombo} : (l₁ - l₂).const = l₁.const - l₂.const := rfl
@[simp] theorem sub_coeffs {l₁ l₂ : LinearCombo} : (l₁ - l₂).coeffs = l₁.coeffs - l₂.coeffs := rfl

/-- Implementation of negation on `LinearCombo`. -/
def neg (lc : LinearCombo) : LinearCombo where
  const := -lc.const
  coeffs := -lc.coeffs

instance : Neg LinearCombo := ⟨neg⟩

@[simp] theorem neg_const {l : LinearCombo} : (-l).const = -l.const := rfl
@[simp] theorem neg_coeffs {l : LinearCombo} : (-l).coeffs = -l.coeffs  := rfl

theorem sub_eq_add_neg (l₁ l₂ : LinearCombo) : l₁ - l₂ = l₁ + -l₂ := by
  rcases l₁ with ⟨a₁, c₁⟩; rcases l₂ with ⟨a₂, c₂⟩
  apply ext
  · simp [Int.sub_eq_add_neg]
  · simp [Coeffs.sub_eq_add_neg]

/-- Implementation of scalar multiplication of a `LinearCombo` by an `Int`. -/
def smul (lc : LinearCombo) (i : Int) : LinearCombo where
  const := i * lc.const
  coeffs := lc.coeffs.smul i

instance : HMul Int LinearCombo LinearCombo := ⟨fun i lc => lc.smul i⟩

@[simp] theorem smul_const {lc : LinearCombo} {i : Int} : (i * lc).const = i * lc.const := rfl
@[simp] theorem smul_coeffs {lc : LinearCombo} {i : Int} : (i * lc).coeffs = i * lc.coeffs := rfl

@[simp] theorem add_eval (l₁ l₂ : LinearCombo) (v : Coeffs) :
    (l₁ + l₂).eval v = l₁.eval v + l₂.eval v := by
  rcases l₁ with ⟨r₁, c₁⟩; rcases l₂ with ⟨r₂, c₂⟩
  simp only [eval, add_const, add_coeffs, Int.add_assoc, Int.add_left_comm]
  congr
  exact Coeffs.dot_distrib_left c₁ c₂ v

@[simp] theorem neg_eval (lc : LinearCombo) (v : Coeffs) : (-lc).eval v = - lc.eval v := by
  rcases lc with ⟨a, coeffs⟩
  simp [eval, Int.neg_add]

@[simp] theorem sub_eval (l₁ l₂ : LinearCombo) (v : Coeffs) :
    (l₁ - l₂).eval v = l₁.eval v - l₂.eval v := by
  simp [sub_eq_add_neg, Int.sub_eq_add_neg]

@[simp] theorem smul_eval (lc : LinearCombo) (i : Int) (v : Coeffs) :
    (i * lc).eval v = i * lc.eval v := by
  rcases lc with ⟨a, coeffs⟩
  simp [eval, Int.mul_add]

theorem smul_eval_comm (lc : LinearCombo) (i : Int) (v : Coeffs) :
    (i * lc).eval v = lc.eval v * i := by
  simp [Int.mul_comm]

/--
Multiplication of two linear combinations.
This is useful only if at least one of the linear combinations is constant,
and otherwise should be considered as a junk value.
-/
def mul (l₁ l₂ : LinearCombo) : LinearCombo :=
  l₂.const * l₁ + l₁.const * l₂ - { const := l₁.const * l₂.const }

theorem mul_eval_of_const_left (l₁ l₂ : LinearCombo) (v : Coeffs) (w : l₁.coeffs.isZero) :
    (mul l₁ l₂).eval v = l₁.eval v * l₂.eval v := by
  have : Coeffs.dot l₁.coeffs v = 0 := IntList.dot_of_left_zero w
  simp [mul, eval, this, Coeffs.sub_eq_add_neg, Coeffs.dot_distrib_left, Int.add_mul, Int.mul_add,
    Int.mul_comm]

theorem mul_eval_of_const_right (l₁ l₂ : LinearCombo) (v : Coeffs) (w : l₂.coeffs.isZero) :
    (mul l₁ l₂).eval v = l₁.eval v * l₂.eval v := by
  have : Coeffs.dot l₂.coeffs v = 0 := IntList.dot_of_left_zero w
  simp [mul, eval, this, Coeffs.sub_eq_add_neg, Coeffs.dot_distrib_left, Int.add_mul, Int.mul_add,
    Int.mul_comm]

theorem mul_eval (l₁ l₂ : LinearCombo) (v : Coeffs) (w : l₁.coeffs.isZero ∨ l₂.coeffs.isZero) :
    (mul l₁ l₂).eval v = l₁.eval v * l₂.eval v := by
  rcases w with w | w
  · rw [mul_eval_of_const_left _ _ _ w]
  · rw [mul_eval_of_const_right _ _ _ w]

end LinearCombo

end Lean.Omega
