#[cfg(any(feature = "debug_annotation", feature = "debug_location"))]
use bstr::ByteSlice;
use std::{cmp::max, cmp::min, io::Write, ops::Range};
use std::collections::{HashMap, VecDeque};
use super::{annotate, io::WriteTo};
use super::diff::{CONTEXT, Hunk, Op};
use super::lines::{ByteIndex, LineIndex, LineIndexRange, Lines, TypedLines};
use triple_accel::{levenshtein, SearchType};

#[derive(Debug, Default)]
pub struct Replacement {
    /// Range of the replacement. `start` and `end` count the inter-line positions.
    /// I.e., index 0 is before the first line (before `lines[0]`), index 1 is between
    /// `lines[0]` and `lines[1]`, etc. These are lines to be deleted (and replaced by
    /// `self.data`), i.e. the context lines are not included. If there are only additions
    /// to a certain place, then `range.start` == `range.end`.
    pub range: Range<LineIndex>,
    /// The range including the context. This does not take into account possible overlap
    /// of contexts of surrounding applied hunks. To get the range that does take it into
    /// account, call `range_with_context`, which uses `max_top_context` and
    /// `max_bottom_context` to adjust the range.
    full_range: Range<LineIndex>,
    /// An indication whether there is a fuzz in the context before the replacement. "Fuzz"
    /// means the expected context (the context in the patch) does not match the actually
    /// encountered context.
    pub fuzz_top: bool,
    /// Fuzz in the context after the replacement.
    pub fuzz_bottom: bool,
    /// Fuzz in the middle context. If the hunk was not split, this is always false; any
    /// middle context fuzz in a non-split hunk is considered to be a content fuzz.
    pub fuzz_middle: bool,
    /// An indication whether there is a fuzz inside the replacement itself.
    pub fuzz_content: bool,
    /// Number of context lines between the preceding hunk and this hunk.
    max_top_context: Option<usize>,
    /// Number of context lines between this hunk and the following hunk. If there are not
    /// enough context lines between the hunks available, we first trim the bottom context
    /// possibly all the way down to zero. This allows the next hunk to have the full top
    /// context. However, if there are not enough context lines at all, we need to trim
    /// the top context.
    max_bottom_context: Option<usize>,
    /// The data this piece of code should be replaced with.
    data: ReplacementData
}

#[derive(derive_more::Debug)]
enum ReplacementData {
    /// A freshly created replacement.
    New,
    /// The hunk can be directly applied to the indicated location.
    Single {
        additions: Lines,
        hunk: Hunk,
    },
    /// The hunk is in the process of splitting.
    NewMulti {
        /// See ReplacementData::Multi::subreplacement.
        subreplacement: ReplacementTable,
    },
    /// The hunk was split.
    Multi {
        /// Positions of subhunks. Those positions always operate within the bounds of the
        /// current replacement. A subreplacement may not contain other subreplacements.
        subreplacement: ReplacementTable,
        /// The hunk that was split.
        full_hunk: Hunk,
    },
    /// Undo data after conversion from Single to Multi.
    Undo,
    /// Resolution extracted from learning data.
    Extracted {
        #[debug("{} bytes", additions.len())]
        additions: Vec<u8>,
    },
}

/// The list of code replacements. The entries are kept sorted.
#[derive(Debug)]
struct ReplacementTable(VecDeque<Replacement>);

#[derive(Debug)]
pub struct Apply {
    pub lines: TypedLines,
    replacement: ReplacementTable,
    /// Vector of hunks that failed to be applied. They are not in any particular order.
    unapplied: Vec<Hunk>,
    pub too_long: usize,
    pub missed: usize,
    pub split: usize,
}

/// Degree of fuzziness in an applied change.
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum AppliedFuzz {
    /// No fuzz.
    None,
    /// Some context fuzz, no content fuzz.
    Context,
    /// Content fuzz (and possibly also context fuzz).
    Content,
}

/// Describes a single applied change (hunk) to the code.
#[derive(Debug)]
pub struct AppliedChange {
    /// Range of the region where the change should be applied, including the context.
    /// It's an inter-line position.
    pub range: Range<LineIndex>,
    /// The lines to be applied instead of `start .. end`.
    pub new_lines: Lines,
    /// The hunk that got applied.
    pub hunk: Hunk,
    /// The fuzziness.
    pub fuzz: AppliedFuzz,
}

/// The result of patch applied to a file.
#[derive(Debug)]
pub struct Applied {
    /// The original lines.
    pub old_lines: TypedLines,
    /// Changes applied to `old_lines`.
    pub changes: Vec<AppliedChange>,
    /// Hunks that could not be applied. They are not in any particular order.
    pub unapplied: Vec<Hunk>,
}

#[derive(Debug)]
pub struct Extract {
    pub apply: Apply,
    target: Vec<Replacement>,
}

impl Default for ReplacementData {
    fn default() -> Self {
        Self::New
    }
}

impl Replacement {
    fn new(range: Range<LineIndex>, full_range: Range<LineIndex>) -> Self {
        Self {
            range,
            full_range,
            ..Default::default()
        }
    }

    pub fn range_with_context(&self) -> Range<LineIndex>
    {
        let Range { mut start, mut end } = self.full_range;
        if let Some(mtc) = self.max_top_context {
            start = max(start, self.range.start.saturating_sub(mtc));
        }
        if let Some(mbc) = self.max_bottom_context {
            end = min(end, self.range.end + mbc);
        }
        start .. end
    }

