Documentation
use crate::common::*;
use std::io;
use wasm_bindgen::prelude::*;
use crate::file::platforms::browser::{
    js_io_error_to_rs_io_error,
    js_io_error_to_rs_io_error_for_delete_directory,
};

#[wasm_bindgen(module = "browser.js")]
extern "C" {
    #[derive(Clone)]
    type JSFileSystemReference;

    #[wasm_bindgen(constructor)]
    fn new(handle: JsValue) -> JSFileSystemReference;

    #[wasm_bindgen(method, js_name = name)]
    fn name(this: &JSFileSystemReference) -> String;

    #[wasm_bindgen(method, js_name = asDirectory)]
    fn as_directory(this: &JSFileSystemReference) -> Option<JSDirectoryReference>;

    #[wasm_bindgen(method, js_name = asFile)]
    fn as_file(this: &JSFileSystemReference) -> Option<JSFileReference>;

    #[derive(Clone)]
    type JSFileReference;

    #[wasm_bindgen(constructor)]
    fn new(handle: JsValue) -> JSFileReference;

    #[wasm_bindgen(method, js_name = name)]
    fn name(this: &JSFileReference) -> String;

    #[wasm_bindgen(catch, method, js_name = readBytes)]
    async fn read_bytes(this: &JSFileReference) -> Result<JsValue, JsValue>;

    #[wasm_bindgen(catch, method, js_name = write)]
    async fn write(this: &JSFileReference, bytes: JsValue) -> Result<JsValue, JsValue>;

    #[wasm_bindgen(catch, method, js_name = modificationEpochMilliseconds)]
    async fn modification_epoch_milliseconds(this: &JSFileReference) -> Result<JsValue, JsValue>;

    #[wasm_bindgen(catch, method, js_name = size)]
    async fn size(this: &JSFileReference) -> Result<JsValue, JsValue>;

    #[wasm_bindgen(method, js_name = toFileSystemReference)]
    fn to_file_system_reference(this: &JSFileReference) -> JSFileSystemReference;

    #[derive(Clone)]
    type JSDirectoryReference;

    #[wasm_bindgen(constructor)]
    fn new(handle: JsValue) -> JSDirectoryReference;

    #[wasm_bindgen(method, js_name = name)]
    fn name(this: &JSDirectoryReference) -> String;

    #[wasm_bindgen(catch, method, js_name = entries)]
    async fn entries(this: &JSDirectoryReference) -> Result<JsValue, JsValue>;

    #[wasm_bindgen(catch, method, js_name = getDirectory)]
    async fn get_directory(this: &JSDirectoryReference, name: String, create: bool) -> Result<JsValue, JsValue>;

    #[wasm_bindgen(catch, method, js_name = getFile)]
    async fn get_file(this: &JSDirectoryReference, name: String, create: bool) -> Result<JsValue, JsValue>;

    #[wasm_bindgen(catch, method, js_name = deleteEmptyDirectory)]
    async fn delete_empty_directory(this: &JSDirectoryReference, name: String) -> Result<JsValue, JsValue>;

    #[wasm_bindgen(catch, method, js_name = deleteDirectoryAll)]
    async fn delete_directory_all(this: &JSDirectoryReference, name: String) -> Result<JsValue, JsValue>;

    #[wasm_bindgen(catch, method, js_name = deleteFile)]
    async fn delete_file(this: &JSDirectoryReference, name: String) -> Result<JsValue, JsValue>;

    #[wasm_bindgen(method, js_name = toFileSystemReference)]
    fn to_file_system_reference(this: &JSDirectoryReference) -> JSFileSystemReference;
}

#[derive(Clone)]
pub struct FileSystemReference(JSFileSystemReference);

impl FileSystemReference {
    pub fn name(&self) -> String {
        self.0.name()
    }

    pub fn as_directory(&self) -> Option<DirectoryReference> {
        self.0.as_directory().map(|d| DirectoryReference(d))
    }

    pub fn as_file(&self) -> Option<FileReference> {
        self.0.as_file().map(|file| FileReference(file))
    }
}

#[derive(Clone)]
pub struct FileReference(JSFileReference);

