use crate::indicatif_log_bridge::LogWrapper;
use chrono::Utc;
use clap::{Command, CommandFactory, Parser};
use clap_complete::{generate, Generator, Shell};
use fern::colors::{Color, ColoredLevelConfig};
use indicatif::{MultiProgress, ProgressBar, ProgressStyle};
use log::{debug, error, info, trace, warn, LevelFilter};
use photo_sort::analysis::bracketed::get_bracketing_info;
use photo_sort::analysis::exif2date::ExifDateType;
use photo_sort::analysis::name_formatters::{BracketInfo, BracketingFormattingPriority};
use photo_sort::{action, AnalysisType, Analyzer, BracketEXIFInformation};
use regex::{Regex, RegexBuilder};
use std::collections::VecDeque;
use std::io;
use std::path::{Path, PathBuf};
use std::str::FromStr;
use std::sync::mpsc::{channel, Receiver, Sender};
use std::sync::Arc;
use threadpool::ThreadPool;

/// A simple command line tool to sort photos by date.
#[derive(Parser, Debug)]
#[command(
    version,
    about,
    long_about = "A tool to rename and sort photos/videos by its EXIF date/metadata. It tries to extract the date
from the EXIF data or file name and renames the image file according to a given
format string."
)]
#[allow(clippy::struct_excessive_bools)]
struct Arguments {
    /// The source directory to read the photos from.
    #[arg(short, long, num_args = 1.., required = true)]
    source_dir: Vec<String>,
    /// The target directory to write the sorted photos to.
    #[arg(short, long)]
    target_dir: String,
    /// Whether to search the source directories recursively.
    /// If the flag is not set only immediate children of the source directories are considered.
    #[arg(short, long, default_value = "false")]
    recursive: bool,
    /// Date format string to use as default date format.
    /// See <https://docs.rs/chrono/latest/chrono/format/strftime/index.html> for more information.
    #[arg(long, default_value = "%Y%m%d-%H%M%S")]
    date_format: String,
    /// The target file format. Everything outside a {...} block is copied as is. The target file format may contain "/" to
    /// indicate that the file should be placed in a subdirectory. Use the `--mkdir` flag to create the subdirectories.
    /// `{name}` is replaced with a filename without the date part.
    /// `{original_name}` is replaced with the original filename without modification (without extension).
    /// `{original_filename}` is replaced with the original filename without modification (with extension).
    /// `{dup}` is replaced with a number if a file with the target name already exists.
    /// `{date}` is replaced with the date string, formatted according to the `date_format` parameter.
    /// `{date?format}` is replaced with the date string, formatted according to the "format" parameter. See <https://docs.rs/chrono/latest/chrono/format/strftime/index.html> for more information.
    /// `{type}` is replaced with MOV or IMG.
    /// `{type?img,vid}` is replaced with `img` if the file is an image, `vid` if the file is a video. Note that, when using other types than IMG or MOV,
    /// and rerunning the program again, the custom type will be seen as part of the file name.
    /// `{ext?upper/lower/copy}` is replaced with the original file extension. If `?upper` or `?lower` is specified, the extension will be made lower/upper case.
    ///      leaving out `?...` or using `copy` copies the original file extension.
    /// Commands of the form {label:cmd} are replaced by {cmd}; if the replacement string is not empty then a prefix of "label" is added.
    /// This might be useful to add separators only if there is e.g. a {dup} part.
    #[arg(short, long, default_value = "{type}{_:date}{-:name}{-:dup}.{ext}")]
    file_format: String,
    /// The target format for files that have no date. The `analysis_mode` allows specifying which method
    /// should be used to derive a date for a file. See the `file_format` option for an extensive description of possible
    /// format values. If not specified, uses the same format as for normal files.
    #[arg(long = "nodate")]
    nodate_file_format: Option<String>,
    /// The target file format for files that do not match the specified extensions list. If not present
    /// files that do not match the extension list are ignored, hence not moved, copied etc. See the `file_format` for an extensive description
    /// of possible format values. By using `--unknown others/{name}{.:ext}` all unknown files are moved to the subdirectory "others" relative
    /// to the target directory (specified by `--target-dir`). To exclude files from being processed at all, use the `--exclude` option.
    #[arg(long = "unknown")]
    unknown_file_format: Option<String>,
    /// Files to exclude completely from processing. Files matched by this pattern are never touched, even by the `--unknown` argument. This
    /// option could be useful to exclude files like `Thumbs.db` or `.DS_Store` from being moved to the `--unknown` folder. The `--exclude`
    /// option matches the relative file path to the respective source directory to the given string (prefixed by the os path separator). `*` may be used to indicate any number of any characters,
    /// excluding path separators. `**` may be used to indicate any number of any characters, including path separators. For example, `--exclude /abc/*/test/**/Thumbs.db`
    /// would exclude any file named `Thumbs.db` in a `abc/<subdir>/test/<any subdirs>/Thumbs.db` folder structure. On Windows use backslash instead.
    /// The `--exclude` may be used multiple times to exclude multiple patterns. If any pattern matches, the file is excluded.
    /// The `--exclude-regex` option works the same but accepts a regular expression instead of literals with wildcards.
    /// By default, the pattern matching is ignoring case. To enable case matching set `--exclude-case`.
    #[arg(long = "exclude")]
    exclude_files: Vec<String>,
    /// Same as `--exclude` but accepts regular expressions instead of literals with wildcards. `*` and `**` wildcards do not work in the regex patterns but
    /// are interpreted as regex match the last pattern character 0 or more times. For a list of supported regex patterns, see <https://docs.rs/regex/latest/regex/#syntax>
    #[arg(long = "exclude-regex")]
    exclude_files_regex: Vec<String>,
    /// When set the exclude pattern to do not ignore upper/lower case.
    #[arg(long)]
    exclude_case: bool,
    /// When set to any, all files are by default ignored, only files matching any pattern provided via `--include` or `--include-regex` argument
    /// are analysed. See `--exclude` for an explanation of supported patterns. To enable case matching set `--include-case`. To specify regex
    /// patterns use `--include-regex`. Note that `--exclude` will take priority over `--include`, meaning that a file matching both an exclude and an include pattern will be excluded.
    #[arg(long = "include")]
    include_files: Vec<String>,
    /// Same as `--include` but accepts regular expressions instead of literals with wildcards. `*` and `**` wildcards do not work in the regex patterns but
    /// are interpreted as regex match the last pattern character 0 or more times. For a list of supported regex patterns, see <https://docs.rs/regex/latest/regex/#syntax>
    #[arg(long = "include_regex")]
    include_files_regex: Vec<String>,
    /// When set the include pattern to do not ignore upper/lower case.
    #[arg(long)]
    include_case: bool,
    /// The target file format for files that can be identified as bracketed photo set with different exposure levels. By using `--bracket` all
    /// files identified as bracketed are moved/renamed/copied with the specified format string instead of the default one specified by `--target-dir`.
    /// The `--bracket` argument provides the following additional format specifiers:
    /// `{bracket?group}` an increasing number, unique for each group of bracketed images,
    /// `{bracket?index}` the index of the current photo in the sequence,
    /// `{bracket?length}` the length of the bracketing sequence,
    /// `{bracket?first}`/`{bracket?last}` the name of the first/last image in the sequence.
    /// Bracketed photos sequences are detected via manufacturer-specific EXIF information.
    /// Note that using the `--bracket` option requires each file to
    /// be analyzed using the EXIF analyzer, even if the Analysis type is set to Name-only.
    /// Currently only works for Sony's cameras. Feel free to open an issue requesting support for other vendors at <https://github.com/0xCCF4/PhotoSort/issues>.
    #[arg(long = "bracket", alias = "bracketed")]
    bracketed_file_format: Option<String>,
    /// When using the `--bracket` option, this flag controls which e.g. date information is used for the file name formatting of intermediary directories.
    /// For example, with `{date}/{date}-{name}.{ext}` as `--bracket` format files of the same bracket group will be placed into different subdirectories
    /// when the date changes between shots, if using the respective date information of each file (`current` mode). Using the `first`/`last` mode
    /// changes the data source for intermediary directory formatting (except final file name) to use the date information of the first respective
    /// last image in the bracketing sequence, hence keeping files of the same bracket group together in the same subdirectory.
    #[arg(long = "bracket-fmt", default_value = "first")]
    bracketing_formatting_priority: BracketingFormattingPriority,
    /// If the file format contains a "/", indicating that the file should be placed in a subdirectory,
    /// the mkdir flag controls if the tool is allowed to create non-existing subdirectories. No folder is created in dry-run mode.
    #[arg(long, default_value = "false", alias = "mkdirs")]
    mkdir: bool,
    /// A comma separated list of file extensions to include in the analysis.
    #[arg(short, long, alias= "ext", default_value = "jpg,jpeg,png,tiff,heif,heic,avif,webp", value_delimiter = ',', num_args = 0..)]
    extensions: Vec<String>,
    #[cfg(feature = "video")]
    /// A comma separated list of video extensions to include in the analysis.
    #[arg(long, default_value = "mp4,mov,avi", value_delimiter = ',', num_args = 0..)]
    video_extensions: Vec<String>,
    #[cfg(not(feature = "video"))]
    /// The sorting mode, possible values are `name_then_exif`, `exif_then_name`, `only_name`, `only_exif`.
    /// Name analysis tries to extract the date from the file name, Exif analysis tries to extract the date from the EXIF data.
    #[arg(short, long, default_value = "exif_then_name")]
    analysis_mode: AnalysisType,
    #[cfg(feature = "video")]
    /// The sorting mode, possible values are `name_then_metadata`, `metadata_then_name`, `only_name`, `only_metadata`.
    /// Name analysis tries to extract the date from the file name, Metadata analysis tries to extract the date from the EXIF data/video metadata.
    #[arg(short, long, default_value = "metadata_then_name")]
    analysis_mode: AnalysisType,
    /// The EXIF date field to use, possible values are `modify`, `creation`, `digitized`. EXIF data contains several date fields.
    /// `Modify` is the modification date, which is updated when the file is edited.
    /// `Create` is the creation date, which is usually the date when the photo was taken.
    /// `Digitize` is the digitized date, which is the date when the photo was digitized (for example, when converting a film photo to a digital image).
    /// For digital cameras, this is usually the same as the creation date.
    /// The default is `create`.
    #[arg(long = "date-field", default_value = "create")]
    exif_date_type: ExifDateType,
    /// The action mode, possible values are `move`, `copy`, `hardlink`, `relative_symlink`, `absolute_symlink`.
    /// `Move` will move the files, `Copy` will copy the files, `Hardlink` (alias: `hard`) will create hardlinks, `RelativeSymlink` (alias: `relsym`) will create relative symlinks, `AbsoluteSymlink` (alias: `abssym`) will create absolute symlinks.
    #[arg(short, long, default_value = "move")]
    move_mode: action::ActualAction,
    /// Dry-run
    /// If set, the tool will not move any files but only print the actions it would take.
    #[arg(short = 'n', long, default_value = "false")]
    dry_run: bool,
    /// Be verbose, if set, the tool will print more information about the actions it takes.
    #[arg(short, long, default_value = "false")]
    verbose: bool,
    /// Debug, if set, the tool will print debug information (including debug implies setting verbose).
    #[arg(short, long, default_value = "false", alias = "vv")]
    debug: bool,
    /// Logfile, if set, the tool will log its output to the specified file. Appending to the specified file if it already exists.
    #[arg(short, long = "log")]
    logfile: Option<String>,
    /// If set, suppresses the output of the tool to stdout/stderr. Only displaying error messages. Specifying a logfile at the same
    /// time will redirect the full output that would have been displayed to stdout/stderr to the logfile. Specifying `--debug` or `--verbose`
    /// plus `--quiet` without a logfile will result in an error.
    #[arg(short, long, default_value = "false")]
    quiet: bool,
    /// If set, display a progress bar while processing files.
    #[arg(short, long, default_value = "false")]
    progress: bool,
    /// If set, use multi-threading
    #[arg(long)]
    threads: Option<usize>,
}

