use bstr::ByteSlice;
use std::{cell::Cell, io, ops::Range, slice};
use super::{annotate, io::WriteTo};
use super::lines::{LineIndex, LineIndexRange, Lines, TypedLines};

#[derive(Clone, Debug, Default)]
pub struct Hunk {
    pub old: Range<LineIndex>,
    pub new: Range<LineIndex>,
    pub lines: Vec<Line>,
    pub(crate) tags: Vec<annotate::Tag>,
    top_overlap: usize,
    bottom_overlap: usize,
    pub(crate) is_split: bool,
    top_context: Cell<Option<usize>>,
    bottom_context: Cell<Option<usize>>,
    inner_context: Cell<Option<bool>>,
}

#[derive(Clone, Copy, Debug, PartialEq)]
pub enum Op {
    Added,
    Removed,
    Context,
}

#[derive(Clone, derive_more::Debug)]
pub struct Line {
    pub op: Op,
    #[debug("{:?}", content.as_bstr())]
    pub content: Vec<u8>,
}

pub struct HunkLinesByteIterator<'a> {
    lines: slice::Iter<'a, Line>,
    current: slice::Iter<'a, u8>,
}

/// Compares two line buffers and returns a vec of diff hunks. Panics if the lines in `a`
/// are not annotated.
pub(crate) fn diff_lines(a: &TypedLines, b: &Lines) -> Vec<Hunk> {
    let input = imara_diff::InternedInput::new(a.iter(), b.iter());
    let mut diff = imara_diff::Diff::compute(imara_diff::Algorithm::Histogram, &input);
    diff.postprocess_lines(&input);
    let builder = HunksBuilder::new(a, b);
    builder.build(diff.hunks())
}

impl Hunk {
    fn new(old_start: LineIndex, new_start: LineIndex) -> Hunk {
        Hunk {
            old: old_start .. old_start,
            new: new_start .. new_start,
            ..Default::default()
        }
    }

    pub fn format_header(&self) -> String {
        let tag_opt = self.tags.get(0);
        let (op, symbol) = if matches!(tag_opt, Some(&tag) if tag != annotate::Tag::unknown()) {
            (" ", annotate::tag_symbol(*tag_opt.unwrap()))
        } else {
            ("", Vec::new())
        };
        format!(
            "@@ -{},{} +{},{} @@{}{}\n",
            self.old.start + 1,
            self.old.end - self.old.start,
            self.new.start + 1,
            self.new.end - self.new.start,
            op,
            symbol.as_bstr(),
        )
    }

    pub(crate) fn format_filter_noncontext<P>(&self, predicate: P) -> Vec<u8>
        where P: Fn(&&Line) -> bool
    {
        self.lines[self.get_noncontext_range()].iter()
            .filter(predicate)
            .flatten()
            .collect()
    }

    pub(crate) fn to_lines_filter_noncontext<P>(&self, predicate: P) -> Lines
        where P: Fn(&&Line) -> bool
    {
        self.lines[self.get_noncontext_range()].iter()
            .filter(predicate)
            .flatten()
            .collect::<Vec<u8>>()
            .into()
    }

    pub(crate) fn get_noncontext_range(&self) -> Range<usize> {
        let start = self.top_context.get().unwrap_or_else(|| {
            let x = self.lines.iter().position(|line| line.op != Op::Context).unwrap();
            self.top_context.set(Some(x));
            x
        });
        let end = self.bottom_context.get().unwrap_or_else(|| {
            let x = self.lines.iter().rposition(|line| line.op != Op::Context).unwrap() + 1;
            self.bottom_context.set(Some(x));
            x
        });
        start .. end
    }

    /// Returns true if the hunk has inner context, i.e. context lines between
    /// deletions/insertions. Such hunk can be split into subhunks. The result is cached.
    pub(crate) fn has_inner_context(&self) -> bool {
        self.inner_context.get().unwrap_or_else(|| {
            let x = self.lines[self.get_noncontext_range()].iter()
                .position(|line| line.op == Op::Context).is_some();
            self.inner_context.set(Some(x));
            x
        })
    }

    pub(crate) fn get_contexts(&self) -> (Vec<u8>, Vec<u8>) {
        let range = self.get_noncontext_range();
        (
            self.lines[0 .. range.start].iter()
                .flatten()
                .collect(),
            self.lines[range.end .. self.lines.len()].iter()
                .flatten()
                .collect(),
        )
    }