    /// Attach the subreplacement table and update the replacement data to reflect
    /// the aggregate of the subhunks. The replacement.data must be New.
    /// Returns a struct that can be used to undo this operation.
    fn set_subreplacement(&mut self, subreplacement: ReplacementTable)
            -> Replacement {
        assert!(matches!(self.data, ReplacementData::New));
        let undo = Self {
            range: self.range.clone(),
            full_range: self.full_range.clone(),
            fuzz_top: self.fuzz_top,
            fuzz_bottom: self.fuzz_bottom,
            fuzz_middle: self.fuzz_middle,
            fuzz_content: self.fuzz_content,
            max_top_context: self.max_top_context,
            max_bottom_context: self.max_bottom_context,
            data: ReplacementData::Undo,
            ..Default::default()
        };
        let first = subreplacement.first();
        let last = subreplacement.last();
        self.range.start = first.range.start;
        self.range.end = last.range.end;
        self.fuzz_top = first.fuzz_top;
        self.fuzz_bottom = last.fuzz_bottom;
        for (i, repl) in subreplacement.iter().enumerate() {
            if i > 0 {
                self.fuzz_middle = self.fuzz_middle.max(repl.fuzz_top);
            }
            if i + 1 < subreplacement.len() {
                self.fuzz_middle = self.fuzz_middle.max(repl.fuzz_bottom);
            }
        }
        if let Some(max_top_context) = self.max_top_context {
            let n = self.range.start - undo.range.start + max_top_context as isize;
            self.max_top_context = Some(n as usize);
        }
        if let Some(max_bottom_context) = self.max_bottom_context {
            let n = undo.range.end - self.range.end + max_bottom_context as isize;
            self.max_bottom_context = Some(n as usize);
        }
        self.fuzz_content = false;
        for repl in subreplacement.iter() {
            self.fuzz_content |= repl.fuzz_content;
        }
        self.data = ReplacementData::NewMulti { subreplacement };
        undo
    }

    /// Undos the set_subreplacement operation. The data is the result of the previous call
    /// to set_subreplacement.
    fn undo(&mut self, repl: Replacement) {
        assert!(matches!(self.data, ReplacementData::NewMulti { .. }));
        assert!(matches!(repl.data, ReplacementData::Undo));
        self.range = repl.range;
        self.full_range = repl.full_range;
        self.fuzz_top = repl.fuzz_top;
        self.fuzz_bottom = repl.fuzz_bottom;
        self.fuzz_middle = repl.fuzz_middle;
        self.fuzz_content = repl.fuzz_content;
        self.max_top_context = repl.max_top_context;
        self.max_bottom_context = repl.max_bottom_context;
        self.data = ReplacementData::New;
    }
}

impl ReplacementTable {
    fn new() -> Self {
        Self(VecDeque::new())
    }

    /// Find the position where the given replacement would fit.
    /// If allow_back_to_back is true, two replacements may come one after another without
    /// any intervening context line.
    /// Returns None when the lines are already occupied by an existing replacement entry.
    fn insert_position(&self, repl: &Replacement, allow_back_to_back: bool)
            -> Option<usize> {
        // find the position where this should go to
        let repl_pos = match self.0.binary_search_by(
            |r| r.range.start.cmp(&repl.range.start)
        ) {
            Ok(_) => return None,   // full overlap
            Err(x) => x,
        };
        if repl_pos > 0 {
            let prev_end = self.0[repl_pos - 1].range.end;
            if prev_end > repl.range.start ||
                    (!allow_back_to_back && prev_end == repl.range.start) {
                // overlap at the beginning
                return None;
            }
        }
        if repl_pos < self.0.len() {
            let next_start = self.0[repl_pos].range.start;
            if next_start < repl.range.end ||
                    (!allow_back_to_back && next_start == repl.range.end) {
                // overlap in the middle or at the end
                return None;
            }
        }
        Some(repl_pos)
    }

    /// Insert the replacement and the corresponding hunk to the given position in the table.
    /// The position must be determined by calling insert_position. The table must not be
    /// changed between the two calls. The replacement.data must be New or NewMulti.
    fn insert(&mut self, repl_pos: usize, mut repl: Replacement, hunk: Hunk)
            -> &Replacement {
        let Range { start, end } = repl.range;
        let full_start = repl.full_range.start;
        repl.max_top_context = if repl_pos > 0 {
            Some((start - self.0[repl_pos - 1].range.end) as usize)
        } else {
            None
        };
        repl.max_bottom_context = if repl_pos < self.0.len() {
            Some(self.0[repl_pos].full_range.start.saturating_delta(end))
        } else {
            None
        };
        let old_data = std::mem::replace(&mut repl.data, ReplacementData::New);
        repl.data = match old_data {
            ReplacementData::New => ReplacementData::Single {
                additions: hunk.to_lines_filter_noncontext(|x| x.op != Op::Removed),
                hunk,
            },
            ReplacementData::NewMulti { subreplacement } => ReplacementData::Multi {
                subreplacement,
                full_hunk: hunk,
            },
            _ => panic!()
        };
        self.0.insert(repl_pos, repl);
        if repl_pos + 1 < self.0.len() {
            self.0[repl_pos + 1].max_top_context = Some(
                (self.0[repl_pos + 1].range.start - end) as usize
            );
        }
        if repl_pos > 0 {
            self.0[repl_pos - 1].max_bottom_context = Some(
                full_start.saturating_delta(self.0[repl_pos - 1].range.end)
            );
        }
        &self.0[repl_pos]
    }

    /// Returns the first replacement. Panics if the table is empty.
    fn first(&self) -> &Replacement {
        &self.0[0]
    }

    /// Returns the last replacement. Panics if the table is empty.
    fn last(&self) -> &Replacement {
        &self.0[self.0.len() - 1]
    }
}

impl std::ops::Deref for ReplacementTable {
    type Target = VecDeque<Replacement>;
    fn deref(&self) -> &Self::Target {
        &self.0
    }
}
impl std::ops::DerefMut for ReplacementTable {
    fn deref_mut(&mut self) -> &mut Self::Target {
        &mut self.0
    }
}

