use bstr::ByteSlice;
use crate::common::{diff::Line, diff::Op};
use crate::common::lines::{LineIndex, LineIndexRange, LinesSlice};
use std::{ops::Range, rc::Rc};
use super::format::{Formatted, Style};

pub struct DiffLineHalf<T: From<Op> + PartialEq> {
    pub(crate) op: T,
    pub(crate) op_highlighted: bool,
    pub(crate) content: Formatted,
}

/// Single line on one side of the interdiff (i.e. half the diff line). `S` is arbitrary
/// type stored in the status line. The interpretation of the status line is completely in the
/// hands of the caller; this module does not use it. `T` is the type of the "operation" flag,
/// i.e. the plus, minus or space character at the start of the half line.
pub enum OptionDiffLineHalf<S, T: From<Op> + PartialEq> {
    /// Render the half line as empty space.
    Empty,
    /// The half line is missing but we would expect it to be present. Render an appropriate
    /// indicator.
    Missing,
    /// The half line contains status info (and action buttons etc.).
    Status(S),
    /// The half line is present.
    Present(DiffLineHalf<T>),
}

pub struct DiffLine<S, T: From<Op> + PartialEq> {
    pub backport: OptionDiffLineHalf<S, T>,
    pub original: OptionDiffLineHalf<S, T>,
}

/// Helper structure to hold lines converted to strings during interdiffing.
pub(crate) struct InterdiffedLine {
    pub(crate) op: Op,
    pub(crate) content: Vec<char>,
}

/// Helper structure to compute interdiff.
pub(crate) struct Interdiff<S, T: From<Op> + PartialEq> {
    left: Vec<InterdiffedLine>,
    right: Vec<InterdiffedLine>,
    out_left: Vec<OptionDiffLineHalf<S, T>>,
    out_right: Vec<OptionDiffLineHalf<S, T>>,
}

/// A builder to go from imara_diff::Diff::hunks() to Vec<InterdiffedLine>.
pub(crate) struct InterdiffedLinesBuilder<'a> {
    before: LinesSlice<'a>,
    after: LinesSlice<'a>,
    result: Vec<InterdiffedLine>,
    pos: LineIndex,
}

/// An imara_diff::TokenSource to get chars from Vec<InterdiffedLine>.
#[derive(Clone)]
struct InterdiffedLinesCharSource<'a> {
    lines: &'a [InterdiffedLine],
    line_pos: usize,
    char_pos: usize,
}

/// A builder to go from char diff to Vec<(left_index, right_index, len)> describing matching
/// substrings.
struct MatchingSubstringBuilder<'a> {
    input: &'a imara_diff::InternedInput<char>,
    result: Vec<(usize, usize, usize)>,
    left_index: usize,
    right_index: usize,
}

impl<S, T: From<Op> + PartialEq> OptionDiffLineHalf<S, T> {
    fn unwrap_mut(&mut self) -> &mut DiffLineHalf<T> {
        if let Self::Present(data) = self {
            data
        } else {
            panic!();
        }
    }

    pub(crate) fn get(&self) -> Option<&DiffLineHalf<T>> {
        if let Self::Present(data) = self {
            Some(&data)
        } else {
            None
        }
    }

    pub(crate) fn op(&self) -> Option<&T> {
        Some(&self.get()?.op)
    }
}

impl<S, T: From<Op> + PartialEq> DiffLine<S, T> {
    pub(crate) fn new_backport(op: T, content: String) -> Self {
        Self {
            backport: OptionDiffLineHalf::Present(DiffLineHalf {
                op,
                op_highlighted: false,
                content: Formatted::from_string(content),
            }),
            original: OptionDiffLineHalf::Empty,
        }
    }

    pub(crate) fn new_header(status: S, op: T, content: String) -> Self {
        Self {
            backport: OptionDiffLineHalf::Status(status),
            original: OptionDiffLineHalf::Present(DiffLineHalf {
                op,
                op_highlighted: false,
                content: Formatted::from_string(content),
            }),
        }
    }