    pub fn iter_lines_by_bytes(&self) -> HunkLinesByteIterator<'_> {
        HunkLinesByteIterator {
            lines: self.lines.iter(),
            current: [].iter(),
        }
    }

    fn push_line(&mut self, line: Line) {
        if line.op != Op::Added {
            self.old.end += 1;
        }
        if line.op != Op::Removed {
            self.new.end += 1;
        }
        self.lines.push(line);
        self.top_context.set(None);
        self.bottom_context.set(None);
    }

    /// Splits the hunk into subhunks by inner context. The subhunks are pushed into the
    /// passed target. The subhunks have their context reconstructed (albeit the tailing
    /// context is currently reconstructed only across a single following subhunk) and
    /// their patch positions are properly calculated.
    /// Note the hunk may be "split" into a single subhunk if it does not
    /// have an inner context.
    pub(crate) fn split(&self, target: &mut Vec<Hunk>) {
        let mut old_offset = self.old.start;
        let mut new_offset = self.new.start;
        let mut context = LineWindow::<3>::new();
        let mut line_iter = self.lines.iter();
        let mut item = line_iter.next();
        // the initial context
        loop {
            match item {
                Some(line) if line.op == Op::Context => {
                    context.push(&line.content, old_offset, new_offset, true);
                    old_offset += 1;
                    new_offset += 1;
                }
                _ => break
            }
            item = line_iter.next();
        }
        // the rest of the hunk
        while item.is_some() {
            let mut hunk = context.to_hunk();
            // changes (non-context)
            loop {
                match item {
                    Some(line) if line.op == Op::Added => {
                        new_offset += 1;
                        hunk.push_line(line.clone());
                    }
                    Some(line) if line.op == Op::Removed => {
                        context.push(&line.content, old_offset, new_offset, false);
                        old_offset += 1;
                        hunk.push_line(line.clone());
                    }
                    _ => break
                }
                item = line_iter.next();
            }
            // context
            let mut cnt = 0;
            loop {
                match item {
                    Some(line) if line.op == Op::Context => {
                        context.push(&line.content, old_offset, new_offset, cnt >= 3);
                        old_offset += 1;
                        new_offset += 1;
                        if cnt < 3 {
                            hunk.push_line(line.clone());
                            cnt += 1;
                        }
                    }
                    _ => break
                }
                item = line_iter.next();
            }
            if let Some(prev_hunk) = context.prev_hunk.take() {
                target.push(prev_hunk);
            }
            if item.is_some() && cnt < 3 {
                context.set_prev_hunk(hunk, cnt);
                continue;
            }
            target.push(hunk);
        }
    }
}

impl<'a> Iterator for HunkLinesByteIterator<'a> {
    type Item = u8;

    fn next(&mut self) -> Option<u8> {
        if let Some(&byte) = self.current.next() {
            return Some(byte);
        }
        let line = self.lines.next()?;
        self.current = line.content.iter();
        Some(match line.op {
            Op::Added => b'+',
            Op::Removed => b'-',
            Op::Context => b' ',
        })
    }
}

impl WriteTo for Hunk {
    fn write_to(&self, f: &mut impl io::Write) -> io::Result<()> {
        f.write(&self.format_header().as_bytes())?;
        for line in self.lines.iter() {
            f.write(match line.op {
                Op::Added => b"+",
                Op::Removed => b"-",
                Op::Context => b" ",
            })?;
            f.write(&line.content)?;
        }
        Ok(())
    }
}

impl<'a> IntoIterator for &'a Line {
    type Item = u8;
    type IntoIter = std::iter::Copied<slice::Iter<'a, u8>>;

    fn into_iter(self) -> Self::IntoIter {
        self.content.iter().copied()
    }
}

struct LineWindow<'a, const N: usize> {
    content: [&'a [u8]; N],
    offsets: [(LineIndex, LineIndex); N],
    owned: [bool; N],
    length: usize,
    prev_hunk: Option<Hunk>,
    prev_needed: usize,
}

impl<'a, const N: usize> LineWindow<'a, N> {
    fn new() -> LineWindow<'a, N> {
        LineWindow {
            content: [b""; N],
            offsets: [(0.into(), 0.into()); N],
            owned: [false; N],
            length: 0,
            prev_hunk: None,
            prev_needed: 0,
        }
    }

    fn push(
        &mut self,
        content: &'a [u8],
        old_offset: LineIndex,
        new_offset: LineIndex,
        owned: bool,
    ) {
        if self.length == N {
            for i in 0..N - 1 {
                self.content[i] = self.content[i + 1];
                self.offsets[i] = self.offsets[i + 1];
                self.owned[i] = self.owned[i + 1];
            }
        } else {
            self.length += 1;
        }
        self.content[self.length - 1] = content;
        self.offsets[self.length - 1] = (old_offset, new_offset);
        self.owned[self.length - 1] = owned;

        if self.prev_hunk.is_some() && self.prev_needed > 0 {
            let hunk = self.prev_hunk.as_mut().unwrap();
            hunk.push_line(Line {
                op: Op::Context,
                content: content.to_vec(),
            });
            hunk.bottom_overlap += 1;
            self.prev_needed -= 1;
        }
    }

    fn set_prev_hunk(&mut self, hunk: Hunk, context_size: usize) {
        assert!(self.prev_hunk.is_none());
        self.prev_hunk = Some(hunk);
        self.prev_needed = N.saturating_sub(context_size);
    }

    fn to_hunk(&self) -> Hunk {
        // works also for self.length == 0, since offsets is initialized to (0, 0)
        let mut hunk = Hunk::new(self.offsets[0].0, self.offsets[0].1);
        hunk.is_split = true;
        for i in 0..self.length {
            hunk.push_line(Line {
                op: Op::Context,
                content: self.content[i].to_vec(),
            });
            if !self.owned[i] {
                hunk.top_overlap = i + 1;
            }
        }
        hunk
    }
}

