//! `Framable` is a property to frame an item

use crate::prelude::*;
use anyhow::Result;

use std::{
    ffi::c_void,
    sync::{
        atomic::{AtomicBool, AtomicI32, AtomicU32, Ordering::SeqCst},
        Arc, RwLock, Weak,
    },
};

use vulkan_rs::prelude::cgmath::Matrix4;

/// Describes the vertical alignment for a `Framable`
#[derive(Copy, Clone, PartialEq)]
pub enum VerticalAlign {
    NotSet,
    Top,
    Middle,
    Bottom,
}

impl Default for VerticalAlign {
    fn default() -> Self {
        Self::NotSet
    }
}

/// Describes the horizontal alignment for a `Framable`
#[derive(Copy, Clone, PartialEq)]
pub enum HorizontalAlign {
    NotSet,
    Left,
    Middle,
    Right,
}

impl Default for HorizontalAlign {
    fn default() -> Self {
        Self::NotSet
    }
}

pub struct Handle {
    ptr: *const c_void,
}

impl Handle {
    fn ptr_eq(&self, other: &Handle) -> bool {
        self.ptr == other.ptr
    }
}

unsafe impl Sync for Handle {}
unsafe impl Send for Handle {}

impl<T> From<Weak<T>> for Handle {
    fn from(weak: Weak<T>) -> Self {
        let ptr: *const T = Weak::into_raw(weak);

        Self {
            ptr: ptr as *const c_void,
        }
    }
}

impl Drop for Handle {
    fn drop(&mut self) {
        unsafe {
            Weak::from_raw(self.ptr);
        }
    }
}

/// `Framable` keeps track of the position and size of an item
/// and calls functions on resize to keep everything aligned correctly
pub struct Framable {
    gui_handler: Arc<GuiHandler>,

    left: AtomicI32,
    right: AtomicI32,
    top: AtomicI32,
    bottom: AtomicI32,

    vertical_alignment: RwLock<VerticalAlign>,
    horizontal_alignment: RwLock<HorizontalAlign>,

    resize_callbacks: RwLock<Vec<(Handle, Box<dyn Fn() -> Result<()> + Send + Sync>)>>,

    x_off: AtomicI32,
    y_off: AtomicI32,
    w: AtomicU32,
    h: AtomicU32,

    framed: AtomicBool,

    ui_layer: AtomicI32,

    // old extent, needed for size check in enable
    window_width: AtomicU32,
    window_height: AtomicU32,

    reference_size: RwLock<Option<(u32, u32)>>,
    reference_scale_position: AtomicBool,
    reference_scale_size: AtomicBool,

    resize_allowed: bool,
}

impl Framable {
    /// Factory method for `Framable`, returns `Arc<Framable>`
    pub fn new(gui_handler: Arc<GuiHandler>, resize_allowed: bool) -> Result<Arc<Self>> {
        Ok(Arc::new(Framable {
            window_width: AtomicU32::new(gui_handler.width()),
            window_height: AtomicU32::new(gui_handler.height()),

            gui_handler,

            left: AtomicI32::new(0),
            right: AtomicI32::new(0),
            top: AtomicI32::new(0),
            bottom: AtomicI32::new(0),

            vertical_alignment: RwLock::new(VerticalAlign::default()),
            horizontal_alignment: RwLock::new(HorizontalAlign::default()),

            resize_callbacks: RwLock::new(Vec::new()),

            x_off: AtomicI32::new(0),
            y_off: AtomicI32::new(0),
            w: AtomicU32::new(0),
            h: AtomicU32::new(0),

            framed: AtomicBool::new(false),

            ui_layer: AtomicI32::new(0),

            reference_size: RwLock::new(None),
            reference_scale_position: AtomicBool::new(false),
            reference_scale_size: AtomicBool::new(false),

            resize_allowed,
        }))
    }

