#![doc = include_str!("../README.md")]
#![allow(clippy::unnecessary_debug_formatting)]

use crate::action::ActualAction;
use crate::analysis::exif2date::ExifDateType;
use crate::analysis::name_formatters::{
    BracketInfo, BracketingFormattingPriority, FileType, NameFormatterInvocationInfo,
};
use action::ActionMode;
use anyhow::{anyhow, Result};
use chrono::NaiveDateTime;
use log::{debug, error, info, trace, warn};
use regex::Regex;
use std::cmp::Ordering;
use std::ffi::OsStr;
use std::fs;
use std::fs::{DirEntry, File};
use std::io::{BufReader, Read, Seek};
use std::path::{Path, PathBuf};
use std::str::FromStr;
use std::sync::LazyLock;

pub mod action;
pub mod analysis;
pub mod name;

/// `AnalysisType` is an enumeration that defines the different types of analysis that can be performed on a file.
///
/// # Variants
///
/// * `OnlyEmbedded` - Represents the action of analyzing a file based only on its Exif data/Video metadata.
/// * `OnlyName` - Represents the action of analyzing a file based only on its name.
/// * `EmbeddedThenName` - Represents the action of analyzing a file based first on its Exif data/Video metadata, then on its name if the Exif data/Video metadata is not sufficient.
/// * `NameThenEmbedded` - Represents the action of analyzing a file based first on its name, then on its Exif data/Video metadata if the name is not sufficient.
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub enum AnalysisType {
    OnlyEmbedded,
    OnlyName,
    EmbeddedThenName,
    NameThenEmbedded,
}
/// Implementation of the `FromStr` trait for `AnalysisType`.
///
/// This allows a string to be parsed into the `AnalysisType` enum.
///
/// # Arguments
///
/// * `s` - A string slice that should be parsed into an `AnalysisType`.
///
/// # Returns
///
/// * `Result<Self, Self::Err>` - Returns `Ok(AnalysisType)` if the string could be parsed into an `AnalysisType`, `Err(anyhow::Error)` otherwise.
impl FromStr for AnalysisType {
    type Err = anyhow::Error;

    fn from_str(s: &str) -> Result<Self> {
        match s.to_lowercase().as_str() {
            "only_exif" | "exif" | "embedded" | "only_embedded" | "metadata" | "e" | "m" => {
                Ok(AnalysisType::OnlyEmbedded)
            }
            "only_name" | "name" | "n" => Ok(AnalysisType::OnlyName),
            "exif_then_name" | "exif_name" | "embedded_then_name" | "metadata_then_name" | "mn"
            | "en" => Ok(AnalysisType::EmbeddedThenName),
            "name_then_exif" | "name_exif" | "name_then_embedded" | "name_then_metadata" | "nm"
            | "ne" => Ok(AnalysisType::NameThenEmbedded),
            _ => Err(anyhow::anyhow!("Invalid analysis type")),
        }
    }
}

/// `AnalyzerSettings` is a struct that holds the settings for an `Analyzer`.
///
/// # Fields
/// * `analysis_type` - An `AnalysisType` that specifies the type of analysis to perform on a file.
/// * `exif_date_type` - Which EXIF date to use when analyzing photos. See [`ExifDateType`] for details.
/// * `source_dirs` - A vector of `Path` references that represent the source directories to analyze.
/// * `target_dir` - A `Path` reference that represents the target directory for the analysis results.
/// * `recursive_source` - A boolean that indicates whether to analyze source directories recursively.
/// * `file_format` - A string that represents the target format of the files to analyze.
/// * `nodate_file_format` - A string that represent the target format of files with no date.
/// * `unknown_file_format` - An optional string that represents the target format of files not matching the list of extensions
/// * `date_format` - A string that represents the format of the dates in the files to analyze.
/// * `extensions` - A vector of strings that represent the file extensions to consider during analysis.
/// * `action_type` - An `ActionMode` that specifies the type of action to perform on a file after analysis.
/// * `mkdir` - A boolean that indicates whether to create the target directory if it does not exist.
/// * `excluded_files` - A list of regexes to check if a file should be excluded from analysis.
/// * `included_files` - A list of regexes to check if a file should be included in the analysis. If this list is not empty, only files matching at least one of the regexes will be included.
/// * `bracketed_file_format` - Which data to select for formatting for non leaf path when the file is part of a bracketed set.
#[derive(Debug, Clone)]
pub struct AnalyzerSettings {
    pub analysis_type: AnalysisType,
    pub exif_date_type: ExifDateType,
    pub source_dirs: Vec<PathBuf>,
    pub target_dir: PathBuf,
    pub recursive_source: bool,
    pub file_format: String,
    pub nodate_file_format: String,
    pub unknown_file_format: Option<String>,
    pub bracketed_file_format: Option<String>,
    pub date_format: String,
    pub bracketing_formatting: BracketingFormattingPriority,
    pub extensions: Vec<String>,
    #[cfg(feature = "video")]
    pub video_extensions: Vec<String>,
    pub action_type: ActionMode,
    pub mkdir: bool,
    pub excluded_files: Vec<Regex>,
    pub included_files: Vec<Regex>,
}

