// A module for constructing and writing the hunk header.
//
// The structure of the hunk header output by delta is
// ```
// (file):(line-number): (code-fragment)
// ```
//
// The code fragment and line number derive from a line of git/diff output that looks like
// ```
// @@ -119,12 +119,7 @@ fn write_to_output_buffer(
// ```
//
// Whether or not file and line-number are included is controlled by the presence of the special
// style attributes 'file' and 'line-number' in the hunk-header-style string. For example, delta
// might output the above hunk header as
// ```
// ───────────────────────────────────────────────────┐
// src/hunk_header.rs:119: fn write_to_output_buffer( │
// ───────────────────────────────────────────────────┘
// ```

use std::fmt::Write as FmtWrite;

use lazy_static::lazy_static;
use regex::Regex;

use super::draw;
use crate::config::{Config, HunkHeaderIncludeFilePath, HunkHeaderIncludeLineNumber};
use crate::delta::{self, DiffType, InMergeConflict, MergeParents, State, StateMachine};
use crate::paint::{self, BgShouldFill, Painter, StyleSectionSpecifier};
use crate::style::{DecorationStyle, Style};

#[derive(Clone, Default, Debug, PartialEq, Eq)]
pub struct ParsedHunkHeader {
    code_fragment: String,
    line_numbers_and_hunk_lengths: Vec<(usize, usize)>,
}

pub enum HunkHeaderIncludeHunkLabel {
    Yes,
    No,
}

impl<'a> StateMachine<'a> {
    #[inline]
    fn test_hunk_header_line(&self) -> bool {
        self.line.starts_with("@@") &&
        // A hunk header can occur within a merge conflict region, but we don't attempt to handle
        // that. See #822.
        !matches!(self.state, State::MergeConflict(_, _))
    }

    pub fn handle_hunk_header_line(&mut self) -> std::io::Result<bool> {
        use DiffType::*;
        use State::*;
        if !self.test_hunk_header_line() {
            return Ok(false);
        }
        let mut handled_line = false;
        if let Some(parsed_hunk_header) = parse_hunk_header(&self.line) {
            let diff_type = match &self.state {
                DiffHeader(Combined(MergeParents::Unknown, InMergeConflict::No)) => {
                    // https://git-scm.com/docs/git-diff#_combined_diff_format
                    let n_parents = self.line.chars().take_while(|c| c == &'@').count() - 1;
                    Combined(MergeParents::Number(n_parents), InMergeConflict::No)
                }
                DiffHeader(diff_type)
                | HunkMinus(diff_type, _)
                | HunkZero(diff_type, _)
                | HunkPlus(diff_type, _) => diff_type.clone(),
                _ => Unified,
            };
            self.state = HunkHeader(
                diff_type,
                parsed_hunk_header,
                self.line.clone(),
                self.raw_line.clone(),
            );
            handled_line = true;
        }
        Ok(handled_line)
    }

    /// Emit the hunk header, with any requested decoration.
    pub fn emit_hunk_header_line(
        &mut self,
        parsed_hunk_header: &ParsedHunkHeader,
        line: &str,
        raw_line: &str,
    ) -> std::io::Result<bool> {
        self.painter.paint_buffered_minus_and_plus_lines();
        self.painter.set_highlighter();
        self.painter.emit()?;

        let ParsedHunkHeader {
            code_fragment,
            line_numbers_and_hunk_lengths,
        } = parsed_hunk_header;

        if self.config.line_numbers {
            self.painter
                .line_numbers_data
                .as_mut()
                .unwrap()
                .initialize_hunk(line_numbers_and_hunk_lengths, self.plus_file.to_string());
        }

        if self.config.hunk_header_style.is_raw {
            write_hunk_header_raw(&mut self.painter, line, raw_line, self.config)?;
        } else if self.config.hunk_header_style.is_omitted {
            writeln!(self.painter.writer)?;
        } else {
            // Add a blank line below the hunk-header-line for readability, unless
            // color_only mode is active.
            if !self.config.color_only {
                writeln!(self.painter.writer)?;
            }

            write_line_of_code_with_optional_path_and_line_number(
                code_fragment,
                line_numbers_and_hunk_lengths,
                None,
                &mut self.painter,
                line,
                if self.plus_file == "/dev/null" {
                    &self.minus_file
                } else {
                    &self.plus_file
                },
                self.config.hunk_header_style.decoration_style,
                &self.config.hunk_header_file_style,
                &self.config.hunk_header_line_number_style,
                &self.config.hunk_header_style_include_file_path,
                &self.config.hunk_header_style_include_line_number,
                &HunkHeaderIncludeHunkLabel::Yes,
                ":",
                self.config,
            )?;
        };
        self.painter.set_highlighter();
        Ok(true)
    }
}

