mod buffered_file;
mod circular_path_buffer;
mod directory_entry;
mod directory_listing;
mod entry;
mod operations;

pub use buffered_file::*;
pub use circular_path_buffer::*;
pub use directory_entry::*;
pub use directory_listing::*;
pub use entry::*;
use operations::*;

use std::fs::{OpenOptions, metadata};
use std::os::unix::ffi::OsStrExt;
use std::path::{Component, Path, PathBuf};

fn is_blank_path(path: &Path) -> bool {
    path == PathBuf::new()
}

pub struct FileDevice {
    /// The path to which the file device is confined. Files and directories
    /// outside of this directory cannot be accessed.
    pub base_path: PathBuf,
    pub default_path: PathBuf,

    pub open_buffer: CircularPathBuffer,
    pub move_buffer: CircularPathBuffer,
    pub name_buffer: CircularPathBuffer,

    pub entry: Option<(Entry, PathBuf)>,

    pub op_success: bool,
    pub new_pointer: u32,
    pub new_length: u32,

    pub enable_create: bool,
    pub enable_move: bool,
    pub enable_delete: bool,
    pub enable_read: bool,
    pub enable_write: bool,
}

impl FileDevice {
    pub fn new() -> Self {
        Self {
            base_path: PathBuf::from("/"),
            default_path: match std::env::current_dir() {
                Ok(dir) => PathBuf::from(dir),
                Err(_) => PathBuf::from("/"),
            },
            open_buffer: CircularPathBuffer::new(),
            move_buffer: CircularPathBuffer::new(),
            name_buffer: CircularPathBuffer::new(),

            entry: None,

            op_success: false,
            new_pointer: 0,
            new_length: 0,

            enable_create: true,
            enable_move: true,
            enable_delete: false,
            enable_read: true,
            enable_write: true,
        }
    }

    pub fn flush_entry(&mut self) {
        if let Some((Entry::File(buffered_file), _)) = &mut self.entry {
            buffered_file.flush();
        }
    }

    pub fn close_entry(&mut self) {
        self.open_buffer.clear();
        self.move_buffer.clear();
        self.name_buffer.clear();
        self.flush_entry();
        self.entry = None;
        self.new_pointer = 0;
        self.new_length = 0;
    }

    pub fn write_to_open_port(&mut self, byte: u8) {
        if let Some(relative_path) = self.open_buffer.push_byte(byte) {
            self.close_entry();
            if !is_blank_path(&relative_path) {
                if let Ok(path) = self.attach_base(&relative_path) {
                    let _ = self.open_entry(&path);
                };
            }
        }
    }

    pub fn set_name_pointer(&mut self, byte: u8) {
        self.name_buffer.set_pointer(byte);
    }

    /// Open the entry at the given path.
    pub fn open_entry(&mut self, path: &Path) -> Result<(), ()> {
        macro_rules! raise_on_err {
            ($res:expr) => {match $res {Ok(v)=>v, Err(_)=>return Err(())} } }

        if !path.starts_with(&self.base_path) { return Err(()); }
        let metadata = raise_on_err!(metadata(&path));
        if metadata.is_file() {
            let open_result = OpenOptions::new()
                .read(self.enable_read)
                .write(self.enable_write)
                .open(&path);
            if let Ok(file) = open_result {
                self.close_entry();
                let file_entry = Entry::File(BufferedFile::new(file));
                self.entry = Some((file_entry, path.to_owned()));
                let relative = remove_base(&path, &self.base_path).unwrap();
                self.name_buffer.populate(relative.as_os_str().as_bytes());
                return Ok(());
            };
        } else if metadata.is_dir() {
            if let Ok(listing) = DirectoryListing::from_path(&path, &self.base_path) {
                let dir_entry = Entry::Directory(listing);
                self.entry = Some((dir_entry, path.to_owned()));
                let relative = remove_base(&path, &self.base_path).unwrap();
                self.name_buffer.populate(relative.as_os_str().as_bytes());
                return Ok(());
            };
        };
        return Err(());
    }