fn setup_loggers<Q: AsRef<Path>>(
    general_log_level: LevelFilter,
    stdout_log_level: LevelFilter,
    file: Option<Q>,
    progress: Option<MultiProgress>,
) -> anyhow::Result<()> {
    let colors = ColoredLevelConfig::new().info(Color::Green);

    let mut config = fern::Dispatch::new().level(general_log_level);

    if let Some(file) = file {
        config = config.chain(
            fern::Dispatch::new()
                .format(move |out, message, record| {
                    if message.to_string().starts_with('[') {
                        out.finish(format_args!("{message}"));
                    } else {
                        out.finish(format_args!(
                            "[{} {} {}] {}",
                            Utc::now().format("%Y-%m-%d %H:%M:%S%.3f"),
                            record.level(),
                            record.target(),
                            message
                        ));
                    }
                })
                .chain(
                    fern::log_file(file)
                        .map_err(|err| anyhow::anyhow!("Failed to open log file: {err:?}"))?,
                ),
        );
    }

    let (max_level, log) = config
        .chain(
            fern::Dispatch::new()
                .format(move |out, message, record| {
                    if message.to_string().starts_with('[') {
                        out.finish(format_args!("{message}"));
                    } else {
                        out.finish(format_args!(
                            "[{} {} {}] {}",
                            Utc::now().format("%Y-%m-%d %H:%M:%S%.3f"),
                            colors.color(record.level()),
                            record.target(),
                            message
                        ));
                    }
                })
                .level(stdout_log_level)
                .chain(std::io::stdout()),
        )
        .into_log();

    if let Some(bar) = progress {
        LogWrapper::new(bar, log)
            .try_init()
            .expect("Failed to setup progressbar logging");

        log::set_max_level(max_level);
    } else {
        log::set_boxed_logger(log)?;
        log::set_max_level(max_level);
    }

    Ok(())
}

