use std::{
    collections::HashMap,
    fs::{self, File},
    io::{Read, Seek, SeekFrom, Write},
    path::{Path, PathBuf},
    sync::{
        atomic::{AtomicBool, AtomicUsize, Ordering},
        mpsc::{self, RecvError, TryRecvError},
        Arc,
    },
};

use anyhow::Result;
use egui_toast::{Toast, ToastKind, ToastOptions};

use crate::{audio::Audio, graphics::TextureSink};
pub use shrooms_vb_core::VBKey;
use shrooms_vb_core::{Sim, EXPECTED_FRAME_SIZE};

mod shrooms_vb_core;

pub struct EmulatorBuilder {
    rom: Option<PathBuf>,
    commands: mpsc::Receiver<EmulatorCommand>,
    sim_count: Arc<AtomicUsize>,
    running: Arc<[AtomicBool; 2]>,
    has_game: Arc<[AtomicBool; 2]>,
    audio_on: Arc<[AtomicBool; 2]>,
    linked: Arc<AtomicBool>,
}

#[derive(PartialEq, Eq, Hash, Clone, Copy, Debug)]
pub enum SimId {
    Player1,
    Player2,
}
impl SimId {
    pub const fn values() -> [Self; 2] {
        [Self::Player1, Self::Player2]
    }
    pub const fn to_index(self) -> usize {
        match self {
            Self::Player1 => 0,
            Self::Player2 => 1,
        }
    }
}

struct Cart {
    rom_path: PathBuf,
    rom: Vec<u8>,
    sram_file: File,
    sram: Vec<u8>,
}

impl Cart {
    fn load(rom_path: &Path, sim_id: SimId) -> Result<Self> {
        let rom = fs::read(rom_path)?;

        let mut sram_file = File::options()
            .read(true)
            .write(true)
            .create(true)
            .truncate(false)
            .open(sram_path(rom_path, sim_id))?;
        sram_file.set_len(8 * 1024)?;

        let mut sram = vec![];
        sram_file.read_to_end(&mut sram)?;
        Ok(Cart {
            rom_path: rom_path.to_path_buf(),
            rom,
            sram_file,
            sram,
        })
    }
}

fn sram_path(rom_path: &Path, sim_id: SimId) -> PathBuf {
    match sim_id {
        SimId::Player1 => rom_path.with_extension("p1.sram"),
        SimId::Player2 => rom_path.with_extension("p2.sram"),
    }
}

impl EmulatorBuilder {
    pub fn new() -> (Self, EmulatorClient) {
        let (queue, commands) = mpsc::channel();
        let builder = Self {
            rom: None,
            commands,
            sim_count: Arc::new(AtomicUsize::new(0)),
            running: Arc::new([AtomicBool::new(false), AtomicBool::new(false)]),
            has_game: Arc::new([AtomicBool::new(false), AtomicBool::new(false)]),
            audio_on: Arc::new([AtomicBool::new(true), AtomicBool::new(true)]),
            linked: Arc::new(AtomicBool::new(false)),
        };
        let client = EmulatorClient {
            queue,
            sim_count: builder.sim_count.clone(),
            running: builder.running.clone(),
            has_game: builder.has_game.clone(),
            audio_on: builder.audio_on.clone(),
            linked: builder.linked.clone(),
        };
        (builder, client)
    }

    pub fn with_rom(self, path: &Path) -> Self {
        Self {
            rom: Some(path.into()),
            ..self
        }
    }

    pub fn build(self) -> Result<Emulator> {
        let mut emulator = Emulator::new(
            self.commands,
            self.sim_count,
            self.running,
            self.has_game,
            self.audio_on,
            self.linked,
        )?;
        if let Some(path) = self.rom {
            emulator.load_cart(SimId::Player1, &path)?;
        }
        Ok(emulator)
    }
}

