use crate::camera::Camera;
use crate::graphics::Graphics;
use crate::{
    graphics::{
        collections::agg_fast_path::AggFastPath,
        transforms::{
            polar::Polar, position::Position, trackball::Trackball, viewport::Viewport, Transform,
        },
    },
    utils::Triangler,
};
use cgmath::InnerSpace;
use glow::HasContext;
use glutin::{
    config::ConfigTemplateBuilder,
    context::{ContextAttributesBuilder, NotCurrentGlContext, PossiblyCurrentContext},
    display::{GetGlDisplay, GlDisplay},
    surface::{GlSurface, Surface, SurfaceAttributesBuilder, WindowSurface},
};
use imgui::Ui;
use imgui_winit_support::{
    winit::{
        dpi::{LogicalSize, PhysicalPosition},
        event_loop::EventLoop,
        window::{Window, WindowBuilder},
    },
    HiDpiMode, WinitPlatform,
};
use nalgebra_glm::perspective;
use nalgebra_glm::Vec3;
use raw_window_handle::HasRawWindowHandle;
use std::num::NonZeroU32;
use std::time::Instant;

pub fn init<FUi>(mut run_ui: FUi)
where
    FUi: FnMut(&mut bool, &mut Ui) + 'static,
{
    let (event_loop, window, surface, context) = create_window();
    let (mut winit_platform, mut imgui_context) = imgui_init(&window);

    {
        let dpi_mode = if let Ok(factor) = std::env::var("IMGUI_EXAMPLE_FORCE_DPI_FACTOR") {
            // Allow forcing of HiDPI factor for debugging purposes
            match factor.parse::<f64>() {
                Ok(f) => HiDpiMode::Locked(f),
                Err(e) => panic!("Invalid scaling factor: {}", e),
            }
        } else {
            HiDpiMode::Default
        };

        winit_platform.attach_window(imgui_context.io_mut(), &window, dpi_mode);
    }

    let gl = unsafe {
        let gl = glow_context(&context);
        gl.enable(glow::DEPTH_TEST);
        gl
    };

    // OpenGL renderer from this crate
    let mut ig_renderer = imgui_glow_renderer::AutoRenderer::initialize(gl, &mut imgui_context)
        .expect("failed to create renderer");

    let mut last_frame = Instant::now();
    let tri_renderer = Triangler::new(ig_renderer.gl_context(), "#version 330");

    let viewport = Trackball::new().unwrap();
    let trans = Polar::new().unwrap();
    let position = Position::new().unwrap();

    let transform = viewport.chain(trans.chain(position));

    let viewport = Viewport::new().unwrap();

    let mut agg_path = AggFastPath::new(&transform, &viewport).unwrap();

    agg_path.compile(ig_renderer.gl_context()).unwrap();

    let program = tri_renderer.program();
    let mut camera = Camera::new(
        Vec3::new(0.5, 0.5, 1.0),  // Camera position (world location
        Vec3::new(0.0, 1.0, 0.0),  // Upward vector
        Vec3::new(0.0, 0.0, -1.0), // Look at vector
    );

    let mut last_position: Option<PhysicalPosition<f64>> = None;
    let mut yaw = -90.0;
    let mut pitch = 0.0;

    event_loop
        .run(move |event, window_target| {
            match event {
                winit::event::Event::NewEvents(_) => {
                    let now = Instant::now();
                    imgui_context
                        .io_mut()
                        .update_delta_time(now.duration_since(last_frame));
                    last_frame = now;
                }
                winit::event::Event::AboutToWait => {
                    winit_platform
                        .prepare_frame(imgui_context.io_mut(), &window)
                        .unwrap();

                    window.request_redraw();
                }
                winit::event::Event::WindowEvent {
                    event: winit::event::WindowEvent::RedrawRequested,
                    ..
                } => {
                    // Render your custom scene, note we need to borrow the OpenGL
                    // context from the `AutoRenderer`, which takes ownership of it.

                    unsafe {
                        let gl = ig_renderer.gl_context();

                        gl.clear(glow::COLOR_BUFFER_BIT | glow::DEPTH_BUFFER_BIT);

                        let loc = gl.get_uniform_location(program, "projection");
                        let size = window.inner_size();
                        let projection =
                            perspective(size.width as f32 / size.height as f32, 45.0, 0.1, 100.0);
                        gl.uniform_matrix_4_f32_slice(loc.as_ref(), false, projection.as_slice());

                        let loc = gl.get_uniform_location(program, "view");
                        let view = camera.get_view_matrix();
                        gl.uniform_matrix_4_f32_slice(loc.as_ref(), false, view.as_slice());
                    }

                    tri_renderer.render(ig_renderer.gl_context());

                    let ui = imgui_context.frame();

                    if ui.is_mouse_pos_valid(ui.io().mouse_pos) {
                        let mouse_pos = ui.io().mouse_pos;
                        if ui.is_mouse_dragging(imgui::MouseButton::Right) {
                            mouse_callback(
                                &mut last_position,
                                PhysicalPosition::new(mouse_pos[0] as f64, mouse_pos[1] as f64),
                                0.05,
                                &mut pitch,
                                &mut yaw,
                                &mut camera,
                            );
                        }
                    }

                    let mut run = true;
                    run_ui(&mut run, ui);

                    if !run {
                        window_target.exit();
                    }

                    let draw_data = imgui_context.render();

                    // Render imgui on top of it
                    ig_renderer
                        .render(draw_data)
                        .expect("error rendering imgui");

                    surface
                        .swap_buffers(&context)
                        .expect("Failed to swap buffers");
                }
                winit::event::Event::WindowEvent {
                    event: winit::event::WindowEvent::CloseRequested,
                    ..
                } => {
                    window_target.exit();
                }

                winit::event::Event::WindowEvent {
                    event: winit::event::WindowEvent::Resized(new_size),
                    ..
                } => {
                    if new_size.width > 0 && new_size.height > 0 {
                        surface.resize(
                            &context,
                            NonZeroU32::new(new_size.width).unwrap(),
                            NonZeroU32::new(new_size.height).unwrap(),
                        );
                    }
                    winit_platform.handle_event(imgui_context.io_mut(), &window, &event);
                }
                winit::event::Event::LoopExiting => {
                    let gl = ig_renderer.gl_context();
                    tri_renderer.destroy(gl);
                }
                event => {
                    winit_platform.handle_event(imgui_context.io_mut(), &window, &event);
                }
            }
        })
        .unwrap()
}

