// xfwl4 -- Wayland compositor for the Xfce Desktop Environment
//
// Copyright (C) 2026 Brian Tarricone <brian@tarricone.org>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU 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 General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program.  If not, see <https://www.gnu.org/licenses/>.
//
// Based on portions of xfwm4/src/compositor.c, which is:
//
// Copyright (C) 2005-2021 Olivier Fourdan

use std::{cell::RefCell, f64::consts::PI, sync::OnceLock};

use smithay::{
    backend::{
        allocator::Fourcc,
        renderer::{
            ImportMem, Renderer, Texture,
            element::{Id, Kind, texture::TextureRenderElement},
            gles::{GlesRenderer, GlesTexture},
        },
    },
    utils::{Logical, Physical, Point, Rectangle, Scale, Size, Transform},
};

use crate::core::{config::Xfwl4Config, util::PhysicalSizeExt};

const SHADOW_RADIUS: f64 = 12.;
const SHADOW_OFFSET_X: i32 = -3 * (SHADOW_RADIUS as i32) / 2;
const SHADOW_OFFSET_Y: i32 = -3 * (SHADOW_RADIUS as i32) / 2;

pub struct ShadowParams {
    pub offset: Point<i32, Physical>,
    pub size: Size<i32, Physical>,
    pub surface_size: Size<i32, Physical>,
}

impl ShadowParams {
    pub fn new(delta_loc: Point<i32, Physical>, delta_width: i32, delta_height: i32, surface_size: Size<i32, Physical>) -> Self {
        let gaussian_size = ((SHADOW_RADIUS * 3.).ceil() as i32 + 1) & !1;
        ShadowParams {
            offset: (SHADOW_OFFSET_X + delta_loc.x, SHADOW_OFFSET_Y + delta_loc.y).into(),
            size: (
                surface_size.w + gaussian_size - delta_width - delta_loc.x,
                surface_size.h + gaussian_size - delta_height - delta_loc.y,
            )
                .into(),
            surface_size,
        }
    }
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ShadowKey {
    pub opacity: i32,
    pub frame_size: Size<i32, Physical>,
    pub delta_loc: Point<i32, Physical>,
    pub delta_width: i32,
    pub delta_height: i32,
}

impl ShadowKey {
    pub fn from_config(config: &Xfwl4Config, frame_size: Size<i32, Physical>) -> Self {
        ShadowKey {
            opacity: config.shadow_opacity(),
            frame_size,
            delta_loc: (config.shadow_delta_x(), config.shadow_delta_y()).into(),
            delta_width: config.shadow_delta_width(),
            delta_height: config.shadow_delta_height(),
        }
    }

    pub fn opacity_f64(&self) -> f64 {
        (self.opacity as f64 / 100.).clamp(0., 1.)
    }

    pub fn params(&self) -> ShadowParams {
        ShadowParams::new(self.delta_loc, self.delta_width, self.delta_height, self.frame_size)
    }
}

#[derive(Debug, Clone)]
pub struct ShadowTexture {
    pub key: ShadowKey,
    pub offset: Point<i32, Physical>,
    pub render_size: Size<i32, Physical>,
    pub tex_id: Id,
    pub tex: GlesTexture,
}

impl ShadowTexture {
    pub fn render(renderer: &mut GlesRenderer, key: ShadowKey) -> Option<Self> {
        let params = key.params();

        if let Some(data) = make_shadow(key.opacity_f64(), &params) {
            let mut rgba = vec![0u8; data.len() * 4];
            // An iterator would be more idiomatic, but in debug builds, rustc doesn't optimize it
            // well, and we get 20fps during window resizes.
            for (i, &alpha) in data.iter().enumerate() {
                rgba[i * 4 + 3] = alpha;
            }

            let size = params.size.to_buffer();
            match renderer.import_memory(&rgba, Fourcc::Abgr8888, size, false) {
                Err(err) => {
                    tracing::warn!("Failed to import shadow texture: {err}");
                    None
                }
                Ok(tex) => Some(ShadowTexture {
                    key,
                    offset: params.offset,
                    render_size: params.size,
                    tex_id: Id::new(),
                    tex,
                }),
            }
        } else {
            None
        }
    }

    pub fn render_element(
        &self,
        renderer: &GlesRenderer,
        shadow_location: Point<f64, Physical>,
        scale: Scale<f64>,
        alpha: f32,
    ) -> TextureRenderElement<GlesTexture> {
        let buffer_scale = 1;
        let tex_src: Rectangle<f64, Logical> = Rectangle::from_size(self.tex.size().to_logical(buffer_scale, Transform::Normal)).to_f64();
        // The shadow is generated at physical resolution, so render it at a logical size of
        // render_size / scale: it lands at a fixed physical pixel size regardless of output scale.
        let render_size = self.render_size.to_f64().to_logical(scale).to_i32_round();
        TextureRenderElement::from_static_texture(
            self.tex_id.clone(),
            renderer.context_id(),
            shadow_location,
            self.tex.clone(),
            buffer_scale,
            Transform::Normal,
            Some(alpha),
            Some(tex_src),
            Some(render_size),
            None,
            Kind::Unspecified,
        )
    }
}

// Holds a few shadow textures keyed by their `ShadowKey`, so a window straddling outputs of
// different scale keeps a texture per output (it is rendered at each output's frame size in the
// same frame) instead of the two evicting each other.  Bounded FIFO: a resize churns through frame
// sizes every frame, so without a cap the cache would grow unboundedly.
const SHADOW_CACHE_CAPACITY: usize = 4;

#[derive(Debug, Clone)]
pub struct ShadowCache(pub RefCell<Vec<ShadowTexture>>);

impl ShadowCache {
    pub fn new() -> Self {
        ShadowCache(RefCell::new(Vec::new()))
    }