    pub(crate) fn new_original(op: T, content: String) -> Self {
        Self {
            backport: OptionDiffLineHalf::Missing,
            original: OptionDiffLineHalf::Present(DiffLineHalf {
                op,
                op_highlighted: true,
                content: Formatted::from_string_style(content, Style::Highlight),
            }),
        }
    }

    pub(crate) fn is_header_with_status<F>(&self, cmp: F) -> bool
        where F: FnOnce(&S) -> bool
    {
        matches!(&self.backport, OptionDiffLineHalf::Status(status) if cmp(status))
    }
}

impl From<&Line> for InterdiffedLine {
    fn from(value: &Line) -> Self {
        Self {
            op: value.op,
            content: value.content.to_str_lossy().chars().collect(),
        }
    }
}

impl<S, T: From<Op> + PartialEq> Interdiff<S, T> {
    pub(crate) fn new(left: Vec<InterdiffedLine>, right: Vec<InterdiffedLine>)
        -> Self
    {
        Self {
            left,
            right,
            out_left: vec![],
            out_right: vec![],
        }
    }

    fn add_partial_line(&mut self, idx: usize, highlight: bool, highlight_op: bool,
                        y: usize, x: usize, count: usize) {
        if count == 0 {
            return;
        }
        let lines = if idx == 0 { &self.left } else { &self.right };
        let line = &lines[y];
        let out = if idx == 0 { &mut self.out_left } else { &mut self.out_right };
        let out_len = out.len();
        let dlh = if out_len > 0 &&
            matches!(
                &out[out_len - 1],
                OptionDiffLineHalf::Present(dlh) if !dlh.content.ends_with_nl()
            )
        {
            // append to the last output line
            out[out_len - 1].unwrap_mut()
        } else {
            // create a new output line
            let dlh = OptionDiffLineHalf::Present(DiffLineHalf {
                op: line.op.into(),
                op_highlighted: false,
                content: Formatted::default(),
            });
            out.push(dlh);
            out[out_len].unwrap_mut()
        };
        if highlight {
            dlh.content.push_style(&line.content[x .. x + count], Style::Highlight);
        } else {
            dlh.content.push(&line.content[x .. x + count]);
        }
        if highlight_op {
            dlh.op_highlighted = true;
        }
    }

    fn highlight_op(&self, pos_line: &[usize; 2]) -> bool {
        let left_line = match self.left.get(pos_line[0]) {
            Some(line) => line,
            None => return false,
        };
        let right_line = match self.right.get(pos_line[1]) {
            Some(line) => line,
            None => return false,
        };
        left_line.op != right_line.op
    }

    fn output_completed_line(&mut self, idx: usize, highlight: bool, highlight_op: bool,
            count: usize, pos_line: &mut [usize; 2], offs_line: &mut [usize; 2])
            -> (bool, usize) {
        if count == 0 {
                return (false, 0);
        }
        let remaining = {
            let lines = if idx == 0 { &self.left } else { &self.right };
            lines[pos_line[idx]].content.len() - offs_line[idx]
        };
        if count < remaining {
            return (false, count);
        }
        self.add_partial_line(idx, highlight, highlight_op, pos_line[idx], offs_line[idx],
                              remaining);
        let count = count - remaining;
        pos_line[idx] += 1;
        offs_line[idx] = 0;
        (true, count)
    }

    fn output_incomplete_line(&mut self, idx: usize, highlight: bool, highlight_op: bool,
            count: usize, pos_line: &[usize; 2], offs_line: &mut [usize; 2])
            -> usize {
        if count == 0 {
            return 0;
        }
        self.add_partial_line(idx, highlight, highlight_op, pos_line[idx], offs_line[idx],
                              count);
        offs_line[idx] += count;
        return 0;
    }

    fn sync(&mut self) {
        while self.out_left.len() < self.out_right.len() {
            self.out_left.push(OptionDiffLineHalf::Missing);
        }
        while self.out_left.len() > self.out_right.len() {
            self.out_right.push(OptionDiffLineHalf::Missing);
        }
    }

