//! Bounded, platform-neutral avatar texture compositing.

use crate::targa::Targa;
use crate::{AppearanceManagerTextureData, AvatarTextureIndex, BakeType, Error};
use libremetaverse_imaging::{ManagedImage, ManagedImageImageChannels as Channels};
use libremetaverse_types::Color4;
use std::collections::HashMap;
use std::path::{Path, PathBuf};
use std::sync::{Mutex, OnceLock};

const CLASSIC_BAKE_SIZE: i32 = 1024;
const EYE_BAKE_SIZE: i32 = 128;

fn cache() -> &'static Mutex<HashMap<String, ManagedImage>> {
    static CACHE: OnceLock<Mutex<HashMap<String, ManagedImage>>> = OnceLock::new();
    CACHE.get_or_init(|| Mutex::new(HashMap::new()))
}

/// A compositor owns its input images so callers can freely reuse or mutate
/// their originals after adding a layer.
pub struct Baker {
    bake_type: BakeType,
    bake_width: i32,
    bake_height: i32,
    textures: Vec<AppearanceManagerTextureData>,
    baked_texture: Option<crate::assets::AssetTexture>,
}

impl Baker {
    #[allow(clippy::unnecessary_wraps)] // The generated constructor uses the crate Result model.
    pub(crate) fn native_new(bake_type: BakeType) -> Result<Self, Error> {
        let size = if bake_type == BakeType::Eyes {
            EYE_BAKE_SIZE
        } else {
            CLASSIC_BAKE_SIZE
        };
        Ok(Self {
            bake_type,
            bake_width: size,
            bake_height: size,
            textures: Vec::new(),
            baked_texture: None,
        })
    }

    pub(crate) fn native_add_texture(
        &mut self,
        texture: AppearanceManagerTextureData,
    ) -> Result<(), Error> {
        if self.textures.len() >= 60 {
            return Err(Error::Argument);
        }
        self.textures.push(texture);
        Ok(())
    }

    pub(crate) fn native_bake(&mut self) -> Result<(), Error> {
        let channels = Channels::COLOR | Channels::ALPHA | Channels::BUMP;
        let mut output = ManagedImage::new(self.bake_width, self.bake_height, channels)?;
        if self.bake_type == BakeType::Eyes {
            fill_dithered(&mut output, Color4::white());
        } else if let Some(first) = self.textures.first() {
            fill_dithered(&mut output, first.color);
        }

        let has_skin = self.textures.iter().any(|layer| {
            layer.texture.is_some()
                && matches!(
                    layer.texture_index,
                    AvatarTextureIndex::HeadBodypaint
                        | AvatarTextureIndex::UpperBodypaint
                        | AvatarTextureIndex::LowerBodypaint
                )
        });

        if self.bake_type == BakeType::Head {
            if let Some(layer) = Self::native_load_resource_layer("head_color.tga".to_owned())? {
                draw_layer(&mut output, layer, false)?;
            }
            if let Some(mask) = Self::native_load_resource_layer("head_alpha.tga".to_owned())? {
                apply_min_alpha_resized(&mut output, mask)?;
            }
            if let Some(mask) = Self::native_load_resource_layer("head_skingrain.tga".to_owned())? {
                multiply_color_mask(&mut output, mask)?;
            }
        } else if !has_skin && matches!(self.bake_type, BakeType::UpperBody | BakeType::LowerBody) {
            let name = if self.bake_type == BakeType::UpperBody {
                "upperbody_color.tga"
            } else {
                "lowerbody_color.tga"
            };
            if let Some(layer) = Self::native_load_resource_layer(name.to_owned())? {
                draw_layer(&mut output, layer, false)?;
            }
        }

        let mut alpha_wearables = Vec::new();
        let mut deferred_head_skin = None;
        let mut deferred_head_tattoos = Vec::new();
        for (position, layer) in self.textures.iter().enumerate() {
            let Some(asset) = &layer.texture else {
                continue;
            };
            let Some(source) = &asset.image else { continue };
            let index = layer.texture_index as i32;
            if (AvatarTextureIndex::LowerAlpha as i32..=AvatarTextureIndex::HairAlpha as i32)
                .contains(&index)
            {
                if source.channels.contains(Channels::ALPHA) {
                    alpha_wearables.push(source.clone()?);
                }
                continue;
            }
            if self.bake_type == BakeType::Head
                && matches!(
                    layer.texture_index,
                    AvatarTextureIndex::HeadBodypaint
                        | AvatarTextureIndex::HeadTattoo
                        | AvatarTextureIndex::HeadUniversalTattoo
                )
            {
                if layer.texture_index == AvatarTextureIndex::HeadBodypaint {
                    deferred_head_skin = Some(layer);
                } else {
                    deferred_head_tattoos.push(layer);
                }
                continue;
            }
            composite_texture(
                &mut output,
                layer,
                self.bake_width,
                self.bake_height,
                self.bake_type,
                has_skin,
                position == 0 && matches!(self.bake_type, BakeType::Skirt | BakeType::Hair),
            )?;
        }
        if let Some(layer) = deferred_head_skin {
            draw_head_layer(&mut output, layer, self.bake_width, self.bake_height)?;
        }
        for layer in deferred_head_tattoos {
            draw_head_layer(&mut output, layer, self.bake_width, self.bake_height)?;
        }
        for mut mask in alpha_wearables {
            mask.resize_nearest_neighbor(self.bake_width, self.bake_height)?;
            apply_min_alpha(&mut output, &mask);
        }

        let texture = crate::assets::AssetTexture::new_with_managed_image(output)?;
        // JPEG2000 is the protocol representation. Keep the decoded image even
        // when a build intentionally excludes that optional codec.
        #[cfg(any(feature = "jpeg2000", feature = "rust-j2k"))]
        texture.encode()?;
        self.baked_texture = Some(texture);
        Ok(())
    }

