use std::str::FromStr;

use aoc_2022::*;

type Input = Vec<Round>;
type Output = i32;

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Shape {
    Rock,
    Paper,
    Scissors,
}

impl Shape {
    fn score(&self) -> i32 {
        match self {
            Shape::Rock => 1,
            Shape::Paper => 2,
            Shape::Scissors => 3,
        }
    }
}

impl FromStr for Shape {
    type Err = Report;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        if s.is_empty() {
            return Err(eyre!("empty string given"));
        }

        match s {
            "A" | "X" => Ok(Shape::Rock),
            "B" | "Y" => Ok(Shape::Paper),
            "C" | "Z" => Ok(Shape::Scissors),
            _ => Err(eyre!("unknown shape ‹{0}›", s)),
        }
    }
}

#[derive(Debug, Clone, Copy)]
enum Outcome {
    Lose,
    Draw,
    Win,
}

#[derive(Debug, Clone, Copy)]
struct Round {
    opponent: Shape,
    me: Shape,
}

impl FromStr for Round {
    type Err = Report;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        let mut split_str = s.split(' ');

        let opponent = split_str.next().unwrap().parse::<Shape>();
        let me = split_str.next().unwrap().parse::<Shape>();

        Ok(Round {
            opponent: opponent?,
            me: me?,
        })
    }
}

fn find_strategy<P>(predicate: P) -> (usize, (Shape, Shape))
where
    P: FnMut(&(usize, (Shape, Shape))) -> bool,
{
    //   R P S
    // R 3 0 6
    // P 6 3 0
    // S 0 6 3
    vec![
        // Loss
        (Shape::Rock, Shape::Paper),
        (Shape::Paper, Shape::Scissors),
        (Shape::Scissors, Shape::Rock),
        // Draw
        (Shape::Rock, Shape::Rock),
        (Shape::Paper, Shape::Paper),
        (Shape::Scissors, Shape::Scissors),
        // Win
        (Shape::Rock, Shape::Scissors),
        (Shape::Paper, Shape::Rock),
        (Shape::Scissors, Shape::Paper),
    ]
    .into_iter()
    .enumerate()
    .find_or_first(predicate)
    .unwrap()
}

fn find_result(strategy: &(Shape, Shape)) -> i32 {
    3 * (find_strategy(|&(_, st)| st == *strategy).0 as i32 / 3)
}

fn find_result_for_outcome(opponent: Shape, outcome: Outcome) -> i32 {
    let range = match outcome {
        Outcome::Lose => 0..3,
        Outcome::Draw => 3..6,
        Outcome::Win => 6..9,
    };
    let (i, (shape, _)) = find_strategy(|&(i, (_, op))| range.contains(&i) && op == opponent);

    3 * (i as i32 / 3) + shape.score()
}

impl Round {
    fn score(&self) -> i32 {
        let shape_score = self.me.score();
        let result_score = find_result(&(self.me, self.opponent));
        shape_score + result_score
    }

    fn expected_outcome(&self) -> Outcome {
        match self.me {
            Shape::Rock => Outcome::Lose,
            Shape::Paper => Outcome::Draw,
            Shape::Scissors => Outcome::Win,
        }
    }

    fn expected_score(&self) -> i32 {
        find_result_for_outcome(self.opponent, self.expected_outcome())
    }
}

struct Day02;
impl Solution<Input, Output> for Day02 {
    fn parse_input<P: AsRef<Path>>(pathname: P) -> Input {
        file_to_structs(pathname)
    }

    fn part_1(input: &Input) -> Output {
        input.iter().map(Round::score).sum()
    }

    fn part_2(input: &Input) -> Output {
        input.iter().map(Round::expected_score).sum()
    }
}

fn main() -> Result<()> {
    Day02::main()
}

test_sample!(day_02, Day02, 15, 12);