use glow::{HasContext, NativeBuffer, NativeVertexArray};

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

use super::colormap::ColorMap;
use super::threed::ThreeD;
use super::transforms::position::Position;
use super::transforms::viewport::Viewport;
use super::{transforms, AttaWithBuffer, AttaWithProgram, AttachWithMouse, Config, Graphics};

pub struct PPI {
    program: Program,
    layer: isize,
    cmap: Option<Box<dyn ColorMap>>,
}

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 transform = ThreeD::new(1.0, 0.1, 100.0, 45.0)?;

        println!("{}", transform.snippet().call(&vec![]).unwrap());

        // let transform = Position::new()?;

        let mut program = Program::new(vertex, fragment, Some(geom), "330 core");
        program.set_transform(&transform);

        Ok(Self {
            program,
            layer: 0,
            cmap: None,
        })
    }

    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, rdpi: f32, adpi: f32) {
        let location = self.program.get_uniform_location(gl, "conf");
        unsafe {
            gl.uniform_4_f32(location.as_ref(), rdpi, adpi, 0f32, 0f32);
        }
    }

    fn bake_data(&self, data: &Data) -> Result<(Vec<f32>, i32)> {
        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.extend([r, *azi, *dt]);
                }
            }
            let len = vertices.len() as i32 / 3;
            return Ok((vertices, len));
        } else {
            return Err(Error::InvalidDataType);
        }
    }

    pub 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 init(&mut self, gl: &glow::Context, config: &PPIConfig) {
        self.cmap
            .as_mut()
            .unwrap()
            .attach_with_program(gl, &self.program)
            .unwrap();

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

        let rdpi = config.rdpi;
        let adpi = config.adpi;
        self.set_conf(gl, rdpi, adpi);
        unsafe {
            gl.uniform_1_f32(origin.as_ref(), 90.0f32.to_radians());
        }
    }
}

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)?;
        Ok(())
    }

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

        Ok(())
    }

    fn draw(&self, gl: &glow::Context, count: i32) -> Result<()> {
        unsafe {
            gl.clear(glow::COLOR_BUFFER_BIT);
            self.cmap
                .as_ref()
                .unwrap()
                .bind_texture(gl, &self.program)?;

            gl.draw_arrays(glow::POINTS, 0, count);
        }
        Ok(())
    }

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

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

    fn set_config(&mut self, gl: &glow::Context, config: Option<&Config>) -> Result<()> {
        if let Some(config) = config {
            if let Config::PPI(config) = config {
                self.init(gl, config);
            } else {
                panic!("Errr config type");
            }
        }

        Ok(())
    }

    fn mount(&mut self, gl: &glow::Context) -> Result<()> {
        unsafe {
            gl.use_program(self.program.native_program);
        }
        Ok(())
    }

    fn unmount(&mut self, gl: &glow::Context) -> Result<()> {
        unsafe {
            gl.use_program(None);
        }
        Ok(())
    }
}

impl AttaWithBuffer for PPI {
    type Data = Data;

    fn bake(&self, data: &Self::Data) -> Result<(Vec<f32>, Option<Vec<u32>>, i32)> {
        // let (rdpi, adpi, data_type) = self.data_info(data)?;
        let baked_buffer = self.bake_data(data)?;
        Ok((baked_buffer.0, None, baked_buffer.1))
    }

    fn init(&self, gl: &glow::Context) -> (NativeVertexArray, NativeBuffer, Option<NativeBuffer>) {
        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);
            gl.bind_vertex_array(None);

            (vao, vbo, None)
        }
    }
}

#[derive(Default, Clone, Debug)]
pub struct PPIConfig {
    pub layer: usize,
    pub rdpi: f32,
    pub adpi: f32,
}

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