use crate::components::Program;
use crate::data_loader::Data;
use crate::graphics::colormap::linear::LinearColormap;
use crate::graphics::ppi::PPIConfig;
use crate::graphics::threed::ThreeD;
use crate::graphics::transforms::viewport::Viewport;
use crate::graphics::{ppi::PPI, Graphics};
use crate::graphics::{AttaWithBuffer, AttaWithProgram, AttachWithMouse, Config, MouseState};
use crate::{errors::*, ui::base};
use glow::{HasContext, NativeBuffer, NativeFramebuffer, NativeTexture, NativeVertexArray};
use imgui::{ImStr, ImString, Textures, Ui};
use log::info;
use std::hash::Hash;
use std::ops::Deref;
use std::{cell::RefCell, collections::HashMap, rc::Rc};
pub type Graphic<Data> = Rc<RefCell<dyn Graphics<Data = Data>>>;
type RcGraphic<T> = Rc<RefCell<T>>;

pub struct App<'a> {
    pub ui_state: State,
    gl: &'a glow::Context,
    viewport: Viewport,
    windows: HashMap<ImString, WindowData<'a>>,
    programs: [Graphic<Data>; 1],
    pub ppi_module: RcGraphic<PPI>,
    program_with_window: HashMap<usize, Vec<ImString>>,
}

impl<'a> App<'a> {
    pub fn new(gl: &'a glow::Context) -> Result<Self> {
        let viewport = Viewport::new()?;

        let mut cmap = LinearColormap::new().unwrap();
        cmap.set_colors(vec![
            [170, 170, 170, 255],
            [0, 34, 255, 255],
            [1, 160, 246, 255],
            [0, 236, 236, 255],
            [0, 216, 0, 255],
            [1, 144, 0, 255],
            [255, 255, 0, 255],
            [231, 192, 0, 255],
            [255, 144, 0, 255],
            [255, 0, 0, 255],
            [214, 0, 0, 255],
            [192, 0, 0, 255],
            [255, 0, 240, 255],
            [150, 0, 180, 255],
        ]);
        cmap.set_range(0.0, 70.0);
        let cmap = Box::new(cmap);

        let mut ppi = PPI::new()?;
        ppi.set_viewport(&viewport);
        ppi.set_colormap(cmap);

        let ppi = Rc::new(RefCell::new(ppi));
        let programs = [ppi.clone() as Graphic<Data>];

        Ok(Self {
            ui_state: State {},
            viewport,
            programs,
            windows: HashMap::with_capacity(30),
            ppi_module: ppi,
            gl,
            program_with_window: HashMap::with_capacity(5),
        })
    }

    pub fn render(&mut self) {
        let mut need_clean = false;
        for (id, program) in self.programs.iter().enumerate() {
            let mut p = program.borrow_mut();

            if self.program_with_window.len() == 0 {
                return;
            }

            p.mount(&self.gl).unwrap();
            self.program_with_window.get(&id).map(|windows| {
                for window in windows.iter() {
                    let window_info = self.windows.get_mut(window).unwrap();

                    // Skip the window if it doesn't need to be redrawn
                    if !window_info.need_redraw {
                        continue;
                    }

                    // If the window needs to be reinitialized, set the config
                    {
                        let conf = if window_info.re_init {
                            window_info.config.as_ref()
                        } else {
                            None
                        };

                        p.set_config(self.gl, conf).unwrap();
                        window_info.re_init = false;
                    }

                    // Attach the modifer to the program
                    if let Some(motion) = window_info.modifer.as_ref() {
                        motion
                            .attach_with_program(&self.gl, p.program_ref())
                            .unwrap();
                    }

                    // Attach the data to the program
                    unsafe {
                        let framebuffer = window_info.gl_fb_resources.as_ref().map(|v| v.native());

                        // Skip the window if the framebuffer is None
                        if framebuffer.is_none() {
                            continue;
                        }

                        self.gl.bind_framebuffer(glow::FRAMEBUFFER, framebuffer);
                        let attach = window_info.attach.get(&id);

                        if attach.is_some() {
                            let vao = attach.unwrap().vao.native();
                            self.gl.bind_vertex_array(Some(vao));
                        }

                        if attach.is_some() {
                            let window_size = window_info.size;
                            self.gl
                                .viewport(0, 0, window_size[0] as i32, window_size[1] as i32);
                            p.draw(&self.gl, attach.as_ref().unwrap().len).unwrap();
                        }

                        if attach.is_some() {
                            self.gl.bind_vertex_array(None);
                        }
                        self.gl.bind_framebuffer(glow::FRAMEBUFFER, None);
                        window_info.need_redraw = false;
                        need_clean = true;
                    }
                }
            });

            // Unmount the program
            p.unmount(&self.gl).unwrap();
        }
    }