lazy_static! {
    static ref HUNK_HEADER_REGEX: Regex = Regex::new(r"@+ ([^@]+)@+(.*\s?)").unwrap();
}

// Parse unified diff hunk header format. See
// https://www.gnu.org/software/diffutils/manual/html_node/Detailed-Unified.html
// https://www.artima.com/weblogs/viewpost.jsp?thread=164293
lazy_static! {
    static ref HUNK_HEADER_FILE_COORDINATE_REGEX: Regex = Regex::new(
        r"(?x)
[-+]
(\d+)            # 1. Hunk start line number
(?:              # Start optional hunk length section (non-capturing)
  ,              #   Literal comma
  (\d+)          #   2. Optional hunk length (defaults to 1)
)?"
    )
    .unwrap();
}

/// Given input like
/// "@@ -74,15 +74,14 @@ pub fn delta("
/// Return " pub fn delta(" and a vector of (line_number, hunk_length) tuples.
fn parse_hunk_header(line: &str) -> Option<ParsedHunkHeader> {
    if let Some(caps) = HUNK_HEADER_REGEX.captures(line) {
        let file_coordinates = &caps[1];
        let line_numbers_and_hunk_lengths = HUNK_HEADER_FILE_COORDINATE_REGEX
            .captures_iter(file_coordinates)
            .map(|caps| {
                (
                    caps[1].parse::<usize>().unwrap(),
                    caps.get(2)
                        .map(|m| m.as_str())
                        // Per the specs linked above, if the hunk length is absent then it is 1.
                        .unwrap_or("1")
                        .parse::<usize>()
                        .unwrap(),
                )
            })
            .collect();
        let code_fragment = caps[2].to_string();
        Some(ParsedHunkHeader {
            code_fragment,
            line_numbers_and_hunk_lengths,
        })
    } else {
        None
    }
}

fn write_hunk_header_raw(
    painter: &mut Painter,
    line: &str,
    raw_line: &str,
    config: &Config,
) -> std::io::Result<()> {
    let (mut draw_fn, pad, decoration_ansi_term_style) =
        draw::get_draw_function(config.hunk_header_style.decoration_style);
    if config.hunk_header_style.decoration_style != DecorationStyle::NoDecoration {
        writeln!(painter.writer)?;
    }
    draw_fn(
        painter.writer,
        &format!("{}{}", line, if pad { " " } else { "" }),
        &format!("{}{}", raw_line, if pad { " " } else { "" }),
        "",
        &config.decorations_width,
        config.hunk_header_style,
        decoration_ansi_term_style,
    )?;
    Ok(())
}

#[allow(clippy::too_many_arguments)]
pub fn write_line_of_code_with_optional_path_and_line_number(
    code_fragment: &str,
    line_numbers_and_hunk_lengths: &[(usize, usize)],
    style_sections: Option<StyleSectionSpecifier>,
    painter: &mut Painter,
    line: &str,
    plus_file: &str,
    decoration_style: DecorationStyle,
    file_style: &Style,
    line_number_style: &Style,
    include_file_path: &HunkHeaderIncludeFilePath,
    include_line_number: &HunkHeaderIncludeLineNumber,
    include_hunk_label: &HunkHeaderIncludeHunkLabel,
    file_path_separator: &str,
    config: &Config,
) -> std::io::Result<()> {
    let (mut draw_fn, _, decoration_ansi_term_style) = draw::get_draw_function(decoration_style);
    let line = if config.color_only {
        line.to_string()
    } else if !code_fragment.is_empty() {
        format!("{code_fragment} ")
    } else {
        "".to_string()
    };

    let plus_line_number = line_numbers_and_hunk_lengths[line_numbers_and_hunk_lengths.len() - 1].0;
    let file_with_line_number = paint_file_path_with_line_number(
        Some(plus_line_number),
        plus_file,
        file_style,
        line_number_style,
        include_file_path,
        include_line_number,
        file_path_separator,
        config,
    );

    if !line.is_empty() || !file_with_line_number.is_empty() {
        write_to_output_buffer(
            &file_with_line_number,
            file_path_separator,
            line,
            style_sections,
            include_hunk_label,
            painter,
            config,
        );
        draw_fn(
            painter.writer,
            &painter.output_buffer,
            &painter.output_buffer,
            "",
            &config.decorations_width,
            config.null_style,
            decoration_ansi_term_style,
        )?;
        painter.output_buffer.clear();
    }

    Ok(())
}