/// Representation of a position of a hunk in code. Matches are sorted according to fields
/// from top to bottom, i.e. first by `annot_k`, then by `k`, etc.
#[derive(Eq, Ord, PartialEq, PartialOrd)]
struct Match {
    /// Coefficient of the match adjusted for annotation. Lower is better.
    annot_k: u32,
    /// Sum of coefficient of matches of deleted lines. Lower is better.
    removed_k: u32,
    /// Coefficient of the match. Lower is better.
    k: u32,
    /// Offset delta. Lower is better.
    offset_delta: usize,
    /// Byte pos of the start of the match in lines.full (inclusive).
    start: ByteIndex,
    /// Byte pos of the end of the match in lines.full (exclusive). We don't use Range<usize>
    /// here as we want to derive Ord and Range does not implement it.
    end: ByteIndex,
}

/// Max number of lines to consider when searching for matching context. In other words,
/// this indicates how many lines can be added downstream to the context to still recognize
/// it as the context (after subtracting the upstream context size).
const MAX_CONTEXT_SEARCH: usize = 16;

/// A fuzzy searcher that helps with searching for the best position of the given hunk
/// in the code.
struct HunkSearcher<'a> {
    /// The hunk we're searching for.
    hunk: Hunk,
    /// The buffer we're searching in.
    lines: &'a TypedLines,
    /// The leading context.
    context1: Vec<u8>,
    /// The leading context, reversed.
    context1_rev: Vec<u8>,
    /// The trailing context.
    context2: Vec<u8>,
    /// The trailing context, reversed.
    context2_rev: Vec<u8>,
    /// The content (removed lines and inner context).
    content: Vec<u8>,
    /// The reversed content.
    content_rev: Vec<u8>,
    /// The offset of the hunk in the original (old) file. Byte position.
    orig_offset: ByteIndex,
    /// The costs of the levenshtein operations.
    costs: levenshtein::EditCosts,
    /// The currently focused area start. Set by `use_match`.
    used_area_start: ByteIndex,
    /// The currently focused area end. Set by `use_match`.
    used_area_end: ByteIndex,
    /// The location at which context1 starts. Set by `use_match` if context1 matches
    /// perfectly.
    context1_start: Option<ByteIndex>,
    /// The current guess of where context1 ends. Set by `use_match`.
    context1_end: ByteIndex,
    /// The current guess of where context2 starts. Set by `use_match`.
    context2_start: ByteIndex,
    /// The location at which context2 ends. Set by `use_match` if context2 matches
    /// perfectly.
    context2_end: Option<ByteIndex>,
    /// The (current) gravity of the content: towards the leading (false) or trailing (true)
    /// context.
    content_bottom: bool,
    /// The cache of removed lines coefficients.
    cache_removed_k: HashMap<Range<LineIndex>, u32>,
    /// All found positions of the hunk. The best position is at the end.
    matches: Vec<Match>,
}

impl<'a> HunkSearcher<'a> {
    /// Allocate the object and populate it with the data obtained from the hunk.
    /// Returns Err if the hunk is too long; passes the hunk back in such case.
    fn new(
        hunk: Hunk,
        lines: &'a TypedLines,
        area: Range<ByteIndex>,
    ) -> Result<Self, Hunk> {
        // Construct the individual parts of the data we're searching for: the leading
        // context, the content and the trailing context.
        // Get the content (the removed lines and the inner context). Store also its
        // reversed value for backward search.
        let content = hunk.format_filter_noncontext(|x| x.op != Op::Added);
        if content.len() > 20000 {
            return Err(hunk);
        }
        let content_rev: Vec<_> = content.iter().rev().copied().collect();
        // Get the leading and trailing context. Reverse the trailing context to allow
        // backward search.
        let (context1, context2) = hunk.get_contexts();
        let context1_rev = context1.iter().rev().copied().collect();
        let context2_rev = context2.iter().rev().copied().collect();
        // Store the costs. It's in fact a const value but we can't construct it as such
        // and using a LazyLock is overkill.
        let costs = levenshtein::EditCosts::new(3, 2, 0, None);
        let orig_offset = lines.get_byte_index(hunk.old.start.into());
        let mut result = Self {
            hunk,
            lines,
            context1,
            context1_rev,
            context2,
            context2_rev,
            content,
            content_rev,
            orig_offset,
            costs,
            used_area_start: 0.into(),
            used_area_end: lines.byte_end(),
            context1_start: None,
            context1_end: 0.into(),
            context2_start: lines.byte_end(),
            context2_end: None,
            content_bottom: false,
            cache_removed_k: HashMap::new(),
            matches: Vec::new(),
        };
        result.find_matches(area);
        Ok(result)
    }