fn parse_regex_patterns<
    A: Iterator<Item = C>,
    B: Iterator<Item = D>,
    C: AsRef<str>,
    D: AsRef<str>,
>(
    non_regex: A,
    regex: B,
    ignore_case: bool,
    help_text: &str,
) -> Vec<Regex> {
    let files_regexes = non_regex.into_iter().map(|file_no_regex| {
        let file_no_regex = file_no_regex.as_ref();
        if file_no_regex.contains(format!("**{}**", std::path::MAIN_SEPARATOR_STR).as_str()) {
            eprintln!("Error: Invalid {help_text} pattern {file_no_regex:?}: '**' is not allowed to follow consecutively after each other with a path separator in between");
            std::process::exit(exitcode::CONFIG);
        }
        let escaped_sep = regex::escape(std::path::MAIN_SEPARATOR_STR);
        format!("^{}$",
                regex::escape(file_no_regex)
                    .replace(format!("{escaped_sep}\\*\\*{escaped_sep}").as_str(), "/.*?")
                    .replace(format!("{escaped_sep}\\*\\*").as_str(), "/.*?")
                    .replace(format!("\\*\\*{escaped_sep}").as_str(), ".*?/")
                    .replace("\\*\\*", ".*?")
                    .replace("\\*", &format!("[^{escaped_sep}]*?")))
    }).chain(regex.map(|x|x.as_ref().to_string())).map(|x| {
        let x = x.as_ref();
        trace!("Compiling include file regex {x:?}");
        match RegexBuilder::new(x).case_insensitive(ignore_case).build() {
            Ok(regex) => regex,
            Err(err) => {
                eprintln!("Failing to compile included files regular expression {x:?}: {err:?}");
                std::process::exit(exitcode::CONFIG);
            }
        }
    }).collect::<Vec<_>>();
    if !files_regexes.is_empty() {
        trace!("{help_text} files regexes:");
        for regex in &files_regexes {
            trace!(" - {}", regex.as_str());
        }
    }
    files_regexes
}

