use std::{
    collections::HashMap,
    fmt,
    time::{Duration, Instant},
};

use iced::{
    Element,
    Length::{Fill, Shrink},
    Point, Rectangle, Renderer, Size, Subscription, Task, Theme,
    keyboard::{self, Key, Modifiers, key::Named},
    mouse, time,
    widget::{
        self, Canvas, button,
        canvas::{Frame, Program},
        column, container, image, row,
    },
    window::{self, Id, Settings},
};
use nes_emu::{
    Break, NES,
};
use tokio::{io::AsyncWriteExt, runtime::Runtime};
use tracing_subscriber::EnvFilter;
mod debugger;
use debugger::{DbgImage, DebuggerMessage, DebuggerState, dbg_image};
mod resize_watcher;
use resize_watcher::resize_watcher;
mod header_menu;
use header_menu::header_menu;
mod hex_view;
use hex_view::{HexEvent, HexView};
mod audio;
use audio::Audio;

// const ROM_FILE: &str = concat!(env!("ROM_DIR"), "/", "even_odd.nes");
// const ROM_FILE: &str = concat!(env!("ROM_DIR"), "/", "crc_check.nes");
// const ROM_FILE: &str = concat!(env!("ROM_DIR"), "/", "ppu_fill_red.nes");
// const ROM_FILE: &str = concat!(env!("ROM_DIR"), "/", "ppu_fill_name_table.nes");
// const ROM_FILE: &str = concat!(env!("ROM_DIR"), "/", "int_nmi_exit_timing.nes");
// const ROM_FILE: &str = concat!(env!("ROM_DIR"), "/", "render-updating.nes");
// const ROM_FILE: &str = concat!(env!("ROM_DIR"), "/", "ppu_palette_shared.nes");
// const ROM_FILE: &str = concat!(env!("ROM_DIR"), "/", "input_test.nes");
// const ROM_FILE: &str = concat!(env!("ROM_DIR"), "/", "scrolling.nes");
// const ROM_FILE: &str = concat!(env!("ROM_DIR"), "/", "scrolling_colors.nes");
// const ROM_FILE: &str = concat!(env!("ROM_DIR"), "/", "sprites.nes");
// const ROM_FILE: &str = concat!(env!("ROM_DIR"), "/", "apu_pulse_channel_1.nes");
// const ROM_FILE: &str = concat!(env!("ROM_DIR"), "/", "apu_pulse_channel_1_evelope.nes");
// const ROM_FILE: &str = concat!(env!("ROM_DIR"), "/", "apu_pulse_1.nes");
// const ROM_FILE: &str = concat!(env!("ROM_DIR"), "/", "apu_triangle.nes");
// const ROM_FILE: &str = "./Super Mario Bros. (World).nes";
// const ROM_FILE: &str = "./cpu_timing_test.nes";
// const ROM_FILE: &str = "../nes-test-roms/instr_test-v5/official_only.nes";
const ROM_FILE: &str = "../Downloads/Legend of Zelda, The (USA).nes";

extern crate nes_emu;

fn main() -> Result<(), iced::Error> {
    tracing_subscriber::fmt()
        .with_env_filter(EnvFilter::from_default_env())
        .init();

    iced::daemon(Emulator::new, Emulator::update, Emulator::view)
        .subscription(Emulator::subscriptions)
        .theme(Theme::Dark)
        .title(Emulator::title)
        .executor::<Runtime>()
        .run()
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum MemoryTy {
    Cpu,
    PPU,
    OAM,
}

impl fmt::Display for MemoryTy {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "Export {self:?} Memory")
    }
}

#[derive(Debug, Clone, PartialEq, Eq)]
enum WindowType {
    Main,
    Memory(MemoryTy, HexView),
    TileMap,
    TileViewer,
    Palette,
    Debugger,
    Apu,
}

#[derive(Debug, Clone, PartialEq, Eq)]
enum HeaderButton {
    // OpenMemory,
    // OpenTileMap,
    // OpenTileViewer,
    // OpenDebugger,
    Open(WindowType),
    OpenRom,
    Reset,
    PowerCycle,
}

impl fmt::Display for HeaderButton {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::Open(WindowType::Memory(MemoryTy::Cpu, _)) => write!(f, "Open Memory Viewer"),
            Self::Open(WindowType::Memory(MemoryTy::PPU, _)) => write!(f, "Open PPU Memory Viewer"),
            Self::Open(WindowType::Memory(MemoryTy::OAM, _)) => write!(f, "Open OAM Memory Viewer"),
            Self::Open(WindowType::TileMap) => write!(f, "Open TileMap Viewer"),
            Self::Open(WindowType::TileViewer) => write!(f, "Open Tile Viewer"),
            Self::Open(WindowType::Debugger) => write!(f, "Open Debugger"),
            Self::Open(WindowType::Palette) => write!(f, "Open Palette"),
            Self::Open(WindowType::Main) => write!(f, "Create new Main window"),
            Self::Open(WindowType::Apu) => write!(f, "Open APU info"),
            Self::Reset => write!(f, "Reset"),
            Self::PowerCycle => write!(f, "Power Cycle"),
            Self::OpenRom => write!(f, "Open ROM file"),
        }
    }
}

