use std::collections::HashSet;
use std::io::BufRead;
use std::io::BufReader;
use std::io::Read;

use regex::Regex;

use common::Solution;
use cpu::OpCode;
use cpu::CPU;

pub struct Day16 {
    matcher: Regex,
    buf: String,
}

impl Day16 {
    pub fn new() -> Self {
        Day16 {
            matcher: Regex::new(r"\d+").unwrap(),
            buf: String::new(),
        }
    }

    fn read(&mut self, reader: &mut impl BufRead, target: &mut [i32]) -> bool {
        self.buf.clear();
        if reader.read_line(&mut self.buf).is_err() {
            return false;
        }

        let mut found = false;

        for (target, num) in target.iter_mut().zip(self.matcher.find_iter(&self.buf)) {
            *target = num.as_str().parse().unwrap();
            found = true
        }

        found
    }

    fn determine_options(
        &mut self,
        mut reader: &mut BufReader<&mut dyn Read>,
    ) -> [HashSet<OpCode>; 16] {
        let mut mappings: [HashSet<OpCode>; 16] = [
            HashSet::new(),
            HashSet::new(),
            HashSet::new(),
            HashSet::new(),
            HashSet::new(),
            HashSet::new(),
            HashSet::new(),
            HashSet::new(),
            HashSet::new(),
            HashSet::new(),
            HashSet::new(),
            HashSet::new(),
            HashSet::new(),
            HashSet::new(),
            HashSet::new(),
            HashSet::new(),
        ];
        let mut before = [0; 6];
        let mut op = [0; 4];
        let mut after = [0; 6];
        while self.read(&mut reader, &mut before) {
            self.read(&mut reader, &mut op);
            self.read(&mut reader, &mut after);
            reader.read_line(&mut self.buf).unwrap_or(0);

            if mappings[op[0] as usize].is_empty() {
                mappings[op[0] as usize]
                    .extend(OpCode::values().filter(|x| x.is_valid(&op, &before, &after)));
            } else {
                for option in OpCode::values().filter(|x| !x.is_valid(&op, &before, &after)) {
                    mappings[op[0] as usize].remove(&option);
                }
                continue;
            }
        }
        mappings
    }

    fn determine_mapping(&self, mut options: [HashSet<OpCode>; 16]) -> [OpCode; 16] {
        let mut mapping: [Option<OpCode>; 16] = [None; 16];
        let mut determined = 0;

        while determined < mapping.len() {
            for op in mapping.iter() {
                if let Some(op) = op {
                    for option in options.iter_mut() {
                        option.remove(op);
                    }
                }
            }

            for (idx, option) in options.iter_mut().enumerate() {
                if option.len() == 1 {
                    mapping[idx] = option.drain().next();
                    option.clear();
                    determined += 1;
                }
            }
        }

        let mut actual_mapping = [OpCode::ADDI; 16];
        for (i, op) in mapping.iter().enumerate() {
            actual_mapping[i] = op.unwrap();
        }

        actual_mapping
    }
}

impl Default for Day16 {
    fn default() -> Self {
        Self::new()
    }
}

impl Solution for Day16 {
    fn part1(&mut self, input: &mut dyn Read) -> String {
        let mut reader = BufReader::new(input);

        let mut before = [0; 6];
        let mut op = [0; 4];
        let mut after = [0; 6];
        let mut counter = 0;

        while self.read(&mut reader, &mut before) {
            self.read(&mut reader, &mut op);
            self.read(&mut reader, &mut after);
            reader.read_line(&mut self.buf).unwrap_or(0);

            let valid = OpCode::values()
                .filter(|x| x.is_valid(&op, &before, &after))
                .count();

            if valid >= 3 {
                counter += 1;
            }
        }
        counter.to_string()
    }

    fn part2(&mut self, input: &mut dyn Read) -> String {
        let mut reader = BufReader::new(input);

        let mappings = self.determine_options(&mut reader);
        let mapping = self.determine_mapping(mappings);

        let mut op = [0; 4];
        // Skip a line
        reader.read_line(&mut self.buf).unwrap();

        let mut cpu = CPU::new();

        while self.read(&mut reader, &mut op) {
            cpu.execute(mapping[op[0] as usize], &op[1..4]).unwrap();
        }

        cpu.registers[0].to_string()
    }
}

#[cfg(test)]
mod tests {
    use common::Solution;
    use day16::Day16;

    #[test]
    fn sample_part1() {
        let input: &[u8] = include_bytes!("samples/16.txt");
        let mut instance = Day16::new();
        assert_eq!("1", instance.part1(&mut input.as_ref()));
    }
}
