use std::sync::{
    Arc, Mutex,
    atomic::{AtomicBool, Ordering},
};

use log::info;
use nes_emu::{Break, NES};
#[cfg(not(target_arch = "wasm32"))]
use tracing_subscriber::EnvFilter;
use web_sys::{
    AudioBuffer, AudioBufferOptions, AudioContext, AudioContextOptions, AudioContextState,
    CanvasRenderingContext2d, HtmlButtonElement, HtmlCanvasElement, HtmlInputElement, ImageBitmap,
    ImageData, KeyboardEvent,
    js_sys::{Function, Reflect, Uint8Array},
    wasm_bindgen::{Clamped, JsValue, UnwrapThrowExt, closure::Closure},
    window,
};

const DEFAULT_ROM: &[u8] = include_bytes!(concat!(
    env!("ROM_DIR"),
    "/input_test.nes" // TODO: select default rom
));

pub fn main() {
    #[cfg(target_arch = "wasm32")]
    {
        console_log::init().expect("Initialize logger");
        std::panic::set_hook(Box::new(console_error_panic_hook::hook));
    }
    #[cfg(not(target_arch = "wasm32"))]
    tracing_subscriber::fmt()
        .with_env_filter(EnvFilter::from_default_env())
        .init();

    let win = window().expect_throw("Window is undefined");
    let doc = win.document().expect_throw("Document is undefined");
    let canvas = doc
        .get_element_by_id("main_view")
        .expect_throw("Failed to get canvas element");
    let canvas = HtmlCanvasElement::from(JsValue::from(canvas));

    let ctx =
        CanvasRenderingContext2d::from(JsValue::from(canvas.get_context("2d").unwrap().unwrap()));
    let cl = Arc::new(Closure::new(move |image| {
        ctx.reset();
        ctx.draw_image_with_image_bitmap(&ImageBitmap::from(image), 0., 0.)
            .expect_throw("Failed to draw image");
    }));
    let audio_ctx_opts = AudioContextOptions::new();
    audio_ctx_opts.set_sample_rate(60. * 3723.);
    // audio_ctx_opts.set_
    let audio = AudioContext::new_with_context_options(&audio_ctx_opts)
        .expect_throw("Failed to create audio context");
    let _ = audio.suspend().expect_throw("Failed to suspend AudioCtx");

    const BUFFER_SIZE: usize = 1 << 10;
    const SAMPLE_RATE: f64 = 60. * 3722.;
    let audio_buffer_opts = AudioBufferOptions::new(BUFFER_SIZE as u32, SAMPLE_RATE as f32);
    audio_buffer_opts.set_number_of_channels(1);
    // let audio_buffer =
    //     AudioBuffer::new(&audio_buffer_opts).expect_throw("Failed to create audio buffer");
    // let mut raw_buf = [0.; BUFFER_SIZE];
    // for i in 0..raw_buf.len() {
    //     raw_buf[i] = (i as f32 / 200.).sin()
    // }
    // audio_buffer
    //     .copy_to_channel(&raw_buf, 0)
    //     .expect_throw("Failed to copy audio data");
    let gain_node = audio
        .create_gain()
        .expect_throw("Failed to create gain node");
    gain_node.set_channel_count(1);
    gain_node.gain().set_value(0.1);
    gain_node
        .connect_with_audio_node_and_output(&audio.destination(), 0)
        .expect_throw("Failed to connect to audio destination");
    // gain_node
    //     .connect_with_audio_node_and_output(&audio.destination(), 1)
    //     .expect_throw("Failed to connect to audio destination 2");
    // let delay_node = audio
    //     .create_delay()
    //     .expect_throw("Failed to create delay node");
    // delay_node.set_channel_count(1);
    // delay_node.delay_time().set_value(0.);
    // delay_node
    //     .connect_with_audio_node(&gain_node)
    //     .expect_throw("Failed to connect to audio destination");
    // source_node.start_with_when(when)

    let nes = Arc::new(Mutex::new({
        let mut n = NES::load_nes_file_mem(DEFAULT_ROM).unwrap();
        n.reset();

        n
    }));
    // let timeout = Arc::new(Mutex::new(None as Option<i32>));
    let poisoned = Arc::new(AtomicBool::new(false));
    // let timeout_cpy = timeout.clone();

    {
        let audio = audio.clone();
        let el = doc.get_element_by_id("pause").unwrap();
        let element = HtmlButtonElement::from(JsValue::from(el.clone()));
        let f: Closure<dyn Fn(JsValue)> = Closure::new(move |_e| {
            if audio.state() == AudioContextState::Suspended {
                let _ = audio.resume();
                element.set_inner_text("Pause");
            } else {
                let _ = audio.suspend();
                element.set_inner_text("Play");
            }
        });
        el.add_event_listener_with_callback("click", &Function::from(f.into_js_value()))
            .expect_throw("Failed to setup pause callback");
    }
    {
        let nes = nes.clone();
        let poisoned = poisoned.clone();
        let element = HtmlInputElement::from(JsValue::from(doc.get_element_by_id("file").unwrap()));
        element.set_accept(".nes");
        let load: Closure<dyn FnMut(JsValue)> = Closure::new(move |v| {
            let v = Uint8Array::from(v);
            // info!("{v:?}");
            let new_emu = NES::load_nes_file_mem(&v.to_vec()).expect("Failed to load nes file");
            if poisoned.swap(true, Ordering::AcqRel) == false {
                let mut nes = nes.lock().unwrap();
                *nes = new_emu;
                nes.power_cycle();
                if poisoned.swap(false, Ordering::AcqRel) == true {}
            }
        });
        let f: Closure<dyn Fn(JsValue)> = Closure::new(move |_e| {
            if let Some(file) = element.files().and_then(|l| l.get(0)) {
                let _ = file.bytes().then(&load);
            }
        });
        doc.get_element_by_id("load")
            .unwrap()
            .add_event_listener_with_callback("click", &Function::from(f.into_js_value()))
            .expect_throw("Failed to setup pause callback");
    }
    doc.get_element_by_id("mute")
        .unwrap()
        .add_event_listener_with_callback(
            "click",
            &Function::from(
                Closure::<dyn Fn()>::new(move || {
                    let win = window().expect_throw("Failed to get window");
                    let v = win.get("play_audio").is_none_or(|v| v.is_falsy());
                    Reflect::set(
                        &win,
                        &JsValue::from_str("play_audio"),
                        &JsValue::from_bool(!v),
                    )
                    .expect_throw("Failed to update audio mute");
                })
                .into_js_value(),
            ),
        )
        .expect_throw("Failed to setup pause callback");
    {
        let nes = nes.clone();
        let poisoned = poisoned.clone();
        let f: Closure<dyn Fn(JsValue)> = Closure::new(move |e| {
            if poisoned.swap(true, Ordering::AcqRel) == false {
                let e = KeyboardEvent::from(e);
                let key = e.key();
                // info!("{}", key);
                if key == "ArrowLeft" {
                    nes.lock().unwrap().controller_1().set_left(true);
                } else if key == "ArrowRight" {
                    nes.lock().unwrap().controller_1().set_right(true);
                } else if key == "ArrowUp" {
                    nes.lock().unwrap().controller_1().set_up(true);
                } else if key == "ArrowDown" {
                    nes.lock().unwrap().controller_1().set_down(true);
                } else if key == "a" || key == "A" {
                    nes.lock().unwrap().controller_1().set_a(true);
                } else if key == "s" || key == "S" {
                    nes.lock().unwrap().controller_1().set_b(true);
                } else if key == "q" || key == "Q" {
                    nes.lock().unwrap().controller_1().set_start(true);
                } else if key == "w" || key == "W" {
                    nes.lock().unwrap().controller_1().set_select(true);
                }
                if poisoned.swap(false, Ordering::AcqRel) == true {}
            }
        });
        win.set_onkeydown(Some(&Function::from(f.into_js_value())));
    }
    {
        let nes = nes.clone();
        let poisoned = poisoned.clone();
        let f: Closure<dyn Fn(JsValue)> = Closure::new(move |e| {
            if poisoned.swap(true, Ordering::AcqRel) == false {
                let e = KeyboardEvent::from(e);
                let key = e.key();
                // info!("{}", key);
                if key == "ArrowLeft" {
                    nes.lock().unwrap().controller_1().set_left(false);
                } else if key == "ArrowRight" {
                    nes.lock().unwrap().controller_1().set_right(false);
                } else if key == "ArrowUp" {
                    nes.lock().unwrap().controller_1().set_up(false);
                } else if key == "ArrowDown" {
                    nes.lock().unwrap().controller_1().set_down(false);
                } else if key == "a" || key == "A" {
                    nes.lock().unwrap().controller_1().set_a(false);
                } else if key == "s" || key == "S" {
                    nes.lock().unwrap().controller_1().set_b(false);
                } else if key == "q" || key == "Q" {
                    nes.lock().unwrap().controller_1().set_start(false);
                } else if key == "w" || key == "W" {
                    nes.lock().unwrap().controller_1().set_select(false);
                }
                if poisoned.swap(false, Ordering::AcqRel) == true {}
            }
        });
        win.set_onkeyup(Some(&Function::from(f.into_js_value())));
    }

    // let timeline = win.document().unwrap().timeline();
    let mut last_frame = audio.current_time();
    let mut raw_audio_buffer = [0f32; BUFFER_SIZE];
    let mut cur_pos = 0;
    let mut cur_total = 3722 * 5; // Start time in samples, set to buffer size...
    let period = Arc::new(Mutex::new(None as Option<Function>));
    let period_cl = period.clone();
    let periodic: Closure<dyn FnMut(JsValue)> = Closure::new(move |_v| {
        let win = window().expect_throw("Window is undefined");
        const FRAME_TIME: f64 = 1. / 60.;
        let mut cancel = false;
        if last_frame + FRAME_TIME > audio.current_time() {
            last_frame += FRAME_TIME;
            if audio.state() == AudioContextState::Running {
                if poisoned.swap(true, Ordering::AcqRel) {
                    cancel = true;
                    // if let Some(_id) = timeout_cpy.lock().ok().and_then(|v| *v) {
                    // win.clear_interval_with_handle(id);
                    // win.cancel_animation_frame(id);
                    info!("Cleared interval due to poison");
                    // } else {
                    //     info!("Not yet set id");
                    // }
                } else {
                    let mut n = nes.lock().unwrap();
                    // info!("Running frame");
                    let mut count = 0;
                    while !n.halted() && !n.run_one_clock_cycle(&Break::default()).ppu_frame {
                        count += 1;
                        if count > 90_000 {
                            info!("Loop overran");
                            break;
                        }
                    }
                    // info!("New samples: {}", n.apu().get_frame_samples().len());
                    for s in
                        &n.apu().get_frame_samples()[..n.apu().get_frame_samples().len().min(3722)]
                    {
                        // raw_audio_buffer[cur_pos] = ((*s as f32) - 127.5) / 128.;
                        raw_audio_buffer[cur_pos] = (*s as f32) / 256.;
                        cur_pos += 1;
                        if cur_pos == BUFFER_SIZE {
                            cur_pos = 0;
                            cur_total += BUFFER_SIZE;
                            if win.get("play_audio").is_none_or(|v| v.is_falsy()) {
                                // info!("{:?}", &raw_audio_buffer[..50]);
                                let audio_buffer = AudioBuffer::new(&audio_buffer_opts)
                                    .expect_throw("Failed to create audio buffer");
                                audio_buffer
                                    .copy_to_channel(&raw_audio_buffer, 0)
                                    .expect_throw("Failed to copy audio data");
                                // info!("S: {:?}", audio_buffer.get_channel_data(0));

                                let source_node = audio
                                    .create_buffer_source()
                                    .expect_throw("Failed to create buffer source");
                                // source_node.set_loop(false);
                                source_node.set_buffer(Some(&audio_buffer));
                                source_node
                                    .connect_with_audio_node(&gain_node)
                                    .expect_throw("Failed to connect to gain node");
                                source_node
                                    .start_with_when((cur_total as f64) / SAMPLE_RATE)
                                    .expect_throw("Failed to start source_node");
                                // info!(
                                //     "At {cur_total} samples, {:02.04}s ahead !loop",
                                //     (cur_total as f64) / SAMPLE_RATE - audio.current_time()
                                // );
                                if (cur_total as f64) / SAMPLE_RATE - audio.current_time() < 0. {
                                    info!("Bumping audio playback");
                                    cur_total += BUFFER_SIZE;
                                }
                            }
                        }
                    }
                    // info!("New samples: {}", n.apu().get_frame_samples().len());
                    n.apu_mut().reset_frame_samples(); // Discard audio samples
                    // info!("Completed Frame in {} cycles", count);
                    let data = Clamped(n.ppu().render_buffer.raw_image());
                    // info!("Creating bitmap");
                    let image = ImageData::new_with_u8_clamped_array(data, 256)
                        .expect_throw("Image data could not be created");

                    let data = win
                        .create_image_bitmap_with_image_data(&image)
                        .expect_throw("Bitmap could not be created");

                    let _ = data.then(&cl);
                    drop(n);
                    if !poisoned.swap(false, Ordering::AcqRel) {
                        panic!("Poisoned logic invalid");
                    }
                }
            }
        }
        if !cancel {
            win.request_animation_frame(period_cl.lock().unwrap().as_ref().unwrap())
                .expect_throw("Failed to setup animation frame");
        }
    });
    let js = Function::from(periodic.into_js_value());
    let win = window().expect_throw("Window is undefined");
    // let timeout_id = win
    //     .set_interval_with_callback_and_timeout_and_arguments_0(&Function::from(js), 16)
    //     .expect_throw("Failed to setup timeout");
    // let timeout_id =
    win.request_animation_frame(&js)
        .expect_throw("Failed to setup animation frame");
    *period.lock().unwrap() = Some(js);
    // *timeout.lock().unwrap() = Some(timeout_id);
}
