/*
    MartyPC
    https://github.com/dbalsom/martypc

    Copyright 2022-2024 Daniel Balsom

    Permission is hereby granted, free of charge, to any person obtaining a
    copy of this software and associated documentation files (the “Software”),
    to deal in the Software without restriction, including without limitation
    the rights to use, copy, modify, merge, publish, distribute, sublicense,
    and/or sell copies of the Software, and to permit persons to whom the
    Software is furnished to do so, subject to the following conditions:

    The above copyright notice and this permission notice shall be included in
    all copies or substantial portions of the Software.

    THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
    IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
    FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
    AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
    LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
    FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
    DEALINGS IN THE SOFTWARE.

    --------------------------------------------------------------------------

    frontend_common::resource_manager::mod.rs

    File and path services for frontends. File operations are abstracted
    to support both local and web filesystems (for wasm compilation).

    Eventually archive support will be added as well.

*/

#[cfg(not(target_arch = "wasm32"))]
mod local_fs;
mod path_manager;
pub mod tree;
#[cfg(target_arch = "wasm32")]
mod wasm;

pub use path_manager::PathConfigItem;
use path_manager::PathManager;
pub use tree::TreeNode as PathTreeNode;

use anyhow::Error;
use regex::Regex;
use std::{collections::HashSet, ffi::OsString, path::PathBuf};

const BASEDIR_TOKEN: &'static str = "$basedir$";

// Resource flags
const RESOURCE_READONLY: u32 = 0x00000001;

#[derive(Copy, Clone, Debug)]
pub enum ResourceItemType {
    Directory,
    LocalFile,
    RemoteDirectory,
    RemoteFile,
    LocalArchive,
    LocalArchiveFile,
    RemoteArchive,
    RemoteArchiveFile,
}

#[derive(Clone, Debug)]
pub struct ResourceItem {
    rtype: ResourceItemType,
    pub(crate) full_path: PathBuf,
    pub(crate) relative_path: Option<PathBuf>,
    pub(crate) filename_only: Option<OsString>,
    flags: u32,
}

pub struct ResourceManager {
    pub pm: PathManager,
    pub ignore_dirs: Vec<String>,
}

impl ResourceManager {
    pub fn new(base_path: PathBuf) -> Self {
        Self {
            pm: PathManager::new(base_path),
            ignore_dirs: Vec::new(),
        }
    }

    pub fn from_config(base_path: PathBuf, config: &[PathConfigItem]) -> Result<Self, Error> {
        let mut rm = Self::new(base_path);
        for item in config {
            rm.pm.add_path(&item.resource, &item.path, item.create)?;
        }
        //rm.pm.create_paths()?;
        Ok(rm)
    }

    pub fn set_ignore_dirs(&mut self, dirs: Vec<String>) {
        self.ignore_dirs = dirs;
    }

    pub fn get_resource_path(&self, resource: &str) -> Option<PathBuf> {
        self.pm.get_resource_path(resource)
    }

    /// Return a unique filename for the given resource, base name, and extension.
    /// Names will be generated by appending digits to the base name until a unique name is found.
    pub fn get_available_filename(
        &self,
        resource: &str,
        base_name: &str,
        extension: Option<&str>,
    ) -> Result<PathBuf, Error> {
        let mut path = self
            .pm
            .get_resource_path(resource)
            .ok_or(anyhow::anyhow!("Resource path not found: {}", resource))?;

        // Generate a regex to extract a sequence of digits from a filename
        let re = Regex::new(r"(\d+)").unwrap();
        let mut largest_num = 0;

        log::debug!("Finding unique filename in: {:?}", path);

        // First, generate a map of all items starting with 'base_name'
        let mut existing_basenames: HashSet<OsString> = HashSet::new();
        match self.enumerate_items(resource, false, false, None) {
            Ok(items) => {
                for item in items {
                    //log::debug!("Item: {:?}", item);
                    if let Some(filename) = item.filename_only.clone() {
                        if filename.to_string_lossy().contains(base_name) {
                            //log::debug!("Found matching basename: {:?}", filename);

                            // Extract any number sequence from the filename
                            re.captures(
                                filename
                                    .to_str()
                                    .ok_or(anyhow::anyhow!("Failed to convert filename to string"))?,
                            )
                            .and_then(|caps| caps.get(1))
                            .and_then(|match_| match_.as_str().parse::<u32>().ok())
                            .map(|num| {
                                if num > largest_num {
                                    largest_num = num
                                }
                            });
                            existing_basenames.insert(filename);
                        }
                    }
                }
            }
            Err(err) => {
                return Err(anyhow::anyhow!(
                    "Failed to enumerate items in resource '{}': {}",
                    resource,
                    err
                ));
            }
        }

        // Generate unique names and check them against the map. We can start searching at
        // 'largest_num'
        let mut i = largest_num;
        let mut test_name_path = PathBuf::from(format!("{}{:04}", base_name, i));
        if let Some(ext) = extension {
            test_name_path.set_extension(ext);
        }
        let mut test_name = test_name_path.into_os_string();

        // for name in existing_basenames.iter() {
        //     log::debug!("Existing name: {} largest num: {}", name.to_str().unwrap(), largest_num);
        // }

        while existing_basenames.contains(&test_name) {
            i += 1;
            test_name_path = PathBuf::from(format!("{}{:04}", base_name, i));
            if let Some(ext) = extension {
                test_name_path.set_extension(ext);
            }
            test_name = test_name_path.into_os_string();
        }

        log::debug!("Found unique filename: {}", test_name.to_str().unwrap());

        path.push(test_name.clone());
        if let Some(ext) = extension {
            path.set_extension(ext);
        }

        // We should have a unique filename now. Check that the file exists before we return it
        // as one last sanity check.
        if ResourceManager::path_exists(&path) {
            log::error!(
                "Failed to create unique filename: File already exists: {}",
                path.to_str().unwrap()
            );
            return Err(anyhow::anyhow!(
                "Failed to create unique filename: File already exists: {}",
                path.to_str().unwrap()
            ));
        }
        Ok(path)
    }

    pub fn path_contains_dir(path: &PathBuf, dir: &str) -> bool {
        path.iter().any(|component| component == dir)
    }

    pub fn path_contains_dirs(path: &PathBuf, dirs: &Vec<&str>) -> bool {
        dirs.iter().any(|&dir| path.iter().any(|component| component == dir))
    }

    pub fn set_relative_paths_for_items(base: PathBuf, items: &mut Vec<ResourceItem>) {
        // Strip the base path from all items.
        for item in items.iter_mut() {
            item.relative_path = Some(
                item.full_path
                    .strip_prefix(&base)
                    .unwrap_or(&item.full_path)
                    .to_path_buf(),
            );
        }
    }
}