#[allow(clippy::too_many_arguments)]
fn paint_file_path_with_line_number(
    line_number: Option<usize>,
    plus_file: &str,
    file_style: &Style,
    line_number_style: &Style,
    include_file_path: &HunkHeaderIncludeFilePath,
    include_line_number: &HunkHeaderIncludeLineNumber,
    separator: &str,
    config: &Config,
) -> String {
    let file_style = match include_file_path {
        HunkHeaderIncludeFilePath::Yes => Some(*file_style),
        HunkHeaderIncludeFilePath::No => None,
    };
    let line_number_style = if matches!(include_line_number, HunkHeaderIncludeLineNumber::Yes)
        && line_number.is_some()
        && !config.hunk_header_style.is_raw
        && !config.color_only
    {
        Some(*line_number_style)
    } else {
        None
    };

    paint::paint_file_path_with_line_number(
        line_number,
        plus_file,
        false,
        separator,
        false,
        file_style,
        line_number_style,
        config,
    )
}

fn write_to_output_buffer(
    file_with_line_number: &str,
    file_path_separator: &str,
    line: String,
    style_sections: Option<StyleSectionSpecifier>,
    include_hunk_label: &HunkHeaderIncludeHunkLabel,
    painter: &mut Painter,
    config: &Config,
) {
    if matches!(include_hunk_label, HunkHeaderIncludeHunkLabel::Yes)
        && !config.hunk_label.is_empty()
    {
        let _ = write!(
            &mut painter.output_buffer,
            "{} ",
            config.hunk_header_file_style.paint(&config.hunk_label)
        );
    }
    if !file_with_line_number.is_empty() {
        // The code fragment in "line" adds whitespace, but if only a line number is printed
        // then the trailing space must be added.
        let space = if line.is_empty() { " " } else { "" };
        let _ = write!(
            &mut painter.output_buffer,
            "{file_with_line_number}{file_path_separator}{space}",
        );
    }
    if !line.is_empty() {
        painter.syntax_highlight_and_paint_line(
            &line,
            style_sections.unwrap_or(StyleSectionSpecifier::Style(config.hunk_header_style)),
            delta::State::HunkHeader(
                DiffType::Unified,
                ParsedHunkHeader::default(),
                "".to_owned(),
                "".to_owned(),
            ),
            BgShouldFill::No,
        );
        painter.output_buffer.pop(); // trim newline
    }
}

#[cfg(test)]
pub mod tests {
    use super::*;
    use crate::ansi::strip_ansi_codes;
    use crate::tests::integration_test_utils;

    #[test]
    fn test_parse_hunk_header() {
        let ParsedHunkHeader {
            code_fragment,
            line_numbers_and_hunk_lengths,
        } = parse_hunk_header("@@ -74,15 +75,14 @@ pub fn delta(\n").unwrap();
        assert_eq!(code_fragment, " pub fn delta(\n");
        assert_eq!(line_numbers_and_hunk_lengths[0], (74, 15),);
        assert_eq!(line_numbers_and_hunk_lengths[1], (75, 14),);
    }

    #[test]
    fn test_parse_hunk_header_with_omitted_hunk_lengths() {
        let ParsedHunkHeader {
            code_fragment,
            line_numbers_and_hunk_lengths,
        } = parse_hunk_header("@@ -74 +75,2 @@ pub fn delta(\n").unwrap();
        assert_eq!(code_fragment, " pub fn delta(\n");
        assert_eq!(line_numbers_and_hunk_lengths[0], (74, 1),);
        assert_eq!(line_numbers_and_hunk_lengths[1], (75, 2),);
    }

