//! `Displayable` is a property to display a background texture

use crate::prelude::*;
use anyhow::Result;
use assetpath::AssetPath;
use utilities::prelude::*;
use vulkan_rs::prelude::*;

use cgmath::{vec2, vec4};

use super::texturedvertex::TexturedVertex;

use std::sync::{
    atomic::{AtomicI32, Ordering::SeqCst},
    {Arc, RwLock},
};

#[derive(Debug, Clone)]
pub enum DisplayableType {
    Path(AssetPath),
    Descriptor(ElementDescriptor),
}

impl From<AssetPath> for DisplayableType {
    fn from(value: AssetPath) -> Self {
        Self::Path(value)
    }
}

impl From<ElementDescriptor> for DisplayableType {
    fn from(value: ElementDescriptor) -> Self {
        Self::Descriptor(value)
    }
}

/// `Displayable` gives the ability to display a texture as background image for an item
pub struct Displayable {
    framable: Arc<Framable>,

    descriptor_set: Arc<DescriptorSet>,

    buffer: Arc<Buffer<TexturedVertex>>,

    displayable_type: RwLock<DisplayableType>,

    ui_layer: AtomicI32,

    left_factor: RwLock<f32>,
    right_factor: RwLock<f32>,
    bottom_factor: RwLock<f32>,
    top_factor: RwLock<f32>,

    left_uv_factor: RwLock<f32>,
    right_uv_factor: RwLock<f32>,
    bottom_uv_factor: RwLock<f32>,
    top_uv_factor: RwLock<f32>,
}

impl Displayable {
    /// Factory method for `Displayable`, returns `Arc<Displayable>`
    ///
    /// # Arguments
    ///
    /// * `framable` is a `Arc<Framable>` instance
    /// * `name` is the name for a png
    pub fn new(
        framable: Arc<Framable>,
        displayable_type: impl Into<DisplayableType>,
    ) -> Result<Arc<Self>> {
        let descriptor_set = framable.gui_handler().image_descriptor_set()?;

        let displayable_type = displayable_type.into();

        if let DisplayableType::Path(path) = &displayable_type {
            let texture = framable
                .gui_handler()
                .displayable_image_from_path(path.clone())?;
            descriptor_set.update(&[DescriptorWrite::combined_samplers(0, &[&texture])])?;
        }

        let buffer = Buffer::builder()
            .set_usage(VK_BUFFER_USAGE_VERTEX_BUFFER_BIT)
            .set_memory_usage(MemoryUsage::CpuOnly)
            .set_size(6)
            .build(framable.gui_handler().device().clone())?;

        let displayable = Arc::new(Displayable {
            framable,
            descriptor_set,
            buffer,

            displayable_type: RwLock::new(displayable_type),

            ui_layer: AtomicI32::new(0),

            left_factor: RwLock::new(0.0),
            right_factor: RwLock::new(1.0),
            bottom_factor: RwLock::new(1.0),
            top_factor: RwLock::new(0.0),

            left_uv_factor: RwLock::new(0.0),
            right_uv_factor: RwLock::new(1.0),
            bottom_uv_factor: RwLock::new(1.0),
            top_uv_factor: RwLock::new(0.0),
        });

        let displayable_clone = displayable.clone();
        let weak_displayable = Arc::downgrade(&displayable);

        displayable.framable.add_callback(
            weak_displayable,
            Box::new(move || displayable_clone.update_frame()),
        );

        Ok(displayable)
    }

    /// Add method
    ///
    /// # Arguments
    ///
    /// * `displayable` is a `&Arc<Displayable>` instance that is going to be added
    pub fn add(self: &Arc<Self>) -> Result<()> {
        self.framable
            .gui_handler()
            .add_displayable(self.ui_layer.load(SeqCst), self.clone())
    }

    /// Delete method, has to be explicitly called, otherwise it will remain in memory
    ///
    /// # Arguments
    ///
    /// * `displayable` is a `&Arc<Displayable>` instance that is going to be deleted
    pub fn delete(self: &Arc<Self>) -> Result<()> {
        self.framable
            .gui_handler()
            .delete_displayable(self.ui_layer.load(SeqCst), self)
    }

    pub fn inner_type(&self) -> DisplayableType {
        self.displayable_type.read().unwrap().clone()
    }

    pub fn set_ui_layer(&self, ui_layer: i32) {
        self.ui_layer.store(ui_layer, SeqCst);
    }

    pub fn clear_callback(self: &Arc<Self>) {
        let weak_displayable = Arc::downgrade(self);
        self.framable.remove_callback(weak_displayable);
    }