pub struct Emulator {
    sims: Vec<Sim>,
    carts: [Option<Cart>; 2],
    audio: Audio,
    commands: mpsc::Receiver<EmulatorCommand>,
    sim_count: Arc<AtomicUsize>,
    running: Arc<[AtomicBool; 2]>,
    has_game: Arc<[AtomicBool; 2]>,
    audio_on: Arc<[AtomicBool; 2]>,
    linked: Arc<AtomicBool>,
    renderers: HashMap<SimId, TextureSink>,
    messages: HashMap<SimId, mpsc::Sender<Toast>>,
}

impl Emulator {
    fn new(
        commands: mpsc::Receiver<EmulatorCommand>,
        sim_count: Arc<AtomicUsize>,
        running: Arc<[AtomicBool; 2]>,
        has_game: Arc<[AtomicBool; 2]>,
        audio_on: Arc<[AtomicBool; 2]>,
        linked: Arc<AtomicBool>,
    ) -> Result<Self> {
        Ok(Self {
            sims: vec![],
            carts: [None, None],
            audio: Audio::init()?,
            commands,
            sim_count,
            running,
            has_game,
            audio_on,
            linked,
            renderers: HashMap::new(),
            messages: HashMap::new(),
        })
    }

    pub fn load_cart(&mut self, sim_id: SimId, path: &Path) -> Result<()> {
        let cart = Cart::load(path, sim_id)?;
        self.reset_sim(sim_id, Some(cart))?;
        Ok(())
    }

    pub fn start_second_sim(&mut self, rom: Option<PathBuf>) -> Result<()> {
        let rom_path = if let Some(path) = rom {
            Some(path)
        } else {
            self.carts[0].as_ref().map(|c| c.rom_path.clone())
        };
        let cart = match rom_path {
            Some(rom_path) => Some(Cart::load(&rom_path, SimId::Player2)?),
            None => None,
        };
        self.reset_sim(SimId::Player2, cart)?;
        self.link_sims();
        Ok(())
    }

    fn reset_sim(&mut self, sim_id: SimId, new_cart: Option<Cart>) -> Result<()> {
        self.save_sram(sim_id)?;

        let index = sim_id.to_index();
        while self.sims.len() <= index {
            self.sims.push(Sim::new());
        }
        self.sim_count.store(self.sims.len(), Ordering::Relaxed);
        let sim = &mut self.sims[index];
        sim.reset();
        if let Some(cart) = new_cart {
            sim.load_cart(cart.rom.clone(), cart.sram.clone())?;
            self.carts[index] = Some(cart);
            self.has_game[index].store(true, Ordering::Release);
        }
        if self.has_game[index].load(Ordering::Acquire) {
            self.running[index].store(true, Ordering::Release);
        }
        Ok(())
    }

    fn link_sims(&mut self) {
        let (first, second) = self.sims.split_at_mut(1);
        let Some(first) = first.first_mut() else {
            return;
        };
        let Some(second) = second.first_mut() else {
            return;
        };
        first.link(second);
        self.linked.store(true, Ordering::Release);
    }

    fn unlink_sims(&mut self) {
        let Some(first) = self.sims.first_mut() else {
            return;
        };
        first.unlink();
        self.linked.store(false, Ordering::Release);
    }

    pub fn pause_sim(&mut self, sim_id: SimId) -> Result<()> {
        self.running[sim_id.to_index()].store(false, Ordering::Release);
        self.save_sram(sim_id)
    }

    fn save_sram(&mut self, sim_id: SimId) -> Result<()> {
        let sim = self.sims.get_mut(sim_id.to_index());
        let cart = self.carts[sim_id.to_index()].as_mut();
        if let (Some(sim), Some(cart)) = (sim, cart) {
            sim.read_sram(&mut cart.sram);
            cart.sram_file.seek(SeekFrom::Start(0))?;
            cart.sram_file.write_all(&cart.sram)?;
        }
        Ok(())
    }

