use crate::diff::entry::{Command, Entry};
use crate::diff::hash::*;
use ddelta;
use flate2::write::DeflateEncoder;
use flate2::Compression;
use rayon::prelude::*;
use std::fs;
use std::fs::File;
use std::io;
use std::io::prelude::*;
use std::io::BufWriter;

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 mut writer = DeflateEncoder::new(
        BufWriter::new(File::create_new(object)?),
        Compression::default(),
    );
    let old_file = format!("{}/{}", old_base, entry.src);
    let new_file = if entry.command == Command::DeltaMove || entry.command == Command::Move {
        format!("{}/{}", new_base, entry.dst.unwrap())
    } else {
        format!("{}/{}", new_base, entry.src)
    };

    match entry.command {
        Command::Add | Command::Update | Command::Copy => {
            io::copy(&mut File::open(old_file)?, &mut writer)?;
        }
        Command::Delta | Command::DeltaMove | Command::DeltaCopy => {
            ddelta::generate_chunked(
                &mut File::open(new_file)?,
                &mut File::open(old_file)?,
                &mut writer,
                D_BUF_SIZE,
                |_| {},
            )
            .unwrap();
        }
        _ => {}
    }
    writer.flush()?;
    Ok(())
}

pub fn new_table(
    diffs: (Vec<(u32, String)>, Vec<(u32, String)>, Vec<(u32, String)>),
    old_base: &String,
    new_base: &String,
    table_path: Option<String>,
    hash_path: String,
) -> Result<Vec<String>, io::Error> {
    let old_hashes = read_hashes(hash_path.clone())?;
    let (entries, new_hashes) = create_entries(
        diffs.clone(),
        new_base.clone(),
        old_base.clone(),
        old_hashes,
    )?;

    if let Some(table_dir) = table_path {
        if entries.len() > 0 {
            write_hashes(hash_path, new_hashes)?;
            fs::create_dir(table_dir.clone())?;

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

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

            let copy_files = entries
                .iter()
                .filter_map(|entry| match entry.command {
                    Command::Add => None,
                    _ => Some(entry.src.clone()),
                })
                .collect::<Vec<_>>();

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

fn create_entries(
    diffs: (Vec<(u32, String)>, Vec<(u32, String)>, Vec<(u32, String)>),
    base_new: String,
    base_old: String,
    old_hashes: Vec<Option<THash>>,
) -> Result<(Vec<Entry>, Vec<Option<THash>>), io::Error> {
    let only_in_new_hashes: Vec<(u32, String, Option<THash>)> = diffs
        .0
        .par_iter()
        .map(|f| -> Result<_, io::Error> {
            Ok((
                f.0,
                f.1.clone(),
                hash_file(format!("{}/{}", base_new, f.1))?,
            ))
        })
        .collect::<Result<Vec<(u32, String, Option<THash>)>, io::Error>>()?;

    let only_in_old_hashes: Vec<(u32, String, Option<THash>)> = diffs
        .1
        .iter()
        .map(|f| (f.0, f.1.clone(), old_hashes[f.0 as usize].clone()))
        .collect();

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

    let same_hashes_new: Vec<(u32, String, Option<THash>)> = diffs
        .2
        .par_iter()
        .map(|f| -> Result<_, io::Error> {
            Ok((
                f.0,
                f.1.clone(),
                hash_file(format!("{}/{}", base_new, f.1))?,
            ))
        })
        .collect::<Result<Vec<(u32, String, Option<THash>)>, io::Error>>()?;

    //////////////////////////////////////////////////////////////////

    let same_v_same = same_hashes_old
        .par_iter()
        .flat_map(|p| same_hashes_new.par_iter().map(move |j| (p, j)))
        .map(|(b, a)| -> Result<Vec<Option<Entry>>, io::Error> {
            let distance = get_distance(
                a.2,
                b.2,
                format!("{}/{}", base_new, a.1),
                format!("{}/{}", base_old, b.1),
            )?;

            Ok(vec![if distance == 0 {
                None
            } else if distance < D_THRESH {
                Some(Entry {
                    command: Command::Delta,
                    src: a.1.clone(),
                    dst: None,
                })
            } else {
                Some(Entry {
                    command: Command::Update,
                    src: a.1.clone(),
                    dst: None,
                })
            }])
        })
        .collect::<Result<Vec<_>, io::Error>>()?;

    let mut same_and_old_v_new = same_hashes_old
        .par_iter()
        .map(|h| (0, h))
        .chain(only_in_old_hashes.par_iter().map(|h| (1, h)))
        .flat_map(|p| only_in_new_hashes.par_iter().map(move |j| (p, j)))
        .map(|((t, b), a)| -> Result<Vec<Option<Entry>>, io::Error> {
            let distance = get_distance(
                a.2,
                b.2,
                format!("{}/{}", base_new, a.1),
                format!("{}/{}", base_old, b.1),
            )?;
            Ok(if t == 0 {
                // from same
                vec![if distance == 0 {
                    Some(Entry {
                        command: Command::Copy,
                        src: a.1.clone(),
                        dst: Some(b.1.clone()),
                    })
                } else if distance < D_THRESH {
                    Some(Entry {
                        command: Command::DeltaCopy,
                        src: a.1.clone(),
                        dst: Some(b.1.clone()),
                    })
                } else {
                    Some(Entry {
                        command: Command::Delete,
                        src: a.1.clone(),
                        dst: None,
                    })
                }]
            } else {
                // from only in old
                if distance == 0 {
                    vec![Some(Entry {
                        command: Command::Move,
                        src: a.1.clone(),
                        dst: Some(b.1.clone()),
                    })]
                } else if distance < D_THRESH {
                    vec![Some(Entry {
                        command: Command::DeltaMove,
                        src: a.1.clone(),
                        dst: Some(b.1.clone()),
                    })]
                } else {
                    vec![
                        Some(Entry {
                            command: Command::Delete,
                            src: a.1.clone(),
                            dst: None,
                        }),
                        Some(Entry {
                            command: Command::Add,
                            src: b.1.clone(),
                            dst: None,
                        }),
                    ]
                }
            })
        })
        .collect::<Result<Vec<_>, io::Error>>()?;

    // either old and same, or new could be empty
    if same_and_old_v_new.is_empty() {
        same_and_old_v_new = only_in_old_hashes
            .iter()
            .map(|o| {
                vec![Some(Entry {
                    command: Command::Add,
                    src: o.1.clone(),
                    dst: None,
                })]
            })
            .chain(only_in_new_hashes.iter().map(|n| {
                vec![Some(Entry {
                    command: Command::Delete,
                    src: n.1.clone(),
                    dst: None,
                })]
            }))
            .collect()
    }

    let entries: Vec<_> = same_and_old_v_new
        .into_iter()
        .chain(same_v_same.into_iter())
        .flatten()
        .filter_map(|p| p)
        .collect();

    let mut new_hashes = same_hashes_new
        .iter()
        .chain(only_in_new_hashes.iter())
        .collect::<Vec<_>>();
    new_hashes.sort_by_key(|h| h.0);

    Ok((entries, new_hashes.iter().map(|h| h.2.clone()).collect()))
}
