const INPUT: &str = include_str!("../data/day2.txt");

const MAX_RED: usize = 12;
const MAX_GREEN: usize = 13;
const MAX_BLUE: usize = 14;

pub fn day2a() -> String {
    let games = INPUT.lines().map(parse_game);

    format!(
        "{}",
        possible_games(games).fold(0, |acc, game| acc + game.0)
    )
}

pub fn day2b() -> String {
    let games = INPUT.lines().map(parse_game);

    let power = games
        .into_iter()
        .map(|game| fewest_cubes(&game).power())
        .fold(0, |acc, power| acc + power);

    format!("{}", power)
}

#[derive(Debug, Clone, PartialEq)]
pub struct Game(usize, Vec<Info>);

#[derive(Debug, Clone, PartialEq)]
pub struct Info {
    red: usize,
    green: usize,
    blue: usize,
}

impl Info {
    fn power(&self) -> usize {
        self.red * self.green * self.blue
    }
}

pub(crate) fn possible_games(games: impl Iterator<Item = Game>) -> impl Iterator<Item = Game> {
    games.filter(|game| {
        game.1
            .iter()
            .all(|info| info.red <= MAX_RED && info.green <= MAX_GREEN && info.blue <= MAX_BLUE)
    })
}

pub(crate) fn fewest_cubes(game: &Game) -> Info {
    let mut min_red = 0;
    let mut min_green = 0;
    let mut min_blue = 0;

    for info in game.1.iter() {
        if info.red > min_red {
            min_red = info.red;
        }
        if info.green > min_green {
            min_green = info.green;
        }
        if info.blue > min_blue {
            min_blue = info.blue;
        }
    }

    Info {
        red: min_red,
        green: min_green,
        blue: min_blue,
    }
}

pub(crate) fn parse_game(input: &str) -> Game {
    let mut parts = input.split(':');
    let number = parse_game_number(parts.next().unwrap());
    let info_line = parts.next().unwrap().split(';');
    let info = info_line
        .map(|block| parse_info(block))
        .collect::<Vec<Info>>();
    Game(number, info)
}

pub(crate) fn parse_game_number(input: &str) -> usize {
    let mut parts = input.split(' ');
    assert_eq!(parts.next(), Some("Game"));
    parts.next().unwrap().parse::<usize>().unwrap()
}

pub(crate) fn parse_info(input: &str) -> Info {
    let mut info = Info {
        red: 0,
        green: 0,
        blue: 0,
    };
    for part in input.trim().split(',') {
        let mut sep = part.trim().split(' ');
        let count = sep.next().unwrap().trim();
        let color = sep.next().unwrap().trim();
        match color {
            "red" => info.red = count.parse::<usize>().unwrap(),
            "green" => info.green = count.parse::<usize>().unwrap(),
            "blue" => info.blue = count.parse::<usize>().unwrap(),
            _ => unreachable!(),
        }
    }

    info
}

#[cfg(test)]
mod test {
    use super::*;

    const INPUT: &str = "Game 1: 3 blue, 4 red; 1 red, 2 green, 6 blue; 2 green
Game 2: 1 blue, 2 green; 3 green, 4 blue, 1 red; 1 green, 1 blue
Game 3: 8 green, 6 blue, 20 red; 5 blue, 4 red, 13 green; 5 green, 1 red
Game 4: 1 green, 3 red, 6 blue; 3 green, 6 red; 3 green, 15 blue, 14 red
Game 5: 6 red, 1 blue, 3 green; 2 blue, 1 red, 2 green";

