use log::*;
use crate::*;

use bedrock_pc::*;
use phosphor::*;

use std::cmp::{min, max};
use std::num::NonZeroU32;


pub fn main(mut args: Switchboard) {
    args.positional("source");

    args.named("debug").short('d');
    args.named("mode").default("dynamic");

    args.named("palette");
    args.named("fullscreen").short('f');
    args.named("show-cursor").short('c');
    args.named("zoom").short('z').quick("3").default("1");
    args.named("size").short('s');

    args.named("decode-stdin").short('i');
    args.named("encode-stdout").short('o');
    args.raise_errors();

    let source             = args.get("source").as_path_opt();
    let debug              = args.get("debug").as_bool();
    let mode               = Mode::from_str(args.get("mode").as_str());
    let override_palette   = args.get("palette").as_str_opt().map(parse_palette);
    let fullscreen         = args.get("fullscreen").as_bool();
    let show_cursor        = args.get("show-cursor").as_bool();
    let zoom_raw           = min(10, max(1, args.get("zoom").as_u32()));
    let zoom               = unsafe { NonZeroU32::new_unchecked(zoom_raw) };
    let initial_dimensions = match args.get("size").as_str_opt() {
        Some(string) => parse_dimensions(string),
        None => DEFAULT_SCREEN_SIZE / (zoom_raw as u16),
    };
    let decode_stdin       = args.get("decode-stdin").as_bool();
    let encode_stdout      = args.get("encode-stdout").as_bool();

    // -----------------------------------------------------------------------

    let Program { bytecode, path } = load_program(source.as_ref());

    let symbols_path = path.as_ref().map(|p| {
        let mut path = p.to_path_buf();
        path.add_extension("sym");
        path
    });

    let metadata = parse_metadata(&bytecode);
    if metadata.is_none() {
        info!("Could not read program metadata");
    }

    let config = EmulatorConfig {
        initial_dimensions,
        fullscreen,
        zoom,
        override_palette,
        show_cursor,

        decode_stdin,
        encode_stdout,

        symbols_path,
        metadata,
    };

    if let Ok(phosphor) = Phosphor::new() {
        match mode {
            Mode::Dynamic => {
                info!("Starting dynamic emulator");
                let mut emulator = DynamicEmulator::new(&config, debug);
                emulator.load_program(&bytecode);
                emulator.run();
                info!("Upgrading dynamic emulator to graphical emulator");
                let emulator = emulator.to_graphical(config);
                emulator.run(phosphor);
            }
            Mode::Graphical => {
                info!("Starting graphical emulator");
                let emulator = GraphicalEmulator::new(config, debug);
                emulator.run(phosphor);
            }
            Mode::Headless => {
                info!("Starting headless emulator");
                let mut emulator = HeadlessEmulator::new(&config, debug);
                emulator.run();
            }
        }
    } else {
        match mode {
            Mode::Dynamic => {
                info!("Could not start graphical event loop");
                info!("Starting headless emulator");
                let mut emulator = HeadlessEmulator::new(&config, debug);
                emulator.run();
            }
            Mode::Graphical => {
                fatal!("Could not start graphical event loop");
            }
            Mode::Headless => {
                info!("Starting headless emulator");
                let mut emulator = HeadlessEmulator::new(&config, debug);
                emulator.run();
            }
        }
    }
}


fn parse_dimensions(string: &str) -> ScreenDimensions {
    fn parse_inner(string: &str) -> Option<ScreenDimensions> {
        let (w_str, h_str) = string.trim().split_once('x')?;
        Some( ScreenDimensions {
            width:  w_str.parse().ok()?,
            height: h_str.parse().ok()?,
        } )
    }
    let dimensions = parse_inner(string).unwrap_or_else(|| {
        fatal!("Invalid dimensions string '{string}'");
    });
    if dimensions.is_zero() {
        fatal!("Screen dimensions must be greater than zero");
    }
    return dimensions;
}

fn parse_palette(string: &str) -> [Colour; 16] {
    fn decode_ascii_hex_digit(ascii: u8) -> Option<u8> {
        match ascii {
            b'0'..=b'9' => Some(ascii - b'0'),
            b'a'..=b'f' => Some(ascii - b'a' + 10),
            b'A'..=b'F' => Some(ascii - b'A' + 10),
            _ => { None }
        }
    }
    fn parse_inner(string: &str) -> Option<[Colour; 16]> {
        let mut c = Vec::new();
        for token in string.split(',') {
            let mut bytes = token.bytes();
            if bytes.len() != 3 { return None; }
            let r = decode_ascii_hex_digit(bytes.next().unwrap())?;
            let g = decode_ascii_hex_digit(bytes.next().unwrap())?;
            let b = decode_ascii_hex_digit(bytes.next().unwrap())?;
            c.push(Colour::from_rgb(r*17, g*17, b*17));
        }
        Some(match c.len() {
            2 => [ c[0],  c[1],  c[0],  c[1],  c[0],  c[1],  c[0],  c[1],
                   c[0],  c[1],  c[0],  c[1],  c[0],  c[1],  c[0],  c[1] ],
            3 => [ c[0],  c[1],  c[0],  c[2],  c[0],  c[1],  c[0],  c[2],
                   c[0],  c[1],  c[0],  c[2],  c[0],  c[1],  c[0],  c[2] ],
            4 => [ c[0],  c[1],  c[2],  c[3],  c[0],  c[1],  c[2],  c[3],
                   c[0],  c[1],  c[2],  c[3],  c[0],  c[1],  c[2],  c[3] ],
            8 => [ c[0],  c[1],  c[2],  c[3],  c[4],  c[5],  c[6],  c[7],
                   c[0],  c[1],  c[2],  c[3],  c[4],  c[5],  c[6],  c[7] ],
           16 => [ c[0],  c[1],  c[2],  c[3],  c[4],  c[5],  c[6],  c[7],
                   c[8],  c[9], c[10], c[11], c[12], c[13], c[14], c[15] ],
            _ => return None,
        })
    }
    parse_inner(string).unwrap_or_else(|| {
        fatal!("Invalid palette string '{string}'");
    })
}


#[derive(Copy, Clone, PartialEq)]
enum Mode {
    Graphical,
    Headless,
    Dynamic,
}

impl Mode {
    pub fn from_str(string: &str) -> Self {
        match string {
            "g" | "graphical" => Self::Graphical,
            "h" | "headless" => Self::Headless,
            "d" | "dynamic" => Self::Dynamic,
            _ => fatal!("Invalid mode string '{string}'"),
        }
    }
}