    pub fn render_ui(&mut self, ui: &Ui, window: &winit::window::Window, run: &mut bool) {
        base(ui, window, run, self);
    }

    pub fn create_framebuffer(
        &mut self,
        id: &str,
        size: [i32; 2],
    ) -> Result<(RcGlFramebuffer<'a>, RcGlTexture<'a>)> {
        let id = &ImString::new(id);

        if self.windows.contains_key(id) {
            let info = self.windows.get(id).unwrap();
            if info.gl_fb_resources.is_some() && info.gl_tex_resources.is_some() {
                return Ok((
                    info.gl_fb_resources.as_ref().unwrap().clone(),
                    info.gl_tex_resources.as_ref().unwrap().clone(),
                ));
            }
        }

        let gl = self.gl;
        let tex = unsafe {
            let framebuffer = gl.create_framebuffer().unwrap();
            gl.bind_framebuffer(glow::FRAMEBUFFER, Some(framebuffer));

            let texture = gl.create_texture().unwrap();
            gl.bind_texture(glow::TEXTURE_2D, Some(texture));
            gl.tex_image_2d(
                glow::TEXTURE_2D,
                0,
                glow::RGB8 as i32,
                size[0],
                size[1],
                0,
                glow::RGB,
                glow::UNSIGNED_BYTE,
                None,
            );
            gl.tex_parameter_i32(
                glow::TEXTURE_2D,
                glow::TEXTURE_MIN_FILTER,
                glow::LINEAR as i32,
            );
            gl.tex_parameter_i32(
                glow::TEXTURE_2D,
                glow::TEXTURE_MAG_FILTER,
                glow::LINEAR as i32,
            );

            gl.framebuffer_texture_2d(
                glow::FRAMEBUFFER,
                glow::COLOR_ATTACHMENT0,
                glow::TEXTURE_2D,
                Some(texture),
                0,
            );

            assert_eq!(
                gl.check_framebuffer_status(glow::FRAMEBUFFER),
                glow::FRAMEBUFFER_COMPLETE
            );

            gl.bind_framebuffer(glow::FRAMEBUFFER, None);
            gl.bind_texture(glow::TEXTURE_2D, None);
            (
                RcGlResource::new(&self.gl, framebuffer),
                RcGlResource::new(&self.gl, texture),
            )
        };

        Ok(tex)
    }

    pub fn prepare(&mut self) {
        for program in self.programs.iter() {
            let mut p = program.borrow_mut();
            p.compile(&self.gl).unwrap();
        }
        unsafe {
            self.gl.enable(glow::DEPTH_TEST);
        }
    }

    pub fn destroy(&mut self) {
        for p in self.programs.iter() {
            let mut p = p.borrow_mut();
            p.unmount(&self.gl).unwrap();
            p.destroy(&self.gl).unwrap();
        }
    }

    pub fn create_ppi_render(&mut self, id: &str, config: Option<PPIConfig>) {
        let id = &ImString::new(id);
        let (vao, vbo, ebo) = self.ppi_module.borrow().init(&self.gl);
        self.windows.get_mut(id).map(|w| {
            w.attach.insert(
                0,
                Attach {
                    vao: RcGlResource::new(&self.gl, vao),
                    vbo: RcGlResource::new(&self.gl, vbo),
                    ebo: ebo.map(|ebo| RcGlResource::new(&self.gl, ebo)),
                    len: 0,
                },
            );
            w.need_redraw = true;
            w.program = 0;
            w.config = Some(config.unwrap_or_default().into());
        });

        let v = self.program_with_window.entry(0).or_insert(vec![]);
        v.push(id.clone());
    }