    /// Replaces the current background image
    ///
    /// # Arguments
    ///
    /// * `name` is the name of the texture in `gui/` without `.png` suffix
    pub fn set_image(&self, displayable_type: impl Into<DisplayableType>) -> Result<()> {
        let displayable_type = displayable_type.into();

        let texture = match displayable_type.clone() {
            DisplayableType::Path(path) => Some(
                self.framable
                    .gui_handler()
                    .displayable_image_from_path(path)?,
            ),
            DisplayableType::Descriptor(descriptor) => {
                let width = (self.framable.right() - self.framable.left()) as u32;
                let height = (self.framable.bottom() - self.framable.top()) as u32;

                if width > 0 && height > 0 {
                    Some(
                        self.framable
                            .gui_handler()
                            .displayable_image_from_descriptor(width, height, descriptor)?,
                    )
                } else {
                    None
                }
            }
        };

        if let Some(texture) = texture {
            self.descriptor_set
                .update(&[DescriptorWrite::combined_samplers(0, &[&texture])])?;
        }

        *self.displayable_type.write().unwrap() = displayable_type;

        Ok(())
    }

    /// Returns the internal vulkan buffer
    pub fn buffer(&self) -> &Arc<Buffer<TexturedVertex>> {
        &self.buffer
    }

    /// Returns the internal vulkan descriptor set
    pub fn descriptor_set(&self) -> Arc<DescriptorSet> {
        self.descriptor_set.clone()
    }

    pub fn set_left_factor(&self, factor: f32) {
        *self.left_factor.write().unwrap() = factor;
    }

    pub fn set_right_factor(&self, factor: f32) {
        *self.right_factor.write().unwrap() = factor;
    }

    pub fn set_top_factor(&self, factor: f32) {
        *self.top_factor.write().unwrap() = factor;
    }

    pub fn set_bottom_factor(&self, factor: f32) {
        *self.bottom_factor.write().unwrap() = factor;
    }

    pub fn set_left_uv_factor(&self, factor: f32) {
        *self.left_uv_factor.write().unwrap() = factor;
    }

    pub fn set_right_uv_factor(&self, factor: f32) {
        *self.right_uv_factor.write().unwrap() = factor;
    }

    pub fn set_top_uv_factor(&self, factor: f32) {
        *self.top_uv_factor.write().unwrap() = factor;
    }

    pub fn set_bottom_uv_factor(&self, factor: f32) {
        *self.bottom_uv_factor.write().unwrap() = factor;
    }

    /// Update frame method if the original frame is invalidated
    pub fn update_frame(&self) -> Result<()> {
        let mut frame = self.buffer.map_complete()?;

        let x_start = self.framable.left() as f32;
        let y_start = self.framable.top() as f32;

        let width = (self.framable.right() - self.framable.left()) as f32;
        let height = (self.framable.bottom() - self.framable.top()) as f32;

        let left = x_start + width * *self.left_factor.read().unwrap();
        let right = x_start + width * *self.right_factor.read().unwrap();
        let top = y_start + height * *self.top_factor.read().unwrap();
        let bottom = y_start + height * *self.bottom_factor.read().unwrap();

        frame[0].position = self.framable.ortho() * vec4(left, bottom, 0.0, 1.0);
        frame[1].position = self.framable.ortho() * vec4(right, bottom, 0.0, 1.0);
        frame[2].position = self.framable.ortho() * vec4(right, top, 0.0, 1.0);
        frame[3].position = self.framable.ortho() * vec4(right, top, 0.0, 1.0);
        frame[4].position = self.framable.ortho() * vec4(left, top, 0.0, 1.0);
        frame[5].position = self.framable.ortho() * vec4(left, bottom, 0.0, 1.0);

        frame[0].texture_coordinates = vec2(
            *self.left_uv_factor.read().unwrap(),
            *self.bottom_uv_factor.read().unwrap(),
        );
        frame[1].texture_coordinates = vec2(
            *self.right_uv_factor.read().unwrap(),
            *self.bottom_uv_factor.read().unwrap(),
        );
        frame[2].texture_coordinates = vec2(
            *self.right_uv_factor.read().unwrap(),
            *self.top_uv_factor.read().unwrap(),
        );
        frame[3].texture_coordinates = vec2(
            *self.right_uv_factor.read().unwrap(),
            *self.top_uv_factor.read().unwrap(),
        );
        frame[4].texture_coordinates = vec2(
            *self.left_uv_factor.read().unwrap(),
            *self.top_uv_factor.read().unwrap(),
        );
        frame[5].texture_coordinates = vec2(
            *self.left_uv_factor.read().unwrap(),
            *self.bottom_uv_factor.read().unwrap(),
        );

        Ok(())
    }
}