    pub fn get(&self, key: ShadowKey) -> Option<ShadowTexture> {
        self.0.borrow().iter().find(|inner| inner.key == key).cloned()
    }

    pub fn set(&self, texture: ShadowTexture) {
        let mut cache = self.0.borrow_mut();
        cache.retain(|inner| inner.key != texture.key);
        if cache.len() >= SHADOW_CACHE_CAPACITY {
            cache.remove(0);
        }
        cache.push(texture);
    }

    pub fn clear(&self) {
        self.0.borrow_mut().clear();
    }

    pub fn render_element(
        &self,
        key: ShadowKey,
        renderer: &mut GlesRenderer,
        surface_location: Point<i32, Physical>,
        scale: Scale<f64>,
        alpha: f32,
    ) -> Option<TextureRenderElement<GlesTexture>> {
        let shadow_location = |tex: &ShadowTexture| surface_location.to_f64() + tex.offset.to_f64();

        if let Some(shadow_tex) = self.get(key) {
            Some(shadow_tex.render_element(renderer, shadow_location(&shadow_tex), scale, alpha))
        } else if let Some(shadow_tex) = ShadowTexture::render(renderer, key) {
            let elem = shadow_tex.render_element(renderer, shadow_location(&shadow_tex), scale, alpha);
            self.set(shadow_tex);
            Some(elem)
        } else {
            None
        }
    }
}

struct GaussianConv {
    size: i32,
    data: Vec<f64>,
}

fn gaussian(r: f64, x: f64, y: f64) -> f64 {
    (1. / (2. * PI * r).sqrt()) * (-(x * x + y * y) / (2. * r * r)).exp()
}

fn make_gaussian_map(r: f64) -> GaussianConv {
    let size = ((r * 3.).ceil() as i32 + 1) & !1;
    let center = size / 2;
    let mut data: Vec<f64> = (0..size)
        .flat_map(|y| (0..size).map(move |x| gaussian(r, (x - center) as f64, (y - center) as f64)))
        .collect();
    let total: f64 = data.iter().sum();
    for v in data.iter_mut() {
        *v /= total;
    }
    GaussianConv { size, data }
}

fn sum_gaussian(map: &GaussianConv, opacity: f64, x: i32, y: i32, width: i32, height: i32) -> u8 {
    let g_size = map.size as usize;
    let center = map.size / 2;
    let fx_start = (center - x).max(0) as usize;
    let fx_end = (width + center - x).min(map.size) as usize;
    let fy_start = (center - y).max(0) as usize;
    let fy_end = (height + center - y).min(map.size) as usize;

    let v: f64 = map
        .data
        .chunks(g_size)
        .skip(fy_start)
        .take(fy_end - fy_start)
        .filter_map(|row| row.get(fx_start..fx_end))
        .flatten()
        .sum();

    (v.min(1.) * opacity * 255.) as u8
}

struct ShadowLookup {
    shadow_corner: Vec<u8>,
    shadow_top: Vec<u8>,
    stride: usize,
}

impl ShadowLookup {
    fn corner(&self, opacity: i32, y: i32, x: i32) -> u8 {
        let idx = (opacity as usize) * self.stride * self.stride + (y as usize) * self.stride + x as usize;
        self.shadow_corner.get(idx).copied().unwrap_or_else(|| {
            tracing::error!(
                "BUG: shadow corner out of bounds: opacity={opacity}, y={y}, x={x}, stride={}",
                self.stride,
            );
            0
        })
    }

    fn set_corner(&mut self, opacity: i32, y: i32, x: i32, val: u8) {
        let idx = (opacity as usize) * self.stride * self.stride + (y as usize) * self.stride + x as usize;
        if let Some(slot) = self.shadow_corner.get_mut(idx) {
            *slot = val;
        } else {
            tracing::error!(
                "BUG: shadow corner set out of bounds: opacity={opacity}, y={y}, x={x}, stride={}",
                self.stride,
            );
        }
    }

    fn top(&self, opacity: i32, x: i32) -> u8 {
        let idx = (opacity as usize) * self.stride + x as usize;
        self.shadow_top.get(idx).copied().unwrap_or_else(|| {
            tracing::error!("BUG: shadow top out of bounds: opacity={opacity}, x={x}, stride={}", self.stride,);
            0
        })
    }