pub(crate) const CONTEXT: usize = 3;

struct HunksBuilder<'a> {
    before: &'a TypedLines,
    after: &'a Lines,
    result: Vec<Hunk>,
    /// Currently processed hunk.
    current: Option<Hunk>,
    /// Number of used lines (imara_diff tokens) on the left (before) side.
    used: LineIndex,
}

impl<'a> HunksBuilder<'a> {
    fn new(before: &'a TypedLines, after: &'a Lines) -> Self {
        Self {
            before,
            after,
            result: Vec::new(),
            current: None,
            used: 0.into(),
        }
    }

    fn top_context_size(&self, before_start: LineIndex, limit_to: usize) -> usize {
        ((before_start - self.used) as usize).min(limit_to)
    }

    /// Add the given range of lines from either the "before" side (if `op` is `Removed`
    /// or `Context`) or the "after" side (if `op` is `Added`). Add the annotation tags from
    /// the "before" side. `self.old`, `self.new` and `self.used` are updated automatically.
    fn add_lines_to_current(&mut self, op: Op, range: Range<LineIndex>) {
        if range.is_empty() {
            return;
        }
        let current = self.current.as_mut().unwrap();
        let from_before = op != Op::Added;
        let removed = op == Op::Removed;
        let source = if from_before { self.before } else { self.after };
        current.lines.extend(
            range.iter()
                .map(|index| Line {
                    op,
                    content: source[index].to_owned(),
                })
        );
        if from_before {
            annotate::extend_dedup(
                &mut current.tags,
                self.before.tag_slice(range.clone())
                    .unwrap()
                    .iter()
                    .map(|tag| tag.set_important(removed))
            );
        }
        match op {
            Op::Added => current.new.end += range.len(),
            Op::Removed => current.old.end += range.len(),
            Op::Context => {
                current.old.end += range.len();
                current.new.end += range.len();
            }
        }
        if from_before {
            assert!(self.used <= range.start);
            self.used = range.end;
        }
    }
    
    /// Start a new current hunk and add the leading context.
    fn start_current(&mut self, before_start: LineIndex, after_start: LineIndex) {
        assert!(self.current.is_none());
        let context_size = self.top_context_size(before_start, CONTEXT);
        self.current = Some(Hunk::new(
            before_start - context_size,
            after_start - context_size,
        ));
        self.add_lines_to_current(Op::Context, before_start - context_size .. before_start);
    }

    /// Move the current hunk to the list of finished hunks.
    fn ship_current(&mut self) {
        let context_size = ((self.before.line_end() - self.used) as usize).min(CONTEXT);
        self.add_lines_to_current(Op::Context, self.used .. self.used + context_size);
        // Keep only one tag from each consecutive subsequence of the same tags.
        self.result.push(self.current.take().unwrap());
    }

    fn process_change(&mut self, before: Range<LineIndex>, after: Range<LineIndex>) {
        if self.current.is_some() {
            let context_size = self.top_context_size(before.start, 2 * CONTEXT + 1);
            if context_size == 2 * CONTEXT + 1 {
                // There is enough context lines between the previous change and this change
                // to form two separate hunks.
                self.ship_current();
            } else {
                self.add_lines_to_current(
                    Op::Context,
                    before.start - context_size .. before.start,
                );
            }
        }
        if self.current.is_none() {
            self.start_current(before.start, after.start);
        }
        self.add_lines_to_current(Op::Removed, before);
        self.add_lines_to_current(Op::Added, after);
    }

    fn build(mut self, iter: imara_diff::HunkIter) -> Vec<Hunk> {
        for h in iter {
            self.process_change(
                Range::<LineIndex>::from_u32(h.before),
                Range::<LineIndex>::from_u32(h.after),
            );
        }
        if self.current.is_some() {
            self.ship_current();
        }
        self.result
    }
}