    pub fn bind_data(&mut self, program_idx: usize, id: &str, data: &Data) -> Result {
        let id = &ImString::new(id);
        use bytemuck::cast_slice;

        let window = self.windows.get_mut(id).unwrap();
        let program = window.program;
        let program = self.programs[program].borrow();

        let data = program.bake(data)?;

        let attach = window.attach.get_mut(&program_idx).unwrap();
        attach.len = data.2;

        unsafe {
            self.gl
                .bind_buffer(glow::VERTEX_ARRAY, Some(attach.vbo.native()));
            self.gl.buffer_data_u8_slice(
                glow::ARRAY_BUFFER,
                cast_slice(data.0.as_slice()),
                glow::STATIC_DRAW,
            );
            if let Some(ebo) = attach.ebo.as_ref() {
                self.gl
                    .bind_buffer(glow::ELEMENT_ARRAY_BUFFER, Some(ebo.native()));
                self.gl.buffer_data_u8_slice(
                    glow::ELEMENT_ARRAY_BUFFER,
                    cast_slice(&data.1.as_ref().unwrap()),
                    glow::STATIC_DRAW,
                );

                self.gl.bind_buffer(glow::ELEMENT_ARRAY_BUFFER, None);
            }
            self.gl.bind_buffer(glow::VERTEX_ARRAY, None);
        }

        Ok(())
    }

    pub fn show_window(&mut self, ui: &Ui) {
        let mut need_resize = vec![];

        for (id, window) in self.windows.iter_mut() {
            ui.window(&window.title)
                .size(window.size, imgui::Condition::FirstUseEver)
                .opened(&mut window.open)
                .flags(imgui::WindowFlags::NO_SCROLLBAR)
                .build(|| {
                    if ui.is_mouse_clicked(imgui::MouseButton::Left) {
                        let io = ui.io();
                        let pos = io.mouse_pos;

                        let window_pos = ui.window_pos();
                        window.last_mouse_position =
                            [pos[0] - window_pos[0], pos[1] - window_pos[1]];
                    }

                    if ui.is_mouse_dragging(imgui::MouseButton::Left) {
                        if ui.is_window_hovered() {
                            let delta = ui.mouse_drag_delta();
                            window.last_mouse_delta = delta;
                            window.accmulate_mouse_delta = [
                                window.accmulate_mouse_delta[0] + delta[0],
                                window.accmulate_mouse_delta[1] + delta[1],
                            ];
                            window.motion = Some(MouseState::Drag {
                                from: window.last_mouse_position,
                                delta: delta,
                            });

                            println!(
                                "Dragging: {:?} {:?}",
                                window.last_mouse_position, window.accmulate_mouse_delta
                            );
                            window.modifer.as_mut().map(|v| {
                                v.exec(window.motion.as_ref().unwrap());
                            });
                            window.need_redraw = true;
                        }
                    }

                    if ui.is_mouse_released(imgui::MouseButton::Left) {
                        if window.size != ui.window_size() {
                            window.size = ui.window_size();
                            println!("resized: {:?}", window.size);
                            need_resize.push((window.title.clone(), ui.window_size()));
                        }
                    }

                    if let Some(texture) = window.gl_tex_resources.as_ref() {
                        let cursor = ui.cursor_pos();
                        imgui::Image::new(texture.native2imguiid(), ui.window_size()).build(ui);
                        ui.set_cursor_pos(cursor);
                        if ui.invisible_button(&window.title, ui.window_size()) {
                            let io = ui.io();
                            let pos = io.mouse_pos;
                            let window_pos = ui.window_pos();
                            let related_pos = [pos[0] - window_pos[0], pos[1] - window_pos[1]];
                        }
                    }
                });
        }

        for (id, size) in need_resize.iter() {
            self.reset_window_size(id, *size);
        }
    }

    pub fn create_render_window(&mut self, title: &str, size: [f32; 2]) -> Result {
        // Insert the window data into the windows hashmap
        let id = ImString::new(title);
        let mut data = WindowData::new(id.clone(), size, None);
        let (fb, tex) =
            self.create_framebuffer(title, [size[0].floor() as i32, size[1].floor() as i32])?;
        data.gl_fb_resources = Some(fb);
        data.gl_tex_resources = Some(tex);
        self.windows.insert(id, data);
        Ok(())
    }

    pub fn set_window_modifer(&mut self, id: &str, modifer: Option<ModiferType>) {
        let id = &ImString::new(id);
        self.windows.get_mut(id).map(|v| {
            v.modifer = modifer;
        });
    }

    pub fn set_config(&mut self, id: &str) -> Option<&mut Config> {
        let id = &ImString::new(id);
        self.windows
            .get_mut(id)
            .map(|v| {
                v.re_init = true;
                v.config.as_mut()
            })
            .flatten()
    }

