// This file is currently unused. We'll want to utilize the code in the future.
use std::{cmp::max, io::Write};
use super::diff::{Hunk, Op};
use super::io::WriteTo;
use triple_accel::levenshtein::levenshtein;

#[derive(Clone, Debug, Default)]
pub(crate) struct Pair<'a> {
    pub(crate) hunk: Option<&'a Hunk>,
    pub(crate) changed: bool,
}

pub(crate) fn find_matches<'a>(left: &[Hunk], right: &'a [Hunk])
    -> Result<Vec<Pair<'a>>, bool>
{
    let l_added: Vec<_> = left.iter()
                            .map(|h| h.format_filter(|x| x.op == Op::Added)).collect();
    let l_removed: Vec<_> = left.iter()
                            .map(|h| h.format_filter(|x| x.op == Op::Removed)).collect();
    let l_context: Vec<_> = left.iter()
                            .map(|h| h.format_filter(|x| x.op == Op::Context)).collect();
    let r_added: Vec<_> = right.iter()
                            .map(|h| h.format_filter(|x| x.op == Op::Added)).collect();
    let r_removed: Vec<_> = right.iter()
                            .map(|h| h.format_filter(|x| x.op == Op::Removed)).collect();
    let r_context: Vec<_> = right.iter()
                            .map(|h| h.format_filter(|x| x.op == Op::Context)).collect();
    // Find Levenshtein distances between each pair of (left hunk, right
    // hunk). Store them as a list of weighted edges, i.e. list of tuples
    // (distance, left hunk index, right hunk index). Do not store edges
    // with a weight larger than a threshold.
    let mut distances: Vec<(usize, usize, usize)> = vec![];
    for i in 0..left.len() {
        for j in 0..right.len() {
            // A (rather arbitrary) threshold for distance acceptance. We
            // accept changes up to 50% of the characters on added+removed
            // lines.
            let threshold = max(l_added[i].len() + l_removed[i].len(),
                                r_added[j].len() + r_removed[j].len()) / 2;
            let dist = (levenshtein(&l_added[i], &r_added[j]) +
                        levenshtein(&l_removed[i], &r_removed[j])) as usize;
            if dist <= threshold {
                // Add also the context but with lower weight. If there are hunks that
                // differ only in the context, we want to match them correctly.
                let dist_c = levenshtein(&l_context[i], &r_context[j]) as usize;
                distances.push((dist + dist_c / 4, i, j));
            }
        }
    }
    // Sort the edges from the lowest weight to the highest weight. Use the stable sort
    // to keep the hunks in the same order in case they match completely.
    distances.sort_by(|a, b| a.0.cmp(&b.0));
    // Use a greedy algorithm to pick up the pairs of hunks.
    let mut result: Vec<Pair> = vec![Pair::default(); left.len()];
    let mut used: Vec<bool> = vec![false; right.len()];
    for (dist, i, j) in distances {
        if result[i].hunk.is_some() || used[j] {
            continue;
        }
        result[i].hunk.replace(&right[j]);
        result[i].changed = dist > 0;
        used[j] = true;
    }

    if used.contains(&false) {
        return Err(result.iter().any(|x| x.hunk.is_none()));
    }
    Ok(result)
}

pub(crate) struct Related<'a, T> {
    pool: Vec<(&'a Hunk, T, Vec<&'a [u8]>)>,
}

impl<'a, T> Related<'a, T> {
    pub(crate) fn new<I>(pool: I) -> Related<'a, T>
    where
        I: IntoIterator<Item = (&'a Hunk, T)>
    {
        Related {
            pool: pool.into_iter()
                      .map(|(h, t)| (h, t, Related::<T>::stripped_lines(h, Op::Added)))
                      .collect(),
        }
    }

    fn strip(s: &[u8]) -> &[u8] {
        let start = match s.iter().position(|x| *x != b' ' && *x != b'\t')
            { Some(x) => x, None => return &s[0..0] };
        let end = s.iter().rposition(|x| *x != b' ' && *x != b'\t').unwrap();
        return &s[start..end + 1];
    }

    fn stripped_lines(hunk: &'a Hunk, op: Op) -> Vec<&'a [u8]> {
        hunk.lines.iter().filter_map(|x| {
                if x.op != op {
                    return None;
                }
                let s = Related::<T>::strip(&x.content);
                if s.len() <= 12 {
                    None
                } else {
                    Some(s)
                }
            }).collect()
    }

    pub(crate) fn find(&mut self, hunk: &Hunk) -> Vec<(&'a Hunk, T)> {
        let mut result = vec![];
        let removed = Related::<T>::stripped_lines(&hunk, Op::Removed);
        let mut i = 0;
        while i < self.pool.len() {
            let candidate = &self.pool[i];
            if removed.iter()
                      .find(|&r| candidate.2.iter().find(|&a| a == r).is_some())
                      .is_none() {
                i += 1;
                continue;
            }
            let candidate = self.pool.remove(i);
            result.push((candidate.0, candidate.1));
        }
        result
    }

    /// Return an iterator over unused hunks from the pool in reversed order.
    pub(crate) fn remaining_rev(&self) -> impl Iterator<Item = (&'a Hunk, &T)> {
        self.pool.iter().rev().map(|(h, t, _)| (*h, t))
    }
}

pub(crate) fn write_hunks<'a, I>(f: &mut impl Write, hunks: I) -> Result<(), std::io::Error>
where
    I: IntoIterator<Item = &'a Hunk>
{
    let mut hunks: Vec<_> = hunks.into_iter().collect();
    hunks.sort_by(|a, b| a.old.start.cmp(&b.old.start));
    for hunk in hunks {
        hunk.write_to(f)?;
    }
    Ok(())
}
