use std::env;
use std::fs;
use std::io::{self, BufRead};

type BusWidth = i32;
type IOType = i32;
type IOData = Vec<IOType>;

fn read_program(filename: &str) -> Vec<BusWidth> {
    let contents = fs::read_to_string(filename)
        .expect(filename);

    contents.split(',').map(|v| {
        v.trim_end().parse().unwrap()
    }).collect()
}

#[derive(Debug)]
enum Arithm {
    Add,
    Mul,
    LessThan,
    Equals,
}

#[derive(Debug)]
enum IO {
    In,
    Out,
}

#[derive(Debug)]
enum Jmp {
    JmpTrue,
    JmpFalse,
}

#[derive(Debug, PartialEq)]
enum ParameterMode {
    Position,
    Immediate,
}

fn arithm(op:Arithm, ip:usize, modes:Vec<ParameterMode>, memory:&mut Vec<BusWidth>) {
    if modes[2] == ParameterMode::Immediate {
        panic!("Instruction can not store in immediate mode parameter. (IP: {})", ip);
    }
    let params:Vec<usize> = (0..=2).map(|n| match &modes[n] {
        ParameterMode::Position =>  memory[ip + n + 1] as usize,
        ParameterMode::Immediate => ip + n + 1,
    }).collect();
    let left   = params[0];
    let right  = params[1];
    let result = params[2];

    match op {
        Arithm::Add =>      memory[result] = memory[left] + memory[right],
        Arithm::Mul =>      memory[result] = memory[left] * memory[right],
        Arithm::LessThan => memory[result] = if memory[left] <  memory[right] { 1 } else { 0 },
        Arithm::Equals =>   memory[result] = if memory[left] == memory[right] { 1 } else { 0 },
    }
}

fn jmp(op:Jmp, ip:usize, modes:Vec<ParameterMode>, memory:&mut Vec<BusWidth>) -> usize {
    let params:Vec<usize> = (0..=2).map(|n| match &modes[n] {
        ParameterMode::Position =>  memory[ip + n + 1] as usize,
        ParameterMode::Immediate => ip + n + 1,
    }).collect();
    let value   = params[0];
    let address = params[1];

    match op {
        Jmp::JmpTrue =>  (if memory[value] != 0 { memory[address] as usize } else { ip + 3 }),
        Jmp::JmpFalse => (if memory[value] == 0 { memory[address] as usize } else { ip + 3 }),
    }
}

fn io(op: IO, ip: usize, modes: Vec<ParameterMode>, memory:&mut Vec<BusWidth>, input:&mut IOData,
      output:&mut IOData, var_io: bool)
{
    let stdin = io::stdin();

    match op {
        IO::In => {
            let address = match &modes[0] {
                ParameterMode::Position => memory[ip + 1] as usize,
                ParameterMode::Immediate =>
                    panic!("Cannot store input in immediate mode parameter. (IP: {})", ip),
            };

            match var_io {
                false => {
                    let invalue = stdin.lock().lines().next().map(|s| {
                        match s {
                            Ok(s) => Some(s.trim_end().trim_start().parse::<BusWidth>().unwrap()),
                            _ => None,
                        }
                    });
                    match invalue {
                        Some(v) => memory[address] = v.unwrap() as BusWidth,
                        _ => {},
                    };
                },
                true => {
                    memory[address] = input.remove(0) as BusWidth;
                },
            }

        },
        IO::Out => {
            let o = match &modes[0] {
                ParameterMode::Position =>  memory[memory[ip + 1] as usize],
                ParameterMode::Immediate => memory[ip + 1],
            };
            println!("{}", o);
            if var_io {
                output.push(o as IOType);
            }
        },
    }
}

fn intcode<F>(mut memory:Vec<BusWidth>, mut input: &mut IOData, mut output:&mut IOData,
              f: F, var_io: bool) where
F: Fn(&mut Vec<BusWidth>, &mut IOData, &mut IOData)
{
    let mut ip = 0;

    while ip < memory.len() {

        let op = memory[ip] % 100;
        let modes:Vec<ParameterMode> = (2..=4).map(|e| memory[ip] / 10i32.pow(e) as BusWidth % 10)
            .map(|n| match n {
                0 => ParameterMode::Position,
                1 => ParameterMode::Immediate,
                p => panic!("Invalid parameter mode {} at IP: {}", p, ip),
            }).collect();
        match op {
            1 => { arithm(Arithm::Add,      ip, modes, &mut memory); ip += 4 },
            2 => { arithm(Arithm::Mul,      ip, modes, &mut memory); ip += 4 },
            7 => { arithm(Arithm::LessThan, ip, modes, &mut memory); ip += 4 },
            8 => { arithm(Arithm::Equals,   ip, modes, &mut memory); ip += 4 },
            3 => { io(IO::In,  ip, modes, &mut memory, &mut input, &mut output, var_io); ip += 2 },
            4 => { io(IO::Out, ip, modes, &mut memory, &mut input, &mut output, var_io); ip += 2 },
            5 => { ip = jmp(Jmp::JmpTrue,  ip, modes, &mut memory); },
            6 => { ip = jmp(Jmp::JmpFalse, ip, modes, &mut memory); },
            99 => break,
            op => panic!("Invalid operation {} at IP: {}", op, ip),
        }

        if var_io {
            f(&mut memory, &mut input, &mut output);
        }
    }
}

fn func(_memory:&mut Vec<BusWidth>, input:&mut IOData, output:&mut IOData) {
    if output.len() > 0 && input.len() > 0 {
        let v = output.remove(0);
        input.insert(1, v);
    }
}

fn main() {
    let args: Vec<String> = env::args().collect();

    let mut opts = getopts::Options::new();
    opts.reqopt("m", "mode", "Mode of IO operations", "[arg|stdio]");
    opts.reqopt("p", "program", "File to load program from", "FILE");
    let matches = match opts.parse(&args[1..]) {
        Ok(m) => { m }
        Err(f) => { panic!(f.to_string()) }
    };
    let program:String = matches.opt_str("program").unwrap();
    let mode = matches.opt_str("mode").unwrap();
    let memory = read_program(&program);

    if mode == "arg" {
        let mut input:IOData = matches.free[0].split(",").map(|v| {
            let i = v.trim_end().parse().unwrap();
            i
        }).collect();
        let mut output = Vec::new();
        while input.len() > 1 {
            intcode(memory.clone(), &mut input, &mut output, &func, true);
        }
    } else if mode == "stdio" {
        let mut input = vec![];
        let mut output = vec![];

        intcode(memory.clone(), &mut input, &mut output, &func, false);
    } else {
        panic!("Unknown mode: {}", mode);
    }
}