    fn reset_window_size(&mut self, id: &ImString, size: [f32; 2]) {
        let window_info = self.windows.get_mut(id).unwrap();
        window_info.need_redraw = true;
        let tex = unsafe {
            self.gl.bind_framebuffer(
                glow::FRAMEBUFFER,
                window_info.gl_fb_resources.as_ref().map(|v| v.native()),
            );
            let texture = self.gl.create_texture().unwrap();
            self.gl.bind_texture(glow::TEXTURE_2D, Some(texture));
            self.gl.tex_image_2d(
                glow::TEXTURE_2D,
                0,
                glow::RGB8 as i32,
                size[0].floor() as i32,
                size[1].floor() as i32,
                0,
                glow::RGB,
                glow::UNSIGNED_BYTE,
                None,
            );
            self.gl.tex_parameter_i32(
                glow::TEXTURE_2D,
                glow::TEXTURE_MIN_FILTER,
                glow::LINEAR as i32,
            );
            self.gl.tex_parameter_i32(
                glow::TEXTURE_2D,
                glow::TEXTURE_MAG_FILTER,
                glow::LINEAR as i32,
            );

            self.gl.framebuffer_texture_2d(
                glow::FRAMEBUFFER,
                glow::COLOR_ATTACHMENT0,
                glow::TEXTURE_2D,
                Some(texture),
                0,
            );

            assert_eq!(
                self.gl.check_framebuffer_status(glow::FRAMEBUFFER),
                glow::FRAMEBUFFER_COMPLETE
            );

            self.gl.bind_framebuffer(glow::FRAMEBUFFER, None);
            self.gl.bind_texture(glow::TEXTURE_2D, None);

            texture
        };

        window_info.gl_tex_resources = Some(RcGlResource::new(self.gl, tex));
    }

    pub fn destroy_window(&mut self) {
        for (id, window) in self.windows.iter() {
            if !window.open {
                self.program_with_window.get_mut(&window.program).map(|v| {
                    v.retain(|k| k != id);
                });
            }
        }
        self.windows.retain(|k, v| v.open == true);
    }
}

#[derive(Debug, Clone)]
pub struct Attach<'a> {
    pub vao: RcGlResource<'a, NativeVertexArray>,
    pub vbo: RcGlResource<'a, NativeBuffer>,
    pub ebo: Option<RcGlResource<'a, NativeBuffer>>,
    pub len: i32,
}

pub struct State {}

pub struct WindowData<'a> {
    pub title: ImString,
    pub open: bool,
    pub copy_from: Option<ImString>,
    pub size: [f32; 2],
    gl_fb_resources: Option<RcGlResource<'a, NativeFramebuffer>>,
    gl_tex_resources: Option<RcGlResource<'a, NativeTexture>>,
    need_redraw: bool,
    program: usize,
    attach: HashMap<usize, Attach<'a>>,

    re_init: bool,
    last_mouse_position: [f32; 2],
    last_mouse_delta: [f32; 2],
    accmulate_mouse_delta: [f32; 2],
    mouse_position: [f32; 2],
    motion: Option<MouseState>,
    config: Option<Config>,
    modifer: Option<ModiferType>,
}

impl<'a> WindowData<'a> {
    pub fn new(title: ImString, size: [f32; 2], modifer: Option<ModiferType>) -> Self {
        Self {
            title,
            open: true,
            copy_from: None,
            size,
            last_mouse_position: [0.0, 0.0],
            last_mouse_delta: [0.0, 0.0],
            accmulate_mouse_delta: [0.0, 0.0],
            mouse_position: [0.0, 0.0],
            motion: None,

            gl_fb_resources: None,
            gl_tex_resources: None,
            need_redraw: false,
            program: 0,
            attach: HashMap::with_capacity(10),
            re_init: false,
            config: None,
            modifer,
        }
    }

    fn set_current_mouse_delta(&mut self, delta: [f32; 2]) {
        self.last_mouse_delta = delta;
        self.accmulate_mouse_delta = [
            self.accmulate_mouse_delta[0] + delta[0],
            self.accmulate_mouse_delta[1] + delta[1],
        ];
    }

    fn set_mouse_postion(&mut self, pos: [f32; 2]) {
        self.mouse_position = pos;
    }

