//! Serializable types for the diff domain.

use serde::{Deserialize, Serialize};

use crate::commit::types::CommitOid;

#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum DiffDeltaStatus {
    Unmodified,
    Added,
    Deleted,
    Modified,
    Renamed,
    Copied,
    Ignored,
    Untracked,
    Typechange,
    Unreadable,
    Conflicted,
}

impl DiffDeltaStatus {
    pub fn from_git2(status: git2::Delta) -> Self {
        match status {
            git2::Delta::Unmodified => Self::Unmodified,
            git2::Delta::Added => Self::Added,
            git2::Delta::Deleted => Self::Deleted,
            git2::Delta::Modified => Self::Modified,
            git2::Delta::Renamed => Self::Renamed,
            git2::Delta::Copied => Self::Copied,
            git2::Delta::Ignored => Self::Ignored,
            git2::Delta::Untracked => Self::Untracked,
            git2::Delta::Typechange => Self::Typechange,
            git2::Delta::Unreadable => Self::Unreadable,
            git2::Delta::Conflicted => Self::Conflicted,
        }
    }
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DiffFile {
    pub oid: Option<CommitOid>,
    pub path: Option<String>,
    pub size: u64,
    pub is_binary: bool,
}

impl DiffFile {
    pub fn from_git2(file: &git2::DiffFile<'_>) -> Self {
        Self {
            oid: if file.is_valid_id() {
                Some(CommitOid::from_git2(file.id()))
            } else {
                None
            },
            path: file.path().map(|p| p.to_string_lossy().to_string()),
            size: file.size(),
            is_binary: file.is_binary(),
        }
    }
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DiffHunk {
    pub old_start: u32,
    pub old_lines: u32,
    pub new_start: u32,
    pub new_lines: u32,
    pub header: String,
}

impl DiffHunk {
    pub fn from_git2(hunk: &git2::DiffHunk<'_>) -> Self {
        Self {
            old_start: hunk.old_start(),
            old_lines: hunk.old_lines(),
            new_start: hunk.new_start(),
            new_lines: hunk.new_lines(),
            header: String::from_utf8_lossy(hunk.header()).to_string(),
        }
    }
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DiffLine {
    pub content: String,
    pub origin: char,
    pub old_lineno: Option<u32>,
    pub new_lineno: Option<u32>,
    pub num_lines: u32,
    pub content_offset: i64,
}

impl DiffLine {
    pub fn from_git2(line: &git2::DiffLine<'_>) -> Self {
        Self {
            content: String::from_utf8_lossy(line.content()).to_string(),
            origin: line.origin(),
            old_lineno: line.old_lineno(),
            new_lineno: line.new_lineno(),
            num_lines: line.num_lines(),
            content_offset: line.content_offset(),
        }
    }
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DiffDelta {
    pub status: DiffDeltaStatus,
    pub old_file: DiffFile,
    pub new_file: DiffFile,
    pub nfiles: u16,
    pub hunks: Vec<DiffHunk>,
    pub lines: Vec<DiffLine>,
}

impl DiffDelta {
    pub fn from_git2(
        delta: &git2::DiffDelta<'_>,
        hunks: Vec<DiffHunk>,
        lines: Vec<DiffLine>,
    ) -> Self {
        Self {
            status: DiffDeltaStatus::from_git2(delta.status()),
            old_file: DiffFile::from_git2(&delta.old_file()),
            new_file: DiffFile::from_git2(&delta.new_file()),
            nfiles: delta.nfiles(),
            hunks,
            lines,
        }
    }
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DiffStats {
    pub files_changed: usize,
    pub insertions: usize,
    pub deletions: usize,
}

impl DiffStats {
    pub fn from_git2(stats: &git2::DiffStats) -> Self {
        Self {
            files_changed: stats.files_changed(),
            insertions: stats.insertions(),
            deletions: stats.deletions(),
        }
    }
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DiffResult {
    pub stats: DiffStats,
    pub deltas: Vec<DiffDelta>,
}

// ---------------------------------------------------------------------------
// Side-by-side diff types
// ---------------------------------------------------------------------------

#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum SideBySideChangeType {
    /// Both sides show the same context line.
    Unchanged,
    /// Line was added (only on the right / new side).
    Added,
    /// Line was removed (only on the left / old side).
    Removed,
    /// A modified region — left shows old line, right shows new line.
    Modified,
    /// Empty row used to align paired add/remove lines.
    Empty,
}

impl SideBySideChangeType {}

/// One row in the side-by-side output.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SideBySideLine {
    /// Line number in the old / left file. `None` for added-only rows.
    pub left_line_no: Option<u32>,
    /// Line number in the new / right file. `None` for deleted-only rows.
    pub right_line_no: Option<u32>,
    /// Content displayed on the left side.
    pub left_content: String,
    /// Content displayed on the right side.
    pub right_content: String,
    /// How this row should be rendered.
    pub change_type: SideBySideChangeType,
}

/// A single file in a side-by-side diff.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SideBySideFile {
    /// Path of the file (prefer the new path for renames).
    pub path: String,
    /// Number of additions in this file.
    pub additions: usize,
    /// Number of deletions in this file.
    pub deletions: usize,
    /// Whether this file is binary.
    pub is_binary: bool,
    /// Whether this file was renamed.
    pub is_rename: bool,
    /// All rows in the side-by-side view.
    pub lines: Vec<SideBySideLine>,
}

/// The complete side-by-side diff result for all files.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SideBySideDiffResult {
    pub files: Vec<SideBySideFile>,
    pub total_additions: usize,
    pub total_deletions: usize,
}

#[derive(Debug, Clone, Default)]
pub struct DiffOptions {
    context_lines: u32,
    pathspec: Vec<String>,
    include_untracked: bool,
    include_ignored: bool,
    include_unmodified: bool,
    ignore_whitespace: bool,
    force_text: bool,
    skip_binary_check: bool,
    reverse: bool,
}

impl DiffOptions {
    pub fn new() -> Self {
        Self::default()
    }

