use std::{collections::HashMap, fmt::Display, path::PathBuf};

#[derive(clap::Parser, Debug)]
struct Args {
    input: PathBuf,
}

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

#[derive(Copy, Clone, Debug, Hash)]
enum Round {
    Loss,
    Draw,
    Win,
}

impl Display for Hand {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
        match self {
            Self::Rock => write!(f, "Rock"),
            Self::Paper => write!(f, "Paper"),
            Self::Scissors => write!(f, "Scissors"),
        }
    }
}

impl Hand {
    fn round(self, other: Self) -> Round {
        use Round::*;
        match (self, other) {
            (Hand::Rock, Hand::Rock) => Draw,
            (Hand::Rock, Hand::Paper) => Loss,
            (Hand::Rock, Hand::Scissors) => Win,
            (Hand::Paper, Hand::Rock) => Win,
            (Hand::Paper, Hand::Paper) => Draw,
            (Hand::Paper, Hand::Scissors) => Loss,
            (Hand::Scissors, Hand::Rock) => Loss,
            (Hand::Scissors, Hand::Paper) => Win,
            (Hand::Scissors, Hand::Scissors) => Draw,
        }
    }
}

#[derive(Debug)]
enum ParseStreamError {
    UnknownCommand,
    BrokenPair,
}

impl std::error::Error for ParseStreamError {}

impl Display for ParseStreamError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
        <Self as std::fmt::Debug>::fmt(self, f)
    }
}

fn parse_stream<'a, I: Iterator<Item = char> + 'a>(
    stream: I,
    mapping: &'a HashMap<char, Hand>,
) -> impl Iterator<Item = Result<(Hand, Hand), ParseStreamError>> + 'a {
    struct ParseStream<'a, I> {
        stream: I,
        mapping: &'a HashMap<char, Hand>,
    }
    impl<'a, I: Iterator<Item = char>> Iterator for ParseStream<'a, I> {
        type Item = Result<(Hand, Hand), ParseStreamError>;
        fn next(&mut self) -> Option<Self::Item> {
            let a = loop {
                match self.stream.next() {
                    Some(c) => match self.mapping.get(&c) {
                        Some(a) => break *a,
                        None if c.is_whitespace() => (),
                        None => return Some(Err(ParseStreamError::UnknownCommand)),
                    },
                    None => return None,
                }
            };
            let b = loop {
                match self.stream.next() {
                    Some(c) if c == '\n' => return Some(Err(ParseStreamError::BrokenPair)),
                    Some(c) => match self.mapping.get(&c) {
                        Some(b) => break *b,
                        None if c.is_whitespace() => (),
                        None => return Some(Err(ParseStreamError::UnknownCommand)),
                    },
                    None => return Some(Err(ParseStreamError::BrokenPair)),
                }
            };
            Some(Ok((a, b)))
        }
    }
    ParseStream { stream, mapping }
}

enum ErrGrabber<J, E, I: Iterator<Item = Result<J, E>>> {
    Running(I),
    Finished,
    Error(E),
}

impl<J, E, I: Iterator<Item = Result<J, E>>> Iterator for ErrGrabber<J, E, I> {
    type Item = J;

    fn next(&mut self) -> Option<J> {
        match self {
            Self::Running(inner) => match inner.next() {
                Some(Ok(v)) => Some(v),
                Some(Err(e)) => {
                    *self = Self::Error(e);
                    None
                }
                None => {
                    *self = Self::Finished;
                    None
                }
            },
            _ => None,
        }
    }
}

impl<J, E, I: Iterator<Item = Result<J, E>>> ErrGrabber<J, E, I> {
    fn new(inner: I) -> Self {
        Self::Running(inner)
    }

    fn check(self) -> Result<(), E> {
        match self {
            Self::Error(e) => Err(e),
            _ => Ok(()),
        }
    }
}

fn score_1(a: Hand, b: Hand) -> u32 {
    (match a.round(b) {
        Round::Loss => 0,
        Round::Draw => 3,
        Round::Win => 6,
    }) + match a {
        Hand::Rock => 1,
        Hand::Paper => 2,
        Hand::Scissors => 3,
    }
}

fn main() -> Result<(), Box<dyn std::error::Error>> {
    use clap::Parser;
    let args = Args::parse();
    let dict = <[(char, Hand); 6] as IntoIterator>::into_iter([
        ('a', Hand::Rock),
        ('b', Hand::Paper),
        ('c', Hand::Scissors),
        ('x', Hand::Rock),
        ('y', Hand::Paper),
        ('z', Hand::Scissors),
    ])
    .collect();
    let mut buffer = String::new();
    let input = {
        use std::io::Read;
        let mut file = std::fs::File::open(&args.input)?;
        file.read_to_string(&mut buffer)?;
        buffer.chars()
    };
    let mut rounds = ErrGrabber::new(parse_stream(input.flat_map(|c| c.to_lowercase()), &dict));
    let score: u32 = (&mut rounds)
        .map(|(theirs, mine)| score_1(mine, theirs))
        .sum();
    rounds.check()?;
    println!("Total score: {}", score);
    Ok(())
}