use common::{
    cache::{get_previous_image_cache, read_cache_file},
    ipc::{BgImg, BgInfo, PixelFormat, Scale},
    log::{debug, error, warn},
};
use waybackend::{Waybackend, objman::ObjectManager, types::ObjectId};

use core::num::NonZeroI32;

mod bump_pool;
mod cell;

use crate::output_info::OutputInfo;
pub use cell::WallpaperCell;

use crate::{
    WaylandObject,
    wayland::{
        wl_compositor, wl_region, wl_surface, wp_fractional_scale_manager_v1,
        wp_fractional_scale_v1, wp_viewport, wp_viewporter, zwlr_layer_shell_v1,
        zwlr_layer_surface_v1,
    },
};
use bump_pool::BumpPool;

struct FrameCallbackHandler {
    callback: Option<ObjectId>,
}

impl FrameCallbackHandler {
    fn new() -> Self {
        FrameCallbackHandler { callback: None }
    }

    fn request_frame_callback(
        &mut self,
        backend: &mut Waybackend,
        objman: &mut ObjectManager<WaylandObject>,
        surface: ObjectId,
    ) {
        let callback = objman.create(WaylandObject::Callback);
        wl_surface::req::frame(backend, surface, callback).unwrap();
        self.callback = Some(callback);
    }
}

/// NOTE: this type cannot be created directly. Create a [WallpaperCell] instead.
pub struct Wallpaper {
    name: Option<Box<str>>,
    desc: Option<Box<str>>,

    output: ObjectId,
    output_name: u32,
    wl_surface: ObjectId,
    wp_viewport: ObjectId,
    wp_fractional: Option<ObjectId>,
    layer_surface: ObjectId,

    width: NonZeroI32,
    height: NonZeroI32,
    scale_factor: Scale,

    ack_serial: u32,
    needs_ack: bool,

    pub configured: bool,
    dirty: bool,

    /// we only allow one single borrow at a time, regardless of whether it is mutable or immutable
    is_borrowed: bool,
    /// we only have a maximum of 3 clones active at a time
    clones: u8,

    frame_callback_handler: FrameCallbackHandler,
    img: BgImg,
    pool: BumpPool,
}

impl Wallpaper {
    fn new(daemon: &mut crate::Daemon, output_info: OutputInfo) -> Self {
        let crate::Daemon {
            objman,
            backend,
            pixel_format,
            compositor,
            shm,
            viewporter,
            fractional_scale_manager,
            layer_shell,
            layer,
            ..
        } = daemon;

        let OutputInfo {
            name,
            desc,
            scale_factor,
            output,
            output_name,
        } = output_info;

        let wl_surface = objman.create(WaylandObject::Surface);
        wl_compositor::req::create_surface(backend, *compositor, wl_surface).unwrap();

        let region = objman.create(WaylandObject::Region);
        wl_compositor::req::create_region(backend, *compositor, region).unwrap();

        wl_surface::req::set_input_region(backend, wl_surface, Some(region)).unwrap();
        wl_region::req::destroy(backend, region).unwrap();

        let layer_surface = objman.create(WaylandObject::LayerSurface);
        zwlr_layer_shell_v1::req::get_layer_surface(
            backend,
            *layer_shell,
            layer_surface,
            wl_surface,
            Some(output),
            *layer,
            &format!("awww-daemon{}", daemon.namespace),
        )
        .unwrap();

        let wp_viewport = objman.create(WaylandObject::Viewport);
        wp_viewporter::req::get_viewport(backend, *viewporter, wp_viewport, wl_surface).unwrap();

        let wp_fractional = if let Some(fract_man) = fractional_scale_manager {
            let fractional = objman.create(WaylandObject::FractionalScale);
            wp_fractional_scale_manager_v1::req::get_fractional_scale(
                backend, *fract_man, fractional, wl_surface,
            )
            .unwrap();
            Some(fractional)
        } else {
            None
        };

        // Configure the layer surface
        zwlr_layer_surface_v1::req::set_anchor(
            backend,
            layer_surface,
            zwlr_layer_surface_v1::Anchor::TOP
                | zwlr_layer_surface_v1::Anchor::BOTTOM
                | zwlr_layer_surface_v1::Anchor::RIGHT
                | zwlr_layer_surface_v1::Anchor::LEFT,
        )
        .unwrap();
        zwlr_layer_surface_v1::req::set_exclusive_zone(backend, layer_surface, -1).unwrap();
        zwlr_layer_surface_v1::req::set_margin(backend, layer_surface, 0, 0, 0, 0).unwrap();
        zwlr_layer_surface_v1::req::set_keyboard_interactivity(
            backend,
            layer_surface,
            zwlr_layer_surface_v1::KeyboardInteractivity::None,
        )
        .unwrap();
        zwlr_layer_surface_v1::req::set_size(backend, layer_surface, 0, 0).unwrap();

        let frame_callback_handler = FrameCallbackHandler::new();
        // commit so that the compositor send the initial configuration
        wl_surface::req::commit(backend, wl_surface).unwrap();

        let pool = BumpPool::new(backend, objman, *shm, 256, 256, *pixel_format);

        debug!("New wallpaper at output: {output_name}");
        Self {
            name,
            desc,
            output,
            output_name,
            wl_surface,
            wp_viewport,
            wp_fractional,
            layer_surface,
            width: NonZeroI32::new(4).unwrap(),
            height: NonZeroI32::new(4).unwrap(),
            scale_factor,
            ack_serial: 0,
            needs_ack: false,
            configured: false,
            dirty: false,
            clones: 0,
            is_borrowed: false,
            frame_callback_handler,
            img: BgImg::Color([0, 0, 0, 0]),
            pool,
        }
    }