    pub(crate) fn native_bake_type_for(index: AvatarTextureIndex) -> BakeType {
        match index {
            AvatarTextureIndex::HeadBodypaint => BakeType::Head,
            AvatarTextureIndex::UpperBodypaint
            | AvatarTextureIndex::UpperGloves
            | AvatarTextureIndex::UpperUndershirt
            | AvatarTextureIndex::UpperShirt
            | AvatarTextureIndex::UpperJacket => BakeType::UpperBody,
            AvatarTextureIndex::LowerBodypaint
            | AvatarTextureIndex::LowerUnderpants
            | AvatarTextureIndex::LowerSocks
            | AvatarTextureIndex::LowerShoes
            | AvatarTextureIndex::LowerPants
            | AvatarTextureIndex::LowerJacket => BakeType::LowerBody,
            AvatarTextureIndex::EyesIris => BakeType::Eyes,
            AvatarTextureIndex::Skirt => BakeType::Skirt,
            AvatarTextureIndex::Hair => BakeType::Hair,
            _ => BakeType::Unknown,
        }
    }

    pub(crate) fn native_load_resource_layer(
        file_name: String,
    ) -> Result<Option<ManagedImage>, Error> {
        let path = Path::new(&file_name);
        let supported = path.extension().is_some_and(|extension| {
            extension.eq_ignore_ascii_case("tga") || extension.eq_ignore_ascii_case("dds")
        });
        if file_name.is_empty() || path.components().count() != 1 || !supported {
            return Ok(None);
        }
        if let Some(image) = cache()
            .lock()
            .unwrap_or_else(std::sync::PoisonError::into_inner)
            .get(&file_name)
        {
            return image.clone().map(Some);
        }
        let image = resource_candidates(&file_name)
            .into_iter()
            .find_map(|path| {
                Targa::decode_to_managed_image_with_string(path.to_string_lossy().into_owned()).ok()
            })
            .or_else(|| bundled_fallback(&file_name).ok().flatten());
        if let Some(image) = image {
            let result = image.clone()?;
            cache()
                .lock()
                .unwrap_or_else(std::sync::PoisonError::into_inner)
                .insert(file_name, image);
            Ok(Some(result))
        } else {
            Ok(None)
        }
    }

    pub(crate) fn native_resize_to_bake_dimensions(
        texture: ManagedImage,
        width: i32,
        height: i32,
    ) -> Result<Option<ManagedImage>, Error> {
        if texture.width == width && texture.height == height {
            return Ok(Some(texture));
        }
        if texture.width < width && texture.height < height {
            return tile(&texture, width, height).map(Some);
        }
        let mut texture = texture;
        texture.resize_nearest_neighbor(width, height)?;
        Ok(Some(texture))
    }