    #[test]
    fn test_parse_hunk_header_added_file() {
        let ParsedHunkHeader {
            code_fragment,
            line_numbers_and_hunk_lengths,
        } = parse_hunk_header("@@ -1,22 +0,0 @@").unwrap();
        assert_eq!(code_fragment, "",);
        assert_eq!(line_numbers_and_hunk_lengths[0], (1, 22),);
        assert_eq!(line_numbers_and_hunk_lengths[1], (0, 0),);
    }

    #[test]
    fn test_parse_hunk_header_deleted_file() {
        let ParsedHunkHeader {
            code_fragment,
            line_numbers_and_hunk_lengths,
        } = parse_hunk_header("@@ -0,0 +1,3 @@").unwrap();
        assert_eq!(code_fragment, "",);
        assert_eq!(line_numbers_and_hunk_lengths[0], (0, 0),);
        assert_eq!(line_numbers_and_hunk_lengths[1], (1, 3),);
    }

    #[test]
    fn test_parse_hunk_header_merge() {
        let ParsedHunkHeader {
            code_fragment,
            line_numbers_and_hunk_lengths,
        } = parse_hunk_header("@@@ -293,11 -358,15 +358,16 @@@ dependencies =").unwrap();
        assert_eq!(code_fragment, " dependencies =");
        assert_eq!(line_numbers_and_hunk_lengths[0], (293, 11),);
        assert_eq!(line_numbers_and_hunk_lengths[1], (358, 15),);
        assert_eq!(line_numbers_and_hunk_lengths[2], (358, 16),);
    }

    #[test]
    fn test_parse_hunk_header_cthulhu() {
        let ParsedHunkHeader {
            code_fragment,
            line_numbers_and_hunk_lengths,
        } = parse_hunk_header("@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ -446,6 -446,6 -446,6 -446,6 -446,6 -446,6 -446,6 -446,6 -446,6 -446,6 -446,6 -446,6 -446,6 -446,6 -446,6 -446,6 -446,6 -446,6 -446,6 -446,6 -446,6 -446,6 -444,17 -446,6 -446,6 -446,6 -446,6 -446,6 -446,6 -446,6 -446,6 -446,6 -446,6 -446,6 -446,6 -446,6 -446,6 -446,6 -446,6 -446,6 -446,6 -446,6 -446,6 -446,6 -446,6 -446,6 -446,6 -446,6 -446,6 -446,6 -446,6 -446,6 -446,6 -446,6 -446,6 -446,6 -446,6 -446,6 -446,6 -446,6 -446,6 -446,6 -446,6 -446,6 -446,6 -446,6 +444,17 @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ int snd_soc_jack_add_gpios(struct snd_s").unwrap();
        assert_eq!(code_fragment, " int snd_soc_jack_add_gpios(struct snd_s");
        assert_eq!(line_numbers_and_hunk_lengths[0], (446, 6),);
        assert_eq!(line_numbers_and_hunk_lengths[1], (446, 6),);
        assert_eq!(line_numbers_and_hunk_lengths[2], (446, 6),);
        assert_eq!(line_numbers_and_hunk_lengths[65], (446, 6),);
    }

    #[test]
    fn test_paint_file_path_with_line_number_default() {
        // hunk-header-style (by default) includes 'line-number' but not 'file'.
        // This test confirms that `paint_file_path_with_line_number` returns a painted line number.
        let config = integration_test_utils::make_config_from_args(&[]);

        let result = paint_file_path_with_line_number(
            Some(3),
            "some-file",
            &config.hunk_header_style,
            &config.hunk_header_line_number_style,
            &config.hunk_header_style_include_file_path,
            &config.hunk_header_style_include_line_number,
            ":",
            &config,
        );

        assert_eq!(result, "\u{1b}[34m3\u{1b}[0m");
    }