    pub fn get_bg_info(&self, pixel_format: PixelFormat) -> BgInfo {
        BgInfo {
            name: self.name.as_deref().unwrap_or("?").into(),
            dim: (self.width.get() as u32, self.height.get() as u32),
            scale_factor: self.scale_factor,
            img: match &self.img {
                BgImg::Color(color) => {
                    let mut color = *color;
                    if pixel_format.must_swap_r_and_b_channels() {
                        color.swap(0, 2);
                    }
                    BgImg::Color(color)
                }
                BgImg::Img(img) => BgImg::Img(img.clone()),
            },
            pixel_format,
        }
    }

    pub fn set_desc(&mut self, desc: Box<str>) {
        debug!("Output {} description: {desc}", self.output_name);
        self.desc = Some(desc);
    }

    pub fn set_dimensions(&mut self, width: i32, height: i32) {
        let width = match NonZeroI32::new(width) {
            Some(width) => width,
            None => {
                error!("Cannot set wallpaper width to 0!");
                return;
            }
        };

        let height = match NonZeroI32::new(height) {
            Some(height) => height,
            None => {
                error!("Cannot set wallpaper height to 0!");
                return;
            }
        };

        if (self.width, self.height) != (width, height) {
            debug!(
                "Output {} new dimensions: {width}x{height}",
                self.output_name
            );

            self.width = width;
            self.height = height;
            self.dirty = true;
        }
    }

    pub fn set_ack_serial(&mut self, serial: u32) {
        self.ack_serial = serial;
        self.needs_ack = true;
    }

    pub fn set_scale(&mut self, scale: Scale) {
        if self.scale_factor == scale || scale.priority() < self.scale_factor.priority() {
            return;
        }

        debug!("Output {} new scale: {scale}", self.output_name);
        self.scale_factor = scale;
        self.dirty = true;
    }

    pub fn commit_surface_changes(
        &mut self,
        backend: &mut Waybackend,
        namespace: &str,
        use_cache: bool,
    ) -> bool {
        if self.needs_ack {
            crate::wayland::zwlr_layer_surface_v1::req::ack_configure(
                backend,
                self.layer_surface,
                self.ack_serial,
            )
            .unwrap();
            self.needs_ack = false;
        }

        if !self.dirty {
            return false;
        }
        self.dirty = false;

        if (!self.configured && use_cache) || self.img.is_set() {
            'brk: {
                let output_name = self.name.as_deref().unwrap_or("?");
                let cache_data = match read_cache_file(output_name) {
                    Ok(cache_data) => cache_data,
                    Err(e) => {
                        warn!("failed to read cache file: {e}");
                        break 'brk;
                    }
                };

                match get_previous_image_cache(output_name, namespace, &cache_data) {
                    Ok(Some(cache)) => {
                        // Note: we do not need to wait for this command because we set SIGCHLD to
                        // SIG_IGN, and posix says that does not generate a zombie process (see
                        // `man 3p _EXIT`

                        unsafe extern "C" {
                            static environ: *const *const core::ffi::c_char;
                        }

                        let crop_gravity_parameters = match cache.crop_gravity {
                            Some(v) => format!("--crop-gravity={v}"),
                            None => "".to_string(),
                        };

                        let cmd = format!(
                            "exec awww img \
                            --outputs='{output_name}' \
                            --resize={} \
                            {crop_gravity_parameters} \
                            --filter={} \
                            --namespace='{namespace}' \
                            --transition-type=none \
                            '{}'\0",
                            cache.resize, cache.filter, cache.img_path
                        );
                        common::log::debug!("{}", cmd);
                        match unsafe { rustix::runtime::kernel_fork() } {
                            Ok(rustix::runtime::Fork::Child(_)) => {
                                let args: [*const u8; 4] = [
                                    c"sh".as_ptr().cast(),
                                    c"-c".as_ptr().cast(),
                                    cmd.as_ptr(),
                                    core::ptr::null(),
                                ];
                                let err = unsafe {
                                    rustix::runtime::execve(
                                        c"/bin/sh",
                                        args.as_ptr(),
                                        environ as *const _,
                                    )
                                };
                                panic!("execve failed: {err}");
                            }
                            Ok(rustix::runtime::Fork::ParentOf(_)) => (),
                            Err(e) => error!("fork failed: {e}"),
                        }
                    }
                    Ok(None) => break 'brk,
                    Err(e) => {
                        warn!("failed get previous image cache: {e}");
                        break 'brk;
                    }
                };
            }
        }