    pub fn context_lines(mut self, lines: u32) -> Self {
        self.context_lines = lines;
        self
    }

    pub fn pathspec(mut self, path: &str) -> Self {
        self.pathspec.push(path.to_string());
        self
    }

    pub fn include_untracked(mut self) -> Self {
        self.include_untracked = true;
        self
    }

    pub fn include_ignored(mut self) -> Self {
        self.include_ignored = true;
        self
    }

    pub fn include_unmodified(mut self) -> Self {
        self.include_unmodified = true;
        self
    }

    pub fn ignore_whitespace(mut self) -> Self {
        self.ignore_whitespace = true;
        self
    }

    pub fn force_text(mut self) -> Self {
        self.force_text = true;
        self
    }

    pub fn skip_binary_check(mut self) -> Self {
        self.skip_binary_check = true;
        self
    }

    pub fn reverse(mut self) -> Self {
        self.reverse = true;
        self
    }

    pub fn to_git2(&self) -> git2::DiffOptions {
        let mut opts = git2::DiffOptions::new();
        if self.context_lines != 3 {
            opts.context_lines(self.context_lines);
        }
        for p in &self.pathspec {
            opts.pathspec(p);
        }
        if self.include_untracked {
            opts.include_untracked(true);
        }
        if self.include_ignored {
            opts.include_ignored(true);
        }
        if self.include_unmodified {
            opts.include_unmodified(true);
        }
        if self.ignore_whitespace {
            opts.ignore_whitespace(true);
        }
        if self.force_text {
            opts.force_text(true);
        }
        if self.skip_binary_check {
            opts.skip_binary_check(true);
        }
        if self.reverse {
            opts.reverse(true);
        }
        opts
    }
}
