use bstr::{BString, ByteSlice, ByteVec};
use imara_diff::{intern, Sink};
use intern::{InternedInput, Interner, Token};
use std::fmt::Write;
use std::{hash::Hash, ops::Range};

use super::{ConsumeBinaryHunk, ConsumeBinaryHunkDelegate, ConsumeHunk, ContextSize, DiffLineKind, HunkHeader};

/// A [`Sink`] that creates a unified diff. It can be used to create a textual diff in the
/// format typically output by `git` or `gnu-diff` if the `-u` option is used.
pub struct UnifiedDiff<'a, T, D>
where
    T: Hash + Eq + AsRef<[u8]>,
    D: ConsumeHunk,
{
    before: &'a [Token],
    after: &'a [Token],
    interner: &'a Interner<T>,

    /// The 0-based start position in the 'before' tokens for the accumulated hunk for display in the header.
    before_hunk_start: u32,
    /// The size of the accumulated 'before' hunk in lines for display in the header.
    before_hunk_len: u32,
    /// The 0-based start position in the 'after' tokens for the accumulated hunk for display in the header.
    after_hunk_start: u32,
    /// The size of the accumulated 'after' hunk in lines.
    after_hunk_len: u32,
    // An index into `before` and the context line to print next,
    // or `None` if this value was never computed to be the correct starting point for an accumulated hunk.
    ctx_pos: Option<u32>,

    /// Symmetrical context before and after the changed hunk.
    ctx_size: u32,

    buffer: Vec<(DiffLineKind, &'a [u8])>,

    delegate: D,

    err: Option<std::io::Error>,
}

impl<'a, T, D> UnifiedDiff<'a, T, D>
where
    T: Hash + Eq + AsRef<[u8]>,
    D: ConsumeHunk,
{
    /// Create a new instance to create a unified diff using the lines in `input`,
    /// which also must be used when running the diff algorithm.
    /// `context_size` is the amount of lines around each hunk which will be passed
    /// to `consume_hunk`.
    ///
    /// `consume_hunk` is called for each hunk with all the information required to create a
    /// unified diff.
    pub fn new(input: &'a InternedInput<T>, consume_hunk: D, context_size: ContextSize) -> Self {
        Self {
            interner: &input.interner,
            before: &input.before,
            after: &input.after,

            before_hunk_start: 0,
            before_hunk_len: 0,
            after_hunk_len: 0,
            after_hunk_start: 0,
            ctx_pos: None,

            ctx_size: context_size.symmetrical,

            buffer: Vec::with_capacity(8),
            delegate: consume_hunk,

            err: None,
        }
    }

    fn print_tokens(&mut self, tokens: &[Token], line_type: DiffLineKind) {
        for &token in tokens {
            let content = self.interner[token].as_ref();
            self.buffer.push((line_type, content));
        }
    }

    fn flush_accumulated_hunk(&mut self) -> std::io::Result<()> {
        if self.nothing_to_flush() {
            return Ok(());
        }

        let ctx_pos = self.ctx_pos.expect("has been set if we started a hunk");
        let end = (ctx_pos + self.ctx_size).min(self.before.len() as u32);
        self.print_context_and_update_pos(ctx_pos..end, end);

        let hunk_start = self.before_hunk_start + 1;
        let hunk_end = self.after_hunk_start + 1;

        let header = HunkHeader {
            before_hunk_start: hunk_start,
            before_hunk_len: self.before_hunk_len,
            after_hunk_start: hunk_end,
            after_hunk_len: self.after_hunk_len,
        };

        self.delegate.consume_hunk(header, &self.buffer)?;

        self.reset_hunks();
        Ok(())
    }

    fn print_context_and_update_pos(&mut self, print: Range<u32>, move_to: u32) {
        self.print_tokens(
            &self.before[print.start as usize..print.end as usize],
            DiffLineKind::Context,
        );

        let len = print.end - print.start;
        self.ctx_pos = Some(move_to);
        self.before_hunk_len += len;
        self.after_hunk_len += len;
    }

    fn reset_hunks(&mut self) {
        self.buffer.clear();
        self.before_hunk_len = 0;
        self.after_hunk_len = 0;
    }

    fn nothing_to_flush(&self) -> bool {
        self.before_hunk_len == 0 && self.after_hunk_len == 0
    }
}

impl<T, D> Sink for UnifiedDiff<'_, T, D>
where
    T: Hash + Eq + AsRef<[u8]>,
    D: ConsumeHunk,
{
    type Out = std::io::Result<D::Out>;

    fn process_change(&mut self, before: Range<u32>, after: Range<u32>) {
        if self.err.is_some() {
            return;
        }
        let start_next_hunk = self
            .ctx_pos
            .is_some_and(|ctx_pos| before.start - ctx_pos > 2 * self.ctx_size);
        if start_next_hunk {
            if let Err(err) = self.flush_accumulated_hunk() {
                self.err = Some(err);
                return;
            }
            let ctx_pos = before.start - self.ctx_size;
            self.ctx_pos = Some(ctx_pos);
            self.before_hunk_start = ctx_pos;
            self.after_hunk_start = after.start - self.ctx_size;
        }
        let ctx_pos = match self.ctx_pos {
            None => {
                // TODO: can this be made so the code above does the job?
                let ctx_pos = before.start.saturating_sub(self.ctx_size);
                self.before_hunk_start = ctx_pos;
                self.after_hunk_start = after.start.saturating_sub(self.ctx_size);
                ctx_pos
            }
            Some(pos) => pos,
        };
        self.print_context_and_update_pos(ctx_pos..before.start, before.end);
        self.before_hunk_len += before.end - before.start;
        self.after_hunk_len += after.end - after.start;

        self.print_tokens(
            &self.before[before.start as usize..before.end as usize],
            DiffLineKind::Remove,
        );
        self.print_tokens(&self.after[after.start as usize..after.end as usize], DiffLineKind::Add);
    }

    fn finish(mut self) -> Self::Out {
        if let Err(err) = self.flush_accumulated_hunk() {
            self.err = Some(err);
        }
        if let Some(err) = self.err {
            return Err(err);
        }
        Ok(self.delegate.finish())
    }
}

/// An implementation that fails if the input isn't UTF-8.
impl<D> ConsumeHunk for ConsumeBinaryHunk<'_, D>
where
    D: ConsumeBinaryHunkDelegate,
{
    type Out = D;

    fn consume_hunk(&mut self, header: HunkHeader, lines: &[(DiffLineKind, &[u8])]) -> std::io::Result<()> {
        self.header_buf.clear();
        self.header_buf
            .write_fmt(format_args!("{header}{nl}", nl = self.newline))
            .map_err(std::io::Error::other)?;

        let buf = &mut self.hunk_buf;
        buf.clear();
        for &(line_type, content) in lines {
            buf.push(line_type.to_prefix() as u8);
            buf.extend_from_slice(content);

            if !content.ends_with_str(self.newline) {
                buf.push_str(self.newline);
            }
        }

        self.delegate.consume_binary_hunk(header, &self.header_buf, buf)?;
        Ok(())
    }

    fn finish(self) -> Self::Out {
        self.delegate
    }
}

/// An implementation that fails if the input isn't UTF-8.
impl ConsumeBinaryHunkDelegate for String {
    fn consume_binary_hunk(&mut self, _header: HunkHeader, header_str: &str, hunk: &[u8]) -> std::io::Result<()> {
        self.push_str(header_str);
        self.push_str(hunk.to_str().map_err(std::io::Error::other)?);
        Ok(())
    }
}

/// An implementation that writes hunks into a byte buffer.
impl ConsumeBinaryHunkDelegate for Vec<u8> {
    fn consume_binary_hunk(&mut self, _header: HunkHeader, header_str: &str, hunk: &[u8]) -> std::io::Result<()> {
        self.push_str(header_str);
        self.extend_from_slice(hunk);
        Ok(())
    }
}

/// An implementation that writes hunks into a hunman-readable byte buffer.
impl ConsumeBinaryHunkDelegate for BString {
    fn consume_binary_hunk(&mut self, _header: HunkHeader, header_str: &str, hunk: &[u8]) -> std::io::Result<()> {
        self.push_str(header_str);
        self.extend_from_slice(hunk);
        Ok(())
    }
}

impl<'a, D> ConsumeBinaryHunk<'a, D>
where
    D: ConsumeBinaryHunkDelegate,
{
    /// Create a new instance that writes stringified hunks to `delegate`, which uses `newline` to separate header and hunk,
    /// as well as hunk lines that don't naturally end in a newline.
    pub fn new(delegate: D, newline: &'a str) -> ConsumeBinaryHunk<'a, D> {
        ConsumeBinaryHunk {
            newline,
            delegate,
            header_buf: String::new(),
            hunk_buf: Vec::with_capacity(128),
        }
    }
}

impl DiffLineKind {
    const fn to_prefix(self) -> char {
        match self {
            DiffLineKind::Context => ' ',
            DiffLineKind::Add => '+',
            DiffLineKind::Remove => '-',
        }
    }
}

impl std::fmt::Display for HunkHeader {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(
            f,
            "@@ -{},{} +{},{} @@",
            self.before_hunk_start, self.before_hunk_len, self.after_hunk_start, self.after_hunk_len
        )
    }
}
