//! Native four-channel color compatible with `LibreMetaverse`'s `Color4`.

#![allow(clippy::cast_possible_truncation)] // Explicit C# numeric conversions are part of the API.
#![allow(clippy::cast_sign_loss)] // FloatToByte clamps before the reference-compatible cast.
#![allow(clippy::float_cmp)] // Exact component equality is required by the reference type.
#![allow(clippy::inherent_to_string)] // The mapped C# method is named ToString.
#![allow(clippy::items_after_statements)] // Constants stay next to their compatibility formulas.
#![allow(clippy::many_single_char_names)] // Color/HSV formulas use the reference channel names.
#![allow(clippy::missing_errors_doc)] // Result shapes are fixed by the public compatibility map.
#![allow(clippy::must_use_candidate)] // Attributes are not part of the mapped C# surface.
#![allow(clippy::needless_pass_by_value)] // Owned arguments mirror mapped C# value parameters.
#![allow(clippy::should_implement_trait)] // Operator entry points have fixed generated names.

use crate::Error;
use crate::compat::Object;
use crate::math_compat::{
    checked_range, clamp_f32, compare_f32, format_f32, hash_f32, lerp_f32, max_f32, min_f32,
};

#[derive(Clone, Copy, Debug, Default, PartialEq)]
pub struct Color4 {
    pub r: f32,
    pub g: f32,
    pub b: f32,
    pub a: f32,
}

impl Color4 {
    #[must_use]
    pub const fn black() -> Self {
        Self {
            r: 0.0,
            g: 0.0,
            b: 0.0,
            a: 1.0,
        }
    }
    #[must_use]
    pub const fn white() -> Self {
        Self {
            r: 1.0,
            g: 1.0,
            b: 1.0,
            a: 1.0,
        }
    }

    pub fn new_with_byte_byte_byte_byte(r: u8, g: u8, b: u8, a: u8) -> Result<Self, Error> {
        const QUANTA: f32 = 1.0 / 255.0;
        Ok(Self {
            r: f32::from(r) * QUANTA,
            g: f32::from(g) * QUANTA,
            b: f32::from(b) * QUANTA,
            a: f32::from(a) * QUANTA,
        })
    }
    pub fn new_with_single_single_single_single(
        r: f32,
        g: f32,
        b: f32,
        a: f32,
    ) -> Result<Self, Error> {
        if r > 1.0 || g > 1.0 || b > 1.0 || a > 1.0 {
            return Err(Error::Argument);
        }
        Ok(Self {
            r: clamp_f32(r, 0.0, 1.0),
            g: clamp_f32(g, 0.0, 1.0),
            b: clamp_f32(b, 0.0, 1.0),
            a: clamp_f32(a, 0.0, 1.0),
        })
    }
    pub fn new_with_bytes_int32_boolean(
        bytes: Vec<u8>,
        pos: i32,
        inverted: bool,
    ) -> Result<Self, Error> {
        Self::from_bytes_slice(&bytes, pos, inverted, false)
    }
    pub fn new_with_bytes_int32_boolean_boolean(
        bytes: Vec<u8>,
        pos: i32,
        inverted: bool,
        alpha_inverted: bool,
    ) -> Result<Self, Error> {
        Self::from_bytes_slice(&bytes, pos, inverted, alpha_inverted)
    }
    fn from_bytes_slice(
        bytes: &[u8],
        pos: i32,
        inverted: bool,
        alpha_inverted: bool,
    ) -> Result<Self, Error> {
        let range = checked_range(pos, 4, bytes.len())?;
        let values = &bytes[range];
        const QUANTA: f32 = 1.0 / 255.0;
        let channel = |value: u8| f32::from(if inverted { 255 - value } else { value }) * QUANTA;
        let mut result = Self {
            r: channel(values[0]),
            g: channel(values[1]),
            b: channel(values[2]),
            a: channel(values[3]),
        };
        if alpha_inverted {
            result.a = 1.0 - result.a;
        }
        Ok(result)
    }
    pub fn from_bytes_with_bytes_int32_boolean(
        bytes: Vec<u8>,
        pos: i32,
        inverted: bool,
    ) -> Result<Self, Error> {
        Self::new_with_bytes_int32_boolean(bytes, pos, inverted)
    }
    pub fn from_bytes_with_bytes_int32_boolean_boolean(
        bytes: Vec<u8>,
        pos: i32,
        inverted: bool,
        alpha_inverted: bool,
    ) -> Result<Self, Error> {
        Self::new_with_bytes_int32_boolean_boolean(bytes, pos, inverted, alpha_inverted)
    }