    /// Get the matches of the hunk, sorted by how good they are. The best match comes first.
    /// The search is performed only in the given area (byte pos in lines.full).
    fn find_matches(&mut self, area: Range<ByteIndex>) {
        // Construct the text we're searching for: the leading context + the content +
        // the trailing context. Note that this is the same as calling
        // hunk.format_filter(|x| x.op != Op::Added).
        let mut needle = self.context1.clone();
        needle.extend_from_slice(&self.content);
        needle.extend_from_slice(&self.context2);
        // Special case: there are only added lines and the range is empty. We're adding
        // text to a new file. It is always a match. Ignore the context.
        if self.content.len() == 0 && area.start == area.end {
            self.matches.push(Match {
                annot_k: 0,
                removed_k: 0,
                k: 0,
                offset_delta: 0,
                start: area.start,
                end: area.end,
            });
            return;
        }
        // Search the full file. The maximum cost was determined experimentally.
        // levenshtein_search_simd_with_opts returns values incrementally during the search.
        // Each yielded match has its start >= previous start. For two matches with the same
        // start, the first end > the second end. We use this to keep only a single match
        // for each start: the one with the lowest k. If there are more such matches, we keep
        // the longer one.
        // While iterating, remember the best k we've seen.
        let mut matches = Vec::new();
        let mut best_k = None;
        let mut prev = None;
        for m in levenshtein::levenshtein_search_simd_with_opts(
            &needle,
            self.lines.byte_slice(area.clone()),
            (needle.len() * 3 / 2) as u32,
            SearchType::All,
            self.costs,
            false
        ) {
            if prev.is_none() {
                best_k = Some(m.k);
                prev = Some(m);
                continue;
            }
            if m.k < best_k.unwrap() {
                best_k = Some(m.k);
            }
            if m.start > prev.as_ref().unwrap().start {
                matches.push(prev.replace(m).unwrap());
                continue;
            }
            let prev_k = prev.as_ref().unwrap().k;
            if m.k < prev_k || (m.k == prev_k && m.end > prev.as_ref().unwrap().end) {
                prev.replace(m);
            }
        }
        if let Some(prev) = prev {
            matches.push(prev);
        }
        if matches.len() == 0 {
            return;
        }
        // Trim the matches that are too far from the best match.
        let limit_k = best_k.unwrap() + (needle.len() / 2) as u32;
        matches.retain(|m| m.k < limit_k);
        // Convert to Match and reverse sort them.
        self.matches = matches.into_iter()
            .map(|m| {
                // Adjust the match offsets to point to the whole buffer.
                let start = area.start + m.start;
                let end = area.start + m.end;
                let line_range = self.lines.calculate_line_range(start .. end);
                // Calculate the annotation distance.
                let annot_k = annotate::distance(
                    &self.hunk.tags,
                    &self.lines.tag_slice(line_range.clone()).unwrap(),
                );
                // Calculate the removed lines distance.
                let removed_k = self.removed_lines_cost(line_range);
                Match {
                    annot_k: annot_k as u32,
                    removed_k,
                    k: m.k,
                    offset_delta: self.orig_offset.delta(m.start.into()),
                    start,
                    end,
                }
            })
            .collect();
        self.matches.sort_unstable_by(|a, b| a.cmp(b).reverse());
        #[cfg(feature = "debug_location")]
        self.debug_location();
    }

    /// Calculate the sum of costs (differences) of removed lines in the given area.
    fn removed_lines_cost(&mut self, line_range: Range<LineIndex>) -> u32 {
        if let Some(&cost) = self.cache_removed_k.get(&line_range) {
            return cost;
        }
        let result = self.hunk.lines.iter()
            .filter(|hunk| hunk.op == Op::Removed)
            .map(|hunk_line| {
                let max_len = (hunk_line.content.len() * 3) as u32;
                line_range.iter()
                    .map(|i| &self.lines[i])
                    .map(|code_line|
                        levenshtein::levenshtein_simd_k_with_opts(
                            &hunk_line.content,
                            code_line,
                            max_len,
                            false,
                            self.costs,
                        ).map(|(k, _)| k)
                        .unwrap_or(max_len)
                    ).min()
                    .unwrap_or(max_len)
            })
            .sum();
        self.cache_removed_k.insert(line_range, result);
        result
    }

    #[cfg(feature = "debug_location")]
    fn debug_location(&self) {
        println!("============ Hunk placement ============");
        self.hunk.write_to(&mut std::io::stdout().lock()).unwrap();
        #[cfg(feature = "debug_annotation")]
        {
            print!(">>> annotation:");
            self.hunk.tags.iter().for_each(|tag| print!(" {}", tag.get_debug_value()));
            println!();
        }
        for (i, m) in self.matches.iter().rev().take(5).enumerate() {
            println!("------------ match {i} ------------");
            println!(
                ">>> annot_k = {}, removed_k = {}, k = {}, offset_delta = {}",
                m.annot_k,
                m.removed_k,
                m.k,
                m.offset_delta,
            );
            let line_range = self.lines.calculate_line_range(m.start .. m.end);
            for index in line_range.iter() {
                let line = &self.lines[index];
                print!("{index} {}", line.as_bstr());
                if !line.ends_with(b"\n") {
                    println!();
                }
            }
            #[cfg(feature = "debug_annotation")]
            {
                print!(">>> annotation:");
                self.lines.tag_slice(line_range).unwrap().iter().for_each(|tag|
                    print!(" {}", tag.get_debug_value())
                );
                println!();
            }
        }
    }

    /// Locate the contexts in the given match and "focus" on the area between them.
    /// Future operations will work in this area. Returns the range of the area (byte pos in
    /// line.full).
    fn use_match(&mut self, m: &Match) -> Range<ByteIndex> {
        let area = &self.lines.byte_slice(m.start .. m.end);
        let area_rev: Vec<_> = area.iter().rev().copied().collect();
        // Guess what the leading context is. The actual code may be different than
        // the patch; it can also be longer or shorter. To accommodate that, set the
        // maximum cost to be quite high. An empty context is treated as a non-match.
        let m_c1 = if self.context1.len() > 0 {
            levenshtein::levenshtein_search_simd_with_opts(
                    &self.context1, area, (self.context1.len() * 2) as u32,
                    SearchType::Best, self.costs, true)
                .next()
        } else {
            None
        };
        self.context1_start = m_c1.as_ref().and_then(|c| if c.k == 0 {
            Some(m.start + c.start)
        } else {
            None
        });
        self.context1_end = m.start + m_c1.as_ref().map_or(0, |c| c.end);
        // Similarly, guess what the trailing context is.
        let m_c2 = if self.context2_rev.len() > 0 {
            levenshtein::levenshtein_search_simd_with_opts(
                    &self.context2_rev, &area_rev, (self.context2_rev.len() * 2) as u32,
                    SearchType::Best, self.costs, true)
                .next()
        } else {
            None
        };
        self.context2_start = ByteIndex::from(m.end) - m_c2.as_ref().map_or(0, |c| c.end);
        self.context2_end = m_c2.as_ref().and_then(|c| if c.k == 0 {
            Some(ByteIndex::from(m.end) - c.start)
        } else {
            None
        });
        // Pick the better match of the two context, i.e. the one with the lower k.
        // We'll use that context as the starting point for the content search.
        self.content_bottom = if m_c1.is_some() && m_c2.is_some() {
            m_c2.unwrap().k < m_c1.unwrap().k
        } else if m_c2.is_some() {
            true
        } else {
            false
        };
        self.used_area_start = m.start;
        self.used_area_end = m.end;
        m.start .. m.end
    }