struct Emulator {
    running: bool,
    nes: NES,
    windows: HashMap<Id, WindowType>,
    debugger: DebuggerState,
    main_win_size: Size,
    prev: [Instant; 2],
    audio: Audio,
}

#[derive(Debug, Clone)]
enum Message {
    OpenRom(NES),
    // Tick(usize),
    // Frame,
    // DMA,
    // CPU,
    // DebugInt,
    WindowClosed(Id),
    WindowOpened(Id),
    Key(keyboard::Event),
    Periodic(Instant),
    SetRunning(bool),
    Header(HeaderButton),
    Hex(Id, HexEvent),
    Debugger(DebuggerMessage),
    SetSize(window::Id, Size),
    Export(MemoryTy),
}

impl Emulator {
    fn new() -> (Self, Task<Message>) {
        let mut nes = nes_emu::NES::load_nes_file(ROM_FILE).expect("Failed to load nes file");
        nes.reset();
        nes.debug_log_mut().enable();
        let (win, task) = iced::window::open(Settings {
            min_size: None,
            ..Settings::default()
        });
        // let (win_2, task_2) = iced::window::open(Settings {
        //     min_size: None,
        //     ..Settings::default()
        // });
        (
            Self {
                nes,
                windows: HashMap::from_iter([
                    (win, WindowType::Main),
                    // (win_2, WindowType::Debugger),
                ]),
                debugger: DebuggerState::new(),
                main_win_size: Size::new(0., 0.),
                running: false,
                prev: [Instant::now(); 2],
                audio: Audio::init(),
            },
            Task::batch([
                task,
                // task_2
            ])
            .discard(), // task.discard(),
        )
    }
    fn title(&self, win: Id) -> String {
        match self.windows.get(&win) {
            Some(WindowType::Main) => "NES emu".into(),
            Some(WindowType::Memory(ty, _)) => format!("NES {ty:?} Memory"),
            Some(WindowType::TileMap) => "NES TileMap".into(),
            Some(WindowType::TileViewer) => "NES Tile Viewer".into(),
            Some(WindowType::Palette) => "NES Palette Viewer".into(),
            Some(WindowType::Debugger) => "NES Debugger".into(),
            Some(WindowType::Apu) => "NES APU Debugger".into(),
            None => todo!(),
        }
    }

    fn open(&mut self, ty: WindowType) -> Task<Message> {
        let (win, task) = iced::window::open(Settings::default());
        self.windows.insert(win, ty);
        return task.discard();
    }