    /// Add method
    ///
    /// # Arguments
    ///
    /// * `framable` - is a `&Arc<Framable>` instance that is going to be added
    pub fn add(self: &Arc<Self>) -> Result<()> {
        // check if window size is the same as last time
        if self.gui_handler.width() != self.window_width.load(SeqCst)
            || self.gui_handler.height() != self.window_height.load(SeqCst)
        {
            // update window size
            self.window_width.store(self.gui_handler.width(), SeqCst);
            self.window_height.store(self.gui_handler.height(), SeqCst);

            // force resize
            self.resize()?;
        }

        if self.resize_allowed() {
            self.gui_handler
                .add_framable(self.ui_layer.load(SeqCst), self.clone())?;
        }

        Ok(())
    }

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

        Ok(())
    }

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

    pub fn ui_layer(&self) -> i32 {
        self.ui_layer.load(SeqCst)
    }

    pub(crate) fn set_reference_size(&self, width: u32, height: u32) -> Result<()> {
        *self.reference_size.write().unwrap() = Some((width, height));
        self.reference_scale_position.store(true, SeqCst);
        self.reference_scale_size.store(true, SeqCst);

        if self.is_framed() {
            self.resize()?;
        }

        Ok(())
    }

    pub(crate) fn allow_position_scale(&self, allowed: bool) -> Result<()> {
        if allowed != self.reference_scale_position.load(SeqCst) {
            self.reference_scale_position.store(allowed, SeqCst);

            if self.is_framed() {
                self.resize()?;
            }
        }

        Ok(())
    }

    pub(crate) fn allow_size_scale(&self, allowed: bool) -> Result<()> {
        if allowed != self.reference_scale_size.load(SeqCst) {
            self.reference_scale_size.store(allowed, SeqCst);

            if self.is_framed() {
                self.resize()?;
            }
        }

        Ok(())
    }

    pub fn resize_allowed(&self) -> bool {
        self.resize_allowed
    }

    /// Method to set the frame to a certain position with certain
    /// width and a certain alignment
    pub fn set_frame(
        &self,
        x_off: i32,
        y_off: i32,
        w: u32,
        h: u32,
        vertical_align: VerticalAlign,
        horizontal_align: HorizontalAlign,
    ) {
        self.x_off.store(x_off, SeqCst);
        self.y_off.store(y_off, SeqCst);
        self.w.store(w, SeqCst);
        self.h.store(h, SeqCst);

        *self.vertical_alignment.write().unwrap() = vertical_align;
        *self.horizontal_alignment.write().unwrap() = horizontal_align;

        self.calculate_frame();

        self.framed.store(true, SeqCst);
    }

    /// Returns the left edge in pixels of this framable,
    /// calculated from the left of the window
    pub fn left(&self) -> i32 {
        self.left.load(SeqCst)
    }

    /// Returns the right edge in pixels of this framable,
    /// calculated from the left of the window
    pub fn right(&self) -> i32 {
        self.right.load(SeqCst)
    }

    /// Returns the top edge in pixels of this framable,
    /// calculated from the top of the window
    pub fn top(&self) -> i32 {
        self.top.load(SeqCst)
    }

    /// Returns the bottom edge in pixels of this framable,
    /// calculated from the top of the window
    pub fn bottom(&self) -> i32 {
        self.bottom.load(SeqCst)
    }

    pub fn position(&self) -> (i32, i32, u32, u32) {
        let x = self.left();
        let y = self.top();

        let w = self.right() - x;
        let h = self.bottom() - y;

        (x, y, w as u32, h as u32)
    }

    /// Returns `true` if `set_frame` got called, otherwise `false`
    pub fn is_framed(&self) -> bool {
        self.framed.load(SeqCst)
    }

    /// Returns the ortho 4x4 matrix, which describes the window
    pub fn ortho(&self) -> Matrix4<f32> {
        self.gui_handler.ortho()
    }

    /// Adds a callback closure which is executed on resize
    pub fn add_callback(
        &self,
        handle: impl Into<Handle>,
        callback: Box<dyn Fn() -> Result<()> + Send + Sync>,
    ) {
        let handle = handle.into();

        if cfg!(debug_assertions)
            && self
                .resize_callbacks
                .read()
                .unwrap()
                .iter()
                .any(|(h, _)| h.ptr_eq(&handle))
        {
            panic!("could not add an object twice");
        }

        self.resize_callbacks
            .write()
            .unwrap()
            .push((handle, callback));
    }

    pub fn remove_callback(&self, handle: impl Into<Handle>) {
        let mut callbacks = self.resize_callbacks.write().unwrap();
        let handle = handle.into();

        if let Some(position) = callbacks.iter().position(|(h, _)| h.ptr_eq(&handle)) {
            #[allow(unused_must_use)]
            {
                callbacks.remove(position);
            }
        }
    }

    // Returns vertical alignment of this framable
    pub fn vertical_alignment(&self) -> VerticalAlign {
        *self.vertical_alignment.read().unwrap()
    }

    // Returns horizontal alignment of this framable
    pub fn horizontal_alignment(&self) -> HorizontalAlign {
        *self.horizontal_alignment.read().unwrap()
    }

    fn calculate_frame(&self) {
        let width = self.gui_handler.width();
        let height = self.gui_handler.height();

        let y_align = match *self.vertical_alignment.read().unwrap() {
            VerticalAlign::Top => 0,
            VerticalAlign::Middle => height / 2,
            VerticalAlign::Bottom => height,
            VerticalAlign::NotSet => {
                if cfg!(debug_assertions) {
                    println!("vertical alignment in framable not set");
                }

                height / 2
            }
        };

        let x_align = match *self.horizontal_alignment.read().unwrap() {
            HorizontalAlign::Left => 0,
            HorizontalAlign::Middle => width / 2,
            HorizontalAlign::Right => width,
            HorizontalAlign::NotSet => {
                if cfg!(debug_assertions) {
                    println!("horizontal alignment in framable not set");
                }

                width / 2
            }
        };

        let scale = match self.reference_size.read().unwrap().as_ref() {
            Some((reference_width, reference_height)) => {
                let width_ratio = width as f32 / *reference_width as f32;
                let height_ratio = height as f32 / *reference_height as f32;

                let min = width_ratio.min(height_ratio);

                min.max(1.0)
            }
            None => 1.0,
        };

        let (left, top) = if self.reference_scale_position.load(SeqCst) {
            let left = x_align as i32 + (self.x_off.load(SeqCst) as f32 * scale) as i32;
            let top = y_align as i32 + (self.y_off.load(SeqCst) as f32 * scale) as i32;

            (left, top)
        } else {
            let left = x_align as i32 + self.x_off.load(SeqCst);
            let top = y_align as i32 + self.y_off.load(SeqCst);

            (left, top)
        };

        let (right, bottom) = if self.reference_scale_size.load(SeqCst) {
            let right = left + (self.w.load(SeqCst) as f32 * scale) as i32;
            let bottom = top + (self.h.load(SeqCst) as f32 * scale) as i32;

            (right, bottom)
        } else {
            let right = left + self.w.load(SeqCst) as i32;
            let bottom = top + self.h.load(SeqCst) as i32;

            (right, bottom)
        };

        self.left.store(left, SeqCst);
        self.right.store(right, SeqCst);
        self.top.store(top, SeqCst);
        self.bottom.store(bottom, SeqCst);
    }

    pub fn change_position(&self, x_offset: i32, y_offset: i32) -> Result<()> {
        assert!(self.is_framed(), "framable needs to be framed first!");

        self.x_off.store(x_offset, SeqCst);
        self.y_off.store(y_offset, SeqCst);

        self.resize()?;

        Ok(())
    }
    pub fn change_position_unscaled(&self, x: i32, y: i32) -> Result<()> {
        assert!(self.is_framed(), "framable needs to be framed first!");

        let left = self.left.load(SeqCst);
        let top = self.top.load(SeqCst);
        let bottom = self.bottom.load(SeqCst);
        let right = self.right.load(SeqCst);

        let width = right - left;
        let height = bottom - top;

        self.left.store(x, SeqCst);
        self.top.store(y, SeqCst);
        self.right.store(x + width, SeqCst);
        self.bottom.store(y + height, SeqCst);

        for (_, callback) in self.resize_callbacks.read().unwrap().iter() {
            callback()?;
        }

        Ok(())
    }

    pub fn resize(&self) -> Result<()> {
        self.calculate_frame();

        for (_, callback) in self.resize_callbacks.read().unwrap().iter() {
            callback()?;
        }

        Ok(())
    }

    pub(crate) fn gui_handler(&self) -> &Arc<GuiHandler> {
        &self.gui_handler
    }
}