    fn set_top(&mut self, opacity: i32, x: i32, val: u8) {
        let idx = (opacity as usize) * self.stride + x as usize;
        if let Some(slot) = self.shadow_top.get_mut(idx) {
            *slot = val;
        } else {
            tracing::error!(
                "BUG: shadow top set out of bounds: opacity={opacity}, x={x}, stride={}",
                self.stride,
            );
        }
    }
}

fn presum_gaussian(map: &GaussianConv) -> ShadowLookup {
    let gaussian_size = map.size;
    let center = gaussian_size / 2;
    let stride = (gaussian_size + 1) as usize;

    let mut lookup = ShadowLookup {
        shadow_corner: vec![0u8; stride * stride * 26],
        shadow_top: vec![0u8; stride * 26],
        stride,
    };

    for x in 0..=gaussian_size {
        let base_top = sum_gaussian(map, 1., x - center, center, gaussian_size * 2, gaussian_size * 2);
        lookup.set_top(25, x, base_top);
        for opacity in 0..25 {
            lookup.set_top(opacity, x, (base_top as i32 * opacity / 25) as u8);
        }

        for y in 0..=x {
            let base_corner = sum_gaussian(map, 1., x - center, y - center, gaussian_size * 2, gaussian_size * 2);
            lookup.set_corner(25, y, x, base_corner);
            lookup.set_corner(25, x, y, base_corner);
            for opacity in 0..25 {
                let val = (base_corner as i32 * opacity / 25) as u8;
                lookup.set_corner(opacity, y, x, val);
                lookup.set_corner(opacity, x, y, val);
            }
        }
    }

    lookup
}

struct ShadowImage {
    data: Vec<u8>,
    width: i32,
}

impl ShadowImage {
    fn new(width: i32, height: i32, fill: u8) -> Self {
        ShadowImage {
            data: vec![fill; (width * height) as usize],
            width,
        }
    }

    fn set(&mut self, x: i32, y: i32, val: u8) {
        let idx = (y * self.width + x) as usize;
        if let Some(slot) = self.data.get_mut(idx) {
            *slot = val;
        } else {
            tracing::error!("shadow image set out of bounds: x={x}, y={y}, width={}", self.width,);
        }
    }
}

struct GaussianLookup {
    map: GaussianConv,
    lookup: ShadowLookup,
}

fn gaussian_lookup() -> &'static GaussianLookup {
    static LOOKUP: OnceLock<GaussianLookup> = OnceLock::new();
    LOOKUP.get_or_init(|| {
        let map = make_gaussian_map(SHADOW_RADIUS);
        let lookup = presum_gaussian(&map);
        GaussianLookup { map, lookup }
    })
}

fn make_shadow(opacity: f64, params: &ShadowParams) -> Option<Vec<u8>> {
    let gl = gaussian_lookup();
    let map = &gl.map;
    let lookup = &gl.lookup;
    let gaussian_size = map.size;
    let center = gaussian_size / 2;

    let swidth = params.size.w;
    let sheight = params.size.h;
    let size = params.surface_size;
    let opacity_int = (opacity * 25.) as i32;

    if swidth < 1 || sheight < 1 {
        None
    } else {
        profiling::scope!("render_shadow_pixels");
        let d = if gaussian_size > 0 {
            lookup.top(opacity_int, gaussian_size)
        } else {
            sum_gaussian(map, opacity, center, center, size.w, size.h)
        };
        let mut img = ShadowImage::new(swidth, sheight, d);

        let ylimit = gaussian_size.min((sheight + 1) / 2);
        let xlimit = gaussian_size.min((swidth + 1) / 2);

        // corners
        for y in 0..ylimit {
            for x in 0..xlimit {
                let d = if xlimit == gaussian_size && ylimit == gaussian_size {
                    lookup.corner(opacity_int, y, x)
                } else {
                    sum_gaussian(map, opacity, x - center, y - center, size.w, size.h)
                };

                img.set(x, y, d);
                img.set(x, sheight - y - 1, d);
                img.set(swidth - x - 1, sheight - y - 1, d);
                img.set(swidth - x - 1, y, d);
            }
        }

        // top/bottom edges
        let x_diff = swidth - (gaussian_size * 2);
        if x_diff > 0 && ylimit > 0 {
            for y in 0..ylimit {
                let d = if ylimit == gaussian_size {
                    lookup.top(opacity_int, y)
                } else {
                    sum_gaussian(map, opacity, center, y - center, size.w, size.h)
                };
                for i in 0..x_diff {
                    img.set(gaussian_size + i, y, d);
                    img.set(gaussian_size + i, sheight - y - 1, d);
                }
            }
        }

        // sides
        for x in 0..xlimit {
            let d = if xlimit == gaussian_size {
                lookup.top(opacity_int, x)
            } else {
                sum_gaussian(map, opacity, x - center, center, size.w, size.h)
            };
            for y in gaussian_size..(sheight - gaussian_size) {
                img.set(x, y, d);
                img.set(swidth - x - 1, y, d);
            }
        }

        Some(img.data)
    }
}