    pub(crate) const fn native_bake_height(&self) -> i32 {
        self.bake_height
    }
    pub(crate) const fn native_bake_width(&self) -> i32 {
        self.bake_width
    }
    pub(crate) const fn native_bake_type(&self) -> BakeType {
        self.bake_type
    }
    pub(crate) fn native_baked_texture(&self) -> Option<crate::assets::AssetTexture> {
        self.baked_texture
            .as_ref()
            .and_then(|texture| texture.native_deep_clone().ok())
    }
    pub(crate) fn native_textures(&self) -> Vec<AppearanceManagerTextureData> {
        self.textures
            .iter()
            .filter_map(clone_texture_data)
            .collect()
    }
}

fn clone_texture_data(
    layer: &AppearanceManagerTextureData,
) -> Option<AppearanceManagerTextureData> {
    Some(AppearanceManagerTextureData {
        alpha_masks: layer.alpha_masks.clone(),
        color: layer.color,
        texture: layer
            .texture
            .as_ref()
            .map(crate::assets::AssetTexture::native_deep_clone)
            .transpose()
            .ok()?,
        texture_id: layer.texture_id,
        texture_index: layer.texture_index,
    })
}

fn resource_candidates(name: &str) -> Vec<PathBuf> {
    let mut roots = Vec::new();
    if let Some(root) = std::env::var_os("LIBREMETAVERSE_RESOURCE_DIR") {
        roots.push(PathBuf::from(root));
    }
    roots.push(PathBuf::from(crate::Settings::resource_dir()));
    roots.push(PathBuf::from("crates/libremetaverse/linden"));
    roots
        .into_iter()
        .map(|root| root.join("character").join(name))
        .collect()
}

fn bundled_fallback(name: &str) -> Result<Option<ManagedImage>, Error> {
    // The canonical character XML is embedded in generated Rust. These neutral
    // base masks keep head/upper/lower baking functional in minimal deployments
    // that deliberately omit the optional viewer resource bundle.
    let (channels, value) = match name {
        "head_color.tga" | "upperbody_color.tga" | "lowerbody_color.tga" => (Channels::COLOR, 255),
        "head_alpha.tga" | "head_skingrain.tga" | "head_hair.tga" => (Channels::GRAY, 255),
        _ => return Ok(None),
    };
    let mut image = ManagedImage::new(512, 512, channels)?;
    image.red.fill(value);
    image.green.fill(value);
    image.blue.fill(value);
    Ok(Some(image))
}

fn tile(source: &ManagedImage, width: i32, height: i32) -> Result<ManagedImage, Error> {
    source.validate()?;
    let mut output = ManagedImage::new(width, height, source.channels)?;
    let sw = usize::try_from(source.width).map_err(|_| Error::Argument)?;
    let sh = usize::try_from(source.height).map_err(|_| Error::Argument)?;
    let dw = usize::try_from(width).map_err(|_| Error::Argument)?;
    let dh = usize::try_from(height).map_err(|_| Error::Argument)?;
    for y in 0..dh {
        for x in 0..dw {
            let src = (y % sh) * sw + x % sw;
            let dst = y * dw + x;
            for (target, input) in [
                (&mut output.red, &source.red),
                (&mut output.green, &source.green),
                (&mut output.blue, &source.blue),
                (&mut output.alpha, &source.alpha),
                (&mut output.bump, &source.bump),
            ] {
                if !input.is_empty() {
                    target[dst] = input[src];
                }
            }
        }
    }
    Ok(output)
}