    /// Return the point where new code would be inserted, assuming no code was deleted.
    /// You'll want to set the appropriate focus by calling use_match before.
    fn insert_point(&self) -> Range<ByteIndex> {
        if self.content_bottom {
            self.context2_start .. self.context2_start
        } else {
            self.context1_end .. self.context1_end
        }
    }

    /// Find the start and end points of the content and return them. The result is
    /// a range of byte pos to lines.full. You'll want to set the appropriate focus by calling
    /// use_match before.
    fn locate_content(&self) -> Range<ByteIndex> {
        if self.content.len() == 0 {
            // There are only additions in this hunk. Use just the context to determine
            // where the lines should be added. Align to the context that was a better
            // fit.
            return self.insert_point();
        }
        let (needle, area) = if self.content_bottom {
            // search from the bottom
            (
                &self.content_rev,
                &(
                    self.lines.byte_slice(self.used_area_start .. self.context2_start)
                        .iter().rev().copied().collect::<Vec<u8>>()
                )[..]
            )
        } else {
            // search from the top
            (
                &self.content,
                self.lines.byte_slice(self.context1_end .. self.used_area_end)
            )
        };
        // Search for the content. The maximum cost was determined experimentally.
        let m_c = levenshtein::levenshtein_search_simd_with_opts(
                needle, area, needle.len() as u32,
                SearchType::Best, self.costs, false)
            .next();
        if m_c.is_none() {
            // Not found. Assume that all of this is new code to be added.
            return self.insert_point();
        }
        let m_c = m_c.unwrap();
        if self.content_bottom {
            self.context2_start - m_c.end .. self.context2_start - m_c.start
        } else {
            self.context1_end + m_c.start .. self.context1_end + m_c.end
        }
    }

    /// Find the start and end points of the hunk including the context and return them.
    /// This requires `use_match` and `locate_content` called before. The result may be
    /// different to the match range; if downstream code includes additional code in its
    /// context, the context is extended to contain the full upstream context.
    // An example. Upstream patch:
    //     ctx1
    //     ctx2
    //    -old (several lines)
    //    +new
    //     ctx3
    //     ctx4
    // is applied to the following code:
    //    ctx1
    //    ctx2
    //    old (several lines)
    //    additional1
    //    additional2
    //    ctx3
    //    ctx4
    // The match will likely end at additional1 or additional2. However, we want to extend
    // the trailing context down to ctx4.
    fn locate_full(&self, content: Range<ByteIndex>, line_content: Range<LineIndex>)
        -> Range<ByteIndex>
    {
        let context1_start = if let Some(start) = self.context1_start {
            start
        } else {
            // No previous perfect match of the leading context. Find it. Consider only
            // MAX_CONTEXT_SEARCH lines.
            let start = self.lines.get_byte_index(
                line_content.start.saturating_sub(MAX_CONTEXT_SEARCH)
            );
            let area = &self.lines.byte_slice(start .. content.start);
            let area_rev: Vec<_> = area.iter().rev().copied().collect();
            // We're interested only in good matches of the original context. Use a low
            // threshold for k.
            let m = levenshtein::levenshtein_search_simd_with_opts(
                &self.context1_rev,
                &area_rev,
                (self.context1_rev.len() / 2) as u32,
                SearchType::Best,
                self.costs,
                false
            ).next();
            // If there's no match, use the default CONTEXT lines.
            match m {
                Some(m) => content.start - m.end,
                None => self.lines.get_byte_index(line_content.start.saturating_sub(CONTEXT))
            }
        };
        let context2_end = if let Some(end) = self.context2_end {
            end
        } else {
            let end = self.lines.get_byte_index(line_content.end + MAX_CONTEXT_SEARCH);
            let area = &self.lines.byte_slice(content.end .. end);
            let m = levenshtein::levenshtein_search_simd_with_opts(
                &self.context2,
                &area,
                (self.context2.len() / 2) as u32,
                SearchType::Best,
                self.costs,
                false
            ).next();
            match m {
                Some(m) => content.end + m.end,
                None => self.lines.get_byte_index(line_content.end + CONTEXT)
            }
        };
        context1_start .. context2_end
    }

    /// Check if the given lines are identical with the given hunk lines. Only removed and
    /// context lines from the hunk are considered.
    fn equal_lines(&self, range: Range<LineIndex>, hunk_range: Range<usize>) -> bool {
        let mut hunk_pos = hunk_range.start;
        for i in range.iter() {
            while hunk_pos < hunk_range.end && self.hunk.lines[hunk_pos].op == Op::Added {
                hunk_pos += 1;
            }
            if
                hunk_pos >= hunk_range.end ||
                self.lines[i] != self.hunk.lines[hunk_pos].content
            {
                return false;
            }
            hunk_pos += 1;
        }
        // Check that the hunk is full processed. If there are lines in the hunk left, this
        // is not equal.
        while hunk_pos < hunk_range.end && self.hunk.lines[hunk_pos].op == Op::Added {
            hunk_pos += 1;
        }
        hunk_pos >= hunk_range.end
    }

    /// Create a Replacement out of the currently used match. Requires `use_match` to be
    /// called before.
    fn create_replacement(&self) -> Replacement {
        let content = self.locate_content();
        let line_content = self.lines.calculate_line_range(content.clone());
        let full_range = self.locate_full(content.clone(), line_content.clone());
        let mut repl = Replacement::new(
            line_content,
            self.lines.calculate_line_range(full_range),
        );
        // Look for fuzz. This is done on lines, not on bytes, to ensure the potential extra
        // characters (which will be mostly spaces in practice) added by the bytes->line
        // conversion are taken into account, too.
        let hunk_content = self.hunk.get_noncontext_range();
        repl.fuzz_top = !self.equal_lines(
            repl.full_range.start .. repl.range.start,
            0 .. hunk_content.start,
        );
        repl.fuzz_bottom = !self.equal_lines(
            repl.range.end .. repl.full_range.end,
            hunk_content.end .. self.hunk.lines.len(),
        );
        repl.fuzz_content = !self.equal_lines(
            repl.range.start .. repl.range.end,
            hunk_content,
        );
        repl
    }

