use std::convert::TryFrom;
use std::env;

#[derive(PartialEq, Eq)]
enum LobbyPoint {
    Floor,
    Full,
    Empty,
}

impl LobbyPoint {
    fn from_char(src: char) -> Self {
        match src {
            'L' => Self::Empty,
            '#' => Self::Full,
            _ => Self::Floor,
        }
    }
}

fn neighbours(xc: usize, yc: usize) -> impl Iterator<Item = (isize, isize)> {
    (-1..2)
        .flat_map(|y| (-1..2).map(move |x| (x, y)))
        .filter(|p| *p != (0, 0))
        .map(move |(x, y)| (x + xc as isize, y + yc as isize))
}

fn build_neighbourhoods_v1(grid: &Vec<Vec<LobbyPoint>>) -> Vec<Vec<Vec<(usize, usize)>>> {
    grid.into_iter()
        .enumerate()
        .map(|(y, row)| {
            row.into_iter()
                .enumerate()
                .map(move |(x, lp)| match lp {
                    LobbyPoint::Floor => vec![],
                    _ => neighbours(x, y)
                        .flat_map(|(xn, yn)| {
                            usize::try_from(yn)
                                .ok()
                                .and_then(|yn| {
                                    grid.get(yn).and_then(|row| {
                                        usize::try_from(xn).ok().and_then(|xn| {
                                            row.get(xn).and_then(|lp| match lp {
                                                LobbyPoint::Floor => None,
                                                _ => Some((xn, yn)),
                                            })
                                        })
                                    })
                                })
                                .into_iter()
                        })
                        .collect(),
                })
                .collect()
        })
        .collect()
}

fn seek_line(
    x0: usize,
    y0: usize,
    xd: isize,
    yd: isize,
    grid: &Vec<Vec<LobbyPoint>>,
) -> Option<(usize, usize)> {
    let x = x0.wrapping_add(xd as usize);
    let y = y0.wrapping_add(yd as usize);
    grid.get(y)
        .and_then(|row| row.get(x))
        .and_then(|lp| match lp {
            LobbyPoint::Floor => seek_line(x, y, xd, yd, grid),
            _ => Some((x, y)),
        })
}

fn neighbours_v2<'a>(
    xc: usize,
    yc: usize,
    grid: &'a Vec<Vec<LobbyPoint>>,
) -> impl Iterator<Item = (usize, usize)> + 'a {
    (-1..2)
        .flat_map(move |y| (-1..2).map(move |x| (x, y)))
        .filter(|p| *p != (0, 0))
        .flat_map(move |(x, y)| seek_line(xc, yc, x, y, grid).into_iter())
}

fn build_neighbourhoods_v2(grid: &Vec<Vec<LobbyPoint>>) -> Vec<Vec<Vec<(usize, usize)>>> {
    grid.into_iter()
        .enumerate()
        .map(|(y, row)| {
            row.into_iter()
                .enumerate()
                .map(move |(x, lp)| match lp {
                    LobbyPoint::Floor => vec![],
                    _ => neighbours_v2(x, y, grid).collect(),
                })
                .collect()
        })
        .collect()
}

fn time_step(
    grid: &Vec<Vec<LobbyPoint>>,
    neighbourhoods: &Vec<Vec<Vec<(usize, usize)>>>,
    threshold: u16,
) -> Vec<Vec<LobbyPoint>> {
    grid.into_iter()
        .zip(neighbourhoods.into_iter())
        .map(|(gr, nr)| {
            gr.into_iter()
                .zip(nr.into_iter())
                .map(|(lp, n)| match lp {
                    LobbyPoint::Floor => LobbyPoint::Floor,
                    _ => {
                        let c: u16 = n
                            .into_iter()
                            .map(|(x, y)| match grid[*y][*x] {
                                LobbyPoint::Full => 1,
                                _ => 0,
                            })
                            .sum();
                        match lp {
                            LobbyPoint::Full => {
                                if c >= threshold {
                                    LobbyPoint::Empty
                                } else {
                                    LobbyPoint::Full
                                }
                            }
                            LobbyPoint::Empty => {
                                if c == 0 {
                                    LobbyPoint::Full
                                } else {
                                    LobbyPoint::Empty
                                }
                            }
                            _ => LobbyPoint::Floor,
                        }
                    }
                })
                .collect()
        })
        .collect()
}

fn parse_grid(src: &str) -> Vec<Vec<LobbyPoint>> {
    src.lines()
        .map(|l| l.chars().map(LobbyPoint::from_char).collect())
        .collect()
}

fn main() {
    let args: Vec<_> = env::args().collect();
    let mut grid = parse_grid(std::fs::read_to_string(&args[1]).unwrap().as_ref());
    let (neighbourhoods, threshold) = match args[2].as_ref() {
        "1" => (build_neighbourhoods_v1(&grid), 4),
        "2" => (build_neighbourhoods_v2(&grid), 5),
        _ => panic!("Version number (1 or 2) expected"),
    };
    loop {
        let grid2 = time_step(&grid, &neighbourhoods, threshold);
        if grid2 == grid {
            break;
        } else {
            grid = grid2;
        }
    }
    println!(
        "{}",
        grid.into_iter()
            .flat_map(|row| row.into_iter().map(|pt| match pt {
                LobbyPoint::Full => 1,
                _ => 0,
            }))
            .sum::<usize>()
    )
}