    fn update(&mut self, message: Message) -> Task<Message> {
        match message {
            // Message::Tick(count) => {
            //     for _ in 0..count {
            //         self.nes.run_one_clock_cycle();
            //     }
            // }
            // Message::Frame => while !self.nes.run_one_clock_cycle().ppu_frame {},
            // Message::DMA => while !self.nes.run_one_clock_cycle().dma {},
            // Message::CPU => while !self.nes.run_one_clock_cycle().cpu_exec {},
            // Message::DebugInt => while !self.nes.run_one_clock_cycle().dbg_int {},
            Message::WindowClosed(id) => {
                if let Some(WindowType::Main) = self.windows.remove(&id) {
                    return iced::exit();
                }
            }
            Message::WindowOpened(_id) => {
                // if let Some(WindowType::Main) = self.windows.get(&id) {
                //     // println!("Running resize");
                //     return iced::window::resize(id, self.main_win_size);
                // }
            }
            Message::Header(HeaderButton::Open(w)) => {
                return self.open(w);
            }
            Message::Header(HeaderButton::OpenRom) => {
                return Task::future(
                    rfd::AsyncFileDialog::new()
                        .set_directory(".")
                        .add_filter("NES", &["nes"])
                        .set_title("Open NES Rom file")
                        .pick_file(),
                )
                .and_then(|p| {
                    Task::future(async move {
                        // println!("Opening: {}", p.path().display());
                        NES::async_load_nes_file(p.path()).await.ok().map(|mut nes| {
                            nes.debug_log_mut().enable();
                            nes
                        })
                    })
                })
                .and_then(|n| Task::done(Message::OpenRom(n)));
            }
            Message::Hex(id, val) => {
                if let Some(WindowType::Memory(_, view)) = self.windows.get_mut(&id) {
                    return view.update(val).map(move |e| Message::Hex(id, e));
                }
            }
            Message::Header(HeaderButton::Reset) => {
                self.nes.reset();
            }
            Message::Header(HeaderButton::PowerCycle) => {
                self.nes.power_cycle();
            }
            Message::Debugger(debugger_message) => {
                self.debugger.update(debugger_message, &mut self.nes)
            }
            Message::SetSize(id, size) => {
                if let Some(WindowType::Main) = self.windows.get(&id) {
                    self.main_win_size = size;
                }
                return Task::future(async {
                    tokio::time::sleep(Duration::from_millis(50)).await;
                })
                .then(move |_| iced::window::resize(id, size));
            }
            Message::OpenRom(nes) => {
                self.nes = nes;
                self.nes.power_cycle();
            }
            Message::Export(ty) => {
                let raw: Vec<_> = match ty {
                    MemoryTy::Cpu => (0..=0xFFFF)
                        .map(|i| self.nes.mem().peek_cpu(i).unwrap_or(0))
                        .collect(),
                    MemoryTy::PPU => (0..=0xFFFF)
                        .map(|i| self.nes.mem().peek_ppu(i).unwrap_or(0))
                        .collect(),
                    MemoryTy::OAM => (0..=0xFF).map(|i| self.nes.ppu().peek_oam(i)).collect(),
                };
                return Task::future(async move {
                    if let Some(file) = rfd::AsyncFileDialog::new()
                        .set_directory(".")
                        .set_file_name("output.dmp")
                        .set_title("Save memory dump")
                        .save_file()
                        .await
                    {
                        tokio::fs::File::create(file.path())
                            .await
                            .expect("Failed to save file")
                            .write_all(&raw)
                            .await
                            .expect("Failed to write dump");
                    }
                })
                .discard();
            }
            Message::Key(key) => match key {
                keyboard::Event::KeyPressed {
                    key: Key::Character(val),
                    modifiers: Modifiers::CTRL,
                    repeat: false,
                    ..
                } => {
                    if val == "t" {
                        self.nes.reset();
                    }
                }
                keyboard::Event::KeyPressed {
                    key,
                    modifiers: Modifiers::NONE,
                    repeat: false,
                    ..
                } => {
                    if key == Key::Character("z".into()) {
                        self.nes.controller_1().set_a(true);
                    } else if key == Key::Character("x".into()) {
                        self.nes.controller_1().set_b(true);
                    } else if key == Key::Character("a".into()) {
                        self.nes.controller_1().set_select(true);
                    } else if key == Key::Character("s".into()) {
                        self.nes.controller_1().set_start(true);
                    } else if key == Key::Named(Named::ArrowDown) {
                        self.nes.controller_1().set_down(true);
                    } else if key == Key::Named(Named::ArrowUp) {
                        self.nes.controller_1().set_up(true);
                    } else if key == Key::Named(Named::ArrowLeft) {
                        self.nes.controller_1().set_left(true);
                    } else if key == Key::Named(Named::ArrowRight) {
                        self.nes.controller_1().set_right(true);
                    } else if key == Key::Named(Named::Play) || key == Key::Named(Named::MediaPlay)
                    {
                        self.running = true;
                    } else if key == Key::Named(Named::Pause)
                        || key == Key::Named(Named::MediaPause)
                    {
                        self.running = false;
                    } else if key == Key::Named(Named::MediaPlayPause)
                        || key == Key::Named(Named::Space)
                    {
                        self.running = !self.running;
                    }
                }
                keyboard::Event::KeyReleased {
                    key,
                    modifiers: Modifiers::NONE,
                    ..
                } => {
                    if key == Key::Character("z".into()) {
                        self.nes.controller_1().set_a(false);
                    } else if key == Key::Character("x".into()) {
                        self.nes.controller_1().set_b(false);
                    } else if key == Key::Character("a".into()) {
                        self.nes.controller_1().set_select(false);
                    } else if key == Key::Character("s".into()) {
                        self.nes.controller_1().set_start(false);
                    } else if key == Key::Named(Named::ArrowDown) {
                        self.nes.controller_1().set_down(false);
                    } else if key == Key::Named(Named::ArrowUp) {
                        self.nes.controller_1().set_up(false);
                    } else if key == Key::Named(Named::ArrowLeft) {
                        self.nes.controller_1().set_left(false);
                    } else if key == Key::Named(Named::ArrowRight) {
                        self.nes.controller_1().set_right(false);
                    }
                }
                _ => (),
            },
            Message::Periodic(_i) => {
                if self.running {
                    // TODO: Smarter frame skip
                    if self.prev[0].elapsed() >= Duration::from_millis(2) {
                        self.nes.run_one_clock_cycle(&Break::default());
                        let mut count = 0;
                        while !self.nes.run_one_clock_cycle(&Break::default()).ppu_frame
                            && !self.nes.halted()
                        {
                            count += 1;
                            if count > 90_000 {
                                println!("Loop overran...");
                                break;
                            }
                        }
                        self.prev[0] = Instant::now();
                        self.audio.submit(self.nes.apu().get_frame_samples());
                        self.nes.apu_mut().reset_frame_samples();
                    }
                } else {
                    self.audio.pause();
                }
            }
            Message::SetRunning(running) => self.running = running,
        }
        // self.image.0.clone_from(self.nes.image());
        Task::none()
    }

