use crate::diff::entry::{Command, Entry};
use crate::diff::hash::*;
use crate::misc::{compress_writer, decompress_reader};
use ddelta;
use gxhash::HashSet;
use indicatif::ParallelProgressIterator;
use rayon::prelude::*;
use smol_str::{format_smolstr, SmolStr};
use std::fs;
use std::fs::File;
use std::io;
use std::io::prelude::*;
use std::io::BufReader;
use std::sync::Mutex;

const D_BUF_SIZE: usize = 1 << 28;
const D_THRESH: u32 = 150; // max is 1993

fn write_data(
    entry: &Entry,
    old_base: &String,
    new_base: &String,
    object: &String,
) -> Result<(), io::Error> {
    let new_file = format!("{}/{}", new_base, entry.src);

    let old_file = match &entry.command {
        Command::DeltaMove { dst } | Command::DeltaCopy { dst } => {
            format!("{}/{}", old_base, dst)
        }
        _ => {
            format!("{}/{}", old_base, entry.src)
        }
    };

    // if file is already compressed, no need to recompress it
    match &entry.command {
        Command::Add | Command::Update | Command::Copy { .. } => {
            fs::copy(old_file, object.to_string())?;
        }
        Command::Delta | Command::DeltaMove { .. } | Command::DeltaCopy { .. } => {
            let mut writer = compress_writer(object.to_string())?;
            ddelta::generate_chunked(
                &mut BufReader::new(File::open(new_file)?),
                &mut decompress_reader(old_file)?,
                &mut writer,
                D_BUF_SIZE,
                |_| {},
            )
            .unwrap();
            writer.flush()?;
        }
        _ => {}
    }
    Ok(())
}

use crate::misc::get_style;
pub fn new_table(
    diffs: (
        Vec<(usize, SmolStr)>,
        Vec<(usize, SmolStr)>,
        Vec<(usize, SmolStr)>,
    ),
    old_base: &String,
    new_base: &String,
    table_path: &Option<String>,
    hash_path: &String,
) -> Result<Vec<SmolStr>, io::Error> {
    let old_hashes = read_hashes(hash_path)?;
    let (entries, mut new_hashes) = create_entries(diffs, &new_base, &old_hashes)?;

    if let Some(table_dir) = table_path {
        if !entries.is_empty() {
            write_hashes(hash_path, &mut new_hashes)?;

            fs::create_dir(table_dir.to_string())?;

            // write index table
            let mut writer = compress_writer(format!("{}/index", table_dir))?;
            for entry in entries.iter() {
                entry.to_io(&mut writer)?;
            }
            writer.flush()?;

            // write objects
            let style = get_style("Writing objects", entries.len());
            entries
                .par_iter()
                .progress_with(style)
                .enumerate()
                .map(|(index, entry)| -> Result<(), io::Error> {
                    let obj = format!("{}/{:X}", table_dir, index);
                    write_data(entry, old_base, new_base, &obj)?;
                    Ok(())
                })
                .collect::<Result<(), io::Error>>()?;

            let copy_files = entries
                .par_iter()
                .filter_map(|entry| match entry.command {
                    // copy means src file still exists
                    Command::Add | Command::Copy { .. } | Command::DeltaCopy { .. } => None,
                    _ => Some(entry.src.clone()),
                })
                .collect::<Vec<_>>();

            return Ok(copy_files);
        }
        Ok(vec![])
    } else {
        println!("{:?}", entries);
        Ok(vec![])
    }
}

