#![feature(let_chains)]
use std::collections::{HashSet, VecDeque};

const GRID_SIZE: usize = include_bytes!("input.txt").len().isqrt();

fn dfs(grid: &[[char; GRID_SIZE]; GRID_SIZE], start: (usize, usize)) -> HashSet<(usize, usize)> {
    let mut queue = VecDeque::from([start]);
    let mut set = HashSet::from([start]);
    let plant_type = grid[start.0][start.1];

    while let Some(next) = queue.pop_front() {
        let to_check = [
            (next.0 + 1, next.1),
            (next.0.wrapping_sub(1), next.1),
            (next.0, next.1 + 1),
            (next.0, next.1.wrapping_sub(1)),
        ];
        for pos in to_check {
            if set.contains(&pos) {
                continue;
            }

            if let Some(plant) = grid.get(pos.0).and_then(|r| r.get(pos.1))
                && *plant == plant_type
            {
                queue.push_back(pos);
                set.insert(pos);
            }
        }
    }

    set
}

// fn dfs_perimeter(
//     perimeter_set: &HashSet<(isize, isize)>,
//     start: (isize, isize),
// ) -> HashSet<(isize, isize)> {
//     let mut queue = VecDeque::from([start]);
//     let mut set = HashSet::from([start]);

//     while let Some(next) = queue.pop_front() {
//         let to_check = [
//             (next.0 + 1, next.1),
//             (next.0 - 1, next.1),
//             (next.0, next.1 + 1),
//             (next.0, next.1 - 1),
//         ];
//         for pos in to_check {
//             if set.contains(&pos) {
//                 continue;
//             }

//             if perimeter_set.contains(&pos) {
//                 queue.push_back(pos);
//                 set.insert(pos);
//             }
//         }
//     }

//     set
// }

fn main() {
    let mut input_iter = include_str!("input.txt").lines().map(|l| {
        let mut iter = l.chars();
        std::array::from_fn::<_, GRID_SIZE, _>(|_| iter.next().unwrap())
    });
    let grid: [_; GRID_SIZE] = std::array::from_fn(|_| input_iter.next().unwrap());

    // Parse all independent regions
    let mut regions = Vec::<(char, HashSet<(usize, usize)>)>::new();
    for row in 0..GRID_SIZE {
        for col in 0..GRID_SIZE {
            if regions.iter().all(|region| !region.1.contains(&(row, col))) {
                regions.push((grid[row][col], dfs(&grid, (row, col))));
            }
        }
    }

    // Sanity check -> are there any duplicated nodes between the regions?
    assert_eq!(
        regions.iter().map(|r| r.1.len()).sum::<usize>(),
        regions
            .iter()
            .map(|r| &r.1)
            .flatten()
            .collect::<HashSet<&(usize, usize)>>()
            .len()
    );

    // Expand grid by two because im going insane
    let mut expanded_grid = [[' '; GRID_SIZE * 2 + 1]; GRID_SIZE * 2 + 1];
    for row in 0..GRID_SIZE {
        for col in 0..GRID_SIZE {
            expanded_grid[row * 2 + 1][col * 2 + 1] = grid[row][col];
        }
    }

    for row in 0..(GRID_SIZE*2+1) {
        print!("|");
        for col in 0..(GRID_SIZE*2+1) {
            print!("{}", expanded_grid[row][col]);
        }
        println!("|");
    }


    // Calculate area and sides of each
    let mut sum = 0;
    for (plant_type, region) in regions.into_iter() {
        let area = region.len();
        let sides = 0;

        // Clone the expanded grid
        let expanded_grid_clone = expanded_grid.clone();
        for pos in region.iter() {
            // None in a position is a special case meaning "overfilled in the negative direction"
            // Since we don't actually access the point there, it just gets treated like -1 would
            let to_check = [
                (pos.0.checked_add(1), Some(pos.1)),
                (pos.0.checked_sub(1), Some(pos.1)),
                (Some(pos.0), pos.1.checked_add(1)),
                (Some(pos.0), pos.1.checked_sub(1)),
            ];
            for outer_pos in to_check {
                if (outer_pos.0.is_none() || outer_pos.1.is_none())
                    || !region.contains(&(outer_pos.0.unwrap(), outer_pos.1.unwrap()))
                {
                    expanded_grid_clone[outer_pos.0.map(|p| p * 2 + 1)]
                }
            }
        }

        sum += area * sides;
        println!("{plant_type}: {area}A {sides}S");
    }

    println!("Result: {sum}");
}