import Mathlib.RingTheory.Polynomial.Basic
import Goppadecoding.poly
import Goppadecoding.diff

namespace Goppadecoding

open Polynomial BigOperators Finset

section shiftpoly

noncomputable def shiftpoly (k: Type _) [Field k] (r: k) (f: k[X]): k[X] :=
  aeval (X + C r) f

theorem shiftpoly_zero (k: Type _) [Field k] (r: k):
  shiftpoly k r 0 = 0 :=
by unfold shiftpoly; simp only [map_zero]

theorem shiftpoly_one (k: Type _) [Field k] (r: k):
  shiftpoly k r 1 = 1 :=
by unfold shiftpoly; simp only [map_one]

theorem shiftpoly_neg (k: Type _) [Field k] (r: k) (g: k[X]):
  shiftpoly k r (-g) = - shiftpoly k r g :=
by unfold shiftpoly; simp only [map_neg]

theorem shiftpoly_add (k: Type _) [Field k] (r: k) (f g: k[X]):
  shiftpoly k r (f+g) = shiftpoly k r f + shiftpoly k r g :=
by unfold shiftpoly; simp only [map_add]

theorem shiftpoly_sub (k: Type _) [Field k] (r: k) (f g: k[X]):
  shiftpoly k r (f-g) = shiftpoly k r f - shiftpoly k r g :=
by unfold shiftpoly; simp only [map_sub]

theorem shiftpoly_mul (k: Type _) [Field k] (r: k) (f g: k[X]):
  shiftpoly k r (f*g) = shiftpoly k r f * shiftpoly k r g :=
by unfold shiftpoly; simp only [map_mul]

theorem shiftpoly_C (k: Type _) [Field k] (r c: k):
  shiftpoly k r (C c) = C c :=
by
  unfold shiftpoly
  rw[aeval_C]
  unfold algebraMap
  unfold Algebra.toRingHom
  unfold algebraOfAlgebra
  simp only [Algebra.id.map_eq_id, RingHomCompTriple.comp_eq]

theorem shiftpoly_X (k: Type _) [Field k] (r: k):
  shiftpoly k r X = X + C r :=
by
  unfold shiftpoly
  rw[aeval_X]

theorem shiftpoly_X_sub_C (k: Type _) [Field k] (r c: k):
  shiftpoly k r (X - C c) = X - C (c - r) :=
by
  rw[shiftpoly_sub,shiftpoly_C,shiftpoly_X]
  simp only [map_sub]
  group

-- could do this via map_sum by setting up more types
theorem shiftpoly_sum (k: Type _) [Field k] (r: k) {U: Type _} [DecidableEq U] {f: U → k[X]}:
  ∀ (S: Finset U),
  shiftpoly k r (∑ s in S, f s) = ∑ s in S, shiftpoly k r (f s) :=
by
  apply Finset.induction
  . simp only [sum_empty, forall_const, shiftpoly_zero]
  intro S s notsinS prevcase
  rw[sum_insert,shiftpoly_add]
  rw[sum_insert,prevcase]
  repeat exact notsinS

-- could do this via map_prod by setting up more types
theorem shiftpoly_prod (k: Type _) [Field k] (r: k) {U: Type _} [DecidableEq U] {f: U → k[X]}:
  ∀ (S: Finset U),
  shiftpoly k r (∏ s in S, f s) = ∏ s in S, shiftpoly k r (f s) :=
by
  apply Finset.induction
  . simp only [prod_empty, forall_const, shiftpoly_one]
  intro S s notsinS prevcase
  rw[prod_insert,shiftpoly_mul]
  rw[prod_insert,prevcase]
  repeat exact notsinS

-- lemma getting towards shiftpoly_degree
theorem shiftpoly_degree_le (k: Type _) [Field k] (r: k) (f: k[X]):
  (shiftpoly k r f).degree ≤ f.degree :=
