/-
Copyright (c) 2014 Parikshit Khanna. All rights reserved.
Released under Apache 2.0 license as described in the file LICENSE.
Authors: Parikshit Khanna, Jeremy Avigad, Leonardo de Moura, Floris van Doorn, Mario Carneiro
-/
module

prelude
public import Init.Data.List.TakeDrop
public import Init.Data.Function

public section

/-!
# Lemmas about `List.zip`, `List.zipWith`, `List.zipWithAll`, and `List.unzip`.
-/

set_option linter.listVariables true -- Enforce naming conventions for `List`/`Array`/`Vector` variables.
set_option linter.indexVariables true -- Enforce naming conventions for index variables.

namespace List

open Nat

/-! ## Zippers -/

/-! ### zipWith -/

theorem zipWith_comm {f : α → β → γ} :
    ∀ {as : List α} {bs : List β}, zipWith f as bs = zipWith (fun b a => f a b) bs as
  | [], _ => List.zipWith_nil_right.symm
  | _ :: _, [] => rfl
  | _ :: _, _ :: _ => congrArg _ zipWith_comm

theorem zipWith_comm_of_comm {f : α → α → β} (comm : ∀ x y : α, f x y = f y x) {l l' : List α} :
    zipWith f l l' = zipWith f l' l := by
  rw [zipWith_comm]
  simp only [comm]

@[simp]
theorem zipWith_self {f : α → α → δ} : ∀ {l : List α}, zipWith f l l = l.map fun a => f a a
  | [] => rfl
  | _ :: _ => congrArg _ zipWith_self

@[deprecated zipWith_self (since := "2025-01-29")] abbrev zipWith_same := @zipWith_self

/--
See also `getElem?_zipWith'` for a variant
using `Option.map` and `Option.bind` rather than a `match`.
-/
@[grind =]
theorem getElem?_zipWith {f : α → β → γ} {i : Nat} :
    (zipWith f as bs)[i]? = match as[i]?, bs[i]? with
      | some a, some b => some (f a b) | _, _ => none := by
  induction as generalizing bs i with
  | nil => cases bs with
    | nil => simp
    | cons b bs => simp
  | cons a as aih => cases bs with
    | nil => simp
    | cons b bs => cases i <;> simp_all

/-- Variant of `getElem?_zipWith` using `Option.map` and `Option.bind` rather than a `match`. -/
theorem getElem?_zipWith' {f : α → β → γ} {i : Nat} :
    (zipWith f l₁ l₂)[i]? = (l₁[i]?.map f).bind fun g => l₂[i]?.map g := by
  induction l₁ generalizing l₂ i with
  | nil => rw [zipWith] <;> simp
  | cons _ _ =>
    cases l₂
    · simp
    · cases i <;> simp_all

theorem getElem?_zipWith_eq_some {f : α → β → γ} {l₁ : List α} {l₂ : List β} {z : γ} {i : Nat} :
    (zipWith f l₁ l₂)[i]? = some z ↔
      ∃ x y, l₁[i]? = some x ∧ l₂[i]? = some y ∧ f x y = z := by
  induction l₁ generalizing l₂ i
  · simp
  · cases l₂ <;> cases i <;> simp_all

theorem getElem?_zip_eq_some {l₁ : List α} {l₂ : List β} {z : α × β} {i : Nat} :
    (zip l₁ l₂)[i]? = some z ↔ l₁[i]? = some z.1 ∧ l₂[i]? = some z.2 := by
  cases z
  rw [zip, getElem?_zipWith_eq_some]; constructor
  · rintro ⟨x, y, h₀, h₁, h₂⟩
    simpa [h₀, h₁] using h₂
  · rintro ⟨h₀, h₁⟩
    exact ⟨_, _, h₀, h₁, rfl⟩

@[grind =]
theorem head?_zipWith {f : α → β → γ} :
    (List.zipWith f as bs).head? = match as.head?, bs.head? with
      | some a, some b => some (f a b) | _, _ => none := by
  simp [head?_eq_getElem?, getElem?_zipWith]

@[grind =]
theorem head_zipWith {f : α → β → γ} (h):
    (List.zipWith f as bs).head h = f (as.head (by rintro rfl; simp_all)) (bs.head (by rintro rfl; simp_all)) := by
  apply Option.some.inj
  rw [← head?_eq_head, head?_zipWith, head?_eq_head, head?_eq_head]

@[simp, grind =]
theorem zipWith_map {μ} {f : γ → δ → μ} {g : α → γ} {h : β → δ} {l₁ : List α} {l₂ : List β} :
    zipWith f (l₁.map g) (l₂.map h) = zipWith (fun a b => f (g a) (h b)) l₁ l₂ := by
  induction l₁ generalizing l₂ <;> cases l₂ <;> simp_all

@[grind =]
theorem zipWith_map_left {l₁ : List α} {l₂ : List β} {f : α → α'} {g : α' → β → γ} :
    zipWith g (l₁.map f) l₂ = zipWith (fun a b => g (f a) b) l₁ l₂ := by
  induction l₁ generalizing l₂ <;> cases l₂ <;> simp_all

@[grind =]
theorem zipWith_map_right {l₁ : List α} {l₂ : List β} {f : β → β'} {g : α → β' → γ} :
    zipWith g l₁ (l₂.map f) = zipWith (fun a b => g a (f b)) l₁ l₂ := by
  induction l₁ generalizing l₂ <;> cases l₂ <;> simp_all

@[grind =]
theorem zipWith_foldr_eq_zip_foldr {f : α → β → γ} {i : δ} {g : γ → δ → δ} :
    (zipWith f l₁ l₂).foldr g i = (zip l₁ l₂).foldr (fun p r => g (f p.1 p.2) r) i := by
  induction l₁ generalizing l₂ <;> cases l₂ <;> simp_all

@[grind =]
theorem zipWith_foldl_eq_zip_foldl {f : α → β → γ} {i : δ} {g : δ → γ → δ} :
    (zipWith f l₁ l₂).foldl g i = (zip l₁ l₂).foldl (fun r p => g r (f p.1 p.2)) i := by
  induction l₁ generalizing i l₂ <;> cases l₂ <;> simp_all

@[simp]
theorem zipWith_eq_nil_iff {f : α → β → γ} {l l'} : zipWith f l l' = [] ↔ l = [] ∨ l' = [] := by
  cases l <;> cases l' <;> simp

@[grind =]
theorem map_zipWith {δ : Type _} {f : α → β} {g : γ → δ → α} {l : List γ} {l' : List δ} :
    map f (zipWith g l l') = zipWith (fun x y => f (g x y)) l l' := by
  induction l generalizing l' with
  | nil => simp
  | cons hd tl hl =>
    · cases l'
      · simp
      · simp [hl]

@[grind =]
theorem take_zipWith : (zipWith f l l').take i = zipWith f (l.take i) (l'.take i) := by
  induction l generalizing l' i with
  | nil => simp
  | cons hd tl hl =>
    cases l'
    · simp
    · cases i
      · simp
      · simp [hl]

@[grind =]
theorem drop_zipWith : (zipWith f l l').drop i = zipWith f (l.drop i) (l'.drop i) := by
  induction l generalizing l' i with
  | nil => simp
  | cons hd tl hl =>
    · cases l'
      · simp
      · cases i
        · simp
        · simp [hl]

@[simp, grind =]
theorem tail_zipWith : (zipWith f l l').tail = zipWith f l.tail l'.tail := by
  rw [← drop_one]; simp [drop_zipWith]

@[grind =]
theorem zipWith_append {f : α → β → γ} {l₁ l₁' : List α} {l₂ l₂' : List β}
    (h : l₁.length = l₂.length) :
    zipWith f (l₁ ++ l₁') (l₂ ++ l₂') = zipWith f l₁ l₂ ++ zipWith f l₁' l₂' := by
  induction l₁ generalizing l₂ with
  | nil =>
    have : l₂ = [] := eq_nil_of_length_eq_zero (by simpa using h.symm)
    simp [this]
  | cons hl tl ih =>
    cases l₂ with
    | nil => simp at h
    | cons _ _ =>
      simp only [length_cons, Nat.succ.injEq] at h
      simp [ih h]

theorem zipWith_eq_cons_iff {f : α → β → γ} {l₁ : List α} {l₂ : List β} :
    zipWith f l₁ l₂ = g :: l ↔
      ∃ a l₁' b l₂', l₁ = a :: l₁' ∧ l₂ = b :: l₂' ∧ g = f a b ∧ l = zipWith f l₁' l₂' := by
  match l₁, l₂ with
  | [], [] => simp
  | [], b :: l₂ => simp
  | a :: l₁, [] => simp
  | a' :: l₁, b' :: l₂ =>
    simp only [cons.injEq]
    constructor
    · rintro ⟨⟨rfl, rfl⟩, rfl⟩
      refine ⟨a', l₁, b', l₂, by simp⟩
    · rintro ⟨a, l₁, b, l₂, ⟨rfl, rfl⟩, ⟨rfl, rfl⟩, rfl, rfl⟩
      simp

theorem zipWith_eq_append_iff {f : α → β → γ} {l₁ : List α} {l₂ : List β} :
    zipWith f l₁ l₂ = l₁' ++ l₂' ↔
      ∃ ws xs ys zs, ws.length = ys.length ∧ l₁ = ws ++ xs ∧ l₂ = ys ++ zs ∧ l₁' = zipWith f ws ys ∧ l₂' = zipWith f xs zs := by
  induction l₁ generalizing l₂ l₁' with
  | nil =>
    simp
    constructor
    · rintro ⟨rfl, rfl⟩
      exact ⟨[], [], [], by simp⟩
    · rintro ⟨_, _, _, -, ⟨rfl, rfl⟩, _, rfl, rfl, rfl⟩
      simp
  | cons x₁ l₁ ih₁ =>
    cases l₂ with
    | nil =>
      constructor
      · simp only [zipWith_nil_right, nil_eq, append_eq_nil_iff, exists_and_left, and_imp]
        rintro rfl  rfl
        exact ⟨[], x₁ :: l₁, [], by simp⟩
      · rintro ⟨_, _, _, _, h₁, _, h₃, rfl, rfl⟩
        simp only [nil_eq, append_eq_nil_iff] at h₃
        obtain ⟨rfl, rfl⟩ := h₃
        simp
    | cons x₂ l₂ =>
      simp only [zipWith_cons_cons]
      rw [cons_eq_append_iff]
      constructor
      · rintro (⟨rfl, rfl⟩ | ⟨_, rfl, h⟩)
        · exact ⟨[], x₁ :: l₁, [], x₂ :: l₂, by simp⟩
        · rw [ih₁] at h
          obtain ⟨ws, xs, ys, zs, h, rfl, rfl, h', rfl⟩ := h
          refine ⟨x₁ :: ws, xs, x₂ :: ys, zs, by simp [h, h']⟩
      · rintro ⟨_, _, _, _, h₁, h₂, h₃, rfl, rfl⟩
        rw [cons_eq_append_iff] at h₂
        rw [cons_eq_append_iff] at h₃
        obtain (⟨rfl, rfl⟩ | ⟨_, rfl, rfl⟩) := h₂
        · simp only [zipWith_nil_left, true_and, nil_eq, reduceCtorEq, false_and, exists_const,
          or_false]
          obtain (⟨rfl, rfl⟩ | ⟨_, rfl, rfl⟩) := h₃
          · simp
          · simp_all
        · obtain (⟨rfl, rfl⟩ | ⟨_, rfl, rfl⟩) := h₃
          · simp_all
          · simp_all [zipWith_append, Nat.succ_inj]

/-- See also `List.zipWith_replicate` in `Init.Data.List.TakeDrop` for a generalization with different lengths. -/
@[simp] theorem zipWith_replicate' {a : α} {b : β} {n : Nat} :
    zipWith f (replicate n a) (replicate n b) = replicate n (f a b) := by
  induction n with
  | zero => rfl
  | succ n ih => simp [replicate_succ, ih]

theorem map_uncurry_zip_eq_zipWith {f : α → β → γ} {l : List α} {l' : List β} :
    map (Function.uncurry f) (l.zip l') = zipWith f l l' := by
  rw [zip]
  induction l generalizing l' with
  | nil => simp
  | cons hl tl ih =>
    cases l' <;> simp [ih]

theorem map_zip_eq_zipWith {f : α × β → γ} {l : List α} {l' : List β} :
    map f (l.zip l') = zipWith (Function.curry f) l l' := by
  rw [zip]
  induction l generalizing l' with
  | nil => simp
  | cons hl tl ih =>
    cases l' <;> simp [ih]

/-! ### zip -/

theorem zip_eq_zipWith : ∀ {l₁ : List α} {l₂ : List β}, zip l₁ l₂ = zipWith Prod.mk l₁ l₂
  | [], _ => rfl
  | _, [] => rfl
  | a :: l₁, b :: l₂ => by simp [zip_cons_cons, zip_eq_zipWith (l₁ := l₁)]

@[grind _=_]
theorem zip_map {f : α → γ} {g : β → δ} :
    ∀ {l₁ : List α} {l₂ : List β}, zip (l₁.map f) (l₂.map g) = (zip l₁ l₂).map (Prod.map f g)
  | [], _ => rfl
  | _, [] => by simp only [map, zip_nil_right]
  | _ :: _, _ :: _ => by simp only [map, zip_cons_cons, zip_map, Prod.map]

@[grind _=_]
theorem zip_map_left {f : α → γ} {l₁ : List α} {l₂ : List β} :
    zip (l₁.map f) l₂ = (zip l₁ l₂).map (Prod.map f id) := by rw [← zip_map, map_id]

@[grind _=_]
theorem zip_map_right {f : β → γ} {l₁ : List α} {l₂ : List β} :
    zip l₁ (l₂.map f) = (zip l₁ l₂).map (Prod.map id f) := by rw [← zip_map, map_id]

@[simp, grind =] theorem tail_zip {l₁ : List α} {l₂ : List β} :
    (zip l₁ l₂).tail = zip l₁.tail l₂.tail := by
  cases l₁ <;> cases l₂ <;> simp

@[grind =]
theorem zip_append :
    ∀ {l₁ r₁ : List α} {l₂ r₂ : List β} (_h : length l₁ = length l₂),
      zip (l₁ ++ r₁) (l₂ ++ r₂) = zip l₁ l₂ ++ zip r₁ r₂
  | [], _, _, _, h => by simp only [eq_nil_of_length_eq_zero h.symm]; rfl
  | _, _, [], _, h => by simp only [eq_nil_of_length_eq_zero h]; rfl
  | _ :: _, _, _ :: _, _, h => by
    simp only [cons_append, zip_cons_cons, zip_append (Nat.succ.inj h)]

@[grind =]
theorem zip_map' {f : α → β} {g : α → γ} :
    ∀ {l : List α}, zip (l.map f) (l.map g) = l.map fun a => (f a, g a)
  | [] => rfl
  | a :: l => by simp only [map, zip_cons_cons, zip_map']

theorem of_mem_zip {a b} : ∀ {l₁ : List α} {l₂ : List β}, (a, b) ∈ zip l₁ l₂ → a ∈ l₁ ∧ b ∈ l₂
  | _ :: l₁, _ :: l₂, h => by
    cases h
    case head => simp
    case tail h =>
    · have := of_mem_zip h
      exact ⟨Mem.tail _ this.1, Mem.tail _ this.2⟩

theorem map_fst_zip :
    ∀ {l₁ : List α} {l₂ : List β}, l₁.length ≤ l₂.length → map Prod.fst (zip l₁ l₂) = l₁
  | [], _, _ => rfl
  | _ :: as, _ :: bs, h => by
    simp [Nat.succ_le_succ_iff] at h
    change _ :: map Prod.fst (zip as bs) = _ :: as
    rw [map_fst_zip (l₁ := as) h]
  | _ :: _, [], h => by simp at h

theorem map_snd_zip :
    ∀ {l₁ : List α} {l₂ : List β}, l₂.length ≤ l₁.length → map Prod.snd (zip l₁ l₂) = l₂
  | _, [], _ => by
    rw [zip_nil_right]
    rfl
  | [], b :: bs, h => by simp at h
  | a :: as, b :: bs, h => by
    simp [Nat.succ_le_succ_iff] at h
    change _ :: map Prod.snd (zip as bs) = _ :: bs
    rw [map_snd_zip (l₂ := bs) h]

theorem map_prod_left_eq_zip {l : List α} {f : α → β} :
    (l.map fun x => (x, f x)) = l.zip (l.map f) := by
  rw [← zip_map']
  congr
  simp

theorem map_prod_right_eq_zip {l : List α} {f : α → β} :
    (l.map fun x => (f x, x)) = (l.map f).zip l := by
  rw [← zip_map']
  congr
  simp

@[simp] theorem zip_eq_nil_iff {l₁ : List α} {l₂ : List β} :
    zip l₁ l₂ = [] ↔ l₁ = [] ∨ l₂ = [] := by
  simp [zip_eq_zipWith]

theorem zip_eq_cons_iff {l₁ : List α} {l₂ : List β} :
    zip l₁ l₂ = (a, b) :: l ↔
      ∃ l₁' l₂', l₁ = a :: l₁' ∧ l₂ = b :: l₂' ∧ l = zip l₁' l₂' := by
  simp only [zip_eq_zipWith, zipWith_eq_cons_iff]
  constructor
  · rintro ⟨a, l₁, b, l₂, rfl, rfl, h, rfl, rfl⟩
    simp only [Prod.mk.injEq] at h
    obtain ⟨rfl, rfl⟩ := h
    simp
  · rintro ⟨l₁', l₂', rfl, rfl, rfl⟩
    refine ⟨a, l₁', b, l₂', by simp⟩

theorem zip_eq_append_iff {l₁ : List α} {l₂ : List β} :
    zip l₁ l₂ = l₁' ++ l₂' ↔
      ∃ ws xs ys zs, ws.length = ys.length ∧ l₁ = ws ++ xs ∧ l₂ = ys ++ zs ∧ l₁' = zip ws ys ∧ l₂' = zip xs zs := by
  simp [zip_eq_zipWith, zipWith_eq_append_iff]

/-- See also `List.zip_replicate` in `Init.Data.List.TakeDrop` for a generalization with different lengths. -/
@[simp] theorem zip_replicate' {a : α} {b : β} {n : Nat} :
    zip (replicate n a) (replicate n b) = replicate n (a, b) := by
  induction n with
  | zero => rfl
  | succ n ih => simp [replicate_succ, ih]

/-! ### zipWithAll -/

@[grind =]
theorem getElem?_zipWithAll {f : Option α → Option β → γ} {i : Nat} :
    (zipWithAll f as bs)[i]? = match as[i]?, bs[i]? with
      | none, none => .none | a?, b? => some (f a? b?) := by
  induction as generalizing bs i with
  | nil => induction bs generalizing i with
    | nil => simp
    | cons b bs bih => cases i <;> simp_all
  | cons a as aih => cases bs with
    | nil =>
      specialize @aih []
      cases i <;> simp_all
    | cons b bs => cases i <;> simp_all

@[grind =]
theorem head?_zipWithAll {f : Option α → Option β → γ} :
    (zipWithAll f as bs).head? = match as.head?, bs.head? with
      | none, none => .none | a?, b? => some (f a? b?) := by
  simp [head?_eq_getElem?, getElem?_zipWithAll]

@[simp, grind =] theorem head_zipWithAll {f : Option α → Option β → γ} (h) :
    (zipWithAll f as bs).head h = f as.head? bs.head? := by
  apply Option.some.inj
  rw [← head?_eq_head, head?_zipWithAll]
  split <;> simp_all

@[simp, grind =] theorem tail_zipWithAll {f : Option α → Option β → γ} :
    (zipWithAll f as bs).tail = zipWithAll f as.tail bs.tail := by
  cases as <;> cases bs <;> simp

@[grind =]
theorem zipWithAll_map {μ} {f : Option γ → Option δ → μ} {g : α → γ} {h : β → δ} {l₁ : List α} {l₂ : List β} :
    zipWithAll f (l₁.map g) (l₂.map h) = zipWithAll (fun a b => f (g <$> a) (h <$> b)) l₁ l₂ := by
  induction l₁ generalizing l₂ <;> cases l₂ <;> simp_all

@[grind =]
theorem zipWithAll_map_left {l₁ : List α} {l₂ : List β} {f : α → α'} {g : Option α' → Option β → γ} :
    zipWithAll g (l₁.map f) l₂ = zipWithAll (fun a b => g (f <$> a) b) l₁ l₂ := by
  induction l₁ generalizing l₂ <;> cases l₂ <;> simp_all

@[grind =]
theorem zipWithAll_map_right {l₁ : List α} {l₂ : List β} {f : β → β'} {g : Option α → Option β' → γ} :
    zipWithAll g l₁ (l₂.map f) = zipWithAll (fun a b => g a (f <$> b)) l₁ l₂ := by
  induction l₁ generalizing l₂ <;> cases l₂ <;> simp_all

@[grind =]
theorem map_zipWithAll {δ : Type _} {f : α → β} {g : Option γ → Option δ → α} {l : List γ} {l' : List δ} :
    map f (zipWithAll g l l') = zipWithAll (fun x y => f (g x y)) l l' := by
  induction l generalizing l' with
  | nil => simp
  | cons hd tl hl =>
    cases l' <;> simp_all

@[simp, grind =] theorem zipWithAll_replicate {a : α} {b : β} {n : Nat} :
    zipWithAll f (replicate n a) (replicate n b) = replicate n (f (some a) (some b)) := by
  induction n with
  | zero => rfl
  | succ n ih => simp [replicate_succ, ih]

/-! ### unzip -/

@[simp, grind =] theorem unzip_fst : (unzip l).fst = l.map Prod.fst := by
  induction l <;> simp_all

@[simp, grind =] theorem unzip_snd : (unzip l).snd = l.map Prod.snd := by
  induction l <;> simp_all

@[grind =]
theorem unzip_eq_map : ∀ {l : List (α × β)}, unzip l = (l.map Prod.fst, l.map Prod.snd)
  | [] => rfl
  | (a, b) :: l => by simp only [unzip_cons, map_cons, unzip_eq_map (l := l)]

-- The argument `l` is explicit so we can rewrite from right to left.
theorem zip_unzip : ∀ l : List (α × β), zip (unzip l).1 (unzip l).2 = l
  | [] => rfl
  | (a, b) :: l => by simp only [unzip_cons, zip_cons_cons, zip_unzip l]

theorem unzip_zip_left :
    ∀ {l₁ : List α} {l₂ : List β}, length l₁ ≤ length l₂ → (unzip (zip l₁ l₂)).1 = l₁
  | [], _, _ => rfl
  | _, [], h => by rw [eq_nil_of_length_eq_zero (Nat.eq_zero_of_le_zero h)]; rfl
  | _ :: _, _ :: _, h => by
    simp only [zip_cons_cons, unzip_cons, unzip_zip_left (le_of_succ_le_succ h)]

theorem unzip_zip_right :
    ∀ {l₁ : List α} {l₂ : List β}, length l₂ ≤ length l₁ → (unzip (zip l₁ l₂)).2 = l₂
  | [], l₂, _ => by simp_all
  | l₁, [], _ => by simp
  | a :: l₁, b :: l₂, h => by
    simp only [zip_cons_cons, unzip_cons, unzip_zip_right (le_of_succ_le_succ h)]

theorem unzip_zip {l₁ : List α} {l₂ : List β} (h : length l₁ = length l₂) :
    unzip (zip l₁ l₂) = (l₁, l₂) := by
  ext
  · rw [unzip_zip_left (Nat.le_of_eq h)]
  · rw [unzip_zip_right (Nat.le_of_eq h.symm)]

theorem zip_of_prod {l : List α} {l' : List β} {xs : List (α × β)} (hl : xs.map Prod.fst = l)
    (hr : xs.map Prod.snd = l') : xs = l.zip l' := by
  rw [← hl, ← hr, ← zip_unzip xs, ← unzip_fst, ← unzip_snd, zip_unzip, zip_unzip]

theorem tail_zip_fst {l : List (α × β)} : l.unzip.1.tail = l.tail.unzip.1 := by
  simp

theorem tail_zip_snd {l : List (α × β)} : l.unzip.2.tail = l.tail.unzip.2 := by
  simp

@[simp, grind =] theorem unzip_replicate {n : Nat} {a : α} {b : β} :
    unzip (replicate n (a, b)) = (replicate n a, replicate n b) := by
  ext1 <;> simp