    #[test]
    fn test_paint_file_path_with_line_number_hyperlinks() {
        use std::{iter::FromIterator, path::PathBuf};

        use crate::utils;

        // hunk-header-style (by default) includes 'line-number' but not 'file'.
        // Normally, `paint_file_path_with_line_number` would return a painted line number.
        // But in this test hyperlinks are activated, and the test ensures that delta.__workdir__ is
        // present in git_config_entries.
        // This test confirms that, under those circumstances, `paint_file_path_with_line_number`
        // returns a hyperlinked file path with line number.

        let config = integration_test_utils::make_config_from_args(&["--features", "hyperlinks"]);
        let relative_path = PathBuf::from_iter(["some-dir", "some-file"]);

        let result = paint_file_path_with_line_number(
            Some(3),
            &relative_path.to_string_lossy(),
            &config.hunk_header_style,
            &config.hunk_header_line_number_style,
            &config.hunk_header_style_include_file_path,
            &config.hunk_header_style_include_line_number,
            ":",
            &config,
        );

        assert_eq!(
            result,
            format!(
                "\u{1b}]8;;file://{}\u{1b}\\\u{1b}[34m3\u{1b}[0m\u{1b}]8;;\u{1b}\\",
                utils::path::fake_delta_cwd_for_tests()
                    .join(relative_path)
                    .to_string_lossy()
            )
        );
    }

    #[test]
    fn test_paint_file_path_with_line_number_empty() {
        // hunk-header-style includes neither 'file' nor 'line-number'.
        // This causes `paint_file_path_with_line_number` to return empty string.
        let config = integration_test_utils::make_config_from_args(&[
            "--hunk-header-style",
            "syntax bold",
            "--hunk-header-decoration-style",
            "omit",
        ]);

        let result = paint_file_path_with_line_number(
            Some(3),
            "some-file",
            &config.hunk_header_style,
            &config.hunk_header_line_number_style,
            &config.hunk_header_style_include_file_path,
            &config.hunk_header_style_include_line_number,
            ":",
            &config,
        );

        // result is
        // "\u{1b}[1msome-file\u{1b}[0m:\u{1b}[34m3\u{1b}[0m"
        assert_eq!(result, "");
    }

    #[test]
    fn test_paint_file_path_with_line_number_empty_hyperlinks() {
        // hunk-header-style includes neither 'file' nor 'line-number'.
        // This causes `paint_file_path_with_line_number` to return empty string.
        // This test confirms that this remains true even when we are requesting hyperlinks.

        let config = integration_test_utils::make_config_from_args(&[
            "--hunk-header-style",
            "syntax bold",
            "--hunk-header-decoration-style",
            "omit",
            "--features",
            "hyperlinks",
        ]);

        let result = paint_file_path_with_line_number(
            Some(3),
            "some-file",
            &config.hunk_header_style,
            &config.hunk_header_line_number_style,
            &config.hunk_header_style_include_file_path,
            &config.hunk_header_style_include_line_number,
            ":",
            &config,
        );

        assert_eq!(result, "");
    }

    #[test]
    fn test_paint_file_path_with_line_number_empty_navigate() {
        let config = integration_test_utils::make_config_from_args(&[
            "--hunk-header-style",
            "syntax bold",
            "--hunk-header-decoration-style",
            "omit",
            "--navigate",
        ]);

        let result = paint_file_path_with_line_number(
            Some(3),
            "δ some-file",
            &config.hunk_header_style,
            &config.hunk_header_line_number_style,
            &config.hunk_header_style_include_file_path,
            &config.hunk_header_style_include_line_number,
            ":",
            &config,
        );

        // result is
        // "\u{1b}[1mδ some-file\u{1b}[0m:\u{1b}[34m3\u{1b}[0m"
        assert_eq!(result, "");
    }

    #[test]
    fn test_not_a_hunk_header_is_handled_gracefully() {
        let config = integration_test_utils::make_config_from_args(&[]);
        let output =
            integration_test_utils::run_delta(GIT_LOG_OUTPUT_WITH_NOT_A_HUNK_HEADER, &config);
        let output = strip_ansi_codes(&output);
        assert!(output.contains("@@@2021-12-05"));
    }

    const GIT_LOG_OUTPUT_WITH_NOT_A_HUNK_HEADER: &str = "\
@@@2021-12-05

src/config.rs                  |   2 +-
src/delta.rs                   |   3 ++-
src/handlers/hunk.rs           |  12 ++++++------
src/handlers/hunk_header.rs    | 119 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------------------------------------------
src/handlers/merge_conflict.rs |   2 +-
src/handlers/submodule.rs      |   4 ++--
src/paint.rs                   |   2 +-
7 files changed, 90 insertions(+), 54 deletions(-)
";
}
