use super::exterior::ExteriorWidget;
use super::interior::InteriorWidget;
use super::{Layer, WindowCoord};
use crate::coords::proj::Mercator;
use crate::coords::Mapper;
use femtovg::{Canvas, Color, FontId, Renderer};
use gtk::glib;
use gtk::subclass::prelude::*;
use gtk::traits::{GLAreaExt, WidgetExt};
use std::cell::RefCell;
use std::num::NonZeroU32;

#[derive(Debug, Default, Clone)]
pub struct RenderConfig {
    pub padding: [f32; 4],
}

#[derive(Debug, Clone)]
pub enum RenderMotion {
    Translate,
    Scale,
    None,
}

impl Default for RenderMotion {
    fn default() -> Self {
        Self::None
    }
}

#[derive(Default)]
pub struct RenderStatus {
    pub(super) window_size: (i32, i32),
    pub(super) scale_rate: Option<f64>,
    pub(super) pointer_location: WindowCoord,
    pub(super) motion: RenderMotion,
    pub(super) scale: f32,
    pub(super) translate: Option<(f32, f32)>,
    translation: Option<(f64, f64)>,
    init_translation: (f64, f64),
}

struct Fonts {
    sans: FontId,
    bold: FontId,
    light: FontId,
}

pub struct Render {
    pub(super) exterior: RefCell<ExteriorWidget>,
    pub(super) interior: RefCell<InteriorWidget>,
    pub(super) canvas: RefCell<Option<femtovg::Canvas<femtovg::renderer::OpenGl>>>,
    pub config: RefCell<RenderConfig>,
    pub status: RefCell<RenderStatus>,
    pub mapper: RefCell<Mapper>,
    pub(super) interior_layers: RefCell<Vec<Layer>>,
}

impl Default for Render {
    fn default() -> Self {
        Self {
            exterior: RefCell::new(ExteriorWidget::default()),
            interior: RefCell::new(InteriorWidget::default()),
            interior_layers: RefCell::new(Vec::new()),
            config: RefCell::new(RenderConfig::default()),
            status: RefCell::new(RenderStatus::default()),
            mapper: RefCell::new(Mercator::default().into()),
            canvas: RefCell::new(None),
        }
    }
}

#[glib::object_subclass]
impl ObjectSubclass for Render {
    const NAME: &'static str = "MyRender";
    type Type = super::Render;
    type ParentType = gtk::GLArea;
}

// Trait shared by all GObjects
impl ObjectImpl for Render {
    fn constructed(&self) {
        self.parent_constructed();
        let area = self.obj();
        area.set_has_stencil_buffer(true);
    }
}

// Trait shared by all widgets
impl WidgetImpl for Render {}

impl GLAreaImpl for Render {
    fn resize(&self, width: i32, height: i32) {
        self.status.borrow_mut().window_size = (width, height);
        self.ensure_canvas();
        let mut canvas = self.canvas.borrow_mut();
        let canvas = canvas.as_mut().unwrap();
        canvas.set_size(
            width as u32,
            height as u32,
            self.obj().scale_factor() as f32,
        );

        let translation = self.status.borrow().translation;
        let scale_rate = self.status.borrow().scale_rate;
        let mapper = self.mapper.borrow();

        if let None = translation {
            let mut status = self.status.borrow_mut();
            status.translation = Some((mapper.get_bounds().0, mapper.get_bounds().2));
            status.init_translation = (mapper.get_bounds().0, mapper.get_bounds().2);
        }

        if let None = scale_rate {
            let scale = (mapper.get_bounds().3 - mapper.get_bounds().2) / canvas.height() as f64;
            self.status.borrow_mut().scale_rate = Some(scale);
        }
    }

    fn render(&self, context: &gtk::gdk::GLContext) -> bool {
        self.ensure_canvas();
        let configs = self.config.borrow();
        {
            let mut canvas = self.canvas.borrow_mut();
            let canvas = canvas.as_mut().unwrap();

            let dpi = self.obj().scale_factor();
            let w = canvas.width();
            let h = canvas.height();

            canvas.clear_rect(
                0,
                0,
                (w as i32 * dpi) as u32,
                (h as i32 * dpi) as u32,
                Color::rgba(0, 0, 0, 255),
            );
        }

        {
            let mut status = self.status.borrow_mut();
            match status.motion {
                RenderMotion::Translate => {
                    if let Some((x, y)) = status.translate {
                        let (ix, iy) = status.init_translation;
                        status.translation = Some((
                            ix + x as f64 * status.scale_rate.unwrap(),
                            iy + y as f64 * status.scale_rate.unwrap(),
                        ));
                    } else {
                        status.init_translation = status.translation.unwrap();
                    }
                }
                RenderMotion::Scale => {
                    let scale_rate = status.scale_rate.unwrap();
                    let scale_flag = status.scale as f64;
                    let step = scale_rate * 0.1;

                    let (tx, ty) = status.translation.unwrap();
                    let (px, py) = status.pointer_location;

                    let scaled = scale_rate + scale_flag * step;
                    status.scale_rate = Some(scaled);
                    let sx = scale_flag * step * px as f64;
                    let sy = scale_flag * step * py as f64;
                    status.translation = Some((tx - sx, ty - sy));
                    status.init_translation = status.translation.unwrap();
                }
                RenderMotion::None => {}
            }
        }

        let c = &(*self.interior_layers.borrow());
        self.interior
            .borrow()
            .draw(c, &self.obj(), self.status.borrow(), configs);

        {
            let mut canvas = self.canvas.borrow_mut();
            let canvas = canvas.as_mut().unwrap();
            self.exterior.borrow().draw(canvas, &self.obj());
            canvas.flush();
        }

        true
    }
}