fn composite_texture(
    output: &mut ManagedImage,
    layer: &AppearanceManagerTextureData,
    width: i32,
    height: i32,
    bake_type: BakeType,
    has_skin: bool,
    preserve_alpha: bool,
) -> Result<(), Error> {
    let source = layer
        .texture
        .as_ref()
        .and_then(|asset| asset.image.as_ref())
        .ok_or(Error::InvalidOperation)?
        .clone()?;
    let mut source = Baker::native_resize_to_bake_dimensions(source, width, height)?
        .ok_or(Error::InvalidOperation)?;
    let skin = matches!(
        layer.texture_index,
        AvatarTextureIndex::HeadBodypaint
            | AvatarTextureIndex::UpperBodypaint
            | AvatarTextureIndex::LowerBodypaint
    );
    if !has_skin && bake_type == BakeType::Head && layer.texture_index == AvatarTextureIndex::Hair {
        if source.channels.contains(Channels::ALPHA) {
            source.alpha.fill(255);
        }
        if let Some(mask) = Baker::native_load_resource_layer("head_hair.tga".to_owned())? {
            multiply_color_mask(&mut source, mask)?;
        }
    }
    if !skin {
        tint(&mut source, layer.color);
    }
    if bake_type == BakeType::Hair {
        if source.channels.contains(Channels::ALPHA) {
            output.bump.clone_from(&source.alpha);
        } else {
            output.bump.fill(255);
        }
    }
    if !skin && bake_type != BakeType::Hair && !layer.alpha_masks.is_empty() {
        let mut combined = ManagedImage::new(width, height, Channels::ALPHA)?;
        let mut normal_count = 0;
        for (param, value) in &layer.alpha_masks {
            if !param.multiply_blend
                && mask_belongs(bake_type, &param.tga_file)
                && (*value > 0.0 || !param.skip_if_zero)
            {
                apply_parameter_mask(&mut combined, param, *value)?;
                normal_count += 1;
            }
        }
        if normal_count == 0 {
            combined.alpha.fill(255);
        }
        for (param, value) in &layer.alpha_masks {
            if param.multiply_blend
                && mask_belongs(bake_type, &param.tga_file)
                && (*value > 0.0 || !param.skip_if_zero)
            {
                apply_parameter_mask(&mut combined, param, *value)?;
            }
        }
        if !source.channels.contains(Channels::ALPHA) {
            source.convert_channels(source.channels | Channels::ALPHA)?;
        }
        apply_min_alpha(&mut source, &combined);
        if layer.texture_index == crate::appearance_manager::morph_layer_for_bake_type(bake_type) {
            output.bump.clone_from(&source.alpha);
        }
    }
    draw_layer(output, source, preserve_alpha)
}

fn draw_head_layer(
    output: &mut ManagedImage,
    layer: &AppearanceManagerTextureData,
    width: i32,
    height: i32,
) -> Result<(), Error> {
    let source = layer
        .texture
        .as_ref()
        .and_then(|asset| asset.image.as_ref())
        .ok_or(Error::InvalidOperation)?
        .clone()?;
    let source = Baker::native_resize_to_bake_dimensions(source, width, height)?
        .ok_or(Error::InvalidOperation)?;
    draw_layer(output, source, false)
}

fn mask_belongs(bake: BakeType, name: &str) -> bool {
    (bake != BakeType::LowerBody || (!name.contains("upper") && !name.contains("shirt")))
        && (bake != BakeType::UpperBody || !name.contains("lower"))
}

fn effective_mask(image: &ManagedImage) -> Vec<u8> {
    if image.channels.contains(Channels::ALPHA) {
        return image.alpha.clone();
    }
    if image.channels.contains(Channels::GRAY) {
        return image.red.clone();
    }
    if image.channels.contains(Channels::COLOR) {
        return image
            .red
            .iter()
            .zip(&image.green)
            .zip(&image.blue)
            .map(|((&red, &green), &blue)| {
                u8::try_from((u16::from(red) + u16::from(green) + u16::from(blue)) / 3)
                    .unwrap_or(255)
            })
            .collect();
    }
    let length = image
        .width
        .checked_mul(image.height)
        .and_then(|value| usize::try_from(value).ok())
        .unwrap_or(0);
    vec![255; length]
}

fn apply_parameter_mask(
    destination: &mut ManagedImage,
    param: &crate::VisualAlphaParam,
    value: f32,
) -> Result<(), Error> {
    let Some(mut source) = Baker::native_load_resource_layer(param.tga_file.clone())? else {
        return Ok(());
    };
    source.resize_nearest_neighbor(destination.width, destination.height)?;
    let mask = effective_mask(&source);
    let threshold = quantize_u8(1.0 - value.clamp(0.0, 1.0));
    for (target, sample) in destination.alpha.iter_mut().zip(mask) {
        let alpha = if sample <= threshold { 0 } else { 255 };
        if param.multiply_blend {
            *target = ((u16::from(*target) * u16::from(alpha)) >> 8) as u8;
        } else {
            *target = (*target).max(alpha);
        }
    }
    Ok(())
}

fn apply_min_alpha_resized(
    destination: &mut ManagedImage,
    mut source: ManagedImage,
) -> Result<(), Error> {
    source.resize_nearest_neighbor(destination.width, destination.height)?;
    let mask = effective_mask(&source);
    for (target, sample) in destination.alpha.iter_mut().zip(mask) {
        *target = (*target).min(sample);
    }
    Ok(())
}