    pub fn compare_to(&self, color: Self) -> Result<i32, Error> {
        let this_hue = self.get_hue()?;
        let that_hue = color.get_hue()?;
        Ok(if this_hue < 0.0 && that_hue < 0.0 {
            if self.r == color.r {
                compare_f32(self.a, color.a)
            } else {
                compare_f32(self.r, color.r)
            }
        } else if this_hue == that_hue {
            compare_f32(self.a, color.a)
        } else {
            compare_f32(this_hue, that_hue)
        })
    }
    pub fn get_bytes_with_method(&self) -> Result<Vec<u8>, Error> {
        self.get_bytes_with_boolean(false)
    }
    pub fn get_bytes_with_boolean(&self, inverted: bool) -> Result<Vec<u8>, Error> {
        let mut bytes = vec![0; 4];
        self.to_bytes_with_bytes_int32_boolean(&mut bytes, 0, inverted)?;
        Ok(bytes)
    }
    pub fn get_float_bytes(&self) -> Result<Vec<u8>, Error> {
        let mut bytes = vec![0; 16];
        self.to_float_bytes(&mut bytes, 0)?;
        Ok(bytes)
    }
    pub fn to_bytes_with_bytes_int32(&self, dest: &mut [u8], pos: i32) -> Result<(), Error> {
        self.to_bytes_with_bytes_int32_boolean(dest, pos, false)
    }
    pub fn to_bytes_with_bytes_int32_boolean(
        &self,
        dest: &mut [u8],
        pos: i32,
        inverted: bool,
    ) -> Result<(), Error> {
        for (offset, value) in [(0, self.r), (1, self.g), (2, self.b), (3, self.a)] {
            let index = checked_range(
                pos.checked_add(offset).ok_or(Error::IndexOutOfRange)?,
                1,
                dest.len(),
            )?
            .start;
            let byte = float_to_byte(value);
            dest[index] = if inverted { 255 - byte } else { byte };
        }
        Ok(())
    }
    pub fn to_float_bytes(&self, dest: &mut [u8], pos: i32) -> Result<(), Error> {
        for (offset, value) in [(0, self.r), (4, self.g), (8, self.b), (12, self.a)] {
            crate::byte_order::write_single_little_endian(
                dest,
                pos.checked_add(offset).ok_or(Error::IndexOutOfRange)?,
                value,
            )?;
        }
        Ok(())
    }
    pub fn get_hue(&self) -> Result<f32, Error> {
        const HUE_MAX: f32 = 360.0;
        let max = max_f32(max_f32(self.r, self.g), self.b);
        let min = min_f32(min_f32(self.r, self.g), self.b);
        let tolerance = (f64::from(max) * 0.000_01).abs();
        if f64::from((max - min).abs()) < tolerance {
            return Ok(-1.0);
        }
        if f64::from((self.r - max).abs()) < tolerance {
            let b_delta = ((max - self.b) * (HUE_MAX / 6.0) + (max - min) / 2.0) / (max - min);
            let g_delta = ((max - self.g) * (HUE_MAX / 6.0) + (max - min) / 2.0) / (max - min);
            Ok(b_delta - g_delta)
        } else if f64::from((self.g - max).abs()) < tolerance {
            let r_delta = ((max - self.r) * (HUE_MAX / 6.0) + (max - min) / 2.0) / (max - min);
            let b_delta = ((max - self.b) * (HUE_MAX / 6.0) + (max - min) / 2.0) / (max - min);
            Ok(HUE_MAX / 3.0 + r_delta - b_delta)
        } else {
            let g_delta = ((max - self.g) * (HUE_MAX / 6.0) + (max - min) / 2.0) / (max - min);
            let r_delta = ((max - self.r) * (HUE_MAX / 6.0) + (max - min) / 2.0) / (max - min);
            Ok(2.0 * HUE_MAX / 3.0 + g_delta - r_delta)
        }
    }
    pub fn from_hsv(hue: f64, saturation: f64, value: f64) -> Result<Self, Error> {
        let (mut r, mut g, mut b) = (0.0, 0.0, 0.0);
        if saturation == 0.0 {
            r = value;
            g = value;
            b = value;
        } else {
            let sector_pos = hue / 60.0;
            let sector_number = sector_pos.floor() as i32;
            let fractional = sector_pos - f64::from(sector_number);
            let p = value * (1.0 - saturation);
            let q = value * (1.0 - saturation * fractional);
            let t = value * (1.0 - saturation * (1.0 - fractional));
            match sector_number {
                0 => {
                    r = value;
                    g = t;
                    b = p;
                }
                1 => {
                    r = q;
                    g = value;
                    b = p;
                }
                2 => {
                    r = p;
                    g = value;
                    b = t;
                }
                3 => {
                    r = p;
                    g = q;
                    b = value;
                }
                4 => {
                    r = t;
                    g = p;
                    b = value;
                }
                5 => {
                    r = value;
                    g = p;
                    b = q;
                }
                _ => {}
            }
        }
        Self::new_with_single_single_single_single(r as f32, g as f32, b as f32, 1.0)
    }
    pub fn lerp(value1: Self, value2: Self, amount: f32) -> Result<Self, Error> {
        Self::new_with_single_single_single_single(
            clamp_f32(lerp_f32(value1.r, value2.r, amount), 0.0, 1.0),
            clamp_f32(lerp_f32(value1.g, value2.g, amount), 0.0, 1.0),
            clamp_f32(lerp_f32(value1.b, value2.b, amount), 0.0, 1.0),
            clamp_f32(lerp_f32(value1.a, value2.a, amount), 0.0, 1.0),
        )
    }
    pub fn to_string(&self) -> String {
        format!(
            "<{}, {}, {}, {}>",
            format_f32(self.r),
            format_f32(self.g),
            format_f32(self.b),
            format_f32(self.a)
        )
    }
    pub fn to_rgb_string(&self) -> Result<String, Error> {
        Ok(format!(
            "<{}, {}, {}>",
            format_f32(self.r),
            format_f32(self.g),
            format_f32(self.b)
        ))
    }
    pub fn equals_with_object(&self, obj: Option<Object>) -> bool {
        matches!(obj, Some(Object::Color4(value)) if Self::eq(*self, value))
    }
    pub fn equals_with_color4(&self, other: Self) -> bool {
        Self::eq(*self, other)
    }
    pub fn get_hash_code(&self) -> i32 {
        hash_f32(self.r) ^ hash_f32(self.g) ^ hash_f32(self.b) ^ hash_f32(self.a)
    }
    pub fn eq(lhs: Self, rhs: Self) -> bool {
        lhs.r == rhs.r && lhs.g == rhs.g && lhs.b == rhs.b && lhs.a == rhs.a
    }
    pub fn ne(lhs: Self, rhs: Self) -> bool {
        !Self::eq(lhs, rhs)
    }
    pub fn add(lhs: Self, rhs: Self) -> Self {
        Self::channel_op(lhs, rhs, |a, b| a + b)
    }
    pub fn sub(lhs: Self, rhs: Self) -> Self {
        Self::channel_op(lhs, rhs, |a, b| a - b)
    }
    pub fn mul(lhs: Self, rhs: Self) -> Self {
        Self::channel_op(lhs, rhs, |a, b| a * b)
    }
    fn channel_op(lhs: Self, rhs: Self, op: impl Fn(f32, f32) -> f32) -> Self {
        Self {
            r: clamp_f32(op(lhs.r, rhs.r), 0.0, 1.0),
            g: clamp_f32(op(lhs.g, rhs.g), 0.0, 1.0),
            b: clamp_f32(op(lhs.b, rhs.b), 0.0, 1.0),
            a: clamp_f32(op(lhs.a, rhs.a), 0.0, 1.0),
        }
    }
}