impl Render {
    fn ensure_canvas(&self) {
        use femtovg::{renderer, Canvas};
        use glow::HasContext;

        if self.canvas.borrow().is_some() {
            return;
        }

        let widget = self.obj();
        widget.attach_buffers();

        static LOAD_FN: fn(&str) -> *const std::ffi::c_void =
            |s| epoxy::get_proc_addr(s) as *const _;
        // SAFETY: Need to get the framebuffer id that gtk expects us to draw into, so femtovg
        // knows which framebuffer to bind. This is safe as long as we call attach_buffers
        // beforehand. Also unbind it here just in case, since this can be called outside render.
        let (mut renderer, fbo) = unsafe {
            let renderer =
                renderer::OpenGl::new_from_function(LOAD_FN).expect("Cannot create renderer");
            let ctx = glow::Context::from_loader_function(LOAD_FN);
            let id = NonZeroU32::new(ctx.get_parameter_i32(glow::DRAW_FRAMEBUFFER_BINDING) as u32)
                .expect("No GTK provided framebuffer binding");
            ctx.bind_framebuffer(glow::FRAMEBUFFER, None);
            (renderer, glow::NativeFramebuffer(id))
        };
        renderer.set_screen_target(Some(fbo));
        let mut canvas = Canvas::new(renderer).expect("Cannot create canvas");
        canvas
            .add_font_dir(std::path::Path::new("./src/assets"))
            .unwrap();

        self.canvas.replace(Some(canvas));
    }

    pub(super) fn window_size(&self) -> Option<(i32, i32)> {
        Some(self.status.borrow().window_size)
    }

    pub(super) fn window_range(&self) -> Option<((f64, f64), (f64, f64))> {
        let padding = self.padding();
        let (w, h) = self.window_size().unwrap();
        let (w, h) = (
            w as f32 - padding[1] - padding[3],
            h as f32 - padding[0] - padding[2],
        );
        let (w, h) = (w as f64, h as f64);

        let mapper = self.mapper.borrow();

        let status = self.status.borrow();
        status.translation.and_then(|(tx, ty)| {
            status.scale_rate.and_then(|scale| {
                let (x1, y1) = (tx + padding[3] as f64, ty + padding[2] as f64);
                let (x2, y2) = (
                    tx + w * scale + padding[3] as f64,
                    ty + h * scale + padding[2] as f64,
                );
                let (x1, y1) = mapper.inverse_map((x1, y1)).unwrap();
                let (x2, y2) = mapper.inverse_map((x2, y2)).unwrap();
                Some(((x1, x2), (y1, y2)))
            })
        })
    }

    pub(super) fn inverse_map(&self, loc: (f32, f32)) -> Option<(f64, f64)> {
        let (x, y) = loc;
        let status = self.status.borrow();
        status.translation.and_then(|(tx, ty)| {
            status.scale_rate.and_then(|scale| {
                let (x, y) = (x as f64, y as f64);
                Some((tx + x * scale, (ty + y * scale)))
            })
        })
    }

    fn padding(&self) -> [f32; 4] {
        self.config.borrow().padding
    }

    pub(super) fn map(&self, loc: (f64, f64)) -> Option<(f32, f32)> {
        let (x, y) = loc;
        let (_, h) = self.window_size().unwrap();
        let status = self.status.borrow();
        status.translation.and_then(|(tx, ty)| {
            status.scale_rate.and_then(|scale| {
                Some((
                    (x - tx as f64) as f32 / scale as f32,
                    h as f32 - (y - ty as f64) as f32 / scale as f32,
                ))
            })
        })
    }

    pub(super) fn set_translation(&self, translation: (f64, f64)) {
        let mut status = self.status.borrow_mut();
        status.translation = Some(translation);
    }

    fn pointer_loc(&self) -> (f32, f32) {
        let (x, y) = self.status.borrow().pointer_location.clone();
        let (_, h) = self.window_size().unwrap();
        (x, h as f32 - y)
    }
}
