use std::{
    cell::RefCell,
    fs::File,
    io::{BufWriter, Read, Write},
    path::Path,
    rc::Rc,
};

#[cfg(feature = "wasm")]
use wasm_bindgen::prelude::*;

pub type SharedMut<T> = Rc<RefCell<T>>;

/// Reads the contents of the file at the given path into
/// a vector of bytes.
pub fn read_file(path: &str) -> Result<Vec<u8>, String> {
    let mut file = match File::open(path) {
        Ok(file) => file,
        Err(_) => return Err(format!("Failed to open file: {}", path)),
    };
    let mut data = Vec::new();
    file.read_to_end(&mut data).unwrap();
    Ok(data)
}

/// Writes the given data to the file at the given path.
pub fn write_file(path: &str, data: &[u8]) -> Result<(), String> {
    let mut file = match File::create(path) {
        Ok(file) => file,
        Err(_) => return Err(format!("Failed to open file: {}", path)),
    };
    file.write_all(data).unwrap();
    Ok(())
}

/// Replaces the extension in the given path with the provided extension.
/// This function allows for simple associated file discovery.
pub fn replace_ext(path: &str, new_extension: &str) -> Option<String> {
    let file_path = Path::new(path);
    let parent_dir = file_path.parent()?;
    let file_stem = file_path.file_stem()?;
    let file_extension = file_path.extension()?;
    if file_stem == file_extension {
        return None;
    }
    let new_file_name = format!("{}.{}", file_stem.to_str()?, new_extension);
    let new_file_path = parent_dir.join(new_file_name);
    Some(String::from(new_file_path.to_str()?))
}

/// Capitalizes the first character in the provided string.
pub fn capitalize(string: &str) -> String {
    let mut chars = string.chars();
    match chars.next() {
        None => String::new(),
        Some(chr) => chr.to_uppercase().collect::<String>() + chars.as_str(),
    }
}

pub fn save_bmp(path: &str, pixels: &[u8], width: u32, height: u32) -> Result<(), String> {
    let file = match File::create(path) {
        Ok(file) => file,
        Err(_) => return Err(format!("Failed to open file: {}", path)),
    };
    let mut writer = BufWriter::new(file);

    // writes the BMP file header
    let file_size = 54 + (width * height * 3);
    writer.write_all(&[0x42, 0x4d]).unwrap(); // "BM" magic number
    writer.write_all(&file_size.to_le_bytes()).unwrap(); // file size
    writer.write_all(&[0x00, 0x00]).unwrap(); // reserved
    writer.write_all(&[0x00, 0x00]).unwrap(); // reserved
    writer.write_all(&[0x36, 0x00, 0x00, 0x00]).unwrap(); // offset to pixel data
    writer.write_all(&[0x28, 0x00, 0x00, 0x00]).unwrap(); // DIB header size
    writer.write_all(&(width as i32).to_le_bytes()).unwrap(); // image width
    writer.write_all(&(height as i32).to_le_bytes()).unwrap(); // image height
    writer.write_all(&[0x01, 0x00]).unwrap(); // color planes
    writer.write_all(&[0x18, 0x00]).unwrap(); // bits per pixel
    writer.write_all(&[0x00, 0x00, 0x00, 0x00]).unwrap(); // compression method
    writer
        .write_all(&[(width * height * 3) as u8, 0x00, 0x00, 0x00])
        .unwrap(); // image size
    writer.write_all(&[0x13, 0x0b, 0x00, 0x00]).unwrap(); // horizontal resolution (72 DPI)
    writer.write_all(&[0x13, 0x0b, 0x00, 0x00]).unwrap(); // vertical resolution (72 DPI)
    writer.write_all(&[0x00, 0x00, 0x00, 0x00]).unwrap(); // color palette
    writer.write_all(&[0x00, 0x00, 0x00, 0x00]).unwrap(); // important colors

    // iterates over the complete array of pixels in reverse order
    // to account for the fact that BMP files are stored upside down
    for y in (0..height).rev() {
        for x in 0..width {
            let [r, g, b] = [
                pixels[((y * width + x) * 3) as usize],
                pixels[((y * width + x) * 3 + 1) as usize],
                pixels[((y * width + x) * 3 + 2) as usize],
            ];
            writer.write_all(&[b, g, r]).unwrap();
        }
        let padding = (4 - ((width * 3) % 4)) % 4;
        for _ in 0..padding {
            writer.write_all(&[0x00]).unwrap();
        }
    }

    Ok(())
}

#[cfg(not(feature = "wasm"))]
pub fn get_timestamp() -> u64 {
    use std::time::{SystemTime, UNIX_EPOCH};

    let now = SystemTime::now();
    now.duration_since(UNIX_EPOCH).unwrap().as_secs()
}

#[cfg(feature = "wasm")]
#[cfg_attr(feature = "wasm", wasm_bindgen)]
pub fn get_timestamp() -> u64 {
    use js_sys::Date;

    (Date::now() / 1000.0) as u64
}

#[cfg(test)]
mod tests {
    use std::path::Path;

    use super::replace_ext;

    #[test]
    fn test_change_extension() {
        let new_path = replace_ext("/path/to/file.txt", "dat").unwrap();
        assert_eq!(
            new_path,
            Path::new("/path/to").join("file.dat").to_str().unwrap()
        );

        let new_path = replace_ext("/path/to/file.with.multiple.dots.txt", "dat").unwrap();
        assert_eq!(
            new_path,
            Path::new("/path/to")
                .join("file.with.multiple.dots.dat")
                .to_str()
                .unwrap()
        );

        let new_path = replace_ext("/path/to/file.without.extension", "dat").unwrap();
        assert_eq!(
            new_path,
            Path::new("/path/to")
                .join("file.without.dat")
                .to_str()
                .unwrap()
        );

        let new_path = replace_ext("/path/to/directory/", "dat");
        assert_eq!(new_path, None);
    }
}