use crate::diff::entry::{read_entries, Command, Entry};
use crate::diff::hash::{hash_bytes, THash};
use crate::misc::{compress_writer, decompress_file_vec, decompress_reader};
use ddelta;
use indicatif::ParallelProgressIterator;
use rayon::prelude::*;
use smol_str::SmolStr;
use std::fs;
use std::fs::File;
use std::io;
use std::io::prelude::*;
use std::io::BufWriter;
use std::path::PathBuf;

struct RamFile {
    pub path: SmolStr,
    pub data: Vec<u8>,
}

impl RamFile {
    pub fn init() -> Self {
        Self {
            path: "".into(),
            data: vec![],
        }
    }

    pub fn del(&mut self) {
        *self = Self::init();
    }

    pub fn write(
        self,
        out_base: String,
        rebase: bool,
    ) -> Result<Option<(SmolStr, (u64, Option<THash>))>, io::Error> {
        if !self.path.is_empty() {
            let out_path = PathBuf::from(format!("{}/{}", out_base, self.path));
            let parent = out_path.parent().unwrap();
            if !parent.exists() {
                fs::create_dir_all(parent)?;
            }
            if rebase {
                let mut file = compress_writer(out_path)?;
                file.write_all(&self.data)?;
                file.flush()?;
                return Ok(Some((self.path, hash_bytes(self.data))));
            } else {
                let mut file = BufWriter::new(File::create(out_path)?);
                file.write_all(&self.data)?;
                file.flush()?;
            }
        }
        Ok(None)
    }
}

use crate::misc::get_style;
use gxhash::HashMap;
pub fn apply_entries(
    repo_base: String,
    out_base: String,
    patch_dirs: Vec<String>,
    rebase: bool,
) -> Result<(Vec<SmolStr>, Vec<(SmolStr, (u64, Option<THash>))>), io::Error> {
    if patch_dirs.is_empty() {
        Ok((vec![], vec![]))
    } else {
        let patch_style = get_style("Reading entries", patch_dirs.len());
        let tables: Vec<(Vec<Entry>, String)> = patch_dirs
            .into_par_iter() // each pach dir is seperate
            .progress_with(patch_style)
            .map(|p| -> Result<_, io::Error> { Ok((read_entries(&p)?, p)) })
            .collect::<Result<Vec<_>, io::Error>>()?;

        let mut fs: HashMap<SmolStr, (Vec<(Entry, usize, String)>, SmolStr)> = HashMap::default();

        for table in tables {
            // entries per table must be sorted (e.g. update must come after copy)
            let mut entries = table.0;
            entries.sort_unstable_by_key(|e| e.command.to_byte());

            for (index, entry) in entries.into_iter().enumerate() {
                fs.entry(entry.src.clone())
                    .and_modify(|v| v.0.push((entry.clone(), index, table.1.clone())))
                    .or_insert((
                        vec![(entry.clone(), index, table.1.clone())],
                        entry.src.clone(),
                    ));
                match &entry.command {
                    Command::Move { dst }
                    | Command::DeltaMove { dst }
                    | Command::Copy { dst }
                    | Command::DeltaCopy { dst } => unsafe {
                        let e = fs.remove_entry(&entry.src).unwrap_unchecked().1;
                        fs.insert(dst.clone(), e);
                    },
                    _ => {}
                }
            }
        }

        let file_style = get_style("Processing files", fs.len());
        let out = fs
            .into_par_iter()
            .progress_with(file_style)
            .map(|(_, table)| {
                process_file(
                    repo_base.clone(),
                    out_base.clone(),
                    (&table.0, table.1),
                    rebase,
                )
            })
            .collect::<Result<Vec<_>, io::Error>>()?;

        let rm_files = out.par_iter().filter_map(|(p, _)| p.clone()).collect();
        let hashes: Vec<_> = out.par_iter().filter_map(|(_, p)| p.clone()).collect();

        Ok((rm_files, hashes))
    }
}

fn process_file(
    repo_base: String,
    out_base: String,
    table: (&Vec<(Entry, usize, String)>, SmolStr),
    rebase: bool,
) -> Result<(Option<SmolStr>, Option<(SmolStr, (u64, Option<THash>))>), io::Error> {
    // entry source will differ, but ultimately be the same file
    // del file will never be file.path

    let mut file = RamFile::init();
    file.path = table.1.clone();
    file.data = decompress_file_vec(format!("{}/{}", repo_base, table.1)).unwrap_or(vec![]);
    let mut del_file: Option<SmolStr> = None;

    for entry in table.0 {
        let data_file = format!("{}/{:X}", entry.2, entry.1);
        match &entry.0.command {
            Command::Add | Command::Update => {
                file.path = table.1.clone();
                file.data = vec![];
                io::copy(&mut decompress_reader(data_file)?, &mut file.data)?;
                del_file = None; // reset in case it was previously deleted
            }
            Command::Delete => {
                del_file = Some(file.path.clone());
                file.del();
            }
            Command::Delta => {
                // delta will never happen on a file that doesnt exist
                let mut out_buf: Vec<u8> = vec![];
                ddelta::apply_chunked(
                    &mut io::Cursor::new(&file.data),
                    &mut out_buf,
                    &mut decompress_reader(data_file)?,
                )
                .unwrap();
                file.data = out_buf;
            }
            Command::Move { dst } | Command::Copy { dst } => {
                file.path = dst.clone();
                del_file = match entry.0.command {
                    Command::Move { .. } => Some(entry.0.src.clone()),
                    _ => None,
                };
            }
            Command::DeltaMove { dst } | Command::DeltaCopy { dst } => {
                file.path = dst.clone();

                let mut out_buf: Vec<u8> = vec![];
                ddelta::apply_chunked(
                    &mut io::Cursor::new(&file.data),
                    &mut out_buf,
                    &mut decompress_reader(data_file)?,
                )
                .unwrap();
                file.data = out_buf;

                del_file = match entry.0.command {
                    Command::DeltaMove { .. } => Some(entry.0.src.clone()),
                    _ => None,
                };
            }
        }
    }

    let hash = file.write(out_base, rebase)?;
    Ok((del_file, hash))
}
