use std::fs::File;
use std::io::{Read, Write};
use std::path::Path;

use chrono::Local;
use platform_dirs::UserDirs;
use zip::write::FileOptions as ZipFileOptions;
use zip::{ZipArchive, ZipWriter};

use crate::image::Image;
use crate::music::Music;
use crate::screencast::Screencast;
use crate::settings::{
    NUM_COLORS, NUM_IMAGES, NUM_MUSICS, NUM_SOUNDS, NUM_TILEMAPS, PYXEL_VERSION,
    RESOURCE_ARCHIVE_DIRNAME,
};
use crate::sound::Sound;
use crate::system::System;
use crate::tilemap::Tilemap;
use crate::types::{Color, Rgb8};
use crate::utils::parse_version_string;

pub trait ResourceItem {
    fn resource_name(item_no: u32) -> String;
    fn is_modified(&self) -> bool;
    fn clear(&mut self);
    fn serialize(&self) -> String;
    fn deserialize(&mut self, version: u32, input: &str);
}

pub struct Resource {
    capture_scale: u32,
    screencast: Screencast,
}

unsafe_singleton!(Resource);

impl Resource {
    pub fn init(fps: u32, capture_scale: u32, capture_sec: u32) {
        Self::set_instance(Self {
            capture_scale: u32::max(capture_scale, 1),
            screencast: Screencast::new(fps, capture_sec),
        });
    }

    pub fn capture_screen(
        &mut self,
        image: &[Vec<Color>],
        colors: &[Rgb8; NUM_COLORS as usize],
        frame_count: u32,
    ) {
        self.screencast.capture(image, colors, frame_count);
    }

    fn export_path() -> String {
        UserDirs::new()
            .unwrap()
            .desktop_dir
            .join(Local::now().format("pyxel-%Y%m%d-%H%M%S").to_string())
            .to_str()
            .unwrap()
            .to_string()
    }
}

pub fn load(filename: &str, image: bool, tilemap: bool, sound: bool, music: bool) {
    let mut archive = ZipArchive::new(
        File::open(&Path::new(&filename))
            .unwrap_or_else(|_| panic!("Unable to open file '{}'", filename)),
    )
    .unwrap_or_else(|_| panic!("Unable to parse zip archive '{}'", filename));
    let version_name = RESOURCE_ARCHIVE_DIRNAME.to_string() + "version";
    let contents = {
        let mut file = archive.by_name(&version_name).unwrap();
        let mut contents = String::new();
        file.read_to_string(&mut contents).unwrap();
        contents
    };
    let version = parse_version_string(&contents).unwrap();
    assert!(
        version <= parse_version_string(PYXEL_VERSION).unwrap(),
        "Unsupported resource file version '{}'",
        contents
    );

    macro_rules! deserialize {
        ($type: ty, $getter: ident, $count: expr) => {
            for i in 0..$count {
                if let Ok(mut file) = archive.by_name(&<$type>::resource_name(i)) {
                    let mut input = String::new();
                    file.read_to_string(&mut input).unwrap();
                    crate::$getter(i).lock().deserialize(version, &input);
                } else {
                    crate::$getter(i).lock().clear();
                }
            }
        };
    }

    if image {
        deserialize!(Image, image, NUM_IMAGES);
    }
    if tilemap {
        deserialize!(Tilemap, tilemap, NUM_TILEMAPS);
    }
    if sound {
        deserialize!(Sound, sound, NUM_SOUNDS);
    }
    if music {
        deserialize!(Music, music, NUM_MUSICS);
    }
}

pub fn save(filename: &str, image: bool, tilemap: bool, sound: bool, music: bool) {
    let path = std::path::Path::new(&filename);
    let file = std::fs::File::create(&path)
        .unwrap_or_else(|_| panic!("Unable to open file '{}'", filename));
    let mut zip = ZipWriter::new(file);
    zip.add_directory(RESOURCE_ARCHIVE_DIRNAME, ZipFileOptions::default())
        .unwrap();
    let version_name = RESOURCE_ARCHIVE_DIRNAME.to_string() + "version";
    zip.start_file(version_name, ZipFileOptions::default())
        .unwrap();
    zip.write_all(PYXEL_VERSION.as_bytes()).unwrap();

    macro_rules! serialize {
        ($type: ty, $getter: ident, $count: expr) => {
            for i in 0..$count {
                if crate::$getter(i).lock().is_modified() {
                    zip.start_file(<$type>::resource_name(i), ZipFileOptions::default())
                        .unwrap();
                    zip.write_all(crate::$getter(i).lock().serialize().as_bytes())
                        .unwrap();
                }
            }
        };
    }

    if image {
        serialize!(Image, image, NUM_IMAGES);
    }
    if tilemap {
        serialize!(Tilemap, tilemap, NUM_TILEMAPS);
    }
    if sound {
        serialize!(Sound, sound, NUM_SOUNDS);
    }
    if music {
        serialize!(Music, music, NUM_MUSICS);
    }
    zip.finish().unwrap();
}

pub fn screenshot(scale: Option<u32>) {
    let filename = Resource::export_path();
    let scale = u32::max(scale.unwrap_or(Resource::instance().capture_scale), 1);
    crate::screen().lock().save(&filename, scale);
    System::instance().disable_next_frame_skip();
}

pub fn reset_capture() {
    Resource::instance().screencast.reset();
}

pub fn screencast(scale: Option<u32>) {
    let filename = Resource::export_path();
    let scale = u32::max(scale.unwrap_or(Resource::instance().capture_scale), 1);
    Resource::instance().screencast.save(&filename, scale);
    System::instance().disable_next_frame_skip();
}
