use std::cell::RefCell;
use std::rc::Rc;

use bytemuck::cast_slice;
use glow::{HasContext, NativeBuffer, NativeFramebuffer, NativeTexture, NativeVertexArray};
use ndarray::{s, IndexLonger};

use crate::components::{CodeType, Program, Shader, Snippet};
use crate::data_loader::{CoordType, Data, DataType};
use crate::errors::*;

use super::colormap::ColorMap;
use super::transforms::viewport::Viewport;
use super::transforms::Transform;
use super::{ty, AttaWithBuffer, AttaWithWindow, Graphics};

pub struct PPI {
    program: Program,
    layer: isize,
    vao: Option<NativeVertexArray>,
    vbo: Option<NativeBuffer>,

    cmap: Option<Box<dyn ColorMap>>,

    framebuffer: Option<NativeFramebuffer>,
    frametexture: Option<NativeTexture>,
    buffer: Vec<[f32; 3]>,

    need_redraw: bool,
}

impl PPI {
    pub fn new() -> Result<Self> {
        let vertex = Shader::new(
            glow::VERTEX_SHADER,
            CodeType::<String>::Path("ppi.vert".into()),
        )?;

        let geom = Shader::new(
            glow::GEOMETRY_SHADER,
            CodeType::<String>::Path("ppi.geom".into()),
        )?;

        let fragment = Shader::new(
            glow::FRAGMENT_SHADER,
            CodeType::<String>::Path("ppi.frag".into()),
        )?;

        let program = Program::new(vertex, fragment, Some(geom), "330 core");

        Ok(Self {
            program,
            layer: 0,
            vao: None,
            vbo: None,
            buffer: Vec::with_capacity(180 * 50),
            framebuffer: None,
            frametexture: None,
            cmap: None,
            need_redraw: true,
        })
    }

    pub fn set_transform<T: Transform>(&mut self, transform: &T) {
        self.program.set_transform(transform);
    }

    pub fn set_viewport(&mut self, viewport: &Viewport) {
        self.program.set_viewport(viewport);
    }

    pub fn set_colormap(&mut self, colormap: Box<dyn ColorMap>) {
        self.program.set_hook("colormap", colormap.snippet_ref());
        self.cmap = Some(colormap);
    }

    pub fn program(&mut self) -> &mut Program {
        &mut self.program
    }

    fn set_conf(&self, gl: &glow::Context, dpir: f32, dpie: f32) {
        let location = self.program.get_uniform_location(gl, "conf");
        unsafe {
            gl.uniform_4_f32(location.as_ref(), dpir, dpie, 0f32, 0f32);
        }
    }

    pub fn add_ppi_layer(&mut self, layer: isize) {
        self.layer = self.layer + layer;
        self.layer.clamp(0, 23);
        self.need_redraw = true;
    }

    pub fn program_ref(&self) -> &Program {
        &self.program
    }

    fn bake_data(&self, data: &Data) -> Result<Vec<[f32; 3]>> {
        let first_block = data.blocks.get(0).unwrap();
        let first_block_data = first_block.data.view();
        if let CoordType::Polar {
            r_range,
            azimuth,
            r,
            ..
        } = &first_block.coord_type
        {
            let azimuth_len = azimuth.len();
            let r_len = r.len();

            let mut vertices = Vec::with_capacity(azimuth_len * r_len);

            for azi_idx in 0..azimuth_len {
                for r_idx in 0..r_len {
                    let azi = azimuth.get(azi_idx).unwrap();
                    let r = r.get(r_idx).unwrap() / r_range[1] as f32;
                    let dt = first_block_data
                        .get([self.layer as usize, azi_idx, r_idx])
                        .unwrap();
                    vertices.push([r, *azi, *dt]);
                }
            }
            return Ok(vertices);
        } else {
            return Err(Error::InvalidDataType);
        }
    }

    fn data_info(&self, data: &Data) -> Result<(f32, f32, DataType)> {
        let first_block = data.blocks.get(0).unwrap();
        if let CoordType::Polar {
            azimuth,
            r,
            r_range,
            ..
        } = &first_block.coord_type
        {
            let mut sorted_azimuth = azimuth.clone();
            sorted_azimuth.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Greater));
            let azi_step = max_step(&sorted_azimuth);
            let r_step = min_step(r) / (r_range[1]) as f32;
            return Ok((r_step, azi_step, first_block.data_type));
        } else {
            return Err(Error::InvalidDataType);
        }
    }
}

fn min_step(data: &Vec<f32>) -> f32 {
    // 计算相邻元素之间的间距
    let mut min_gap = f32::MAX;
    for window in data.windows(2) {
        if let [a, b] = window {
            let gap = b - a;
            if gap < min_gap {
                min_gap = gap;
            }
        }
    }
    return min_gap;
}