fn create_entries(
    diffs: (
        Vec<(usize, SmolStr)>,
        Vec<(usize, SmolStr)>,
        Vec<(usize, SmolStr)>,
    ),
    base_new: &String,
    old_hashes: &Vec<(u64, Option<THash>)>,
) -> Result<(Vec<Entry>, Vec<(SmolStr, (u64, Option<THash>))>), io::Error> {
    let only_in_new_hashes: Vec<(SmolStr, (u64, Option<THash>))> = if diffs.0.is_empty() {
        vec![]
    } else {
        let style_new = get_style("Computing hashes", diffs.0.len());
        diffs
            .0
            .into_par_iter()
            .progress_with(style_new)
            .map(|f| -> Result<_, io::Error> {
                Ok((
                    f.1.clone(),
                    hash_file(&format_smolstr!("{}/{}", base_new, f.1), false)?,
                ))
            })
            .collect::<Result<Vec<_>, io::Error>>()?
    };

    let only_in_old_hashes: Vec<(SmolStr, (u64, Option<THash>))> = diffs
        .1
        .into_iter()
        .map(|f| (f.1, old_hashes[f.0]))
        .collect();

    let same_hashes_old: Vec<(SmolStr, (u64, Option<THash>))> = diffs
        .2
        .iter()
        .map(|f| (f.1.clone(), old_hashes[f.0]))
        .collect();

    let same_hashes_new: Vec<(SmolStr, (u64, Option<THash>))> = if diffs.2.is_empty() {
        vec![]
    } else {
        let style_same = get_style("Computing hashes", diffs.2.len());
        diffs
            .2
            .into_par_iter()
            .progress_with(style_same)
            .map(|f| -> Result<_, io::Error> {
                Ok((
                    f.1.clone(),
                    hash_file(&format_smolstr!("{}/{}", base_new, f.1), false)?,
                ))
            })
            .collect::<Result<Vec<_>, io::Error>>()?
    };

    //////////////////////////////////////////////////////////////////
    let same_v_same = if same_hashes_old.is_empty() {
        vec![]
    } else {
        let style_same_v_same = get_style("Computing entries", same_hashes_old.len());
        same_hashes_old
            .par_iter()
            .zip(same_hashes_new.par_iter())
            .progress_with(style_same_v_same)
            .map(|(b, a)| -> Vec<Option<Entry>> {
                let distance = get_distance(b.1, a.1);

                vec![if distance == 0 {
                    None
                } else if distance < D_THRESH {
                    Some(Entry {
                        command: Command::Delta,
                        src: a.0.clone(),
                    })
                } else {
                    Some(Entry {
                        command: Command::Update,
                        src: a.0.clone(),
                    })
                }]
            })
            .flatten()
            .filter_map(|p| p)
            .collect()
    };

    // actually we dont need another, as all the rest will be looking in new and will thus be a
    // delete without any data to be stored / copied

    let same_and_new: Vec<_> = same_hashes_new
        .iter()
        .map(|h| (true, h))
        .chain(only_in_new_hashes.iter().map(|p| (false, p)))
        .collect();

    let mut same_and_new_v_old = if same_and_new.is_empty() || only_in_old_hashes.is_empty() {
        vec![]
    } else {
        let indexes = Mutex::new(HashSet::<usize>::default());
        let style_new_vs_old = get_style("Computing entries", only_in_old_hashes.len());
        only_in_old_hashes
            .par_iter()
            .progress_with(style_new_vs_old)
            .map(|oioh| -> _ {
                // get lowest sanh for a given oioh

                let (index, (distance, sanh)) = unsafe {
                    same_and_new
                        .par_iter()
                        .map(|sanh| -> _ {
                            let distance = get_distance(oioh.1, sanh.1 .1);
                            (distance, sanh)
                        })
                        .enumerate()
                        .min_by_key(|p| p.1 .0)
                        .unwrap_unchecked()
                };

                indexes.lock().unwrap().insert(index);

                let b = oioh;
                let (from_same, a) = sanh;

                if *from_same {
                    vec![if distance == 0 {
                        Entry {
                            command: Command::Copy { dst: b.0.clone() },
                            src: a.0.clone(),
                        }
                    } else if distance < D_THRESH {
                        Entry {
                            command: Command::DeltaCopy { dst: b.0.clone() },
                            src: a.0.clone(),
                        }
                    } else {
                        Entry {
                            command: Command::Add,
                            src: b.0.clone(),
                        }
                    }]
                } else {
                    // from only in old
                    if distance == 0 {
                        vec![Entry {
                            command: Command::Move { dst: b.0.clone() },
                            src: a.0.clone(),
                        }]
                    } else if distance < D_THRESH {
                        vec![Entry {
                            command: Command::DeltaMove { dst: b.0.clone() },
                            src: a.0.clone(),
                        }]
                    } else {
                        vec![
                            Entry {
                                command: Command::Delete,
                                src: a.0.clone(),
                            },
                            Entry {
                                command: Command::Add,
                                src: b.0.clone(),
                            },
                        ]
                    }
                }
            })
            .flatten()
            .chain(
                same_and_new
                    .par_iter()
                    .enumerate()
                    .filter(|(i, (from_same, _))| {
                        !indexes.lock().unwrap().contains(i) && !from_same
                    })
                    .map(|(_, (_, e))| Entry {
                        command: Command::Delete,
                        src: e.0.clone(),
                    }),
            )
            .collect::<Vec<Entry>>()
    };

    // either same and new (only old) or old is empty (only same and new)
    let len = only_in_old_hashes.len() + only_in_new_hashes.len();
    if same_and_new_v_old.is_empty() && len != 0 {
        same_and_new_v_old = only_in_old_hashes
            .iter()
            .map(|o| Entry {
                command: Command::Add,
                src: o.0.clone(),
            })
            .chain(only_in_new_hashes.iter().map(|n| Entry {
                command: Command::Delete,
                src: n.0.clone(),
            }))
            .collect()
    }

    let entries: Vec<_> = same_and_new_v_old
        .into_iter()
        .chain(same_v_same.into_iter())
        .collect();

    let new_hashes: Vec<_> = same_hashes_new
        .into_iter()
        .chain(only_in_new_hashes.into_iter())
        .collect();

    Ok((entries, new_hashes))
}