    /// Reverse the gravity. Useful for trying whether aligning to the other
    /// (worse) context gives less fuzz.
    fn try_other_gravity(&mut self) {
        // Note that when there was both no leading and no trailing context,
        // reversing the gravity will produce the same result. It's not worth
        // detecting that case, though. It will be rare.
        self.content_bottom = !self.content_bottom;
    }
}

enum ApplyCreateReplacementResult {
    NotAdded,
    Added { repl_pos: usize, repl: Replacement, area: Range<ByteIndex> },
}

impl Apply {
    pub fn new(mut lines: TypedLines) -> Apply {
        lines.annotate();
        Apply {
            lines,
            replacement: ReplacementTable::new(),
            unapplied: vec![],
            too_long: 0,
            missed: 0,
            split: 0,
        }
    }

    #[cfg(feature = "debug_annotation")]
    fn debug_annotation(&self, searcher: &HunkSearcher, m: &Match) {
        if searcher.hunk.is_split {
            return;
        }
        let line_range = self.lines.calculate_line_range(m.start .. m.end);
        let mut dest_tags = self.lines.tag_slice(line_range).unwrap().to_vec();
        dest_tags.dedup();
        if searcher.hunk.tags != dest_tags {
            let mut tags = Vec::new();
            println!("============ Hunk annotation ============");
            searcher.hunk.write_to(&mut std::io::stdout().lock()).unwrap();
            print!(">>> upstream:  ");
            for tag in searcher.hunk.tags.iter() {
                print!(" {}", tag.get_debug_value());
                if !tags.contains(tag) {
                    tags.push(*tag);
                }
            }
            println!();
            print!(">>> downstream:");
            for tag in dest_tags.iter() {
                print!(" {}", tag.get_debug_value());
                if !tags.contains(tag) {
                    tags.push(*tag);
                }
            }
            println!();
            tags.sort_by_key(|tag| tag.get_debug_value());
            for tag in tags {
                println!("{} {}", tag.get_debug_value(), &annotate::tag_symbol(tag).as_bstr());
            }
        }
    }

    /// Pops the first match from the `searcher`, creates a Replacement, adds it to the
    /// given `table` and returns `Added`. If the match cannot be added, returns `NotAdded`.
    /// Panics if there's no match available in the `searcher`.
    fn create_replacement(
        &self,
        searcher: &mut HunkSearcher,
        table: &ReplacementTable,
    ) -> ApplyCreateReplacementResult {
        let m = searcher.matches.pop().unwrap();
        let area = searcher.use_match(&m);
        let mut repl = searcher.create_replacement();
        if repl.fuzz_content {
            // The content did not match exactly. Try from the other context. It helps
            // with cases such as:
            //    ctx1
            //    (empty line)
            //    ctx2
            //   -(empty line)
            //   -line
            //    ctx3
            // applied to this code:
            //   ctx1
            //   (empty line)
            //   line
            //   ctx3_mangled
            // In this case, the empty line got eaten by the leading context and produces
            // a content fuzz. If we match from the trailing context (which is otherwise
            // a worse fit), the empty line becomes a part of the content.
            // If we get a content fuzz from both sides, keep the original one (which is
            // the one with the better context fit).
            searcher.try_other_gravity();
            let repl2 = searcher.create_replacement();
            if !repl2.fuzz_content {
                let _ = std::mem::replace(&mut repl, repl2);
            }
        }
        // Check that the place is not already occupied by a different hunk. If we're
        // working with a subhunk, be more relaxed and allow hunks to be applied one
        // right after another. The advantage is that patches like:
        //     ctx1
        //    -old1
        //    +new1
        //     ctx2
        //    -old2
        //    +new2
        //     ctx3
        // can be applied to this code:
        //     ctx1
        //     old1
        //     old2
        //     ctx3
        // (i.e. when the inner context is not present at all). The disadvantage is
        // that two different subhunks with a similar contexts that only insert some
        // code can be wrongly applied to the same place.
        if let Some(repl_pos) = table.insert_position(&repl, searcher.hunk.is_split) {
            #[cfg(feature = "debug_annotation")]
            self.debug_annotation(searcher, &m);
            return ApplyCreateReplacementResult::Added {
                repl_pos,
                repl,
                area,
            };
        };
        ApplyCreateReplacementResult::NotAdded
    }

    /// Attempts to split the placed hunk to subhunks. Updates `repl_pos` and `repl` and
    /// returns true if it made sense to split the hunk and the split succeeded. Otherwise,
    /// returns false and keeps `repo_pos` and `repl` unmodified.
    fn split_hunk(
        &self,
        hunk: &Hunk,
        repl_pos: &mut usize,
        repl: &mut Replacement,
        area: Range<ByteIndex>,
    ) -> bool {
        if !repl.fuzz_content || hunk.is_split || !hunk.has_inner_context() {
            return false;
        }
        let mut table = ReplacementTable::new();
        let mut subhunks = vec![];
        hunk.split(&mut subhunks);
        for subhunk in subhunks {
            let mut searcher = match HunkSearcher::new(subhunk, &self.lines, area.clone()) {
                Ok(searcher) => searcher,
                Err(_) => return false
            };
            let (subrepl_pos, subrepl) = loop {
                if searcher.matches.is_empty() {
                    return false;
                }
                match self.create_replacement(&mut searcher, &table) {
                    ApplyCreateReplacementResult::Added { repl_pos, repl, .. } =>
                        break (repl_pos, repl),
                    _ => continue
                }
            };
            table.insert(subrepl_pos, subrepl, searcher.hunk);
        }
        // Could be split. Update the replacement. However, the result may not be
        // insertable into the table self.replacement. We did check that the area
        // does not collide but the check happens on the content, not on the contexts.
        // But when we matched the subhunks, we were using the whole area, i.e.
        // including the contexts. Some subhunk may got applied to one of the
        // contexts. This may cause a new collision. This is an unlikely case; while
        // the perfect solution would be to recalculate the colliding hunk, this
        // would lead to a DFS, which is not an acceptable time complexity. Just
        // bail out and do not split the hunk in such case.
        let undo = repl.set_subreplacement(table);
        if let Some(new_repl_pos) = self.replacement.insert_position(repl, false) {
            *repl_pos = new_repl_pos;
            true
        } else {
            repl.undo(undo);
            false
        }
    }