fn max_step(data: &Vec<f32>) -> f32 {
    // 计算相邻元素之间的间距
    let mut max_gap = f32::MIN;
    for window in data.windows(2) {
        if let [a, b] = window {
            let gap = b - a;
            if gap > max_gap {
                max_gap = gap;
            }
        }
    }
    return max_gap;
}

impl Graphics for PPI {
    fn compile(&mut self, gl: &glow::Context) -> Result<()> {
        self.program.compile(gl)?;
        self.cmap
            .as_mut()
            .unwrap()
            .attach_with_program(gl, &self.program)?;
        unsafe {
            let vao = gl.create_vertex_array().unwrap();
            gl.bind_vertex_array(Some(vao));
            let vbo = gl.create_buffer().unwrap();
            gl.bind_buffer(glow::ARRAY_BUFFER, Some(vbo));
            gl.enable_vertex_attrib_array(0);
            gl.vertex_attrib_pointer_f32(0, 3, glow::FLOAT, false, 12, 0);

            self.vao = Some(vao);
            self.vbo = Some(vbo);
            gl.bind_vertex_array(None);
        }

        // let location = self.program.get_uniform_location(gl, "conf");
        let origin = self.program.get_uniform_location(gl, "polar_origin");

        unsafe {
            // gl.uniform_4_f32(location.as_ref(), dpir, dpie, 0.0, 0.0);
            gl.uniform_1_f32(origin.as_ref(), 90.0f32.to_radians());
        }

        Ok(())
    }

    fn destroy(&mut self, gl: &glow::Context) -> Result<()> {
        self.program.destroy(gl);

        unsafe {
            self.vao.map(|vao| gl.delete_vertex_array(vao));
            self.vbo.map(|vbo| gl.delete_buffer(vbo));
            self.framebuffer.map(|fb| gl.delete_framebuffer(fb));
            self.frametexture.map(|ft| gl.delete_texture(ft));
            self.cmap.as_mut().unwrap().destroy(gl);
        }

        Ok(())
    }

    fn draw(&mut self, gl: &glow::Context) -> Result<()> {
        if !self.need_redraw {
            return Ok(());
        }
        unsafe {
            gl.use_program(Some(self.program.native_program.unwrap()));
            if self.framebuffer.is_some() {
                gl.bind_framebuffer(glow::FRAMEBUFFER, self.framebuffer);
            }

            gl.viewport(0, 0, 800, 600);
            gl.clear(glow::COLOR_BUFFER_BIT);
            self.cmap
                .as_ref()
                .unwrap()
                .bind_texture(gl, &self.program)?;

            gl.depth_mask(false);
            gl.bind_vertex_array(Some(self.vao.unwrap()));
            gl.draw_arrays(glow::POINTS, 0, self.buffer.len() as i32);
            gl.depth_mask(true);

            if self.framebuffer.is_some() {
                gl.bind_framebuffer(glow::FRAMEBUFFER, None);
            }

            gl.bind_vertex_array(None);
            gl.use_program(None);

            self.need_redraw = false;
        }
        Ok(())
    }
}

impl AttaWithBuffer for PPI {
    type Data = Data;

    fn attach_with_buffer(&mut self, gl: &glow::Context, data: &Self::Data) -> Result<()> {
        let (rdpi, adpi, data_type) = self.data_info(data)?;
        let baked_buffer = self.bake_data(data)?;
        self.buffer = baked_buffer;
        unsafe {
            gl.use_program(self.program.native_program);
            self.set_conf(gl, rdpi, adpi);
            gl.bind_vertex_array(self.vao);
            gl.buffer_data_u8_slice(
                glow::ARRAY_BUFFER,
                cast_slice(&self.buffer),
                glow::STATIC_DRAW,
            );
        }
        self.need_redraw = true;

        Ok(())
    }
}

impl AttaWithWindow for PPI {
    fn attach_with_window(&mut self, gl: &glow::Context) -> Result<imgui::TextureId> {
        unsafe {
            if self.framebuffer.is_none() {
                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,
                    800,
                    600,
                    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);
                self.framebuffer = Some(framebuffer);
                self.frametexture = Some(texture);
                Ok(imgui::TextureId::from(texture.0.get() as usize))
            } else {
                Ok(imgui::TextureId::from(
                    self.frametexture.unwrap().0.get() as usize
                ))
            }
        }
    }
}

mod test {
    #[test]
    fn test_ppi() {
        use super::*;
        // let ppi = PPI::new().unwrap();
    }
}