    fn subscriptions(&self) -> Subscription<Message> {
        Subscription::batch([
            window::close_events().map(Message::WindowClosed),
            window::open_events().map(Message::WindowOpened),
            keyboard::listen().map(Message::Key),
            time::every(Duration::from_secs_f64(1./60.)).map(Message::Periodic),
        ])
    }

    fn view(&self, win: Id) -> Element<'_, Message> {
        // println!("Running view");
        match self.windows.get(&win) {
            Some(WindowType::Main) => {
                let content = column![
                    self.dropdowns(),
                    Element::from(Canvas::new(self).width(256. * 2.).height(240. * 2.)),
                    // self.cpu_state(),
                    // self.controls(),
                ]
                .height(Shrink);
                resize_watcher(content)
                    .on_resize(move |s| Message::SetSize(win, s))
                    .width(Shrink)
                    .height(Shrink)
                    .into()
            }
            Some(WindowType::Memory(ty, view)) => {
                let hex = match ty {
                    MemoryTy::Cpu => view
                        .render_cpu(self.nes.mem())
                        .map(move |e| Message::Hex(win, e)),
                    MemoryTy::PPU => view
                        .render_ppu(self.nes.mem(), self.nes.ppu())
                        .map(move |e| Message::Hex(win, e)),
                    MemoryTy::OAM => view
                        .render_oam(self.nes.ppu())
                        .map(move |e| Message::Hex(win, e)),
                };
                let content = column![row![header_menu("Export", [*ty], Message::Export)], hex]
                    .width(Fill)
                    .height(Fill);
                container(content).width(Fill).height(Fill).into()
            }
            Some(WindowType::TileMap) => {
                dbg_image(DbgImage::NameTable(self.nes.mem(), self.nes.ppu())).into()
            }
            Some(WindowType::TileViewer) => {
                dbg_image(DbgImage::PatternTable(self.nes.mem(), self.nes.ppu())).into()
            }
            Some(WindowType::Palette) => {
                dbg_image(DbgImage::Palette(self.nes.ppu())).into()
            }
            Some(WindowType::Debugger) => column![
                row![
                    button(image("./images/ic_fluent_play_24_filled.png"))
                        .on_press(Message::SetRunning(true)),
                    button(image("./images/ic_fluent_pause_24_filled.png"))
                        .on_press(Message::SetRunning(false)),
                ],
                self.debugger.view(&self.nes).map(Message::Debugger)
            ]
            .width(Fill)
            .height(Fill)
            .into(),
            Some(WindowType::Apu) => {
                self.nes.apu().view()
            }
            None => panic!("Window not found"),
            // _ => todo!(),
        }
    }

    fn dropdowns(&self) -> Element<'_, Message> {
        row![
            header_menu(
                "Console",
                [
                    HeaderButton::OpenRom,
                    HeaderButton::Reset,
                    HeaderButton::PowerCycle,
                ],
                Message::Header
            ),
            header_menu(
                "Debugging",
                [
                    HeaderButton::Open(WindowType::Debugger),
                    HeaderButton::Open(WindowType::Memory(MemoryTy::Cpu, HexView {})),
                    HeaderButton::Open(WindowType::Memory(MemoryTy::PPU, HexView {})),
                    HeaderButton::Open(WindowType::Memory(MemoryTy::OAM, HexView {})),
                    HeaderButton::Open(WindowType::TileMap),
                    HeaderButton::Open(WindowType::TileViewer),
                    HeaderButton::Open(WindowType::Palette),
                    HeaderButton::Open(WindowType::Apu),
                ],
                Message::Header
            )
        ]
        .width(Fill)
        .into()
    }
}

impl Program<Message> for Emulator {
    type State = ();

    fn draw(
        &self,
        _state: &Self::State,
        renderer: &Renderer,
        _theme: &Theme,
        bounds: iced::Rectangle,
        _cursor: mouse::Cursor,
    ) -> Vec<iced::widget::canvas::Geometry<Renderer>> {
        let mut frame = Frame::new(renderer, bounds.size());
        frame.scale(2.);
        frame.draw_image(
            Rectangle::new(Point::new(0., 0.), Size::new(256., 240.)),
            widget::canvas::Image::new(widget::image::Handle::from_rgba(
                256,
                240,
                self.nes.image().image(),
            ))
            .filter_method(image::FilterMethod::Nearest)
            .snap(true),
        );
        vec![frame.into_geometry()]
    }
}