    fn set_motion(&mut self, motion: MouseState) {
        self.motion = Some(motion);
    }

    fn set_need_redraw(&mut self) {
        self.need_redraw = true;
    }

    fn set_config(&mut self, config: Config) {
        self.config = Some(config);
    }

    fn set_re_init(&mut self) {
        self.re_init = true;
    }

    fn on_mouse_drag(&mut self) {
        let state = MouseState::Drag {
            from: self.last_mouse_position,
            delta: self.last_mouse_delta,
        };
        self.set_motion(state.clone());

        self.modifer.as_mut().map(|m| {});

        self.set_need_redraw();
    }
}

macro_rules! modifer_exec {
    ($(($t:ty => $b:tt),)+) => {
        impl ModiferType {
            pub fn exec(&mut self, motion: &MouseState) {
                match self {
                    $(
                        ModiferType::$b(b) => {
                                b.attach_with_mouse(motion);
                        }
                    )+
                    _ => {}
                }
            }
        }

        impl AttaWithProgram for ModiferType {
            fn attach_with_program(&self, gl: &glow::Context, program: &Program) -> Result {
                match self {
                    $(

                        ModiferType::$b(b) => {
                                b.attach_with_program(gl, program)?;
                        }

                    )+
                    _ => {
                    }
            }
            Ok(())
            }

        }

        $(
        impl From<$t> for ModiferType {
            fn from(t: $t) -> Self {
                ModiferType::$b(t)
            }
        }
        )+
    }

}

pub enum ModiferType {
    ThreeD(ThreeD),
    TwoD,
}

modifer_exec!((ThreeD => ThreeD),);

pub type RcGlFramebuffer<'a> = RcGlResource<'a, NativeFramebuffer>;
pub type RcGlTexture<'a> = RcGlResource<'a, NativeTexture>;
pub type RcGlVertexArray<'a> = RcGlResource<'a, NativeVertexArray>;
pub type RcGlBuffer<'a> = RcGlResource<'a, NativeBuffer>;

// pub type RcGlResource<'a, T> = Rc<GlResource<'a, T>>;
#[derive(Debug, Clone)]
pub struct RcGlResource<'a, T: Resource>(Rc<GlResource<'a, T>>);

impl<'a, T: Resource> RcGlResource<'a, T> {
    pub fn new(gl: &'a glow::Context, resource: T) -> Self {
        Self(Rc::new(GlResource::new(resource, gl)))
    }

    pub fn native(&self) -> T {
        self.0.resource.clone()
    }
}

impl<'a, T: Resource> Deref for RcGlResource<'a, T> {
    type Target = T;
    fn deref(&self) -> &Self::Target {
        self.0.deref()
    }
}

trait Resource: Clone + std::fmt::Debug {
    fn drop_self(&self, gl: &glow::Context);
}

impl Resource for NativeVertexArray {
    fn drop_self(&self, gl: &glow::Context) {
        unsafe {
            gl.delete_vertex_array(*self);
        }
    }
}

impl Resource for NativeBuffer {
    fn drop_self(&self, gl: &glow::Context) {
        unsafe {
            gl.delete_buffer(*self);
        }
    }
}

impl Resource for NativeFramebuffer {
    fn drop_self(&self, gl: &glow::Context) {
        unsafe {
            gl.delete_framebuffer(*self);
        }
    }
}

impl Resource for NativeTexture {
    fn drop_self(&self, gl: &glow::Context) {
        unsafe {
            gl.delete_texture(*self);
        }
    }
}

#[derive(Debug)]
struct GlResource<'a, T: Resource> {
    gl: &'a glow::Context,
    resource: T,
}

impl<T: Resource> Deref for GlResource<'_, T> {
    type Target = T;
    fn deref(&self) -> &Self::Target {
        &self.resource
    }
}

impl<'a, T: Resource> GlResource<'a, T> {
    pub fn new(resource: T, gl: &'a glow::Context) -> Self {
        Self { resource, gl }
    }
}

impl<T: Resource> Drop for GlResource<'_, T> {
    fn drop(&mut self) {
        self.resource.drop_self(self.gl);
        info!("Dropping resource: {:?}", self.resource);
    }
}

impl<'a> RcGlResource<'a, NativeTexture> {
    fn native2imguiid(&self) -> imgui::TextureId {
        imgui::TextureId::new(self.native().0.get() as usize)
    }
}