    /// For each hunk of the file, find where it applies and add it to the replacement table.
    /// Hunks that did not apply are collected into the `unapplied` field. The internal
    /// statistics are updated accordingly. If a hunk contains inner context and applies with
    /// content fuzz or inner fuzz, an attempt is made to split it and apply it per subhunks.
    /// If it can be applied that way, it is recorded and becomes the way to apply the hunk.
    /// The stats and hunk data then reflect the aggregated subhunks instead of the hunk.
    pub fn add_hunks(&mut self, hunks: Vec<Hunk>) {
        // Construct a HunkSearcher for each hunk.
        let mut searchers = Vec::new();
        let full_area = 0.into() .. self.lines.byte_end();
        for hunk in hunks {
            let searcher = match HunkSearcher::new(hunk, &self.lines, full_area.clone()) {
                Err(hunk) => {
                    self.too_long += 1;
                    self.unapplied.push(hunk);
                    continue;
                }
                Ok(searcher) => searcher
            };
            searchers.push(searcher);
        }
        loop {
            // Move hunks with no match to unapplied.
            let prev_unapplied_len = self.unapplied.len();
            self.unapplied.extend(
                searchers
                    .extract_if(.., |searcher| searcher.matches.is_empty())
                    .map(|searcher| searcher.hunk)
            );
            self.missed += self.unapplied.len() - prev_unapplied_len;
            if searchers.is_empty() {
                break;
            }
            // Find the best match.
            let best = searchers.iter()
                .map(|s| &s.matches[0])
                .enumerate()
                .min_by_key(|&(_, m)| m)
                .unwrap()
                .0;
            let searcher = &mut searchers[best];
            // Add the best match to the replacement table.
            let (mut repl_pos, mut repl, area) =
                match self.create_replacement(searcher, &self.replacement) {
                    // Did not work out. Pick the next best match.
                    ApplyCreateReplacementResult::NotAdded => continue,
                    ApplyCreateReplacementResult::Added { repl_pos, repl, area } =>
                        (repl_pos, repl, area)
                };
            // Try to split the hunk if it makes sense.
            if self.split_hunk(&searcher.hunk, &mut repl_pos, &mut repl, area) {
                self.split += 1;
            }
            let searcher = searchers.remove(best);
            self.replacement.insert(repl_pos, repl, searcher.hunk);
        }
        self.unapplied.sort_unstable_by_key(|h| h.old.start);
    }

    /// Return the number of collected replacements.
    pub fn collected_hunks(&self) -> usize {
        self.replacement.len()
    }

    /// Apply everything.
    pub fn apply(mut self) -> Applied {
        let mut changes = vec![];
        while let Some(repl) = self.replacement.pop_front() {
            let full_range = repl.range_with_context();
            let fuzz = AppliedFuzz::from_replacement(&repl);
            let (new_lines, hunk) = match repl.data {
                ReplacementData::Single { additions, hunk } => {
                    let before = self.lines.line_slice(full_range.start .. repl.range.start);
                    let middle = additions.as_line_slice();
                    let after = self.lines.line_slice(repl.range.end .. full_range.end);
                    let mut new_lines = Lines::with_capacity(
                        before.byte_len() + middle.byte_len() + after.byte_len(),
                        before.line_len() + middle.line_len() + after.line_len()
                    );
                    new_lines.push_lines(before);
                    new_lines.push_lines(middle);
                    new_lines.push_lines(after);
                    (new_lines, hunk)
                }
                ReplacementData::Multi { mut subreplacement, full_hunk } => {
                    let mut new_lines = Lines::new();
                    let mut pos = full_range.start;
                    while let Some(subrepl) = subreplacement.pop_front() {
                        let additions = match subrepl.data {
                            ReplacementData::Single { additions, ..} => additions,
                            _ => panic!()
                        };
                        new_lines.push_lines(
                            self.lines.line_slice(pos .. subrepl.range.start)
                        );
                        new_lines.push_lines(additions.as_line_slice());
                        pos = subrepl.range.end;
                    }
                    new_lines.push_lines(self.lines.line_slice(pos .. full_range.end));
                    (new_lines, full_hunk)
                }
                _ => panic!()
            };
            changes.push(AppliedChange {
                range: full_range,
                new_lines: new_lines,
                hunk,
                fuzz,
            });
        }
        Applied {
            old_lines: self.lines,
            changes,
            unapplied: self.unapplied,
        }
    }
}

impl AppliedFuzz {
    fn from_replacement(repl: &Replacement) -> Self {
        if repl.fuzz_content {
            Self::Content
        } else if repl.fuzz_top || repl.fuzz_bottom || repl.fuzz_middle {
            Self::Context
        } else {
            Self::None
        }
    }
}

impl Extract {
    pub fn new(lines: TypedLines) -> Extract {
        Extract {
            apply: Apply::new(lines),
            target: vec![]
        }
    }

