/* diff_styling.rs
 *
 * Copyright 2026 Christian Kruse
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
 *
 * SPDX-License-Identifier: AGPL-3.0-or-later
 */

use adw::prelude::*;
use gtk::{gdk, pango};
use relm4::gtk;

use crate::git::{DiffLineKind, WordRange};

const WORD_ADD_LIGHT: (u8, u8, u8) = (0x57, 0xe3, 0x89); // @green_2
const WORD_DEL_LIGHT: (u8, u8, u8) = (0xf6, 0x61, 0x51); // @red_1
const WORD_ADD_DARK: (u8, u8, u8) = (0x8f, 0xf0, 0xa4); // @green_1
const WORD_DEL_DARK: (u8, u8, u8) = (0xff, 0x7b, 0x63); // ≈ @red_1

// reduced opacity to increase contrast with font color (~ 35 %)
const ALPHA: u16 = 22937;

pub fn new_content_attr_list() -> pango::AttrList {
    let attr_list = pango::AttrList::new();
    attr_list.insert(pango::AttrInt::new_insert_hyphens(false));
    attr_list
}

pub fn apply_word_attrs(
    attr_list: &pango::AttrList,
    kind: DiffLineKind,
    ranges: Option<&[WordRange]>,
    content_byte_len: usize,
    is_dark: bool,
) {
    let Some(ranges) = ranges else {
        return;
    };

    let (r8, g8, b8) = match (kind, is_dark) {
        (DiffLineKind::Addition, false) => WORD_ADD_LIGHT,
        (DiffLineKind::Addition, true) => WORD_ADD_DARK,
        (DiffLineKind::Deletion, false) => WORD_DEL_LIGHT,
        (DiffLineKind::Deletion, true) => WORD_DEL_DARK,
        // word_ranges is only populated for paired Addition/Deletion lines
        (DiffLineKind::Context, _) => return,
    };

    let r = (r8 as u16) * 257;
    let g = (g8 as u16) * 257;
    let b = (b8 as u16) * 257;
    let max = content_byte_len as u32;

    for range in ranges {
        let s = (range.start as u32).min(max);
        let e = (range.end as u32).min(max);

        if s >= e {
            continue;
        }

        let mut bg = pango::AttrColor::new_background(r, g, b);
        bg.set_start_index(s);
        bg.set_end_index(e);
        attr_list.insert(bg);

        let mut alpha = pango::AttrInt::new_background_alpha(ALPHA);
        alpha.set_start_index(s);
        alpha.set_end_index(e);
        attr_list.insert(alpha);
    }
}

const ADD_BG: (u8, u8, u8) = (46, 194, 126);
const DEL_BG: (u8, u8, u8) = (224, 27, 36);
const LINE_BG_ALPHA: f32 = 0.15;
const WORD_BG_ALPHA: f32 = 0.35;

fn rgba(rgb: (u8, u8, u8), alpha: f32) -> gdk::RGBA {
    gdk::RGBA::new(rgb.0 as f32 / 255.0, rgb.1 as f32 / 255.0, rgb.2 as f32 / 255.0, alpha)
}

#[derive(Clone, Debug)]
pub struct DiffTags {
    pub addition: gtk::TextTag,
    pub deletion: gtk::TextTag,
    pub hunk_header: gtk::TextTag,
    pub word_addition: gtk::TextTag,
    pub word_deletion: gtk::TextTag,
}

impl DiffTags {
    pub fn install(buffer: &gtk::TextBuffer, is_dark: bool) -> Self {
        let table = buffer.tag_table();

        let addition = gtk::TextTag::new(Some("diff-addition"));
        let deletion = gtk::TextTag::new(Some("diff-deletion"));
        let hunk_header = gtk::TextTag::new(Some("diff-hunk-header"));
        let word_addition = gtk::TextTag::new(Some("diff-word-addition"));
        let word_deletion = gtk::TextTag::new(Some("diff-word-deletion"));

        hunk_header.set_property("foreground-rgba", gdk::RGBA::new(0.5, 0.5, 0.5, 1.0));
        hunk_header.set_property("pixels-above-lines", 4i32);
        hunk_header.set_property("pixels-below-lines", 4i32);

        table.add(&addition);
        table.add(&deletion);
        table.add(&hunk_header);
        table.add(&word_addition);
        table.add(&word_deletion);

        let tags = Self {
            addition,
            deletion,
            hunk_header,
            word_addition,
            word_deletion,
        };
        tags.refresh_colors(is_dark);

        tags
    }

    pub fn refresh_colors(&self, is_dark: bool) {
        self.addition
            .set_property("background-rgba", rgba(ADD_BG, LINE_BG_ALPHA));
        self.deletion
            .set_property("background-rgba", rgba(DEL_BG, LINE_BG_ALPHA));

        let word_add_rgb = if is_dark { WORD_ADD_DARK } else { WORD_ADD_LIGHT };
        let word_del_rgb = if is_dark { WORD_DEL_DARK } else { WORD_DEL_LIGHT };

        self.word_addition
            .set_property("background-rgba", rgba(word_add_rgb, WORD_BG_ALPHA));
        self.word_deletion
            .set_property("background-rgba", rgba(word_del_rgb, WORD_BG_ALPHA));

        let header_bg = if is_dark {
            gdk::RGBA::new(1.0, 1.0, 1.0, 0.06)
        } else {
            gdk::RGBA::new(0.0, 0.0, 0.0, 0.05)
        };

        self.hunk_header.set_property("paragraph-background-rgba", header_bg);
    }
}

pub fn apply_word_tags(
    buffer: &gtk::TextBuffer,
    tags: &DiffTags,
    kind: DiffLineKind,
    ranges: Option<&[WordRange]>,
    line_text: &str,
    line_start_char_offset: i32,
) {
    let Some(ranges) = ranges else {
        return;
    };

    let tag = match kind {
        DiffLineKind::Addition => &tags.word_addition,
        DiffLineKind::Deletion => &tags.word_deletion,
        DiffLineKind::Context => return,
    };

    let line_byte_len = line_text.len();

    for range in ranges {
        let s_byte = range.start.min(line_byte_len);
        let e_byte = range.end.min(line_byte_len);
        if s_byte >= e_byte {
            continue;
        }

        // Convert byte offsets to character offsets within the line.
        let s_char = line_text[..s_byte].chars().count() as i32;
        let e_char = line_text[..e_byte].chars().count() as i32;

        let start = buffer.iter_at_offset(line_start_char_offset + s_char);
        let end = buffer.iter_at_offset(line_start_char_offset + e_char);

        buffer.apply_tag(tag, &start, &end);
    }
}
