// SPDX-License-Identifier: GPL-3.0-or-later
use std::borrow::Cow;
use std::sync::OnceLock;

use rsvg::prelude::HandleExt;
use xml::attribute::OwnedAttribute;
use xml::name::OwnedName;
use xml::reader::XmlEvent as ReaderEvent;
use xml::writer::{EmitterConfig, XmlEvent as WriterEvent};
use xml::{EventReader, EventWriter};

use gtk::{gdk, gio, glib, prelude::*};

const SYMBOLIC_SIZE: i32 = 32;
const SYMBOLIC_SCALE: i32 = 2;

pub fn rgba_to_hex(rgb: &gdk::RGBA) -> String {
    format!(
        "#{}{}{}",
        to_hex(rgb.red() * 255.0),
        to_hex(rgb.green() * 255.0),
        to_hex(rgb.blue() * 255.0)
    )
}

fn to_hex(n: f32) -> String {
    // Borrowed from https://github.com/emgyrz/colorsys.rs
    let s = format!("{:X}", n.round() as u32);
    if s.len() == 1 {
        "0".to_string() + &s
    } else {
        s
    }
}

pub async fn load_symbolic(bytes: glib::Bytes) -> anyhow::Result<gtk::IconPaintable> {
    let (file, stream) = gio::File::new_tmp_future(
        Some("emblem-XXXXXX-symbolic.svg"),
        glib::Priority::default(),
    )
    .await?;
    let output_stream = stream.output_stream();
    output_stream
        .write_bytes_future(&bytes, glib::Priority::default())
        .await?;
    output_stream
        .close_future(glib::Priority::default())
        .await?;

    Ok(gtk::IconPaintable::for_file(
        &file,
        SYMBOLIC_SIZE,
        SYMBOLIC_SCALE,
    ))
}

pub fn contrast_level(bg_color: &gdk::RGBA, fg_color: &gdk::RGBA) -> f32 {
    let bg_luminance = get_luminance(bg_color);
    let fg_luminance = get_luminance(fg_color);
    (bg_luminance.max(fg_luminance) + 0.05) / (bg_luminance.min(fg_luminance) + 0.05)
}

fn get_srgb(c: f32) -> f32 {
    if c <= 0.03928 {
        c / 12.92
    } else {
        ((c + 0.055) / 1.055).powf(2.4)
    }
}

pub fn get_luminance(color: &gdk::RGBA) -> f32 {
    let red = get_srgb(color.red());
    let blue = get_srgb(color.blue());
    let green = get_srgb(color.green());
    red * 0.2126 + blue * 0.0722 + green * 0.7152
}

pub fn interpolate(color1: &gdk::RGBA, color2: &gdk::RGBA) -> gdk::RGBA {
    gdk::RGBA::new(
        (color1.red() + color2.red()) / 2.0,
        (color1.green() + color2.green()) / 2.0,
        (color1.blue() + color2.blue()) / 2.0,
        (color1.alpha() + color2.alpha()) / 2.0,
    )
}

pub fn black_has_better_contrast(color: &gdk::RGBA) -> bool {
    let white = gdk::RGBA::WHITE;
    let white_contrast = contrast_level(color, &white);

    // lerp between alpha(black, 0.8) and color.
    // This corresponds to the black that will be displayed.
    let black = gdk::RGBA::new(
        color.red() * 0.2,
        color.green() * 0.2,
        color.blue() * 0.2,
        color.alpha() * 0.1,
    );

    let black_contrast = contrast_level(color, &black);

    // We prefer white, so a weight is added.
    black_contrast >= 3.0 * white_contrast
}

pub fn strip_svg_header(svg: &str) -> String {
    static RE: OnceLock<regex::Regex> = OnceLock::new();

    let re = RE.get_or_init(|| regex::Regex::new(r".*<\??xml.*\?>").expect("Invalid Regex"));
    let result: Vec<&str> = re.splitn(svg, 2).collect();

    match result.len() {
        0 => "",
        1 => result[0],
        2 => result[1],
        _ => unreachable!(),
    }
    .to_string()
}

pub async fn read_stream<S: glib::object::IsA<gio::InputStream>>(
    stream: S,
) -> Result<Vec<u8>, glib::Error> {
    let mut buffer = Vec::<u8>::with_capacity(4096);

    loop {
        let bytes = stream
            .read_bytes_future(4096, glib::Priority::default())
            .await?;
        if bytes.is_empty() {
            break;
        }
        buffer.extend_from_slice(&bytes);
    }
    stream.close_future(glib::Priority::default()).await?;

    Ok(buffer)
}