by
  by_cases f0: f = 0
  . rw[f0]
    rw[shiftpoly_zero,degree_zero]
  unfold shiftpoly
  rw[aeval_eq_sum_range]
  calc
    _ ≤ (range (f.natDegree + 1)).sup (fun i ↦ (coeff f i • (X + C r) ^ i).degree) := by apply degree_sum_le
    _ ≤ f.degree := by
          apply Finset.sup_le
          intro j jinrange
          rw[mem_range] at jinrange
          have fdegdeg: f.degree = f.natDegree := degree_eq_natDegree f0
          rw[fdegdeg]
          calc
            _ = degree (C (coeff f j) * (X + C r) ^ j) := by rw[C_mul']
            _ ≤ degree ((X + C r) ^ j) := degree_C_mul_le_degree _ _
            _ = j := by simp only [degree_pow, degree_X_add_C, nsmul_eq_mul, mul_one]
            _ ≤ _ := by norm_cast; exact Nat.lt_succ.mp jinrange

theorem shiftpoly_X_pow_degree (k: Type _) [Field k] (r s: k) (d: ℕ) (snonzero: s ≠ 0):
  (shiftpoly k r (C s * X^d)).degree = d :=
by
  unfold shiftpoly
  simp only [map_mul, aeval_C, map_pow, aeval_X, degree_mul, degree_pow, degree_X_add_C, nsmul_eq_mul, mul_one]
  unfold algebraMap
  unfold Algebra.toRingHom
  unfold algebraOfAlgebra
  simp[snonzero]

theorem shiftpoly_degree (k: Type _) [Field k] (r: k) (f: k[X]):
  (shiftpoly k r f).degree = f.degree :=
by
  by_cases f0: f = 0
  . rw[f0]
    rw[shiftpoly_zero,degree_zero]
  have leadnonzero: f.leadingCoeff ≠ 0 := leadingCoeff_ne_zero.mpr f0
  let g := C f.leadingCoeff * X^f.natDegree
  have fgdeg: (f-g).degree < f.degree := by
    apply degree_sub_lt
    . simp only [degree_mul, ne_eq, leadingCoeff_eq_zero, f0, not_false_eq_true, degree_C, degree_pow, degree_X, nsmul_eq_mul, mul_one, zero_add]
      exact degree_eq_natDegree f0
    . simp only [ne_eq, f0, not_false_eq_true]
    . simp only [leadingCoeff_mul, leadingCoeff_C, monic_X_pow, Monic.leadingCoeff, mul_one]
  have shiftgdeg: (shiftpoly k r g).degree = f.degree := by calc
    _ = (f.natDegree: WithBot ℕ) := shiftpoly_X_pow_degree k r f.leadingCoeff f.natDegree leadnonzero
    _ = f.degree := Eq.symm (degree_eq_natDegree f0)
  have shiftfgdeg: (shiftpoly k r (f-g)).degree < (shiftpoly k r g).degree := by calc
    _ ≤ (f-g).degree := shiftpoly_degree_le k r (f-g)
    _ < f.degree := fgdeg
    _ = (shiftpoly k r g).degree := shiftgdeg.symm
  calc
    _ = degree (shiftpoly k r (g+(f-g))) := by group
    _ = degree (shiftpoly k r g + shiftpoly k r (f-g)) := by rw[shiftpoly_add]
    _ = (shiftpoly k r g).degree := degree_add_eq_if_degree_lt shiftfgdeg
    _ = f.degree := shiftgdeg

theorem shiftpoly_monomial (k: Type _) [Field k] (r: k) (n: ℕ) (c: k):
  shiftpoly k r (((monomial: ℕ → k →ₗ[k] k[X]) n) c)
  = C c * (X + C r)^n :=
by
  unfold shiftpoly
  rw[aeval_monomial]
  apply congr_arg
  simp only

theorem shiftpoly_derivative_monomial (k: Type _) [Field k] (r: k) (n: ℕ) (c: k):
  (derivative: k[X] →ₗ[k] k[X]) (shiftpoly k r (((monomial: ℕ → k →ₗ[k] k[X]) n) c))
  = shiftpoly k r ((derivative: k[X] →ₗ[k] k[X]) (((monomial: ℕ → k →ₗ[k] k[X]) n) c)) :=
by
  rw[derivative_monomial]
  repeat rw[shiftpoly_monomial]
  rw[derivative_mul,derivative_pow]
  simp only [derivative_C, zero_mul, map_natCast, ge_iff_le, map_add, derivative_X, add_zero, mul_one, zero_add, map_mul]
  rw[mul_assoc]

theorem shiftpoly_diff (k: Type _) [Field k] (r: k) (f: k[X]):
  (shiftpoly k r f).diff = shiftpoly k r f.diff :=
by
  have fsum: f = ∑ i in range (f.natDegree + 1), (monomial: ℕ → k →ₗ[k] k[X]) i (coeff f i) := as_sum_range f
  rw[fsum]
  rw[diff,AddHom.toFun_eq_coe,LinearMap.coe_toAddHom]
  rw[shiftpoly_sum,derivative_sum,derivative_sum,shiftpoly_sum]
  apply sum_congr; simp only
  intro x _
  rw[shiftpoly_derivative_monomial]

theorem shiftpoly_eval (k: Type _) [Field k] (r c: k) (f: k[X]):
  (shiftpoly k r f).eval c = f.eval (c + r) :=
by
  have fsum: f = ∑ i in range (f.natDegree + 1), (monomial: ℕ → k →ₗ[k] k[X]) i (coeff f i) := as_sum_range f
  rw[fsum]
  calc
    _ = (shiftpoly k r (∑ i in range (f.natDegree + 1), (monomial: ℕ → k →ₗ[k] k[X]) i (coeff f i))).eval c := rfl
    _ = (∑ i in range (f.natDegree + 1), shiftpoly k r ((monomial: ℕ → k →ₗ[k] k[X]) i (coeff f i))).eval c := by rw[shiftpoly_sum]
    _ = (∑ i in range (f.natDegree + 1), C (coeff f i) * (X + C r)^i).eval c := by
          apply congr_arg
          apply sum_congr; simp only
          intro i _
          rw[shiftpoly_monomial]
    _ = ∑ i in range (f.natDegree + 1), ((eval c: k[X] → k) (C (coeff f i) * (X + C r)^i)) := by rw[eval_finset_sum]
    _ = ∑ i in range (f.natDegree + 1), (coeff f i) * (c + r)^i := by simp only [eval_mul, eval_C, eval_pow, eval_add, eval_X]
    _ = ∑ i in range (f.natDegree + 1), ((eval (c+r): k[X] → k) (C (coeff f i) * X^i)) := by simp only [eval_mul, eval_C, eval_pow, eval_X]
    _ = (∑ i in range (f.natDegree + 1), C (coeff f i) * X^i).eval (c+r) := by rw[eval_finset_sum]
    _ = _ := by
          apply congr_arg
          apply sum_congr; simp only
          intro i _
          rw[C_mul_X_pow_eq_monomial]

end shiftpoly