static RE_DETECT_NAME_FORMAT_COMMAND: LazyLock<regex::Regex> = LazyLock::new(|| {
    regex::Regex::new(
        r"\{([^}]*)}", // finds { ... } blocks
    )
    .expect("Failed to compile regex")
});

static RE_COMMAND_SPLIT: LazyLock<regex::Regex> = LazyLock::new(|| {
    regex::Regex::new(
        r"^(([^:]*):)?(.*)$", // splits command into modifiers:command
    )
    .expect("Failed to compile regex")
});

static LOCK_MOVE_OPERATION: LazyLock<std::sync::Mutex<()>> =
    LazyLock::new(|| std::sync::Mutex::new(()));

/// `Analyzer` is a struct that represents an analyzer for files.
///
/// # Fields
///
/// * `name_transformers` - A list of `NameTransformer` objects that are used to transform the names of files during analysis.
/// * `name_formatters` - A list of `NameFormatter` objects that are used to generate the new names of files after analysis.
/// * `settings` - An `AnalyzerSettings` object that holds the settings for the `Analyzer`.
pub struct Analyzer {
    name_transformers:
        Vec<Box<dyn analysis::filename2date::FileNameToDateTransformer + Send + Sync>>,
    name_formatters: Vec<Box<dyn analysis::name_formatters::NameFormatter + Send + Sync>>,
    pub settings: AnalyzerSettings,
}

/// Implementation of methods for the `Analyzer` struct.
///
/// # Methods
///
/// * [`new`](#method.new) - Creates a new `Analyzer` with the given settings.
/// * [`add_transformer`](#method.add_transformer) - Adds a name transformer to the `Analyzer`.
/// * [`analyze_name`](#method.analyze_name) - Analyzes the name of a file.
/// * [`analyze_embedded_metadata`](#method.analyze_embedded_metadata) - Analyzes the Exif data/Video metadata of a file.
/// * [`analyze`](#method.analyze) - Analyzes a file based on the `Analyzer`'s settings.
/// * [`compose_file_name`](#method.compose_file_name) - Composes a file name based on the given date, name, and duplicate counter.
/// * [`do_file_action`](#method.do_file_action) - Performs the file action specified in the `Analyzer`'s settings on a file.
/// * [`is_valid_extension`](#method.is_valid_extension) - Checks if a file has a valid extension.
/// * [`rename_files_in_folder`](#method.rename_files_in_folder) - Renames files in a folder based on the `Analyzer`'s settings.
/// * [`run`](#method.run) - Runs the `Analyzer`, renaming files in the source directories based on the `Analyzer`'s settings.
impl Analyzer {
    /// Creates a new `Analyzer` with the given settings.
    ///
    /// # Arguments
    ///
    /// * `settings` - An `AnalyzerSettings` object that holds the settings for the `Analyzer`.
    ///
    /// # Returns
    ///
    /// * `Result<Analyzer>` - Returns `Ok(Analyzer)` if the `Analyzer` could be created successfully, `Err(anyhow::Error)` otherwise.
    ///
    /// # Errors
    ///
    /// * If the target directory does not exist.
    /// * If a source directory does not exist.
    /// * If an error occurs while getting the standard name transformers.
    pub fn new(settings: AnalyzerSettings) -> Result<Analyzer> {
        let analyzer = Analyzer {
            name_transformers: Vec::default(),
            name_formatters: Vec::default(),
            settings,
        };

        if !analyzer.settings.target_dir.exists() {
            return Err(anyhow!("Target directory does not exist"));
        }
        for source in &analyzer.settings.source_dirs {
            if !source.exists() {
                return Err(anyhow!("Source directory {source:?} does not exist"));
            }
        }

        Ok(analyzer)
    }