    fn cond_sync(&mut self, offs_line: &[usize; 2]) {
        // If we stayed in the middle of the left or right line, we'll
        // have to wait with the sync. We can't insert a break there.
        if offs_line[0] == 0 && offs_line[1] == 0 {
            self.sync();
        }
    }

    pub(crate) fn generate(mut self) -> Vec<Rc<DiffLine<S, T>>> {
        // Get the list of tuples describing matching substrings. Each tuple is
        // (left_index, right_index, len).
        let input = imara_diff::InternedInput::new(
            InterdiffedLinesCharSource::new(&self.left),
            InterdiffedLinesCharSource::new(&self.right),
        );
        let mut diff = imara_diff::Diff::compute(imara_diff::Algorithm::Histogram, &input);
        diff.postprocess_no_heuristic(&input);
        let builder = MatchingSubstringBuilder::new(&input);
        let eqlist = builder.build(diff.hunks());
        // The above emulates Python's difflib.SequenceMatcher.get_matching_blocks.

        // index of the currently processed line on the left and right side
        let mut pos_line = [0, 0];
        // current offsets in the currently processed line (indicated by pos_line)
        let mut offs_line = [0, 0];
        // index of the currently processed item in eqlist
        let mut cur_eq_idx = 0;
        // the previously processed eqlist item
        let mut last_eq = (0, 0, 0);
        // numbers of characters that need to be consumed
        let (mut x_l, mut x_r) = (0, 0);

        loop {
            let (n_l, n_r, n) = eqlist[cur_eq_idx];
            if n < 5 && cur_eq_idx < eqlist.len() - 1 {
                cur_eq_idx += 1;
                continue;
            }
            x_l += n_l - (last_eq.0 + last_eq.2);
            x_r += n_r - (last_eq.1 + last_eq.2);

            let (mut repeat_l, mut repeat_r) = (true, true);
            while repeat_l || repeat_r {
                let hl_op = self.highlight_op(&pos_line);
                (repeat_l, x_l) = self.output_completed_line(0, true, hl_op, x_l,
                                                             &mut pos_line, &mut offs_line);
                (repeat_r, x_r) = self.output_completed_line(1, true, hl_op, x_r,
                                                             &mut pos_line, &mut offs_line);
            }
            // sync after outputting all differing lines
            self.cond_sync(&offs_line);

            let hl_op = self.highlight_op(&pos_line);
            x_l = self.output_incomplete_line(0, true, hl_op, x_l, &pos_line, &mut offs_line);
            x_r = self.output_incomplete_line(1, true, hl_op, x_r, &pos_line, &mut offs_line);

            if n == 0 {
                break;
            }

            x_l += n;
            x_r += n;
            let (mut repeat_l, mut repeat_r) = (true, true);
            while repeat_l || repeat_r {
                let hl_op = self.highlight_op(&pos_line);
                (repeat_l, x_l) = self.output_completed_line(0, false, hl_op, x_l,
                                                             &mut pos_line, &mut offs_line);
                (repeat_r, x_r) = self.output_completed_line(1, false, hl_op, x_r,
                                                             &mut pos_line, &mut offs_line);
                // Sync after outputting the first matching line. This is
                // necessary if we were not able to sync earlier.
                self.cond_sync(&offs_line);
            }

            let hl_op = self.highlight_op(&pos_line);
            x_l = self.output_incomplete_line(0, false, hl_op, x_l, &pos_line, &mut offs_line);
            x_r = self.output_incomplete_line(1, false, hl_op, x_r, &pos_line, &mut offs_line);

            last_eq = eqlist[cur_eq_idx];
            cur_eq_idx += 1;
        }

        self.sync();
        self.out_left.into_iter().zip(self.out_right)
            .map(|(backport, original)| Rc::new(DiffLine { backport, original }))
            .collect()
    }