fn build_manpage(path: &Path) -> anyhow::Result<()> {
    let man = clap_mangen::Man::new(Arguments::command());
    let mut buffer: Vec<u8> = Vec::new();
    man.render(&mut buffer)?;

    std::fs::write(path.join("photo-sort.1"), buffer)?;

    Ok(())
}

fn build_completions(shell: &str) -> anyhow::Result<()> {
    fn print_completions<G: Generator>(generator: G, cmd: &mut Command) {
        generate(
            generator,
            cmd,
            cmd.get_name().to_string(),
            &mut io::stdout(),
        );
    }

    let generator = Shell::from_str(shell).map_err(|_| {
        anyhow::anyhow!("The shell {shell} is not supported for completion generation")
    })?;
    let mut cmd = Arguments::command();
    eprintln!("Generating completion file for {generator}...");
    print_completions(generator, &mut cmd);

    Ok(())
}

#[allow(clippy::too_many_lines)]
pub fn main() {
    let mut build_abort = false;
    if let Ok(var) = std::env::var("BUILD_MANPAGE") {
        if let Err(err) = build_manpage(&PathBuf::from(var)) {
            eprintln!("Error building manpage: {err:?}");
            std::process::exit(exitcode::IOERR);
        }
        build_abort = true;
    }
    if let Ok(var) = std::env::var("BUILD_SHELL") {
        if let Err(err) = build_completions(var.as_str()) {
            eprintln!("Error building shell completions: {err:?}");
            std::process::exit(exitcode::IOERR);
        }
        build_abort = true;
    }
    if build_abort {
        return;
    }

    let args = Arguments::parse();

    let log_level_general = {
        let mut log_level = LevelFilter::Warn;
        if args.verbose {
            log_level = LevelFilter::Info;
        }
        if args.debug {
            log_level = LevelFilter::Trace;
        }

        if args.quiet && args.logfile.is_none() {
            if args.debug || args.verbose {
                eprintln!("Error: Cannot use --debug/--verbose with --quiet. Maybe you wanted to specify a --logfile to log the full output to, while suppressing the STDOUT/STDERR output?");
                std::process::exit(exitcode::CONFIG);
            }

            log_level = LevelFilter::Error;
        }

        log_level
    };

    let console_log_level = if args.quiet {
        LevelFilter::Error
    } else {
        log_level_general
    };

    let multi = MultiProgress::new();
    let multi_clone = args.progress.then_some(multi.clone());

    if let Err(e) = setup_loggers(
        log_level_general,
        console_log_level,
        args.logfile,
        multi_clone,
    ) {
        eprintln!("Error starting application: {e:?}");
        std::process::exit(exitcode::CONFIG);
    }

    debug!("Initializing program");

    debug!("Video features enabled: {}", cfg!(feature = "video"));

    let excluded_files_regexes = parse_regex_patterns(
        args.exclude_files.into_iter(),
        args.exclude_files_regex.into_iter(),
        !args.exclude_case,
        "exclude",
    );
    let included_files_regexes = parse_regex_patterns(
        args.include_files.into_iter(),
        args.include_files_regex.into_iter(),
        !args.include_case,
        "include",
    );

    let bracket_mode = args.bracketed_file_format.is_some();
    let result = Analyzer::new(photo_sort::AnalyzerSettings {
        analysis_type: args.analysis_mode,
        exif_date_type: args.exif_date_type,
        source_dirs: args.source_dir.iter().map(PathBuf::from).collect(),
        target_dir: PathBuf::from(args.target_dir.as_str()),
        recursive_source: args.recursive,
        file_format: args.file_format.clone(),
        nodate_file_format: args.nodate_file_format.unwrap_or(args.file_format.clone()),
        unknown_file_format: args.unknown_file_format,
        bracketed_file_format: args.bracketed_file_format,
        date_format: args.date_format.clone(),
        extensions: args.extensions.clone(),
        mkdir: args.mkdir,
        action_type: if args.dry_run {
            action::ActionMode::DryRun(args.move_mode)
        } else {
            action::ActionMode::Execute(args.move_mode)
        },
        #[cfg(feature = "video")]
        video_extensions: args.video_extensions.clone(),
        excluded_files: excluded_files_regexes,
        included_files: included_files_regexes,
        bracketing_formatting: args.bracketing_formatting_priority,
    });
    let mut analyzer = match result {
        Ok(a) => {
            debug!("Program initialized");
            a
        }
        Err(e) => {
            eprintln!("{e:?}");
            std::process::exit(exitcode::CONFIG);
        }
    };

    // add file name -> date parsers
    analyzer.add_transformer(photo_sort::analysis::filename2date::NaiveFileNameParser::default());

    // add date -> file name formatters
    analyzer.add_formatter(photo_sort::analysis::name_formatters::FormatName::default());
    analyzer.add_formatter(photo_sort::analysis::name_formatters::FormatDuplicate::default());
    analyzer.add_formatter(photo_sort::analysis::name_formatters::FormatDate::default());
    analyzer.add_formatter(photo_sort::analysis::name_formatters::FormatFileType::default());
    analyzer.add_formatter(photo_sort::analysis::name_formatters::FormatExtension::default());
    analyzer.add_formatter(photo_sort::analysis::name_formatters::BracketedFormat::default());
    analyzer.add_formatter(photo_sort::analysis::name_formatters::FormatOriginalName::default());
    analyzer
        .add_formatter(photo_sort::analysis::name_formatters::FormatOriginalFileName::default());

    debug!("Running program");

    let mut files = Vec::new();

    for source_dir in &analyzer.settings.source_dirs {
        info!("Processing source folder: {}", source_dir.display());
        let result = analyzer.find_files_in_source(
            source_dir.clone(),
            analyzer.settings.recursive_source,
            &mut files,
        );
        if let Err(err) = result {
            error!("Error processing folder: {err}");
        }
    }

    debug!("Found {} files in source folders", files.len());
    files.sort();
    trace!("Sorting finished.");

    let threadpool = args.threads.map(|v| v.max(1)).map(ThreadPool::new);
    let (sender, receiver) = channel();

    let context = match threadpool {
        None => ExecutionContext::SingleThreaded(Box::new(NormalContext { analyzer })),
        Some(pool) => ExecutionContext::MultiThreaded(ThreadPoolContext {
            output: sender,
            receiver,
            pool,
            analyzer: Arc::new(analyzer),
        }),
    };

    let jobs = files.len();

    let bar = args.progress.then(|| {
        let bar = ProgressBar::new(files.len() as u64);
        bar.set_style(
            ProgressStyle::with_template(
                "{spinner:.green} [{elapsed_precise}] [{wide_bar:.green/grey}] {pos}/{len} ({eta})",
            )
            .unwrap()
            .progress_chars("=>-"),
        );
        multi.add(bar.clone());
        bar
    });

    let mut bracketed_queue = VecDeque::<(PathBuf, BracketEXIFInformation)>::new();
    let mut bracket_indices_used = std::collections::HashSet::new();
    let mut bracket_group_index = 1;

    let file_count = files.len();
    for (i, file) in files.into_iter().enumerate() {
        if let Some(bar) = &bar {
            bar.set_message(format!("{}", file.display()));
        }

        #[allow(clippy::unnecessary_debug_formatting)]
        if bracket_mode {
            let photo_file = match is_photo_extension(&file, &context) {
                Ok(is_video) => is_video,
                Err(e) => {
                    eprintln!("The file {file:?} has an invalid file extension which can not be represented as a UTF-8 string. Error: {e}");
                    std::process::exit(exitcode::IOERR);
                }
            };
            if !photo_file {
                trace!(
                    "File {file:?} is not detected as a photo file, skipping bracketing analysis"
                );
            }
            match photo_file
                .then(|| get_bracketing_info(&file))
                .transpose()
                .map(Option::flatten)
            {
                Ok(Some(info)) => {
                    trace!("File {file:?} is detected as a bracketed photo with index {} in its sequence", info.index);
                    let drain = if let Some(last) = bracketed_queue.back() {
                        if last.0.parent() != file.parent() {
                            trace!("Detected end of bracket sequence: parent path mismatch");
                            true
                        } else if bracket_indices_used.contains(&info.index) {
                            trace!(
                                "Detected end of bracket sequence: index already seen {} ({:?})",
                                info.index,
                                bracket_indices_used
                            );
                            true
                        } else {
                            false
                        }
                    } else {
                        false
                    };
                    if drain {
                        drain_bracketed_queue(
                            &mut bracketed_queue,
                            &mut bracket_indices_used,
                            &context,
                            bar.as_ref(),
                            i,
                            &mut bracket_group_index,
                        );
                    }

                    bracket_indices_used.insert(info.index);
                    bracketed_queue.push_back((file, info));
                }
                Ok(None) => {
                    if !bracketed_queue.is_empty() {
                        trace!("Detected end of bracket sequence: non-bracketed file");
                        drain_bracketed_queue(
                            &mut bracketed_queue,
                            &mut bracket_indices_used,
                            &context,
                            bar.as_ref(),
                            i,
                            &mut bracket_group_index,
                        );
                    }

                    process_file(file, &context, None);
                }
                Err(e) => {
                    error!("Error processing file {}: {e}", file.display());

                    process_file(file, &context, None);
                }
            }
        } else {
            process_file(file, &context, None);
        }

        if let Some(bar) = &bar {
            bar.set_position(i.saturating_sub(bracketed_queue.len()) as u64);
        }
    }

    if !bracketed_queue.is_empty() {
        trace!("Detected end of bracket sequence: processing end");
        drain_bracketed_queue(
            &mut bracketed_queue,
            &mut bracket_indices_used,
            &context,
            bar.as_ref(),
            file_count,
            &mut bracket_group_index,
        );
    }

    if let Some(context) = context.multi_threading() {
        if let Some(bar) = &bar {
            bar.set_position(0);
        }
        let mut count = 0;
        for (i, ()) in context.iter().take(jobs).enumerate() {
            if let Some(bar) = &bar {
                bar.set_position(i as u64);
            }
            count += 1;
        }

        if count != jobs {
            error!("Not all jobs got executed");
        }
    }

    if let Some(bar) = &bar {
        bar.finish_with_message("Finished processing files");
    }

    debug!("Finished execution");
    std::process::exit(exitcode::OK);
}