    /// Adds a name transformer to the `Analyzer`.
    ///
    /// # Arguments
    /// * `transformer` - A `NameTransformer` object that is used to transform the names of files during analysis.
    pub fn add_transformer<
        T: 'static + analysis::filename2date::FileNameToDateTransformer + Send + Sync,
    >(
        &mut self,
        transformer: T,
    ) {
        self.name_transformers.push(Box::new(transformer));
    }

    /// Adds a name formatter to the `Analyzer`.
    ///
    /// # Arguments
    /// * `formatter` - A `NameFormatter` object that is used to generate the new names of files after analysis.
    pub fn add_formatter<T: 'static + analysis::name_formatters::NameFormatter + Send + Sync>(
        &mut self,
        formatter: T,
    ) {
        self.name_formatters.push(Box::new(formatter));
    }

    fn analyze_name(&self, name: &str) -> Result<(Option<NaiveDateTime>, String)> {
        let result = analysis::get_name_time(name, &self.name_transformers)?;
        match result {
            Some((time, name)) => Ok((Some(time), name)),
            None => Ok((None, name.to_string())),
        }
    }

    fn analyze_photo_exif<S: Read + Seek>(
        file: S,
        date_type: ExifDateType,
    ) -> Result<Option<NaiveDateTime>> {
        let exif_time = analysis::exif2date::get_exif_time(file, date_type)?;
        Ok(exif_time)
    }

    #[cfg(feature = "video")]
    fn analyze_video_metadata<P: AsRef<Path>>(path: P) -> Result<Option<NaiveDateTime>> {
        let video_time = analysis::video2date::get_video_time(path)?;
        Ok(video_time)
    }

    fn analyze_embedded_metadata<A: AsRef<Path>>(&self, path: A) -> Result<Option<NaiveDateTime>> {
        let path = path.as_ref();

        #[cfg(feature = "video")]
        let video = self.is_valid_video_extension(path.extension())?;
        let photo = self.is_valid_photo_extension(path.extension())?;

        #[cfg(feature = "video")]
        {
            if video && photo {
                return Err(anyhow::anyhow!("File has both photo and video extensions. Do not include the same extension in both settings"));
            }
        }

        if photo {
            let file = File::open(path)?;
            return Analyzer::analyze_photo_exif(&file, self.settings.exif_date_type);
        }
        #[cfg(feature = "video")]
        if video {
            return Analyzer::analyze_video_metadata(path);
        }

        Err(anyhow::anyhow!("File extension is not valid"))
    }

    /// Analyzes a file for a date based on the `Analyzer`'s settings.
    ///
    /// # Arguments
    /// * `path` - A `PathBuf` that represents the path of the file to analyze.
    ///
    /// # Returns
    /// * `Result<(Option<NaiveDateTime>, String)>` - Returns a tuple containing an `Option<NaiveDateTime>` and a `String`.
    ///   The `Option<NaiveDateTime>` represents the date and time extracted from the file, if any.
    ///   The `String` represents the transformed name of the file.
    ///
    /// # Errors
    /// This function will return an error if:
    /// * The file name cannot be retrieved or is invalid.
    /// * The file cannot be opened.
    /// * An error occurs during the analysis of the file's Exif data or name.
    pub fn analyze<A: AsRef<Path>>(&self, path: A) -> Result<(Option<NaiveDateTime>, String)> {
        let path = path.as_ref();

        let name = path
            .file_name()
            .ok_or(anyhow::anyhow!("No file name"))?
            .to_str()
            .ok_or(anyhow::anyhow!("Invalid file name"))?;

        let valid_extension = self
            .is_valid_extension(path.extension())
            .unwrap_or_else(|err| {
                warn!("Error checking file extension: {err}");
                false
            });
        if !valid_extension {
            warn!("Skipping file with invalid extension: {}", path.display());
            return Err(anyhow::anyhow!("Invalid file extension"));
        }

        Ok(match self.settings.analysis_type {
            AnalysisType::OnlyEmbedded => {
                let exif_result = self
                    .analyze_embedded_metadata(path)
                    .map_err(|e| anyhow!("Error analyzing embedded data: {e}"))?;
                let name_result = self.analyze_name(name);

                match name_result {
                    Ok((_, name)) => (exif_result, name),
                    Err(_err) => (exif_result, name.to_string()),
                }
            }
            AnalysisType::OnlyName => self.analyze_name(name)?,
            AnalysisType::EmbeddedThenName => {
                let metadata_result = self.analyze_embedded_metadata(path);
                let exif_result = match metadata_result {
                    Err(e) => {
                        warn!(
                            "Error analyzing embedded data: {} for {}",
                            e,
                            path.display()
                        );
                        info!("Falling back to name analysis");
                        None
                    }
                    Ok(date) => date,
                };
                let name_result = self.analyze_name(name);

                match exif_result {
                    Some(date) => match name_result {
                        Ok((_, name)) => (Some(date), name),
                        Err(_err) => (Some(date), name.to_string()),
                    },
                    None => name_result?,
                }
            }
            AnalysisType::NameThenEmbedded => {
                let name_result = self.analyze_name(name)?;
                if name_result.0.is_none() {
                    (self.analyze_embedded_metadata(path)?, name_result.1)
                } else {
                    name_result
                }
            }
        })
    }

    /// Replaces {name}, {date}, ... in a format with actual values
    fn replace_filepath_parts<'a, 'b>(
        &self,
        format_string: &'b str,
        info: &'a NameFormatterInvocationInfo,
    ) -> Result<String> {
        #[derive(Debug)]
        enum FormatString<'a> {
            Literal(String),
            Command(&'a str, String),
        }
        impl FormatString<'_> {
            fn formatted_string(self) -> String {
                match self {
                    FormatString::Literal(str) | FormatString::Command(_, str) => str,
                }
            }
        }

        let detect_commands = RE_DETECT_NAME_FORMAT_COMMAND.captures_iter(format_string);

        let mut final_string: Vec<FormatString<'b>> = Vec::new();

        let mut current_string_index = 0;
        for capture in detect_commands {
            let match_all = capture.get(0).expect("Capture group 0 should always exist");
            let start = match_all.start();
            let end = match_all.end();

            if start > current_string_index {
                final_string.push(FormatString::Literal(
                    format_string[current_string_index..start].to_string(),
                ));
            }

            // {prefix:cmd}
            // let full_match_string = match_all.as_str();
            // prefix:cmd
            let inner_command_string = capture
                .get(1)
                .expect("Capture group 2 should always exist")
                .as_str();

            let inner_command_capture = RE_COMMAND_SPLIT
                .captures(inner_command_string)
                .expect("Should always match");

            // prefix
            let command_modifier = inner_command_capture.get(2).map_or("", |x| x.as_str());
            // cmd
            let actual_command = inner_command_capture.get(3).map_or("", |x| x.as_str());

            let mut found_command = false;

            for formatter in &self.name_formatters {
                if let Some(matched) = formatter.argument_template().captures(actual_command) {
                    let mut command_substitution = match formatter.replacement_text(matched, info) {
                        Ok(replaced_text) => replaced_text,
                        Err(err) => {
                            return Err(anyhow!("Failed to format the file name with the given format string: {actual_command:?}. Got error: {{{err}}}"));
                        }
                    };

                    if !command_substitution.is_empty() && !command_modifier.is_empty() {
                        // prefix_substitution
                        command_substitution = format!("{command_modifier}{command_substitution}");
                    }
                    found_command = true;
                    final_string.push(FormatString::Command(
                        inner_command_string,
                        command_substitution,
                    ));
                    break;
                }
            }

            if !found_command {
                return Err(anyhow!("Failed to format file name with the given format string. There exists no formatter for the format command: {{{actual_command}}}"));
            }

            current_string_index = end;
        }
        if format_string.len() > current_string_index {
            final_string.push(FormatString::Literal(
                format_string[current_string_index..].to_string(),
            ));
        }

        trace!("Parsed format string {format_string:?} to");
        for part in &final_string {
            match part {
                FormatString::Literal(str) => trace!(" - Literal: {str:?}"),
                FormatString::Command(cmd, str) => trace!(" - Command: {cmd:?}\t{str:?}"),
            }
        }

        Ok(final_string
            .into_iter()
            .map(FormatString::formatted_string)
            .collect::<String>())
    }

    /// Performs the file action specified in the `Analyzer`'s settings on a file.
    ///
    /// # Arguments
    ///
    /// * `path` - A `PathBuf` that represents the path of the file to perform the action on.
    /// * `bracket_info` - The information regarding file bracketing. Can be extracted with the `get_bracketing_info` method.
    ///
    /// # Returns
    ///
    /// * `Result<()>` - Returns `Ok(())` if the file action could be performed successfully, `Err(anyhow::Error)` otherwise.
    ///
    /// # Errors
    ///
    /// This function will return an error if:
    /// * The analysis of the file fails.
    /// * An IO error occurs while analyzing the date
    /// * An IO error occurs while doing the file action
    #[allow(clippy::too_many_lines)]
    pub fn run_file<P: AsRef<Path>>(
        &self,
        path: P,
        bracket_info: &Option<BracketInfo>,
    ) -> Result<()> {
        let path = path.as_ref();

        let mut data = match self.analyze_context(path) {
            Ok(None) => return Ok(()), // skip file
            Ok(Some(mut result)) => {
                result.bracket_info = bracket_info.as_ref();
                result
            }
            Err(err) => {
                error!("Error analyzing file {}: {err}", path.display());
                return Err(anyhow!("Error analyzing file: {err}"));
            }
        };

        let new_file_path = |file_name_info: &NameFormatterInvocationInfo| -> Result<PathBuf> {
            let format_string = if data.file_type == FileType::None {
                self.settings
                    .unknown_file_format
                    .as_ref()
                    .ok_or(anyhow!("No unknown format string specified"))?
                    .as_str()
            } else if let (Some(bracket_info), Some(_)) =
                (&self.settings.bracketed_file_format, &bracket_info)
            {
                bracket_info.as_str()
            } else if data.date.is_some() {
                self.settings.file_format.as_str()
            } else {
                self.settings.nodate_file_format.as_str()
            };

            let path_split: Vec<_> = format_string.split('/').collect();
            let len = path_split.len();
            let path_split: Vec<_> = path_split
                .into_iter()
                .enumerate()
                .map(|(index, component)| {
                    let is_leaf = index == len - 1;
                    let bracketing_info = file_name_info.bracket_info;

                    if let Some(bracketing_info) = bracketing_info {
                        if is_leaf {
                            self.replace_filepath_parts(component, file_name_info)
                        } else {
                            match self.settings.bracketing_formatting {
                                BracketingFormattingPriority::First => {
                                    if let Some(bracketed_format) = &bracketing_info.analysis_first
                                    {
                                        let mut bracketed_format =
                                            bracketed_format.as_ref().clone();
                                        bracketed_format.bracket_info = Some(bracketing_info);
                                        self.replace_filepath_parts(component, &bracketed_format)
                                    } else {
                                        self.replace_filepath_parts(component, file_name_info)
                                    }
                                }
                                BracketingFormattingPriority::Last => {
                                    if let Some(bracketed_format) = &bracketing_info.analysis_last {
                                        let mut bracketed_format =
                                            bracketed_format.as_ref().clone();
                                        bracketed_format.bracket_info = Some(bracketing_info);
                                        self.replace_filepath_parts(component, &bracketed_format)
                                    } else {
                                        self.replace_filepath_parts(component, file_name_info)
                                    }
                                }
                                BracketingFormattingPriority::Current => {
                                    self.replace_filepath_parts(component, file_name_info)
                                }
                            }
                        }
                    } else {
                        self.replace_filepath_parts(component, file_name_info)
                    }
                })
                .collect();

            for entry in &path_split {
                if let Err(err) = entry {
                    return Err(anyhow!("Failed to format filename: {err}"));
                }
            }
            let path_split = path_split.into_iter().map(Result::unwrap);

            let mut target_path = self.settings.target_dir.clone();
            for path_component in path_split {
                let component = path_component.replace(['/', '\\'], "");
                if component != ".." {
                    target_path.push(component);
                }
            }
            Ok(target_path)
        };

        let mut new_path = new_file_path(&data)?;
        let mut dup_counter = 0;

        let lock_rename = match LOCK_MOVE_OPERATION.lock() {
            Ok(guard) => Some(guard),
            Err(err) => {
                warn!("Failed to acquire lock for move operation: {err}");
                None
            }
        };

        while new_path.exists() {
            debug!("Target file already exists: {}", new_path.display());

            let source_file_stream = std::fs::File::open(&new_path).map_err(|err| {
                anyhow!(
                    "Failed to open source file for de-duplication check: {}: {err}",
                    new_path.display()
                )
            })?;
            match std::fs::File::open(path) {
                Ok(target_file_stream) => {
                    // now compare file contents

                    match streams_equal(source_file_stream, target_file_stream) {
                        Err(err) => {
                            error!(
                                "Failed to compare files: {} {} -> {}.",
                                path.display(),
                                new_path.display(),
                                err
                            );
                        }
                        Ok(true) => {
                            // source = target

                            drop(lock_rename);

                            debug!("De-duplicating file {} because it is identical to the target file {}.", path.display(), new_path.display());

                            return match self.settings.action_type {
                                ActionMode::Execute(ActualAction::Move) => fs::remove_file(path).map_err(|err| anyhow!("Failed to remove source file during de-duplication: {}: {err}", path.display())),
                                ActionMode::Execute(ActualAction::Copy | ActualAction::Hardlink | ActualAction::RelativeSymlink | ActualAction::AbsoluteSymlink) => Ok(()),
                                ActionMode::DryRun(_action) => {
                                    Ok(())
                                }
                            };
                        }
                        Ok(false) => {
                            // source != target
                        }
                    }
                }
                Err(err) => {
                    warn!(
                        "Failed to open target file for de-duplication check: {}: {err}",
                        path.display()
                    );
                }
            }

            dup_counter += 1;
            data.duplicate_counter = Some(dup_counter);
            let new_path_candidate = new_file_path(&data)?;

            if new_path_candidate == new_path {
                return Err(anyhow!("Target file {} already exists; content differs. Consider providing the {{:dup}} format specifier to the format string to resolve duplicate naming.", new_path.display()));
            }

            new_path = new_path_candidate;
        }

        if dup_counter > 0 {
            info!("De-duplicated target file: {}", new_path.display());
        }

        action::file_action(
            path,
            &new_path,
            &self.settings.action_type,
            self.settings.mkdir,
            lock_rename,
        )?;
        Ok(())
    }

    /// Analyzes a file and returns the analysis context, which includes the file path, whether it's an unknown file, the extracted date, and the information for name formatting.
    ///
    /// # Arguments
    /// * `path` - A `PathBuf` that represents the path of the file to analyze.
    ///
    /// # Returns
    /// * `Some<(bool, NameFormatterInvocationInfo)>` - true=unknown file, false=known file, and the information for name formatting if the analysis was successful.
    /// * `None` - If the file should be skipped (e.g., due to invalid extension and no unknown format specified).
    ///
    /// # Errors
    /// This function will return an error if:
    /// * The file name cannot be retrieved or is invalid.
    /// * The file cannot be opened.
    /// * An error occurs during the analysis of the file's Exif data or name.
    pub fn analyze_context<P: AsRef<Path>>(
        &self,
        path: P,
    ) -> Result<Option<NameFormatterInvocationInfo<'static>>> {
        let path = path.as_ref();
        let valid_ext = self.is_valid_extension(path.extension());
        let is_unknown_file = match valid_ext {
            Ok(false) => {
                if self.settings.unknown_file_format.is_none() {
                    info!(
                        "Skipping file because extension is not in the list and no unknown format specified: {}",
                        path.display()
                    );
                    return Ok(None);
                }

                debug!("Processing unknown file: {}", path.display());
                true
            }
            Ok(true) => {
                debug!("Processing file: {}", path.display());
                false
            }
            Err(err) => {
                warn!("Error checking file extension: {err}");
                return Ok(None);
            }
        };

        let (date, cleaned_name) = if is_unknown_file {
            (
                None,
                path.with_extension("")
                    .file_name()
                    .ok_or(anyhow::anyhow!("No file name"))?
                    .to_str()
                    .ok_or(anyhow::anyhow!("Invalid file name"))?
                    .to_string(),
            )
        } else {
            let (date, cleaned_name) = self.analyze(path).map_err(|err| {
                error!("Error extracting date: {err}");
                err
            })?;
            let cleaned_name = name::clean_image_name(cleaned_name.as_str());

            debug!("Analysis results: Date: {date:?}, Cleaned name: {cleaned_name:?}",);

            if date.is_none() {
                warn!("No date was derived for file {}.", path.display());
            }

            (date, cleaned_name)
        };

        let date_string = match date {
            None => "NODATE".to_string(),
            Some(date) => date.format(&self.settings.date_format).to_string(),
        };

        let mut ftype = FileType::None;
        if self.is_valid_photo_extension(path.extension())? {
            ftype = FileType::Image;
        }
        #[cfg(feature = "video")]
        if self.is_valid_video_extension(path.extension())? {
            ftype = FileType::Video;
        }

        let file_name_info = NameFormatterInvocationInfo {
            date,
            date_string,
            date_default_format: self.settings.date_format.clone(),
            bracketing_formatting: self.settings.bracketing_formatting,
            file_type: ftype,
            cleaned_name,
            duplicate_counter: None,
            extension: path
                .extension()
                .map_or(String::new(), |ext| ext.to_string_lossy().to_string()),
            bracket_info: None,
            original_name: path
                .with_extension("")
                .file_name()
                .unwrap_or_default()
                .to_string_lossy()
                .to_string(),
            original_filename: path
                .file_name()
                .unwrap_or_default()
                .to_string_lossy()
                .to_string(),
        };
        Ok(Some(file_name_info))
    }

    /// Checks if a file has a valid photo extension.
    ///
    /// # Arguments
    /// * `ext` - An `Option<&OsStr>` that represents the file extension to check.
    ///
    /// # Returns
    /// * `Result<bool>` - Returns `Ok(true)` if the file has a valid photo extension, `Ok(false)` if it does not
    ///
    /// # Errors
    /// This function will return an error if:
    /// * The file extension is not valid UTF-8.
    pub fn is_valid_photo_extension(&self, ext: Option<&OsStr>) -> Result<bool> {
        match ext {
            None => Ok(false),
            Some(ext) => {
                let ext = ext
                    .to_str()
                    .ok_or(anyhow::anyhow!("Invalid file extension"))?
                    .to_lowercase();
                Ok(self
                    .settings
                    .extensions
                    .iter()
                    .any(|valid_ext| ext == valid_ext.as_str()))
            }
        }
    }

    /// Checks if a file has a valid video extension.
    ///
    /// # Arguments
    /// * `ext` - An `Option<&OsStr>` that represents the file extension to check.
    ///
    /// # Returns
    /// * `Result<bool>` - Returns `Ok(true)` if the file has a valid video extension, `Ok(false)` if it does not
    ///
    /// # Errors
    /// This function will return an error if:
    /// * The file extension is not valid UTF-8.
    #[cfg(feature = "video")]
    pub fn is_valid_video_extension(&self, ext: Option<&OsStr>) -> Result<bool> {
        match ext {
            None => Ok(false),
            Some(ext) => {
                let ext = ext
                    .to_str()
                    .ok_or(anyhow::anyhow!("Invalid file extension"))?
                    .to_lowercase();
                Ok(self
                    .settings
                    .video_extensions
                    .iter()
                    .any(|valid_ext| ext == valid_ext.as_str()))
            }
        }
    }

    /// Checks if a file has a valid extension (photo or video).
    ///
    /// # Arguments
    /// * `ext` - An `Option<&OsStr>` that represents the file extension to check.
    ///
    /// # Returns
    /// * `Result<bool>` - Returns `Ok(true)` if the file has a valid extension, `Ok(false)` if it does not
    ///
    /// # Errors
    /// This function will return an error if:
    /// * The file extension is not valid UTF-8.
    pub fn is_valid_extension(&self, ext: Option<&OsStr>) -> Result<bool> {
        let valid_photo = self.is_valid_photo_extension(ext)?;
        #[cfg(feature = "video")]
        let valid_video = self.is_valid_video_extension(ext)?;
        #[cfg(not(feature = "video"))]
        let valid_video = false;
        Ok(valid_photo || valid_video)
    }

    /// Finds all files in a source directory and its subdirectories.
    ///
    /// # Arguments
    /// * `directory` - The directory to search for files.
    /// * `recursive` - A boolean that indicates whether to search subdirectories.
    /// * `result` - A mutable reference to a vector of `PathBuf` objects that will hold the results.
    ///
    /// # Errors
    /// This function will return an error if:
    /// * The directory cannot be read or other IO errors occur.
    pub fn find_files_in_source<P: AsRef<Path>>(
        &self,
        directory: P,
        recursive: bool,
        result: &mut Vec<PathBuf>,
    ) -> Result<()> {
        let mut entries =
            fs::read_dir(directory.as_ref())?.collect::<Vec<std::io::Result<DirEntry>>>();
        entries.sort_by(|a, b| match (a, b) {
            (Ok(a), Ok(b)) => a.path().cmp(&b.path()),
            (Err(_), Ok(_)) => Ordering::Less,
            (Ok(_), Err(_)) => Ordering::Greater,
            (Err(_), Err(_)) => Ordering::Equal,
        });
        let directory = directory.as_ref().canonicalize()?;
        for entry in entries {
            let entry = entry?;
            let path = entry.path();
            if path.is_dir() {
                if recursive {
                    debug!("Processing subfolder: {}", path.display());
                    self.find_files_in_source(path, recursive, result)?;
                }
            } else {
                let path = path.canonicalize()?;
                let path_no_prefix = format!(
                    "{}{}",
                    std::path::MAIN_SEPARATOR_STR,
                    path.strip_prefix(&directory).unwrap_or(&path).display()
                );
                trace!("Found file: {path_no_prefix}");

                let mut include_it = self.settings.included_files.is_empty();
                for include_pattern in &self.settings.included_files {
                    let matching = include_pattern.is_match(path_no_prefix.as_str());
                    trace!(
                        " - Include pattern: {} {}",
                        include_pattern.as_str(),
                        if matching { "[INCLUDE]" } else { "" }
                    );
                    include_it = include_it || matching;
                }

                let mut exclude_it = false;
                for exclude_pattern in &self.settings.excluded_files {
                    let matching = exclude_pattern.is_match(path_no_prefix.as_str());
                    trace!(
                        " - Exclude pattern: {} {}",
                        exclude_pattern.as_str(),
                        if matching { "[EXCLUDE]" } else { "" }
                    );
                    exclude_it = exclude_it || matching;
                }

                if include_it && !exclude_it {
                    trace!(" -> Included file: {path:?}");
                    result.push(path);
                }
            }
        }

        Ok(())
    }
}

mod exifutils;

pub struct BracketEXIFInformation {
    pub index: u32,
}

fn streams_equal<R1: Read, R2: Read>(a: R1, b: R2) -> std::io::Result<bool> {
    let mut a = BufReader::new(a);
    let mut b = BufReader::new(b);

    let mut buf_a = [0u8; 8192];
    let mut buf_b = [0u8; 8192];

    loop {
        let read_a = a.read(&mut buf_a)?;
        let read_b = b.read(&mut buf_b)?;

        if read_a != read_b {
            return Ok(false);
        }

        if read_a == 0 {
            return Ok(true);
        }

        if buf_a[..read_a] != buf_b[..read_b] {
            return Ok(false);
        }
    }
}