    pub fn stop_second_sim(&mut self) -> Result<()> {
        self.save_sram(SimId::Player2)?;
        self.renderers.remove(&SimId::Player2);
        self.sims.truncate(1);
        self.sim_count.store(self.sims.len(), Ordering::Relaxed);
        self.running[SimId::Player2.to_index()].store(false, Ordering::Release);
        self.has_game[SimId::Player2.to_index()].store(false, Ordering::Release);
        self.linked.store(false, Ordering::Release);
        Ok(())
    }

    pub fn run(&mut self) {
        let mut eye_contents = vec![0u8; 384 * 224 * 2];
        let mut audio_samples = vec![];
        loop {
            let p1_running = self.running[SimId::Player1.to_index()].load(Ordering::Acquire);
            let p2_running = self.running[SimId::Player2.to_index()].load(Ordering::Acquire);
            let mut idle = p1_running || p2_running;
            if p1_running && p2_running {
                Sim::emulate_many(&mut self.sims);
            } else if p1_running {
                self.sims[SimId::Player1.to_index()].emulate();
            } else if p2_running {
                self.sims[SimId::Player2.to_index()].emulate();
            }

            for sim_id in SimId::values() {
                let Some(renderer) = self.renderers.get_mut(&sim_id) else {
                    continue;
                };
                let Some(sim) = self.sims.get_mut(sim_id.to_index()) else {
                    continue;
                };
                if sim.read_pixels(&mut eye_contents) {
                    idle = false;
                    if renderer.queue_render(&eye_contents).is_err() {
                        self.renderers.remove(&sim_id);
                    }
                }
            }
            let p1_audio =
                p1_running && self.audio_on[SimId::Player1.to_index()].load(Ordering::Acquire);
            let p2_audio =
                p2_running && self.audio_on[SimId::Player2.to_index()].load(Ordering::Acquire);
            let weight = if p1_audio && p2_audio { 0.5 } else { 1.0 };
            if p1_audio {
                if let Some(sim) = self.sims.get_mut(SimId::Player1.to_index()) {
                    sim.read_samples(&mut audio_samples, weight);
                }
            }
            if p2_audio {
                if let Some(sim) = self.sims.get_mut(SimId::Player2.to_index()) {
                    sim.read_samples(&mut audio_samples, weight);
                }
            }
            if audio_samples.is_empty() {
                audio_samples.resize(EXPECTED_FRAME_SIZE, 0.0);
            } else {
                idle = false;
            }
            self.audio.update(&audio_samples);
            audio_samples.clear();
            if idle {
                // The game is paused, and we have output all the video/audio we have.
                // Block the thread until a new command comes in.
                match self.commands.recv() {
                    Ok(command) => self.handle_command(command),
                    Err(RecvError) => {
                        return;
                    }
                }
            }
            loop {
                match self.commands.try_recv() {
                    Ok(command) => self.handle_command(command),
                    Err(TryRecvError::Empty) => {
                        break;
                    }
                    Err(TryRecvError::Disconnected) => {
                        return;
                    }
                }
            }
        }
    }

