language: rust prompt: https://adventofcode.com/2020/day/11 the book: https://doc.rust-lang.org/stable/book/


// Run with `cargo run`
use std::convert::TryInto;
use std::fs;

#[derive(PartialEq, Eq, Clone, Copy, Debug)]
enum Seat {
    Empty,
    Full,
}
use Seat::*;

type SparseMatrix<A> = Vec<Vec<Option<A>>>;

// Note: This is particularly messy, I should come back when I've got time.
fn main() {
    let input = load_input();
    println!(
        "Found {} full seats when it settled (v1).",
        count_full_seats(fix(change_seats_v1, &input))
    );
    println!(
        "Found {} full seats when it settled (v2).",
        count_full_seats(fix(change_seats_v2, &input))
    );
}

fn count_full_seats(seats: SparseMatrix<Seat>) -> usize {
    seats
        .into_iter()
        .flatten()
        .filter_map(|seat| match seat {
            Some(Full) => Some(Full),
            _ => None,
        })
        .count()
}

fn change_seats_v1(seats: &SparseMatrix<Seat>) -> SparseMatrix<Seat> {
    //eprintln!("=== seats ===\n{}", debug_view_of_seats(seats));
    deep_enum_map(seats, |cell, (i, j)| {
        let ns = neighbors(seats, i, j);
        match cell {
            Empty => {
                if ns.into_iter().all(|n| n == Empty) {
                    Full
                } else {
                    *cell
                }
            }
            Full => {
                if ns.into_iter().filter(|n| *n == Full).count() >= 4 {
                    Empty
                } else {
                    *cell
                }
            }
        }
    })
}

fn neighbors(seats: &SparseMatrix<Seat>, row: usize, col: usize) -> Vec<Seat> {
    let row: i64 = row.try_into().unwrap();
    let col: i64 = col.try_into().unwrap();
    let r_bound: i64 = seats.len().try_into().unwrap();
    let c_bound: i64 = seats[0].len().try_into().unwrap();
    [
        (-1, -1),
        (-1, 0),
        (-1, 1),
        (0, -1),
        (0, 1),
        (1, -1),
        (1, 0),
        (1, 1),
    ]
    .iter()
    .map(|(r_off, c_off)| (r_off + row, c_off + col))
    .filter(|(row, col)| 0 <= *row && *row < r_bound && 0 <= *col && *col < c_bound)
    .filter_map(|(row, col)| seats[row as usize][col as usize])
    .collect()
}

fn change_seats_v2(seats: &SparseMatrix<Seat>) -> SparseMatrix<Seat> {
    //eprintln!("=== seats ===\n{}", debug_view_of_seats(seats));
    let width = seats.len();
    let height = seats[0].len();
    deep_enum_map(seats, |cell, (i, j)| {
        let ns = line_of_sight(seats, i, j, width, height);
        match cell {
            Empty => {
                if ns.into_iter().all(|n| n == Empty) {
                    Full
                } else {
                    *cell
                }
            }
            Full => {
                if ns.into_iter().filter(|n| *n == Full).count() >= 5 {
                    Empty
                } else {
                    *cell
                }
            }
        }
    })
}

fn line_of_sight(
    seats: &SparseMatrix<Seat>,
    row: usize,
    col: usize,
    width: usize,
    height: usize,
) -> Vec<Seat> {
    let row: i64 = row.try_into().unwrap();
    let col: i64 = col.try_into().unwrap();
    let r_bound: i64 = width.try_into().unwrap();
    let c_bound: i64 = height.try_into().unwrap();
    [
        (-1, -1),
        (-1, 0),
        (-1, 1),
        (0, -1),
        (0, 1),
        (1, -1),
        (1, 0),
        (1, 1),
    ]
    .iter()
    .filter_map(|(r_off, c_off)| {
        let mut r = row + r_off;
        let mut c = col + c_off;
        let ptr_in_bounds = |r, c| 0 <= r && r < r_bound && 0 <= c && c < c_bound;
        let seat = |r, c| seats[r as usize][c as usize];
        while ptr_in_bounds(r, c) && seat(r, c).is_none() {
            r += r_off;
            c += c_off;
        }
        if ptr_in_bounds(r, c) {
            seat(r, c)
        } else {
            None
        }
    })
    .collect()
}

// https://en.wikipedia.org/wiki/Fixed-point_combinator
// More accessible in SICP: https://mitpress.mit.edu/sites/default/files/sicp/index.html
fn fix<A: PartialEq + Clone>(f: impl Fn(&A) -> A, initial: &A) -> A {
    let mut prev = initial.clone();
    let mut curr = f(&initial);
    while curr != prev {
        prev = curr;
        curr = f(&prev);
    }
    curr.clone()
}

fn deep_enum_map<A, F>(matrix: &SparseMatrix<A>, transform: F) -> SparseMatrix<A>
where
    F: Fn(&A, (usize, usize)) -> A,
{
    matrix
        .iter()
        .enumerate()
        .map(|(i, row)| {
            row.iter()
                .enumerate()
                .map(|(j, cell)| match cell {
                    None => None,
                    Some(value) => Some(transform(value, (i, j))),
                })
                .collect()
        })
        .collect()
}

fn load_input() -> SparseMatrix<Seat> {
    fs::read_to_string("./input")
        .unwrap()
        .lines()
        .map(|line| {
            line.chars()
                .map(|c| match c {
                    'L' => Some(Empty),
                    _ => None,
                })
                .collect::<Vec<_>>()
        })
        .collect::<Vec<_>>()
}

#[allow(dead_code)]
fn debug_view_of_seats(seats: &SparseMatrix<Seat>) -> String {
    seats
        .iter()
        .map(|row| {
            row.iter()
                .map(|seat| match seat {
                    None => ' ',
                    Some(Empty) => 'L',
                    Some(Full) => 'X',
                })
                .fold(String::new(), |mut s, c| {
                    s.push(c);
                    s
                })
        })
        .fold(String::new(), |mut a, b| {
            a.push_str("\n");
            a.push_str(&b);
            a
        })
}