fn float_to_byte(value: f32) -> u8 {
    (clamp_f32(value, 0.0, 1.0) * 255.0).floor() as u8
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn byte_and_float_serialization_match_golden_values() {
        let color = Color4::new_with_single_single_single_single(1.0, 0.5, 0.25, 0.0).unwrap();
        assert_eq!(color.get_bytes_with_method().unwrap(), [255, 127, 63, 0]);
        assert_eq!(
            color.get_bytes_with_boolean(true).unwrap(),
            [0, 128, 192, 255]
        );
        let decoded =
            Color4::new_with_bytes_int32_boolean_boolean(vec![0, 128, 192, 255], 0, true, true)
                .unwrap();
        assert_eq!(decoded.r, 1.0);
        assert_eq!(decoded.a, 1.0);
        let float_bytes = color.get_float_bytes().unwrap();
        assert_eq!(&float_bytes[0..4], &1.0_f32.to_le_bytes());
        assert_eq!(&float_bytes[4..8], &0.5_f32.to_le_bytes());
    }

    #[test]
    fn output_preserves_partial_mutation_before_bad_index() {
        let mut output = [0xaa; 2];
        assert_eq!(
            Color4::white().to_bytes_with_bytes_int32(&mut output, 0),
            Err(Error::IndexOutOfRange)
        );
        assert_eq!(output, [255, 255]);
        let mut floats = [0xaa; 6];
        assert_eq!(
            Color4::white().to_float_bytes(&mut floats, 0),
            Err(Error::IndexOutOfRange)
        );
        assert_eq!(&floats[..4], &1.0_f32.to_le_bytes());
        assert_eq!(&floats[4..], &[0xaa; 2]);
    }

    #[test]
    fn constructor_clamping_hsv_and_channel_operators_match_reference() {
        assert_eq!(
            Color4::new_with_single_single_single_single(-1.0, 0.5, 0.0, 1.0).unwrap(),
            Color4 {
                r: 0.0,
                g: 0.5,
                b: 0.0,
                a: 1.0
            }
        );
        assert_eq!(
            Color4::new_with_single_single_single_single(1.0001, 0.0, 0.0, 0.0),
            Err(Error::Argument)
        );
        assert_eq!(
            Color4::from_hsv(0.0, 1.0, 1.0).unwrap(),
            Color4 {
                r: 1.0,
                g: 0.0,
                b: 0.0,
                a: 1.0
            }
        );
        assert_eq!(
            Color4::add(
                Color4 {
                    r: 0.75,
                    g: 0.25,
                    b: 0.0,
                    a: 0.5
                },
                Color4 {
                    r: 0.5,
                    g: 0.5,
                    b: 0.5,
                    a: 0.75
                }
            ),
            Color4 {
                r: 1.0,
                g: 0.75,
                b: 0.5,
                a: 1.0
            }
        );
    }

    #[test]
    fn hue_comparison_strings_and_hashes_match_reference_rules() {
        let red = Color4 {
            r: 1.0,
            g: 0.0,
            b: 0.0,
            a: 1.0,
        };
        let green = Color4 {
            r: 0.0,
            g: 1.0,
            b: 0.0,
            a: 1.0,
        };
        assert_eq!(red.get_hue().unwrap(), 0.0);
        assert_eq!(green.get_hue().unwrap(), 120.0);
        assert_eq!(red.compare_to(green).unwrap(), -1);
        assert_eq!(red.to_string(), "<1, 0, 0, 1>");
        assert_eq!(red.to_rgb_string().unwrap(), "<1, 0, 0>");
        let negative_zero = Color4 { r: -0.0, ..red };
        let positive_zero = Color4 { r: 0.0, ..red };
        assert_eq!(negative_zero.get_hash_code(), positive_zero.get_hash_code());
        assert!(red.equals_with_object(Some(Object::from(red))));
    }
}