pub fn svg_size(bytes: &[u8]) -> Option<(f64, f64)> {
    let handle = rsvg::Handle::from_data(bytes).ok()??;

    handle.intrinsic_size_in_pixels()
}

pub async fn load_texture_from_bytes(bytes: &glib::Bytes) -> Result<gdk::Texture, glib::Error> {
    let (sender, receiver) = futures_channel::oneshot::channel();

    std::thread::spawn(glib::clone!(
        #[strong]
        bytes,
        move || {
            let result = gdk::Texture::from_bytes(&bytes);
            sender.send(result).unwrap()
        }
    ));

    receiver.await.unwrap()
}

/// Removes header and inserts size if missing.
pub fn sanitize(svg: &str, width: f64, height: f64) -> anyhow::Result<String> {
    let mut output: Vec<u8> = Vec::new();

    let reader = EventReader::from_str(svg);
    let config = EmitterConfig {
        write_document_declaration: false,
        ..Default::default()
    };
    let mut writer = EventWriter::new_with_config(&mut output, config);

    for e in reader {
        match e? {
            ReaderEvent::StartDocument { .. } => (),
            ReaderEvent::StartElement {
                name,
                attributes,
                namespace,
            } => {
                let attributes = if name.local_name == "svg" {
                    let mut attributes = attributes.clone();
                    let has_height = attributes
                        .iter()
                        .any(|attr| attr.name.local_name == "width");
                    let has_width = attributes
                        .iter()
                        .any(|attr| attr.name.local_name == "height");

                    if !has_height {
                        let attr = OwnedAttribute {
                            value: format!("{height}px"),
                            name: OwnedName {
                                local_name: "height".to_string(),
                                namespace: None,
                                prefix: None,
                            },
                        };
                        attributes.push(attr);
                    }

                    if !has_width {
                        let attr = OwnedAttribute {
                            value: format!("{width}px"),
                            name: OwnedName {
                                local_name: "width".to_string(),
                                namespace: None,
                                prefix: None,
                            },
                        };
                        attributes.push(attr);
                    }
                    attributes
                } else {
                    attributes
                };
                writer.write(WriterEvent::StartElement {
                    name: name.borrow(),
                    attributes: Cow::Owned(attributes.iter().map(|x| x.borrow()).collect()),
                    namespace: Cow::Owned(namespace),
                })?
            }
            e => {
                if let Some(e) = e.as_writer_event() {
                    writer.write(e)?
                }
            }
        }
    }

    Ok(String::from_utf8(output)?)
}

pub fn fe_color_matrix_values(
    foreground_color: Option<gdk::RGBA>,
    success: Option<gdk::RGBA>,
    error: Option<gdk::RGBA>,
    warning: Option<gdk::RGBA>,
) -> String {
    // Colors of light mode taken form https://gnome.pages.gitlab.gnome.org/libadwaita/doc/main/named-colors.html
    let fg = foreground_color.unwrap_or_else(|| gdk::RGBA::new(0.0, 0.0, 0.0, 0.8));
    let _sc = success.unwrap_or_else(|| gdk::RGBA::parse("#26a269").unwrap());
    let _ec = error.unwrap_or_else(|| gdk::RGBA::parse("#c01c28").unwrap());
    let _wc = warning.unwrap_or_else(|| gdk::RGBA::parse("#ae7b03").unwrap());

    // NOTE For a foreground color (r,g,b,a) matrix looks like:
    //
    // <feColorMatrix in="SourceGraphic" type="matrix"
    //                values="0 0 0 0 r
    //                        0 0 0 0 g
    //                        0 0 0 0 b
    //                        0 0 0 a 0"/>

    let (r, g, b, a) = (fg.red(), fg.green(), fg.blue(), fg.alpha());

    #[rustfmt::skip]
    let string = format!(
        "{} {} {} {} {} {} {} {} {} {} {} {} {} {} {} {} {} {} {} {}",
        0, 0, 0, 0, r,
        0, 0, 0, 0, g,
        0, 0, 0, 0, b,
        0, 0, 0, a, 0,
    );

    string
}