    /// Add the given status header to the backport side. The upstream side will contain
    /// the given text, which must be terminated by newline.
    pub(crate) fn add_header(&mut self, status: S, op: T, header: String) {
        self.sync();
        self.out_left.push(OptionDiffLineHalf::Status(status));
        self.out_right.push(OptionDiffLineHalf::Present(DiffLineHalf {
            op,
            op_highlighted: false,
            content: Formatted::from_string(header),
        }));
    }
}

impl<'a> InterdiffedLinesBuilder<'a> {
    pub(crate) fn new(before: LinesSlice<'a>, after: LinesSlice<'a>) -> Self {
        Self {
            before,
            after,
            result: vec![],
            pos: 0.into(),
        }
    }

    fn push_context(&mut self, until: LineIndex) {
        for i in (self.pos .. until).iter() {
            self.result.push(InterdiffedLine {
                op: Op::Context,
                content: self.before[i].to_str_lossy().chars().collect(),
            });
        }
    }

    fn process_change(&mut self, before: Range<LineIndex>, after: Range<LineIndex>) {
        self.push_context(before.start);
        for i in before.iter() {
            self.result.push(InterdiffedLine {
                op: Op::Removed,
                content: self.before[i].to_str_lossy().chars().collect(),
            });
        }
        for i in after.iter() {
            self.result.push(InterdiffedLine {
                op: Op::Added,
                content: self.after[i].to_str_lossy().chars().collect(),
            });
        }
        self.pos = before.end;
    }

    pub(crate) fn build(mut self, iter: imara_diff::HunkIter) -> Vec<InterdiffedLine> {
        for h in iter {
            self.process_change(
                Range::<LineIndex>::from_u32(h.before),
                Range::<LineIndex>::from_u32(h.after),
            );
        }
        self.push_context(self.before.line_end());
        self.result
    }
}

impl<'a> InterdiffedLinesCharSource<'a> {
    fn new(lines: &'a [InterdiffedLine]) -> Self {
        Self {
            lines,
            line_pos: 0,
            char_pos: 0,
        }
    }
}

impl<'a> Iterator for InterdiffedLinesCharSource<'a> {
    type Item = char;

    fn next(&mut self) -> Option<Self::Item> {
        loop {
            if self.line_pos >= self.lines.len() {
                return None;
            } else if self.char_pos < self.lines[self.line_pos].content.len() {
                let result = self.lines[self.line_pos].content[self.char_pos];
                self.char_pos += 1;
                return Some(result);
            } else {
                self.char_pos = 0;
                self.line_pos += 1;
            }
        }
    }
}

impl<'a> imara_diff::TokenSource for InterdiffedLinesCharSource<'a> {
    type Token = char;
    type Tokenizer = Self;

    fn tokenize(&self) -> Self::Tokenizer {
        self.clone()
    }

    fn estimate_tokens(&self) -> u32 {
        self.lines.iter().map(|line| line.content.len())
            .sum::<usize>()
            .min(u32::MAX as usize)
            as u32
    }
}

impl<'a> MatchingSubstringBuilder<'a> {
    fn new(input: &'a imara_diff::InternedInput<char>) -> Self {
        Self {
            input,
            result: vec![],
            left_index: 0,
            right_index: 0,
        }
    }

    fn push_result(&mut self, left_until: usize) -> usize {
        let len = left_until - self.left_index;
        self.result.push((self.left_index, self.right_index, len));
        len
    }

    fn process_change(&mut self, before: Range<u32>, after: Range<u32>) {
        self.push_result(before.start as usize);
        self.left_index = before.end as usize;
        self.right_index = after.end as usize;
    }

    fn build(mut self, iter: imara_diff::HunkIter) -> Vec<(usize, usize, usize)> {
        for h in iter {
            self.process_change(h.before, h.after);
        }
        if self.push_result(self.input.before.len()) > 0 {
            // Push a dummy entry (left_len, right_len, 0).
            self.result.push((self.input.before.len(), self.input.after.len(), 0));
        }
        self.result
    }
}
