import «Common»

namespace Day13

------------------------------------------------------------------------------------

inductive Tile
| Sand : Tile
| Rock : Tile
deriving Repr, BEq

instance : LawfulBEq Tile where
  rfl := λ{a} ↦ by cases a <;> decide
  eq_of_beq := by intros a b; cases a <;> cases b <;> simp <;> decide

instance: ToString Tile where
toString := λ
| Tile.Sand => "."
| Tile.Rock => "#"

structure ParseCharError where
  unexpectedCharacter : Char

instance : ToString ParseCharError where
toString := λc ↦ s!"Unexpected Character '{c.unexpectedCharacter}', expected '.' or '#'."

private def Tile.ofChar? : Char → Except ParseCharError Tile
| '.' => pure Tile.Sand
| '#' => pure Tile.Rock
| c => throw {unexpectedCharacter := c}

abbrev ParsedInput := List $ Parsing.RectangularGrid Tile

abbrev ParseInputError := Parsing.RectangularGrid.ParseError ParseCharError

def parse (input : String) : Except ParseInputError ParsedInput :=
  let blocks := input.toSubstring.splitOn "\n\n"
  if blocks.isEmpty then
    throw Parsing.RectangularGrid.ParseError.NoInput
  else
    blocks.mapM $ Parsing.RectangularGrid.ofSubstring Tile.ofChar?