#[allow(clippy::similar_names)]
fn drain_bracketed_queue(
    queue: &mut VecDeque<(PathBuf, BracketEXIFInformation)>,
    index_list: &mut std::collections::HashSet<u32>,
    context: &ExecutionContext,
    bar: Option<&ProgressBar>,
    target_progress: usize,
    group_index: &mut usize,
) {
    let analyzer = match context {
        ExecutionContext::SingleThreaded(context) => &context.analyzer,
        ExecutionContext::MultiThreaded(context) => &context.analyzer,
    };

    index_list.clear();
    if queue.is_empty() {
        return;
    }

    let mut list = Vec::with_capacity(queue.len());
    while let Some((file, info)) = queue.pop_front() {
        list.push((file, info));
    }
    list.sort_by_key(|a| a.1.index);

    let first = list.first().unwrap();
    let last = list.last().unwrap();
    let sequence_length = list.len();

    let analysis_first = match analyzer.analyze_context(&first.0) {
        Ok(Some(analysis)) => Some(Arc::new(analysis)),
        Ok(None) => None,
        Err(err) => {
            warn!(
                "Error analyzing first file of bracketing sequence {}: {err}",
                last.0.display()
            );
            None
        }
    };
    let analysis_last = match analyzer.analyze_context(&last.0) {
        Ok(Some(analysis)) => Some(Arc::new(analysis)),
        Ok(None) => None,
        Err(err) => {
            warn!(
                "Error analyzing last file of bracketing sequence {}: {err}",
                last.0.display()
            );
            None
        }
    };

    let info = BracketInfo {
        first: first.0.clone(),
        last: last.0.clone(),
        sequence_number: 0,
        sequence_length,
        group_index: *group_index,
        analysis_first,
        analysis_last,
    };
    *group_index += 1;

    for (i, file) in list.iter().enumerate() {
        let mut info = info.clone();
        info.sequence_number = file.1.index;
        process_file(file.0.clone(), context, Some(info));

        if let Some(bar) = bar {
            bar.set_position(target_progress.saturating_sub(list.len() - 1 - i) as u64);
        }
    }
}

