#![allow(non_snake_case)]
use {
    anyhow::{
        anyhow,
        Context,
        Result,
    },
    regex::Regex,
    std::{
        env::args,
        fs::File,
        io::{
            BufRead,
            BufReader,
        },
        path::Path,
    },
};

#[derive(Debug)]
enum Direction {
    Up,
    Down,
    Left,
    Right,
}

type Move = (Direction, usize);

#[derive(Clone, Copy, PartialEq)]
struct Position {
    x: isize,
    y: isize,
}

struct Rope {
    length: usize,
    knots: Vec<Position>,
}

impl Rope {
    fn new(length: usize) -> Self {
        let knots = (0..length).into_iter().map(|_| Position { x: 0, y: 0 }).collect();
        Self {
            length,
            knots,
        }
    }

    #[allow(dead_code)]
    fn head(&self) -> Position {
        self.knots[0]
    }

    fn tail(&self) -> Position {
        self.knots[self.length - 1]
    }

    fn lead(&mut self, direction: &Direction) {
        match direction {
            Direction::Down  => self.knots[0].y += 1,
            Direction::Up    => self.knots[0].y -= 1,
            Direction::Right => self.knots[0].x += 1,
            Direction::Left  => self.knots[0].x -= 1,
        }
    }

    fn follow(&mut self, index: usize) {
        let Δx = self.knots[index - 1].x - self.knots[index].x;
        let Δy = self.knots[index - 1].y - self.knots[index].y;
        let Δx_abs = isize::abs(Δx);
        let Δy_abs = isize::abs(Δy);
        
        if Δx == 0 && Δy == 0 {
            // Already touching. Stay as is.
        } else if Δx == 0 {
            if Δy_abs <= 1 {
                // Already touching. Stay as is.
            } else if Δy_abs == 2 {
                self.knots[index].y += Δy.signum();
            } else {
                unreachable!();
            }
        } else if Δy == 0 {
            if Δx_abs <= 1 {
                // Already touching. Stay as is.
            } else if Δx_abs == 2 {
                self.knots[index].x += Δx.signum();
            } else {
                unreachable!();
            }
        } else if Δx_abs == 1 && Δy_abs == 1 {
            // Already touching. Stay as is.
        } else if Δx_abs >= 1 && Δy_abs >= 1 {
            self.knots[index].x += Δx.signum();
            self.knots[index].y += Δy.signum();
        } else {
            unreachable!();
        }
    }

    fn step(&mut self, direction: &Direction) {
        self.lead(direction);
        for knot in 1..self.length {
            self.follow(knot);
        }
    }
}

fn read_input<T: AsRef<Path>>(filename: T) -> Result<Vec<Move>> {
    let reader = BufReader::new(File::open(filename)?);
    let re = Regex::new(r#"(?P<direction>[UDRL]) (?P<steps>[0-9]+)"#)?;

    reader.lines().map(
        |v| {
            let s = v?;
            let caps = re.captures(&s).ok_or_else(|| anyhow!("Regex match failed: {s}"))?;
            let steps = caps.name("steps").ok_or_else(|| anyhow!("Missing steps: {s}"))?.as_str()
                .parse()?;
            let direction = match caps.name("direction")
                .ok_or_else(|| anyhow!("Missing direction: {s}"))?.as_str()
            {
                "U" => Direction::Up,
                "D" => Direction::Down,
                "L" => Direction::Left,
                "R" => Direction::Right,
                _   => return Err(anyhow!("Failed to parse direction from: {s}")),
            };
            Ok((direction, steps))
        }
    ).collect()
}

#[allow(dead_code)]
fn plot(rope: &Rope, visited: &[Position]) {
    let min_x =
        [rope.head(), rope.tail()].iter().chain(visited.iter()).map(|pos| pos.x).min().unwrap();
    let max_x =
        [rope.head(), rope.tail()].iter().chain(visited.iter()).map(|pos| pos.x).max().unwrap();
    let min_y =
        [rope.head(), rope.tail()].iter().chain(visited.iter()).map(|pos| pos.y).min().unwrap();
    let max_y =
        [rope.head(), rope.tail()].iter().chain(visited.iter()).map(|pos| pos.y).max().unwrap();

    println!("x: {min_x}..{max_x}, y: {min_y}..{max_y}");
    for y in min_y..=max_y {
        for x in min_x..=max_x {
            let pos = Position { x, y };

            if pos == rope.head() {
                print!("H");
            } else if pos == rope.tail() {
                print!("T");
            } else if pos == (Position { x: 0, y: 0 }) {
                print!("s");
            } else if visited.contains(&pos) {
                print!("#");
            } else {
                print!(".");
            }
        }
        println!();
    }
    println!();
}

fn part1(input: &[Move]) -> Result<usize> {
    let mut rope = Rope::new(2);
    let mut visited = vec![rope.tail()];

    for (direction, steps) in input {
        for _ in 0..*steps {
            rope.step(direction);
            let tail = rope.tail();
            if !visited.contains(&tail) {
                visited.push(tail);
            }
            // plot(&rope, &visited);
        }
    }

    Ok(visited.len())
}

fn part2(input: &[Move]) -> Result<usize> {
    let mut rope = Rope::new(10);
    let mut visited = vec![rope.tail()];

    for (direction, steps) in input {
        for _ in 0..*steps {
            rope.step(direction);
            let tail = rope.tail();
            if !visited.contains(&tail) {
                visited.push(tail);
            }
            // plot(&rope, &visited);
        }
    }

    Ok(visited.len())
}

fn main() -> Result<()> {
    let ( do_part_1, do_part_2 ) = aoc::do_parts();

    let filename = args().nth(1).ok_or_else(|| anyhow!("Missing input filename"))?;
    let input = read_input(filename).context("Could not read input")?;
    if do_part_1 {
        let solution = part1(&input).context("No solution for part 1")?;
        println!("Part1, solution found to be: {}", solution);
    }
    if do_part_2 {
        let solution = part2(&input).context("No solution for part 2")?;
        println!("Part2, solution found to be: {}", solution);
    }
    Ok(())
}