        let (width, height) = (self.width.get(), self.height.get());
        debug!(
            "Output {} new configuration: width: {width}, height: {height}, scale_factor: {}",
            self.output_name, self.scale_factor
        );

        wp_viewport::req::set_destination(backend, self.wp_viewport, width, height).unwrap();

        let (w, h) = self.scale_factor.mul_dim(width, height);
        self.pool.resize(backend, w, h);

        self.frame_callback_handler.callback = None;

        wl_surface::req::commit(backend, self.wl_surface).unwrap();
        self.configured = true;
        true
    }

    pub fn has_name(&self, name: &str) -> bool {
        self.name.as_ref().is_some_and(|s| s.as_ref() == name)
    }

    pub fn has_output(&self, output: ObjectId) -> bool {
        self.output == output
    }

    pub fn has_output_name(&self, name: u32) -> bool {
        self.output_name == name
    }

    pub fn has_layer_surface(&self, layer_surface: ObjectId) -> bool {
        self.layer_surface == layer_surface
    }

    pub fn try_set_buffer_release_flag(
        &mut self,
        backend: &mut Waybackend,
        buffer: ObjectId,
    ) -> bool {
        self.pool
            .set_buffer_release_flag(backend, buffer, self.clones > 0)
    }

    pub fn is_draw_ready(&self) -> bool {
        self.frame_callback_handler.callback.is_none()
    }

    pub fn has_callback(&self, callback: ObjectId) -> bool {
        self.frame_callback_handler.callback == Some(callback)
    }

    pub fn has_fractional_scale(&self, fractional_scale: ObjectId) -> bool {
        self.wp_fractional.is_some_and(|f| f == fractional_scale)
    }

    pub fn get_dimensions(&self) -> (u32, u32) {
        let dim = self
            .scale_factor
            .mul_dim(self.width.get(), self.height.get());
        (dim.0 as u32, dim.1 as u32)
    }

    pub fn canvas_change<F, T>(
        &mut self,
        backend: &mut Waybackend,
        objman: &mut ObjectManager<WaylandObject>,
        pixel_format: PixelFormat,
        f: F,
    ) -> T
    where
        F: FnOnce(&mut [u8]) -> T,
    {
        f(self.pool.get_drawable(backend, objman, pixel_format))
    }

    pub fn frame_callback_completed(&mut self) {
        self.frame_callback_handler.callback = None;
    }

    pub fn clear(
        &mut self,
        backend: &mut Waybackend,
        objman: &mut ObjectManager<WaylandObject>,
        pixel_format: PixelFormat,
        color: [u8; 4],
    ) {
        let channels = pixel_format.channels() as usize;
        self.canvas_change(backend, objman, pixel_format, |canvas| {
            for pixel in canvas.chunks_exact_mut(channels) {
                pixel[0..channels].copy_from_slice(&color[0..channels]);
            }
        });
    }

    pub fn set_img_info(&mut self, img_info: BgImg) {
        debug!(
            "output {} - drawing: {}",
            self.name.as_deref().unwrap_or(""),
            img_info
        );
        self.img = img_info;
    }

    pub fn destroy(&mut self, backend: &mut Waybackend) {
        // Careful not to panic here, since we call this on drop

        wp_viewport::req::destroy(backend, self.wp_viewport).unwrap();
        if let Some(fractional) = self.wp_fractional {
            wp_fractional_scale_v1::req::destroy(backend, fractional).unwrap();
        }
        zwlr_layer_surface_v1::req::destroy(backend, self.layer_surface).unwrap();
        self.pool.destroy(backend);

        debug!(
            "Destroyed output {} - {}",
            self.name.as_deref().unwrap_or("?"),
            self.desc.as_deref().unwrap_or("?")
        );
    }

    fn attach_buffer_and_damage_surface(
        &mut self,
        backend: &mut Waybackend,
        objman: &mut ObjectManager<WaylandObject>,
    ) {
        let surface = self.wl_surface;
        let buf = self.pool.get_committable_buffer();
        let (width, height) = (self.pool.width(), self.pool.height());

        wl_surface::req::attach(backend, surface, Some(buf), 0, 0).unwrap();
        wl_surface::req::damage_buffer(backend, surface, 0, 0, width, height).unwrap();
        self.frame_callback_handler
            .request_frame_callback(backend, objman, surface);
    }
}

/// attaches all pending buffers and damages all surfaces with one single request
pub fn attach_buffers_and_damage_surfaces(
    backend: &mut Waybackend,
    objman: &mut ObjectManager<WaylandObject>,
    wallpapers: &[WallpaperCell],
) {
    for wallpaper in wallpapers {
        wallpaper
            .borrow_mut()
            .attach_buffer_and_damage_surface(backend, objman);
    }
}

/// commits multiple wallpapers at once with a single message through the socket
pub fn commit_wallpapers(backend: &mut Waybackend, wallpapers: &[WallpaperCell]) {
    for wallpaper in wallpapers {
        let wallpaper = wallpaper.borrow();
        wl_surface::req::commit(backend, wallpaper.wl_surface).unwrap();
    }
}