    #[test]
    fn parses_a_game() {
        let test_cases = [
            (
                "Game 1: 3 blue, 4 red; 1 red, 2 green, 6 blue; 2 green",
                Game(
                    1,
                    vec![
                        Info {
                            red: 4,
                            green: 0,
                            blue: 3,
                        },
                        Info {
                            red: 1,
                            green: 2,
                            blue: 6,
                        },
                        Info {
                            red: 0,
                            green: 2,
                            blue: 0,
                        },
                    ],
                ),
                "Game 1",
            ),
            (
                "Game 2: 1 blue, 2 green; 3 green, 4 blue, 1 red; 1 green, 1 blue",
                Game(
                    2,
                    vec![
                        Info {
                            red: 0,
                            green: 2,
                            blue: 1,
                        },
                        Info {
                            red: 1,
                            green: 3,
                            blue: 4,
                        },
                        Info {
                            red: 0,
                            green: 1,
                            blue: 1,
                        },
                    ],
                ),
                "Game 2",
            ),
            (
                "Game 3: 8 green, 6 blue, 20 red; 5 blue, 4 red, 13 green; 5 green, 1 red",
                Game(
                    3,
                    vec![
                        Info {
                            red: 20,
                            green: 8,
                            blue: 6,
                        },
                        Info {
                            red: 4,
                            green: 13,
                            blue: 5,
                        },
                        Info {
                            red: 1,
                            green: 5,
                            blue: 0,
                        },
                    ],
                ),
                "Game 3",
            ),
            (
                "Game 4: 1 green, 3 red, 6 blue; 3 green, 6 red; 3 green, 15 blue, 14 red",
                Game(
                    4,
                    vec![
                        Info {
                            red: 3,
                            green: 1,
                            blue: 6,
                        },
                        Info {
                            red: 6,
                            green: 3,
                            blue: 0,
                        },
                        Info {
                            red: 14,
                            green: 3,
                            blue: 15,
                        },
                    ],
                ),
                "Game 4",
            ),
            (
                "Game 5: 6 red, 1 blue, 3 green; 2 blue, 1 red, 2 green",
                Game(
                    5,
                    vec![
                        Info {
                            red: 6,
                            green: 3,
                            blue: 1,
                        },
                        Info {
                            red: 1,
                            green: 2,
                            blue: 2,
                        },
                    ],
                ),
                "Game 5",
            ),
        ];

        for (input, expected, message) in test_cases {
            assert_eq!(parse_game(input), expected, "{}", message);
        }
    }

    #[test]
    fn possible_games() {
        let games = INPUT.lines().map(parse_game);
        let games = super::possible_games(games).collect::<Vec<Game>>();
        assert_eq!(games.len(), 3);
        assert_eq!(
            games.into_iter().map(|g| g.0).collect::<Vec<usize>>(),
            vec![1, 2, 5]
        );
    }

    #[test]
    fn fewest_cubes() {
        let test_cases = [
            (
                "Game 1: 3 blue, 4 red; 1 red, 2 green, 6 blue; 2 green",
                Info {
                    red: 4,
                    green: 2,
                    blue: 6,
                },
                "Game 1",
            ),
            (
                "Game 2: 1 blue, 2 green; 3 green, 4 blue, 1 red; 1 green, 1 blue",
                Info {
                    red: 1,
                    green: 3,
                    blue: 4,
                },
                "Game 2",
            ),
            (
                "Game 3: 8 green, 6 blue, 20 red; 5 blue, 4 red, 13 green; 5 green, 1 red",
                Info {
                    red: 20,
                    green: 13,
                    blue: 6,
                },
                "Game 3",
            ),
            (
                "Game 4: 1 green, 3 red, 6 blue; 3 green, 6 red; 3 green, 15 blue, 14 red",
                Info {
                    red: 14,
                    green: 3,
                    blue: 15,
                },
                "Game 4",
            ),
            (
                "Game 5: 6 red, 1 blue, 3 green; 2 blue, 1 red, 2 green",
                Info {
                    red: 6,
                    green: 3,
                    blue: 2,
                },
                "Game 5",
            ),
        ];

        for (input, expected, message) in test_cases {
            assert_eq!(
                super::fewest_cubes(&super::parse_game(input)),
                expected,
                "{}",
                message
            );
        }
    }

    #[test]
    fn game_power() {
        let test_cases = [
            (
                "Game 1: 3 blue, 4 red; 1 red, 2 green, 6 blue; 2 green",
                48,
                "Game 1",
            ),
            (
                "Game 2: 1 blue, 2 green; 3 green, 4 blue, 1 red; 1 green, 1 blue",
                12,
                "Game 2",
            ),
            (
                "Game 3: 8 green, 6 blue, 20 red; 5 blue, 4 red, 13 green; 5 green, 1 red",
                1560,
                "Game 3",
            ),
            (
                "Game 4: 1 green, 3 red, 6 blue; 3 green, 6 red; 3 green, 15 blue, 14 red",
                630,
                "Game 4",
            ),
            (
                "Game 5: 6 red, 1 blue, 3 green; 2 blue, 1 red, 2 green",
                36,
                "Game 5",
            ),
        ];

        for (input, expected, message) in test_cases {
            assert_eq!(
                super::fewest_cubes(&super::parse_game(input)).power(),
                expected,
                "{}",
                message
            );
        }
    }
}