use crate::diff::entry::{read_entries, Command, Entry};
use ddelta;
use flate2::read::DeflateDecoder;
use rayon::prelude::*;
use std::collections::HashMap;
use std::fs;
use std::fs::File;
use std::io;
use std::io::prelude::*;
use std::io::BufReader;
use std::path::PathBuf;

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

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

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

    pub fn write(self, out_base: String) -> Result<(), 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)?;
            }
            fs::write(out_path, self.data)?;
        }
        Ok(())
    }
}

pub fn apply_entries(
    repo_base: String,
    out_base: String,
    patch_dirs: Vec<String>,
) -> Result<Vec<String>, io::Error> {
    let tables: Vec<(Vec<Entry>, String)> = patch_dirs
        .iter()
        .map(|p| -> Result<Vec<Entry>, io::Error> { read_entries(p.clone()) })
        .collect::<Result<Vec<_>, io::Error>>()?
        .into_iter()
        .zip(patch_dirs)
        .collect();

    let mut fs: HashMap<String, (Vec<(Entry, usize, String)>, String)> = HashMap::new();
    let mut f_names: Vec<String> = vec![];

    // populate filesystem with all entries
    for table in tables {
        for (index, entry) in table.0.iter().enumerate() {
            f_names.push(entry.src.clone());
            let value = (entry.clone(), index, table.1.clone());
            fs.entry(entry.src.clone())
                .and_modify(|v| v.0.push(value.clone()))
                .or_insert((vec![value], entry.src.clone()));
            match entry.command {
                Command::Move | Command::DeltaMove => {
                    let e = fs.remove_entry(&entry.src).unwrap().1;
                    fs.insert(entry.dst.clone().unwrap(), e);
                }
                _ => {}
            }
        }
    }

    let all_files = fs
        .clone()
        .into_par_iter()
        .map(|(_, table)| process_file(repo_base.clone(), out_base.clone(), table))
        .collect::<Result<Vec<Option<String>>, io::Error>>()?
        .into_iter()
        .filter_map(|p| p)
        .collect();

    Ok(all_files)
}

fn decompress_file(file: String) -> Result<impl Read, io::Error> {
    Ok(DeflateDecoder::new(BufReader::new(File::open(file)?)))
}

fn process_file(
    repo_base: String,
    out_base: String,
    table: (Vec<(Entry, usize, String)>, String),
) -> Result<Option<String>, io::Error> {
    // do what you gotta do (but atomically)
    // entry source will differ, but ultimately be the same file

    let mut file = RamFile::init();
    file.path = table.1.clone();
    file.data = fs::read(format!("{}/{}", repo_base, table.1)).unwrap_or(vec![]);
    let mut del_files: Option<String> = None;
    for entry in table.0 {
        let data_file = format!("{}/{:X}", entry.2, entry.1);
        match entry.0.command {
            Command::Add | Command::Update => {
                file.data = vec![];
                io::copy(&mut decompress_file(data_file)?, &mut file.data)?;
            }
            Command::Delete => {
                del_files = Some(file.path.clone());
                file.del()
            }
            Command::Delta => {
                let mut out_buf: Vec<u8> = vec![];
                ddelta::apply_chunked(
                    &mut io::Cursor::new(&file.data),
                    &mut out_buf,
                    &mut decompress_file(data_file)?,
                )
                .unwrap();
                file.data = vec![];
                io::copy(&mut io::Cursor::new(out_buf), &mut file.data)?;
            }
            Command::Move | Command::Copy => {
                file.path = entry.0.dst.as_ref().unwrap().clone();
                file.data = fs::read(format!("{}/{}", repo_base, entry.0.src.clone()))?;
                del_files = if entry.0.command == Command::Move {
                    Some(entry.0.src.clone())
                } else {
                    None
                };
            }
            Command::DeltaMove | Command::DeltaCopy => {
                file.path = entry.0.dst.as_ref().unwrap().clone();
                file.data = fs::read(format!("{}/{}", repo_base, entry.0.src.clone()))?;

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

                del_files = if entry.0.command == Command::DeltaMove {
                    Some(entry.0.src.clone())
                } else {
                    None
                };
            }
        }
    }

    file.write(out_base)?;
    Ok(del_files)
}