    fn handle_command(&mut self, command: EmulatorCommand) {
        match command {
            EmulatorCommand::ConnectToSim(sim_id, renderer, messages) => {
                self.renderers.insert(sim_id, renderer);
                self.messages.insert(sim_id, messages);
            }
            EmulatorCommand::LoadGame(sim_id, path) => {
                if let Err(error) = self.load_cart(sim_id, &path) {
                    self.report_error(sim_id, format!("Error loading rom: {error}"));
                }
            }
            EmulatorCommand::StartSecondSim(path) => {
                if let Err(error) = self.start_second_sim(path) {
                    self.report_error(
                        SimId::Player2,
                        format!("Error starting second sim: {error}"),
                    );
                }
            }
            EmulatorCommand::StopSecondSim => {
                if let Err(error) = self.stop_second_sim() {
                    self.report_error(
                        SimId::Player2,
                        format!("Error stopping second sim: {error}"),
                    );
                }
            }
            EmulatorCommand::Pause => {
                for sim_id in SimId::values() {
                    if let Err(error) = self.pause_sim(sim_id) {
                        self.report_error(sim_id, format!("Error pausing: {error}"));
                    }
                }
            }
            EmulatorCommand::Resume => {
                for sim_id in SimId::values() {
                    let index = sim_id.to_index();
                    if self.has_game[index].load(Ordering::Acquire) {
                        self.running[index].store(true, Ordering::Relaxed);
                    }
                }
            }
            EmulatorCommand::SetAudioEnabled(p1, p2) => {
                self.audio_on[SimId::Player1.to_index()].store(p1, Ordering::Release);
                self.audio_on[SimId::Player2.to_index()].store(p2, Ordering::Release);
            }
            EmulatorCommand::Link => {
                self.link_sims();
            }
            EmulatorCommand::Unlink => {
                self.unlink_sims();
            }
            EmulatorCommand::Reset(sim_id) => {
                if let Err(error) = self.reset_sim(sim_id, None) {
                    self.report_error(sim_id, format!("Error resetting sim: {error}"));
                }
            }
            EmulatorCommand::SetKeys(sim_id, keys) => {
                if let Some(sim) = self.sims.get_mut(sim_id.to_index()) {
                    sim.set_keys(keys);
                }
            }
            EmulatorCommand::Exit(done) => {
                for sim_id in SimId::values() {
                    if let Err(error) = self.save_sram(sim_id) {
                        self.report_error(sim_id, format!("Error saving sram on exit: {error}"));
                    }
                }
                let _ = done.send(());
            }
        }
    }

    fn report_error(&self, sim_id: SimId, message: String) {
        let messages = self
            .messages
            .get(&sim_id)
            .or_else(|| self.messages.get(&SimId::Player1));
        if let Some(msg) = messages {
            let toast = Toast::new()
                .kind(ToastKind::Error)
                .options(ToastOptions::default().duration_in_seconds(5.0))
                .text(&message);
            if msg.send(toast).is_ok() {
                return;
            }
        }
        eprintln!("{}", message);
    }
}

#[derive(Debug)]
pub enum EmulatorCommand {
    ConnectToSim(SimId, TextureSink, mpsc::Sender<Toast>),
    LoadGame(SimId, PathBuf),
    StartSecondSim(Option<PathBuf>),
    StopSecondSim,
    Pause,
    Resume,
    SetAudioEnabled(bool, bool),
    Link,
    Unlink,
    Reset(SimId),
    SetKeys(SimId, VBKey),
    Exit(oneshot::Sender<()>),
}

#[derive(Clone)]
pub struct EmulatorClient {
    queue: mpsc::Sender<EmulatorCommand>,
    sim_count: Arc<AtomicUsize>,
    running: Arc<[AtomicBool; 2]>,
    has_game: Arc<[AtomicBool; 2]>,
    audio_on: Arc<[AtomicBool; 2]>,
    linked: Arc<AtomicBool>,
}

impl EmulatorClient {
    pub fn has_player_2(&self) -> bool {
        self.sim_count.load(Ordering::Acquire) == 2
    }
    pub fn is_running(&self, sim_id: SimId) -> bool {
        self.running[sim_id.to_index()].load(Ordering::Acquire)
    }
    pub fn has_game(&self, sim_id: SimId) -> bool {
        self.has_game[sim_id.to_index()].load(Ordering::Acquire)
    }
    pub fn are_sims_linked(&self) -> bool {
        self.linked.load(Ordering::Acquire)
    }
    pub fn is_audio_enabled(&self, sim_id: SimId) -> bool {
        self.audio_on[sim_id.to_index()].load(Ordering::Acquire)
    }
    pub fn send_command(&self, command: EmulatorCommand) -> bool {
        match self.queue.send(command) {
            Ok(()) => true,
            Err(err) => {
                eprintln!(
                    "could not send command {:?} as emulator is shut down",
                    err.0
                );
                false
            }
        }
    }
}