-- The things I do just to be able to #eval the parse result...
theorem parse_not_empty_list {input : String} :
match parse input with
| Except.ok r => r.length > 0
| Except.error _ => True := by
  unfold parse
  simp only
  cases h₁ : (input.toSubstring.splitOn "\n\n").isEmpty
  <;> simp only [Bool.false_eq_true, ↓reduceIte, gt_iff_lt]
  rw[←List.mapM'_eq_mapM]
  unfold List.mapM'
  have h₂ : (input.toSubstring.splitOn "\n\n") ≠ [] := List.isEmpty_eq_false.mp h₁
  cases h₃ : input.toSubstring.splitOn "\n\n"
  case false.nil => contradiction
  case false.cons b bs =>
    simp only [bind_pure_comp]
    cases Parsing.RectangularGrid.ofSubstring Day13.Tile.ofChar? b
    case error e => exact True.intro
    case ok t =>
      cases List.mapM' (Parsing.RectangularGrid.ofSubstring Day13.Tile.ofChar?) bs
      case error e => exact True.intro
      case ok ts =>
        have h₄ := List.length_cons t ts
        unfold bind Monad.toBind Except.instMonad
        simp only [Except.bind, Except.map, h₄, Nat.zero_lt_succ]

------------------------------------------------------------------------------------

private abbrev Grid := Parsing.RectangularGrid Tile

private inductive LookDirection (grid : Grid)
| Horizontal
| Vertical

private def LookDirection.width : (LookDirection grid) → Nat
| Horizontal => grid.width
| Vertical => grid.height

private def LookDirection.height : (LookDirection grid) → Nat
| Horizontal => grid.height
| Vertical => grid.width

private structure LookDirection.Coordinate (lookDirection : LookDirection grid) where
  x : Fin lookDirection.width
  y : Fin lookDirection.height

private def LookDirection.Coordinate.toGridCoordinate {grid : Grid} {lookDirection : LookDirection grid} (c : lookDirection.Coordinate) : grid.Coordinate :=
  match lookDirection with
  | Horizontal => {x := c.x, y := c.y}
  | Vertical => {y := c.x, x := c.y}

private def LookDirection.get {grid : Grid} {lookDirection : LookDirection grid} (coordinate : LookDirection.Coordinate lookDirection) : Tile :=
  grid[coordinate.toGridCoordinate]

private def getCoordinateOffset {ld : LookDirection grid} (mirror : Fin (ld.width - 1)) (offset : Nat) : Option (Fin ld.width × Fin ld.width) :=
  if h : offset <= mirror ∧ mirror + offset + 1 < ld.width then
    some (
      ⟨mirror - offset, Nat.lt_of_le_of_lt (Nat.sub_le mirror offset) (Nat.lt_of_pred_lt mirror.isLt)⟩,
      ⟨mirror + offset + 1, h.right⟩)
  else
    none

private theorem LookDirection.not_empty (lookDirection : LookDirection grid) : lookDirection.width > 0 ∧ lookDirection.height > 0 := by
  unfold width height
  cases lookDirection <;> simp only[grid.not_empty, and_true]

private def canBeMirror (smudgesNeeded : Nat) (lookDirection : LookDirection grid) (index : Fin (lookDirection.width - 1)) : Bool :=
  areRowsMirrored 0 ⟨lookDirection.height.pred, Nat.pred_lt $ Nat.ne_zero_iff_zero_lt.mpr lookDirection.not_empty.right⟩
where
  areRowsMirrored : Nat → Fin (lookDirection.height) → Bool := λ smudges r ↦
    let (smudges, isCurrentRowMirrored) := isRowMirrored smudges r 0
    match r with
    | ⟨0,_⟩ => isCurrentRowMirrored && smudges == smudgesNeeded
    | ⟨rp + 1, h₁⟩ =>
      isCurrentRowMirrored && areRowsMirrored smudges ⟨rp, Nat.lt_of_succ_lt h₁⟩
  isRowMirrored : Nat →  Fin (lookDirection.height) → Nat → (Nat × Bool) := λsmudges r o ↦
    match h : getCoordinateOffset index o with
    | none => (smudges,true) --always consider outside as mirrored.
    | some (i₁, i₂) =>
      let c₁ : lookDirection.Coordinate := {x := i₁, y := r}
      let c₂ : lookDirection.Coordinate := {x := i₂, y := r}
      if lookDirection.get c₁ == lookDirection.get c₂ then
        isRowMirrored smudges r (o+1)
      else if smudges < smudgesNeeded then
        isRowMirrored (smudges+1) r (o+1)
      else
        (smudges, false)
    termination_by _ _ o => lookDirection.width - (index + o + 1)
    decreasing_by
      all_goals
      unfold getCoordinateOffset at h
      split at h
      case isFalse => contradiction
      case isTrue h₁ => omega


private def findMirrorPlanes (smudgesNeeded : Nat) (lookDirection : LookDirection grid) : List (Fin lookDirection.width.pred) :=
  match h : lookDirection.width with
  | 0 => absurd h $ Nat.ne_zero_iff_zero_lt.mpr lookDirection.not_empty.left
  | 1 => []
  | wp + 2 =>
    have h : wp+2 = lookDirection.width := h.symm
    have : wp < lookDirection.width.pred := Nat.pred_lt_pred (Nat.add_one_ne_zero _) $ Nat.add_one_le_iff.mp (Nat.le_of_eq h)
    h▸(findMirrorPlanes_aux ⟨wp,this⟩ [])
where
  findMirrorPlanes_aux (r : Fin lookDirection.width.pred) (p : List (Fin lookDirection.width.pred)) : List (Fin lookDirection.width.pred) :=
    let p := if canBeMirror smudgesNeeded lookDirection r then
      r :: p
    else
      p
    match r with
    | ⟨0,_⟩ => p
    | ⟨rp+1,isLt⟩ => findMirrorPlanes_aux ⟨rp, Nat.lt_of_succ_lt isLt⟩ p

private def score (smudgesNeeded : Nat) (input : ParsedInput) : Nat :=
  let horizontalScores := input.foldl (λs g ↦
    (findMirrorPlanes smudgesNeeded (LookDirection.Horizontal : LookDirection g)).foldl (·+·.val+1) s) 0
  let verticalScores := input.foldl (λs g ↦
    (findMirrorPlanes smudgesNeeded (LookDirection.Vertical : LookDirection g)).foldl (λa b ↦ a+(b.val+1)*100) s) 0
  horizontalScores + verticalScores

def part1 : ParsedInput → Nat := score 0
def part2 : ParsedInput → Nat := score 1

------------------------------------------------------------------------------------

open DayPart
instance : Parse ⟨13, by simp⟩ (ι := ParsedInput) where
  parse := Except.mapError toString ∘ parse

instance : Part ⟨13,_⟩ Parts.One (ι := ParsedInput) (ρ := Nat) where
  run := some ∘ part1

instance : Part ⟨13,_⟩ Parts.Two (ι := ParsedInput) (ρ := Nat) where
  run := some ∘ part2
------------------------------------------------------------------------------------

private def testInput := "#.##..##.
..#.##.#.
##......#
##......#
..#.##.#.
..##..##.
#.#.##.#.

#...##..#
#....#..#
..##..###
#####.##.
#####.##.
..##..###
#....#..#"

#eval parse testInput

#eval match h : parse testInput with
| Except.error _ => none
| Except.ok l =>
  have h₁ : 0 < l.length := by
    have h₂ := parse_not_empty_list (input := testInput)
    simp only[h] at h₂
    exact h₂
  let t := l[0]
  let LD := (LookDirection t)
  let ld : LD := LookDirection.Horizontal
  let midx := 0
  if h₂ : midx < ld.width - 1 then
    ToString.toString <$> getCoordinateOffset (ld := ld) ⟨midx, h₂⟩ 1
  else
    none

#eval match h : parse testInput with
| Except.error _ => []
| Except.ok l =>
  have h₁ : 0 < l.length := by
    have h₂ := parse_not_empty_list (input := testInput)
    simp only[h] at h₂
    exact h₂
  let t := l[0]
  let LD := (LookDirection t)
  let ld : LD := LookDirection.Horizontal
  ToString.toString <$> findMirrorPlanes 0 ld

#eval match parse testInput with
| Except.error _ => 0
| Except.ok l => part1 l


#eval match parse testInput with
| Except.error _ => 0
| Except.ok l => part2 l