    fn add_resolved_hunk(&mut self, hunk: &Hunk) {
        let Range { start: ctx1_end, end: ctx2_start } = hunk.get_noncontext_range();
        let start = hunk.old.start + ctx1_end;
        let mut end = start;
        let mut additions = vec![];
        let mut i = ctx1_end;
        while i < ctx2_start {
            let hline = &hunk.lines[i];
            if hline.op == Op::Removed || hline.op == Op::Context {
                assert!(hline.content == &self.apply.lines[end]);
                end += 1;
            }
            if hline.op == Op::Added || hline.op == Op::Context {
                additions.extend_from_slice(&hline.content);
            }
            i += 1;
        }
        let data = ReplacementData::Extracted { additions };
        self.target.push(Replacement {
            range: start .. end,
            data,
            ..Default::default()
        });
    }

    pub fn add_resolved_hunks(&mut self, hunks: &[Hunk]) {
        for hunk in hunks.iter() {
            self.add_resolved_hunk(hunk);
        }
    }

    pub fn iter_pairs(&self) -> ReplacementPairIterator<'_> {
        ReplacementPairIterator {
            extract: self,
            index_applied: 0,
            index_extracted: 0,
            next_index_applied: None,
            extracted_skipped: 0,
            chained: 0,
        }
    }

    /// Write the pair of applied (repl_applied) and extracted (repl_extracted) replacement to
    /// files. The applied hunk is written to f_a; if the hunk was split during application,
    /// the original (full) hunk is written. The code before applying repl_applied is written
    /// to f_i. The code after applying repl_extracted is written to f_o.
    pub fn write_pair_to(
        &self,
        repl_applied: &Replacement,
        repl_extracted: &Replacement,
        f_a: &mut impl Write,
        f_i: &mut impl Write,
        f_o: &mut impl Write,
    ) -> std::io::Result<()> {
        // the patch (a)
        let hunk = match &repl_applied.data {
            ReplacementData::Single { hunk, .. } => hunk,
            ReplacementData::Multi { full_hunk, .. } => full_hunk,
            _ => panic!()
        };
        hunk.write_to(f_a)?;
        // the input (i)
        for i in repl_applied.range_with_context().iter() {
            f_i.write(&self.apply.lines[i])?;
        }
        // the output (o)
        self.format_lines(repl_applied, repl_extracted, |x| f_o.write(x).and(Ok(())))?;
        Ok(())
    }

    fn format_lines<F, E>(
        &self,
        repl_applied: &Replacement,
        repl_extracted: &Replacement,
        mut f: F,
    ) -> Result<(), E>
        where F: FnMut(&[u8]) -> Result<(), E>
    {
        let Range { start, end } = repl_applied.range_with_context();
        for i in (start .. (end + 1)).iter() {
            if i == repl_extracted.range.end {
                let additions = match repl_extracted.data {
                    ReplacementData::Extracted { ref additions, .. } => additions,
                    _ => panic!()
                };
                for line in additions.split_inclusive(|&c| c == b'\n') {
                    f(line)?;
                }
            }
            if i >= end {
                break;
            }
            if i >= repl_extracted.range.start && i < repl_extracted.range.end {
                continue;
            }
            f(&self.apply.lines[i])?;
        }
        Ok(())
    }
}

pub struct ReplacementPairIterator<'e> {
    extract: &'e Extract,
    index_applied: usize,
    index_extracted: usize,
    next_index_applied: Option<usize>,
    pub extracted_skipped: usize,
    pub chained: usize,
}

impl<'e> Iterator for ReplacementPairIterator<'e> {
    type Item = (&'e Replacement, Option<&'e Replacement>);

    fn next(&mut self) -> Option<Self::Item> {
        loop {
            if
                self.index_extracted == self.extract.target.len() &&
                self.index_applied < self.extract.apply.replacement.len()
            {
                self.index_applied += 1;
                return Some((&self.extract.apply.replacement[self.index_applied - 1], None));
            }
            if self.index_applied == self.extract.apply.replacement.len() {
                self.extracted_skipped += self.extract.target.len() - self.index_extracted;
                self.index_extracted = self.extract.target.len();
                return None;
            }

            let repl_applied = &self.extract.apply.replacement[self.index_applied];
            let repl_extracted = &self.extract.target[self.index_extracted];

            if let Some(next_index_applied) = self.next_index_applied {
                if self.index_applied < next_index_applied {
                    self.index_applied += 1;
                    return Some((repl_applied, None));
                }
                self.next_index_applied = None;
            }

            if repl_applied.range.start > repl_extracted.range.end {
                self.index_extracted += 1;
                self.extracted_skipped += 1;
                continue;
            }
            if repl_extracted.range.start > repl_applied.range.end {
                self.index_applied += 1;
                return Some((repl_applied, None));
            }
            // detect a chain of overlaps
            let mut end_applied = repl_applied.range.end;
            let mut end_extracted = repl_extracted.range.end;
            let mut chained_applied = self.index_applied;
            let mut chained_extracted = self.index_extracted;
            loop {
                if
                    chained_extracted + 1 < self.extract.target.len() &&
                    self.extract.target[chained_extracted + 1].range.start < end_applied
                {
                    end_extracted = self.extract.target[chained_extracted + 1].range.end;
                    chained_extracted += 1;
                    continue;
                }
                if
                    chained_applied + 1 < self.extract.apply.replacement.len() &&
                    self.extract.apply.replacement[chained_applied + 1].range.start <
                        end_extracted
                {
                    end_applied =
                        self.extract.apply.replacement[chained_applied + 1].range.end;
                    chained_applied += 1;
                    continue;
                }
                break;
            }
            if
                chained_applied > self.index_applied ||
                chained_extracted > self.index_extracted
            {
                // chain detected
                self.chained += 1;
                self.next_index_applied = Some(chained_applied + 1);
                self.index_extracted = chained_extracted + 1;
                continue;
            }

            self.index_applied += 1;
            self.index_extracted += 1;
            return Some((repl_applied, Some(repl_extracted)));
        }
    }
}
