use std::{
    io::{self, BufWriter, Cursor, Seek, Write},
    num::NonZeroU64,
    path::{Path, PathBuf},
};

use fs_err as fs;
use gzp::par::compress::{ParCompress, ParCompressBuilder};

use super::warn_user_about_loading_sevenz_in_memory;
use crate::{
    BUFFER_CAPACITY, QuestionAction, QuestionPolicy, Result, archive,
    commands::warn_user_about_loading_zip_in_memory,
    extension::{CompressionFormat::*, Extension, split_first_compression_format},
    info_accessible,
    utils::{
        BytesFmt, FileVisibilityPolicy, file_size,
        io::lock_and_flush_output_stdio,
        threads::{logical_thread_count, physical_thread_count},
        user_wants_to_continue,
    },
};

/// Compress files into `output_file`.
///
/// # Arguments:
/// - `files`: is the list of paths to be compressed: ["dir/file1.txt", "dir/file2.txt"]
/// - `extensions`: is a list of compression formats for compressing, example: [Tar, Gz] (in compression order)
/// - `output_file` is the resulting compressed file name, example: "archive.tar.gz"
///
/// # Return value
/// - Returns `Ok(true)` if compressed all files normally.
/// - Returns `Ok(false)` if user opted to abort compression mid-way.
pub fn compress_files(
    files: Vec<PathBuf>,
    extensions: Vec<Extension>,
    output_file: fs::File,
    output_path: &Path,
    follow_symlinks: bool,
    question_policy: QuestionPolicy,
    file_visibility_policy: FileVisibilityPolicy,
    level: Option<i16>,
) -> Result<bool> {
    // If the input files contain a directory, then the total size will be underestimated
    let file_writer = BufWriter::with_capacity(BUFFER_CAPACITY, output_file);

    let mut writer: Box<dyn Send + Write> = Box::new(file_writer);

    // Grab previous encoder and wrap it inside of a new one
    let chain_writer_encoder = |format: &_, encoder| -> Result<_> {
        let encoder: Box<dyn Send + Write> = match format {
            Gzip => Box::new({
                // by default, ParCompress uses a default compression level of 3
                // instead of the regular default that flate2 uses
                let parz: ParCompress<gzp::deflate::Gzip, _> = ParCompressBuilder::new()
                    .compression_level(
                        level.map_or_else(Default::default, |l| gzp::Compression::new((l as u32).clamp(0, 9))),
                    )
                    .num_threads(logical_thread_count())
                    .expect("gpz: num_threads must be greater than 0")
                    .from_writer(encoder);
                parz
            }),
            Bzip => Box::new(bzip2::write::BzEncoder::new(
                encoder,
                level.map_or_else(Default::default, |l| bzip2::Compression::new((l as u32).clamp(1, 9))),
            )),
            Bzip3 => {
                #[cfg(not(feature = "bzip3"))]
                return Err(crate::Error::bzip3_no_support());

                #[cfg(feature = "bzip3")]
                Box::new(
                    // Use block size of 16 MiB
                    bzip3::write::Bz3Encoder::new(encoder, 16 * 2_usize.pow(20))?,
                )
            }
            Lz4 => Box::new(lz4_flex::frame::FrameEncoder::new(encoder).auto_finish()),
            Lzma => {
                let options = level.map_or_else(Default::default, |l| {
                    lzma_rust2::LzmaOptions::with_preset((l as u32).clamp(0, 9))
                });
                let writer = lzma_rust2::LzmaWriter::new_use_header(encoder, &options, None)?;
                Box::new(writer.auto_finish())
            }
            Xz => {
                let mut options = level.map_or_else(Default::default, |l| {
                    lzma_rust2::XzOptions::with_preset((l as u32).clamp(0, 9))
                });
                let dict_size = options.lzma_options.dict_size as u64;
                options.set_block_size(NonZeroU64::new(dict_size));
                // Use up to 256 PHYSICAL cores for compression
                let writer = lzma_rust2::XzWriterMt::new(encoder, options, physical_thread_count() as u32)?;
                Box::new(writer.auto_finish())
            }
            Lzip => {
                let options = level.map_or_else(Default::default, |l| {
                    lzma_rust2::LzipOptions::with_preset((l as u32).clamp(0, 9))
                });
                let writer = lzma_rust2::LzipWriter::new(encoder, options);
                Box::new(writer.auto_finish())
            }
            Snappy => Box::new({
                let parz: ParCompress<gzp::snap::Snap, _> = ParCompressBuilder::new()
                    .compression_level(gzp::par::compress::Compression::new(
                        level.map_or_else(Default::default, |l| (l as u32).clamp(0, 9)),
                    ))
                    .num_threads(logical_thread_count())
                    .expect("gpz: num_threads must be greater than 0")
                    .from_writer(encoder);
                parz
            }),
            Zstd => {
                let mut zstd_encoder = zstd::stream::write::Encoder::new(
                    encoder,
                    level.map_or(zstd::DEFAULT_COMPRESSION_LEVEL, |l| {
                        (l as i32).clamp(zstd::zstd_safe::min_c_level(), zstd::zstd_safe::max_c_level())
                    }),
                )?;
                // Use all available PHYSICAL cores for compression
                zstd_encoder.multithread(physical_thread_count() as u32)?;
                Box::new(zstd_encoder.auto_finish())
            }
            Brotli => {
                let default_level = 11; // Same as brotli CLI, default to highest compression
                let level = level.unwrap_or(default_level).clamp(0, 11) as u32;
                let win_size = 22; // default to 2^22 = 4 MiB window size
                Box::new(brotli::CompressorWriter::new(encoder, BUFFER_CAPACITY, level, win_size))
            }
            Tar | Zip | Rar | SevenZip => unreachable!(),
        };
        Ok(encoder)
    };

    let (first_format, formats) = split_first_compression_format(&extensions);

    for format in formats.iter().rev() {
        writer = chain_writer_encoder(format, writer)?;
    }

    match first_format {
        Gzip | Bzip | Bzip3 | Lz4 | Lzma | Xz | Lzip | Snappy | Zstd | Brotli => {
            writer = chain_writer_encoder(&first_format, writer)?;
            let mut reader = fs::File::open(&files[0])?;
            io::copy(&mut reader, &mut writer)?;
            info_accessible!("Input file size: {}", BytesFmt(file_size(&files[0])?));
        }
        Tar => {
            archive::tar::build_archive(
                &files,
                output_path,
                &mut writer,
                file_visibility_policy,
                follow_symlinks,
            )?;
            writer.flush()?;
        }
        Zip => {
            if !formats.is_empty() {
                // Make thread own locks to keep output messages adjacent
                let _locks = lock_and_flush_output_stdio();

                warn_user_about_loading_zip_in_memory();
                if !user_wants_to_continue(output_path, question_policy, QuestionAction::Compression)? {
                    return Ok(false);
                }
            }

            let mut vec_buffer = Cursor::new(vec![]);

            archive::zip::build_archive(
                &files,
                output_path,
                &mut vec_buffer,
                file_visibility_policy,
                follow_symlinks,
            )?;
            vec_buffer.rewind()?;
            io::copy(&mut vec_buffer, &mut writer)?;
        }
        Rar => {
            #[cfg(feature = "unrar")]
            return Err(archive::rar::no_compression());

            #[cfg(not(feature = "unrar"))]
            return Err(crate::Error::rar_no_support());
        }
        SevenZip => {
            if !formats.is_empty() {
                // Make thread own locks to keep output messages adjacent
                let _locks = lock_and_flush_output_stdio();

                warn_user_about_loading_sevenz_in_memory();
                if !user_wants_to_continue(output_path, question_policy, QuestionAction::Compression)? {
                    return Ok(false);
                }
            }

            let mut vec_buffer = Cursor::new(vec![]);
            archive::sevenz::build_archive(&files, output_path, &mut vec_buffer, file_visibility_policy)?;
            vec_buffer.rewind()?;
            io::copy(&mut vec_buffer, &mut writer)?;
        }
    }

    Ok(true)
}