    pub fn write_to_move_port(&mut self, byte: u8) {
        if let Some(dest) = self.move_buffer.push_byte(byte) {
            if let Some((_, source)) = &self.entry {
                if is_blank_path(&dest) {
                    match self.enable_delete {
                        true => self.op_success = delete_entry(&source),
                        false => self.op_success = false,
                    }
                } else if let Ok(destination) = self.attach_base(&dest) {
                    match self.enable_move {
                        true => self.op_success = move_entry(&source, &destination),
                        false => self.op_success = false,
                    }
                }
            } else {
                if is_blank_path(&dest) {
                    self.op_success = false;
                } else if let Ok(destination) = self.attach_base(&dest) {
                    match self.enable_create {
                        true => self.op_success = create_file(&destination),
                        false => self.op_success = false,
                    }
                }
            }
            self.close_entry();
        }
    }

    /// Attempt to open the parent directory of the current entry.
    pub fn ascend_to_parent(&mut self) {
        if let Some((_, path)) = &self.entry {
            if let Some(parent_path) = path.parent() {
                self.op_success = self.open_entry(&parent_path.to_owned()).is_ok();
            }
        } else {
            self.op_success = self.open_entry(&self.default_path.to_owned()).is_ok();
        }
    }

    /// Attempt to open the currently-selected child.
    pub fn descend_to_child(&mut self) {
        if let Some((Entry::Directory(listing), _)) = &self.entry {
            if let Some(child_path) = listing.child_path() {
                if let Ok(child_path) = self.attach_base(&child_path) {
                    self.op_success = self.open_entry(&child_path).is_ok();
                }
            };
        }
    }

    /// Return true if the currently-open entry is a directory.
    pub fn entry_type(&self) -> bool {
        match self.entry {
            Some((Entry::Directory(_), _)) => true,
            _ => false,
        }
    }

    /// Return true if the currently-selected child is a directory.
    pub fn read_child_name(&mut self) -> u8 {
        if let Some((Entry::Directory(listing), _)) = &mut self.entry {
            listing.child_name().read_byte()
        } else {
            0
        }
    }

    pub fn set_child_name_pointer(&mut self, byte: u8) {
        if let Some((Entry::Directory(listing), _)) = &mut self.entry {
            listing.child_name().set_pointer(byte);
        }
    }

    /// Return true if the currently-selected child is a directory.
    pub fn child_type(&self) -> bool {
        if let Some((Entry::Directory(listing), _)) = &self.entry {
            match listing.child_type() {
                Some(EntryType::Directory) => true,
                Some(EntryType::File) => false,
                None => false,
            }
        } else {
            false
        }
    }

    pub fn read_byte(&mut self) -> u8 {
        match &mut self.entry {
            Some((Entry::File(buffered_file), _)) => buffered_file.read_byte(),
            _ => 0,
        }
    }

    pub fn write_byte(&mut self, byte: u8) {
        match &mut self.entry {
            Some((Entry::File(buffered_file), _)) => buffered_file.write_byte(byte),
            _ => (),
        }
    }

    pub fn pointer(&mut self) -> u32 {
        match &mut self.entry {
            Some((Entry::File(buffered_file), _)) => buffered_file.pointer(),
            Some((Entry::Directory(listing), _)) => listing.selected(),
            _ => 0,
        }
    }

    pub fn commit_pointer(&mut self) {
        let pointer = std::mem::take(&mut self.new_pointer);
        match &mut self.entry {
            Some((Entry::File(buffered_file), _)) => buffered_file.set_pointer(pointer),
            Some((Entry::Directory(listing), _)) => listing.set_selected(pointer),
            _ => (),
        }
    }

    pub fn length(&mut self) -> u32 {
        match &mut self.entry {
            Some((Entry::File(buffered_file), _)) => buffered_file.length(),
            Some((Entry::Directory(listing), _)) => listing.length(),
            _ => 0,
        }
    }

    pub fn commit_length(&mut self) {
        let length = std::mem::take(&mut self.new_length);
        match &mut self.entry {
            Some((Entry::File(buffered_file), _)) => buffered_file.set_length(length),
            _ => (),
        }
    }

    fn attach_base(&self, relative_path: &Path) -> Result<PathBuf, ()> {
        let mut full_path = self.base_path.clone();
        let mut has_root = false;
        for component in relative_path.components() {
            match component {
                Component::Normal(s) => full_path.push(s),
                Component::ParentDir => return Err(()),
                Component::CurDir => continue,
                Component::RootDir => has_root = true,
                Component::Prefix(_) => continue,
            }
        };
        match has_root {
            true => Ok(full_path),
            false => Err(())
        }
    }
}

impl Drop for FileDevice {
    fn drop(&mut self) {
        self.close_entry();
    }
}