impl FileReference {
    pub async fn read_bytes(&self) -> io::Result<Bytes> {
        self.0.read_bytes().await.map(|ba| Bytes::from(js_sys::Uint8Array::try_from(ba).unwrap().to_vec())).map_err(|error| js_io_error_to_rs_io_error(error, false))
    }

    pub async fn read_utf8(&self) -> io::Result<String> {
        Ok(String::from_utf8_lossy(&self.read_bytes().await?).into_owned())
    }

    pub async fn write(&self, data: &[u8]) -> io::Result<()> {
        let uint8array = js_sys::Uint8Array::from(data);
        self.0.write(uint8array.buffer().into()).await.map(|_| ()).map_err(|error| js_io_error_to_rs_io_error(error, false))
    }

    pub async fn modification_date(&self) -> io::Result<std::time::SystemTime> {
        let ms = self.0.modification_epoch_milliseconds().await.map_err(|error| js_io_error_to_rs_io_error(error, false))?;
        let ms: u64 = unsafe { ms.as_f64().unwrap().to_int_unchecked() };
        Ok(std::time::SystemTime::UNIX_EPOCH + std::time::Duration::from_millis(ms))
    }

    pub fn name(&self) -> String {
        self.0.name()
    }

    pub async fn size(&self) -> io::Result<usize> {
        let size = self.0.size().await.map_err(|error| js_io_error_to_rs_io_error(error, false))?;
        Ok(unsafe { size.as_f64().unwrap().to_int_unchecked() })
    }
}

impl From<FileReference> for FileSystemReference {
    fn from(value: FileReference) -> Self {
        FileSystemReference(value.0.to_file_system_reference())
    }
}

#[derive(Clone)]
pub struct DirectoryReference(JSDirectoryReference);

impl DirectoryReference {
    pub fn name(&self) -> String {
        self.0.name()
    }

    pub async fn entries(&self) -> io::Result<Vec<FileSystemReference>> {
        let entries = self.0.entries().await.map_err(|error| js_io_error_to_rs_io_error(error, true))?;
        let mut entries_2 = vec![];
        for entry in js_sys::Array::try_from(entries).unwrap() {
            entries_2.push(FileSystemReference(entry.try_into().unwrap()));
        }
        Ok(entries_2)
    }

    pub async fn get_directory(&self, name: &str) -> io::Result<DirectoryReference> {
        let reference = self.0.get_directory(name.into(), false).await.map_err(|error| js_io_error_to_rs_io_error(error, true))?;
        Ok(DirectoryReference(reference.try_into().unwrap()))
    }

    pub async fn get_directory_or_create(&self, name: &str) -> io::Result<DirectoryReference> {
        let reference = self.0.get_directory(name.into(), true).await.map_err(|error| js_io_error_to_rs_io_error(error, true))?;
        Ok(DirectoryReference(reference.try_into().unwrap()))
    }

    pub async fn get_file(&self, name: &str) -> io::Result<FileReference> {
        let reference = self.0.get_file(name.into(), false).await.map_err(|error| js_io_error_to_rs_io_error(error, false))?;
        Ok(FileReference(reference.try_into().unwrap()))
    }

    pub async fn get_file_or_create(&self, name: &str) -> io::Result<FileReference> {
        let reference = self.0.get_file(name.into(), true).await.map_err(|error| js_io_error_to_rs_io_error(error, false))?;
        Ok(FileReference(reference.try_into().unwrap()))
    }

    pub async fn delete_empty_directory(&self, name: &str) -> io::Result<()> {
        self.0.delete_empty_directory(name.into()).await.map(|_| ())
            .map_err(|error| js_io_error_to_rs_io_error_for_delete_directory(error))
    }

    pub async fn delete_directory_all(&self, name: &str) -> io::Result<()> {
        self.0.delete_directory_all(name.into()).await.map(|_| ())
            .map_err(|error| js_io_error_to_rs_io_error_for_delete_directory(error))
    }

    pub async fn delete_file(&self, name: &str) -> io::Result<()> {
        self.0.delete_file(name.into()).await.map(|_| ()).map_err(|error| js_io_error_to_rs_io_error(error, false))
    }
}

impl From<DirectoryReference> for FileSystemReference {
    fn from(value: DirectoryReference) -> Self {
        FileSystemReference(value.0.to_file_system_reference())
    }
}