struct ThreadPoolContext {
    pub pool: ThreadPool,
    pub output: Sender<()>,
    pub receiver: Receiver<()>,
    pub analyzer: Arc<Analyzer>,
}

struct NormalContext {
    pub analyzer: Analyzer,
}

enum ExecutionContext {
    MultiThreaded(ThreadPoolContext),
    SingleThreaded(Box<NormalContext>),
}

impl ExecutionContext {
    pub fn multi_threading(self) -> Option<Receiver<()>> {
        if let ExecutionContext::MultiThreaded(context) = self {
            Some(context.receiver)
        } else {
            None
        }
    }
}

fn process_file(
    file: PathBuf,
    context: &ExecutionContext,
    bracket_info: Option<BracketInfo<'static>>,
) {
    match context {
        ExecutionContext::SingleThreaded(context) => {
            let result = context.analyzer.run_file(&file, &bracket_info);
            if let Err(err) = result {
                error!("Error processing file: {err}");
            }
        }
        ExecutionContext::MultiThreaded(context) => {
            let output = context.output.clone();
            let analyzer = context.analyzer.clone();
            context.pool.execute(move || {
                let result = analyzer.run_file(&file, &bracket_info);
                if let Err(err) = result {
                    error!("Error processing file: {err}");
                }
                output.send(()).expect("thread pool channel closed");
            });
        }
    }
}

fn is_photo_extension<P: AsRef<Path>>(path: P, context: &ExecutionContext) -> anyhow::Result<bool> {
    match context {
        ExecutionContext::SingleThreaded(context) => context
            .analyzer
            .is_valid_photo_extension(path.as_ref().extension()),
        ExecutionContext::MultiThreaded(context) => context
            .analyzer
            .is_valid_photo_extension(path.as_ref().extension()),
    }
}