fn multiply_color_mask(
    destination: &mut ManagedImage,
    mut source: ManagedImage,
) -> Result<(), Error> {
    source.resize_nearest_neighbor(destination.width, destination.height)?;
    let mask = effective_mask(&source);
    if !destination.channels.contains(Channels::COLOR) {
        return Ok(());
    }
    for (index, sample) in mask.into_iter().enumerate() {
        destination.red[index] =
            ((u16::from(destination.red[index]) * u16::from(sample)) >> 8) as u8;
        destination.green[index] =
            ((u16::from(destination.green[index]) * u16::from(sample)) >> 8) as u8;
        destination.blue[index] =
            ((u16::from(destination.blue[index]) * u16::from(sample)) >> 8) as u8;
    }
    Ok(())
}

fn fill_dithered(image: &mut ManagedImage, color: Color4) {
    let (red, green, blue) = (
        quantize_u8(color.r),
        quantize_u8(color.g),
        quantize_u8(color.b),
    );
    let alt = |value: u8| if value == 255 { 254 } else { value + 1 };
    let width = usize::try_from(image.width).unwrap_or(1);
    for (index, target_red) in image.red.iter_mut().enumerate() {
        let column = index % width;
        let row = index / width;
        let alternate = ((column ^ row) & 0x10) == 0;
        *target_red = if alternate { alt(red) } else { red };
        image.green[index] = if alternate { green } else { alt(green) };
        image.blue[index] = if alternate { blue } else { alt(blue) };
    }
    image.alpha.fill(255);
    image.bump.fill(0);
}

fn tint(image: &mut ManagedImage, color: Color4) {
    if image.channels.contains(Channels::GRAY) && !image.channels.contains(Channels::COLOR) {
        let gray = image.red.clone();
        image.channels = Channels(image.channels.0 & !Channels::GRAY.0) | Channels::COLOR;
        image.red.clone_from(&gray);
        image.green.clone_from(&gray);
        image.blue = gray;
    }
    if !image.channels.contains(Channels::COLOR) {
        return;
    }
    let (red, green, blue) = (
        u16::from(quantize_u8(color.r)),
        u16::from(quantize_u8(color.g)),
        u16::from(quantize_u8(color.b)),
    );
    for index in 0..image.red.len() {
        image.red[index] = ((u16::from(image.red[index]) * red) >> 8) as u8;
        image.green[index] = ((u16::from(image.green[index]) * green) >> 8) as u8;
        image.blue[index] = ((u16::from(image.blue[index]) * blue) >> 8) as u8;
    }
}

#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
fn quantize_u8(value: f32) -> u8 {
    (value.clamp(0.0, 1.0) * 255.0).round() as u8
}

fn draw_layer(
    output: &mut ManagedImage,
    mut source: ManagedImage,
    add_source_alpha: bool,
) -> Result<(), Error> {
    if source.width != output.width || source.height != output.height {
        source.resize_nearest_neighbor(output.width, output.height)?;
    }
    let color = source.channels.contains(Channels::COLOR);
    let gray = source.channels.contains(Channels::GRAY);
    let alpha = source.channels.contains(Channels::ALPHA);
    for index in 0..output.red.len() {
        let source_alpha = if alpha { source.alpha[index] } else { 255 };
        let inverse_alpha = 255_u16 - u16::from(source_alpha);
        if color || gray {
            let sr = source.red[index];
            let sg = if color { source.green[index] } else { sr };
            let sb = if color { source.blue[index] } else { sr };
            if alpha {
                output.red[index] = ((u16::from(output.red[index]) * inverse_alpha
                    + u16::from(sr) * u16::from(source_alpha))
                    >> 8) as u8;
                output.green[index] = ((u16::from(output.green[index]) * inverse_alpha
                    + u16::from(sg) * u16::from(source_alpha))
                    >> 8) as u8;
                output.blue[index] = ((u16::from(output.blue[index]) * inverse_alpha
                    + u16::from(sb) * u16::from(source_alpha))
                    >> 8) as u8;
            } else {
                output.red[index] = sr;
                output.green[index] = sg;
                output.blue[index] = sb;
            }
        }
        if add_source_alpha && alpha {
            output.alpha[index] = output.alpha[index].min(source_alpha);
        }
        if source.channels.contains(Channels::BUMP) {
            output.bump[index] = source.bump[index];
        }
    }
    Ok(())
}

fn apply_min_alpha(output: &mut ManagedImage, mask: &ManagedImage) {
    let effective = if mask.channels.contains(Channels::ALPHA) {
        &mask.alpha
    } else {
        &mask.red
    };
    for (target, source) in output.alpha.iter_mut().zip(effective) {
        *target = (*target).min(*source);
    }
}