fn mouse_callback(
    last_position: &mut Option<PhysicalPosition<f64>>,
    current_position: PhysicalPosition<f64>,
    sensitivity: f64,
    pitch: &mut f64,
    yaw: &mut f64,
    camera: &mut Camera,
) {
    use cgmath::{Deg, Euler, Quaternion};

    let xpos = current_position.x;
    let ypos = current_position.y;

    if last_position.is_none() {
        *last_position = Some(PhysicalPosition::new(xpos, ypos));
    }

    let _last_position = last_position.unwrap();

    let mut xoffset = xpos - _last_position.x;
    let mut yoffset = _last_position.y - ypos; // reversed since y-coordinates go from bottom to top

    *last_position = Some(PhysicalPosition::new(xpos, ypos));

    xoffset *= sensitivity;
    yoffset *= sensitivity;

    *yaw += xoffset;
    *pitch += yoffset;

    if *pitch > 89.0 {
        *pitch = 89.0;
    }

    if *pitch < -89.0 {
        *pitch = -89.0;
    }

    let euler_deg = Euler::new(Deg(*pitch), Deg(*yaw), Deg(0.0));

    let c = Quaternion::from(euler_deg).normalize();

    camera.set_front(Vec3::new(c.v.x as f32, c.v.y as f32, c.v.z as f32));
}

fn create_window() -> (
    EventLoop<()>,
    Window,
    Surface<WindowSurface>,
    PossiblyCurrentContext,
) {
    let event_loop = EventLoop::new().unwrap();

    let window_builder = WindowBuilder::new()
        .with_title("TEST")
        .with_inner_size(LogicalSize::new(1024, 768));

    let (window, cfg) = glutin_winit::DisplayBuilder::new()
        .with_window_builder(Some(window_builder))
        .build(&event_loop, ConfigTemplateBuilder::new(), |mut cfgs| {
            cfgs.next().unwrap()
        })
        .expect("Failed to create window");

    let window = window.unwrap();

    let context_attributes =
        ContextAttributesBuilder::new().build(Some(window.raw_window_handle()));

    let context = unsafe {
        cfg.display()
            .create_context(&cfg, &context_attributes)
            .unwrap()
    };

    let surface_attributes = SurfaceAttributesBuilder::<WindowSurface>::new()
        .with_srgb(Some(true))
        .build(
            window.raw_window_handle(),
            NonZeroU32::new(1024).unwrap(),
            NonZeroU32::new(768).unwrap(),
        );

    let surface = unsafe {
        cfg.display()
            .create_window_surface(&cfg, &surface_attributes)
            .unwrap()
    };

    let context = context.make_current(&surface).unwrap();

    (event_loop, window, surface, context)
}

fn glow_context(context: &PossiblyCurrentContext) -> glow::Context {
    unsafe {
        glow::Context::from_loader_function_cstr(|s| context.display().get_proc_address(s).cast())
    }
}

fn imgui_init(window: &Window) -> (WinitPlatform, imgui::Context) {
    let mut imgui_context = imgui::Context::create();
    imgui_context.set_ini_filename(None);

    let mut winit_platform = WinitPlatform::init(&mut imgui_context);
    winit_platform.attach_window(
        imgui_context.io_mut(),
        window,
        imgui_winit_support::HiDpiMode::Rounded,
    );

    imgui_context
        .fonts()
        .add_font(&[imgui::FontSource::DefaultFontData { config: None }]);

    imgui_context.io_mut().font_global_scale = (1.0 / winit_platform.hidpi_factor()) as f32;

    (winit_platform, imgui_context)
}
