use anyhow::Result;
use glow::HasContext;
use std::{
    cell::{RefCell, RefMut},
    collections::HashSet,
    num::NonZeroU32,
    path::PathBuf,
    rc::Rc,
};

use femtovg::{
    Canvas, FontId, ImageFlags, ImageId, ImageSource, Paint, Path, PixelFormat, Transform2D,
    imgref::{Img, ImgVec},
    renderer,
    rgb::{RGB, RGBA, RGBA8},
};
use fontconfig::Fontconfig;
use gtk::{glib, prelude::*, subclass::prelude::*};
use relm4::gtk::gdk_pixbuf::Pixbuf;
use relm4::{Sender, gtk};
use resource::resource;

use crate::{
    APP_CONFIG,
    configuration::Action,
    math::{Vec2D, rect_ensure_in_bounds, rect_round},
    sketch_board::SketchBoardInput,
    tools::{CropTool, Drawable, Tool},
};

use super::{font_stack, set_font_stack};

const TRANSPARENCY_SQUARE_SIZE: usize = 64;

#[derive(Default)]
pub struct FemtoVGArea {
    canvas: RefCell<Option<femtovg::Canvas<femtovg::renderer::OpenGl>>>,
    font: RefCell<Option<FontId>>,
    inner: RefCell<Option<FemtoVgAreaMut>>,
    request_render: RefCell<Option<Vec<Action>>>,
    sender: RefCell<Option<Sender<SketchBoardInput>>>,
}

pub struct FemtoVgAreaMut {
    background_image: Pixbuf,
    background_image_id: Option<femtovg::ImageId>,
    transparent_background_id: Option<femtovg::ImageId>,
    active_tool: Rc<RefCell<dyn Tool>>,
    crop_tool: Rc<RefCell<CropTool>>,
    scale_factor: f32,
    offset: Vec2D,
    drawables: Vec<Box<dyn Drawable>>,
    undo_stack: Vec<HistoryEntry>,
    redo_stack: Vec<HistoryEntry>,
    zoom_scale: f32,
    last_scale: f32,
    pointer_offset: Vec2D,
    last_offset: Vec2D,
    drag_offset: Vec2D,
    is_drag: bool,
    is_reset: bool,
}

enum HistoryEntry {
    Drawable(Box<dyn Drawable>),
    ClearAll(Vec<Box<dyn Drawable>>),
}

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

impl ObjectImpl for FemtoVGArea {
    fn constructed(&self) {
        self.parent_constructed();
        let area = self.obj();
        area.set_has_stencil_buffer(true);
        area.queue_render();
    }
}

impl WidgetImpl for FemtoVGArea {
    fn realize(&self) {
        self.parent_realize();
    }
    fn unrealize(&self) {
        self.obj().make_current();
        self.canvas.borrow_mut().take();
        self.parent_unrealize();
    }
}

impl GLAreaImpl for FemtoVGArea {
    fn resize(&self, width: i32, height: i32) {
        self.ensure_canvas();

        let mut bc = self.canvas.borrow_mut();
        let canvas = bc.as_mut().unwrap(); // this unwrap is safe as long as we call "ensure_canvas" before

        let w = canvas.width();
        let h = canvas.height();

        canvas.set_size(
            if width == 0 { w } else { width as u32 },
            if height == 0 { h } else { height as u32 },
            self.obj().scale_factor() as f32,
        );

        // update scale factor
        self.inner()
            .as_mut()
            .expect("Did you call init before using FemtoVgArea?")
            .update_transformation(canvas);
    }
    fn render(&self, _context: &gtk::gdk::GLContext) -> glib::Propagation {
        self.ensure_canvas();

        let mut bc = self.canvas.borrow_mut();
        let canvas = bc.as_mut().unwrap(); // this unwrap is safe as long as we call "ensure_canvas" before
        let font = self.font.borrow().unwrap(); // this unwrap is safe as long as we call "ensure_canvas" before
        let mut actions = self.request_render.borrow_mut();

        // if we got requested to render a frame
        if let Some(a) = actions.take() {
            // render image
            let image = match self
                .inner()
                .as_mut()
                .expect("Did you call init before using FemtoVgArea?")
                .render_native_resolution(canvas, font)
            {
                Ok(t) => t,
                Err(e) => {
                    eprintln!("Error while rendering image: {e}");
                    return glib::Propagation::Stop;
                }
            };

            // send result
            self.sender
                .borrow()
                .as_ref()
                .expect("Did you call init before using FemtoVgArea?")
                .emit(SketchBoardInput::RenderResult(image, a));

            // reset request
            *actions = None;
        }
        if let Err(e) = self
            .inner()
            .as_mut()
            .expect("Did you call init before using FemtoVgArea?")
            .render_framebuffer(canvas, font)
        {
            eprintln!("Error rendering to framebuffer: {e}");
        }
        glib::Propagation::Stop
    }
}
impl FemtoVGArea {
    pub fn init(
        &self,
        sender: Sender<SketchBoardInput>,
        crop_tool: Rc<RefCell<CropTool>>,
        active_tool: Rc<RefCell<dyn Tool>>,
        background_image: Pixbuf,
    ) {
        let initial_scale = APP_CONFIG.read().input_scale().unwrap_or(0.0);
        self.inner().replace(FemtoVgAreaMut {
            background_image,
            background_image_id: None,
            transparent_background_id: None,
            active_tool,
            crop_tool,
            scale_factor: 1.0,
            offset: Vec2D::zero(),
            drawables: Vec::new(),
            undo_stack: Vec::new(),
            redo_stack: Vec::new(),
            zoom_scale: initial_scale,
            pointer_offset: Vec2D::zero(),
            last_offset: Vec2D::zero(),
            drag_offset: Vec2D::zero(),
            last_scale: initial_scale,
            is_drag: false,
            is_reset: false,
        });
        self.sender.borrow_mut().replace(sender);
    }
    fn ensure_canvas(&self) {
        if self.canvas.borrow().is_none() {
            let c = self
                .setup_canvas()
                .expect("Cannot setup renderer and canvas");
            self.canvas.borrow_mut().replace(c);
        }

        if self.font.borrow().is_none()
            && let Some(first) = font_stack().first()
        {
            self.font.borrow_mut().replace(*first);
        }
    }

    fn build_text_context(&self) -> Result<(femtovg::TextContext, Vec<FontId>)> {
        let text_context = femtovg::TextContext::default();
        let mut loaded_fonts = Vec::new();
        let mut loaded_paths = HashSet::<(PathBuf, u32)>::new();

        let app_config = APP_CONFIG.read();
        let fontconfig = Fontconfig::new();

        let mut load_font = |family: &str, style: Option<&str>| -> Result<FontId> {
            let fc = fontconfig
                .as_ref()
                .ok_or(anyhow::anyhow!("Could not initialize Fontconfig"))?;

            let font = fc
                .find(family, style)
                .map_err(|e| anyhow::anyhow!("Font family '{}' not found: {}", family, e))?;

            let face_index = font.index.unwrap_or(0).max(0) as u32;

            if !loaded_paths.insert((font.path.clone(), face_index)) {
                return Err(anyhow::anyhow!("Font '{}' already loaded", family));
            }
            let data = std::fs::read(&font.path)
                .map_err(|e| anyhow::anyhow!("Failed to read font file: {}", e))?;

            text_context
                .add_shared_font_with_index(data, face_index)
                .map_err(|e| anyhow::anyhow!("Failed to load font: {}", e))
        };

        match load_font(
            app_config.font().family().unwrap_or(""),
            app_config.font().style(),
        ) {
            Ok(id) => {
                loaded_fonts.push(id);
            }
            Err(e) => {
                eprintln!("Primary font: {}", e);
            }
        }

        if loaded_fonts.is_empty() {
            let fallback = text_context
                .add_font_mem(&resource!("src/assets/Roboto-Regular.ttf"))
                .expect("Cannot add font");
            loaded_fonts.push(fallback);
        }

        for family in app_config.font().fallback() {
            match load_font(family, None) {
                Ok(id) => {
                    loaded_fonts.push(id);
                }
                Err(e) => {
                    eprintln!("Fallback font: {}", e);
                }
            }
        }

        // Append a color emoji font as the last fallback so emoji render out of the box
        // (femtovg's default image-loading feature draws their bitmap/COLR glyphs).
        // fontconfig resolves the generic "emoji" family to the installed color emoji
        // font, e.g. Noto Color Emoji. It is skipped if already loaded or unavailable.
        // reconsider with #309
        match load_font("emoji", None) {
            Ok(id) => {
                loaded_fonts.push(id);
            }
            Err(e) => {
                eprintln!("Emoji font: {}", e);
            }
        }

        Ok((text_context, loaded_fonts))
    }

    fn setup_canvas(&self) -> Result<femtovg::Canvas<femtovg::renderer::OpenGl>> {
        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 (text_context, loaded_fonts) = self.build_text_context()?;
        let canvas = Canvas::new_with_text_context(renderer, text_context)?;

        set_font_stack(loaded_fonts.clone());
        if let Some(first) = loaded_fonts.first() {
            self.font.borrow_mut().replace(*first);
        }

        Ok(canvas)
    }

    pub fn inner(&self) -> RefMut<'_, Option<FemtoVgAreaMut>> {
        self.inner.borrow_mut()
    }
    pub fn request_render(&self, actions: &[Action]) {
        self.request_render.borrow_mut().replace(actions.into());
        self.obj().queue_render();
    }
    pub fn set_parent_sender(&self, sender: Sender<SketchBoardInput>) {
        self.sender.borrow_mut().replace(sender);
    }
}

impl FemtoVgAreaMut {
    pub fn commit(&mut self, drawable: Box<dyn Drawable>) {
        self.undo_stack
            .push(HistoryEntry::Drawable(drawable.clone_box()));
        self.drawables.push(drawable);
        self.redo_stack.clear();
    }

    pub fn undo(&mut self) -> bool {
        match self.undo_stack.pop() {
            Some(HistoryEntry::Drawable(history_drawable)) => {
                let mut drawable = self.drawables.pop().unwrap_or(history_drawable);
                drawable.handle_undo();
                self.redo_stack.push(HistoryEntry::Drawable(drawable));
                true
            }
            Some(HistoryEntry::ClearAll(drawables)) => {
                self.restore_clear_all(drawables);
                true
            }
            None => false,
        }
    }

    pub fn redo(&mut self) -> bool {
        match self.redo_stack.pop() {
            Some(HistoryEntry::Drawable(mut drawable)) => {
                drawable.handle_redo();
                self.undo_stack
                    .push(HistoryEntry::Drawable(drawable.clone_box()));
                self.drawables.push(drawable);
                true
            }
            Some(HistoryEntry::ClearAll(_)) => {
                self.apply_clear_all();
                true
            }
            None => false,
        }
    }

    pub fn clear_all(&mut self) -> bool {
        if self.drawables.is_empty() {
            return false;
        }

        self.apply_clear_all();
        self.redo_stack.clear();
        true
    }

    fn apply_clear_all(&mut self) {
        let mut drawables = std::mem::take(&mut self.drawables);
        Self::handle_drawables_undo(&mut drawables);
        self.undo_stack.push(HistoryEntry::ClearAll(drawables));
    }

    fn restore_clear_all(&mut self, mut drawables: Vec<Box<dyn Drawable>>) {
        Self::handle_drawables_redo(&mut drawables);
        self.redo_stack
            .push(HistoryEntry::ClearAll(Self::clone_drawables(&drawables)));
        self.drawables = drawables;
    }

    fn handle_drawables_undo(drawables: &mut [Box<dyn Drawable>]) {
        for d in drawables.iter_mut().rev() {
            d.handle_undo();
        }
    }

    fn handle_drawables_redo(drawables: &mut [Box<dyn Drawable>]) {
        for d in drawables.iter_mut() {
            d.handle_redo();
        }
    }

    fn clone_drawables(drawables: &[Box<dyn Drawable>]) -> Vec<Box<dyn Drawable>> {
        drawables.iter().map(|d| d.clone_box()).collect()
    }

    pub fn set_active_tool(&mut self, active_tool: Rc<RefCell<dyn Tool>>) {
        self.active_tool = active_tool;
    }

    pub fn render_native_resolution(
        &mut self,
        canvas: &mut femtovg::Canvas<femtovg::renderer::OpenGl>,
        font: FontId,
    ) -> anyhow::Result<ImgVec<RGBA8>> {
        let bounds = (
            Vec2D::zero(),
            Vec2D::new(
                self.background_image.width() as f32,
                self.background_image.height() as f32,
            ),
        );
        // get offset and size of the area in question
        let (pos, size) = self
            .crop_tool
            .borrow()
            .get_crop()
            .map(|c| c.get_rectangle())
            .map(|rect| rect_ensure_in_bounds(rect, bounds))
            .map(rect_round)
            .filter(|(_, size)| !size.is_zero())
            .unwrap_or(bounds);

        // create render-target
        let image_id = canvas.create_image_empty(
            size.x as usize,
            size.y as usize,
            PixelFormat::Rgba8,
            ImageFlags::empty(),
        )?;
        canvas.set_render_target(femtovg::RenderTarget::Image(image_id));

        // apply offset
        let mut transform = Transform2D::identity();
        transform.translate(-pos.x, -pos.y);
        canvas.reset_transform();
        canvas.set_transform(&transform);

        self.render(
            canvas,
            font,
            false,
            femtovg::Color::rgbaf(0.0, 0.0, 0.0, 0.0),
            false,
        )?;

        // return screenshot
        let result = canvas.screenshot();

        // clean up
        canvas.set_render_target(femtovg::RenderTarget::Screen);
        canvas.delete_image(image_id);

        Ok(result?)
    }

    pub fn render_framebuffer(
        &mut self,
        canvas: &mut femtovg::Canvas<femtovg::renderer::OpenGl>,
        font: FontId,
    ) -> Result<()> {
        canvas.set_render_target(femtovg::RenderTarget::Screen);

        // setup transform to image coordinates
        let mut transform = Transform2D::identity();
        transform.scale(self.scale_factor, self.scale_factor);
        transform.translate(self.offset.x, self.offset.y);

        canvas.reset_transform();
        canvas.set_transform(&transform);

        self.render(
            canvas,
            font,
            true,
            femtovg::Color::rgbaf(0.0, 0.0, 0.0, 0.0),
            true,
        )?;

        Ok(())
    }

    fn render(
        &mut self,
        canvas: &mut femtovg::Canvas<femtovg::renderer::OpenGl>,
        font: FontId,
        render_crop: bool,
        outside_bg_color: femtovg::Color,
        onscreen: bool,
    ) -> Result<()> {
        // clear canvas

        canvas.clear_rect(0, 0, canvas.width(), canvas.height(), outside_bg_color);

        // render background
        self.render_background_image(canvas, onscreen)?;

        let bounds = (
            Vec2D::zero(),
            Vec2D::new(
                self.background_image.width() as f32,
                self.background_image.height() as f32,
            ),
        );
        // render the whole stack
        for d in &mut self.drawables {
            d.draw(canvas, font, bounds)?;
        }

        // render active tool
        if let Some(d) = self.active_tool.borrow().get_drawable() {
            d.draw(canvas, font, bounds)?;
        }

        // render crop tool
        if render_crop && let Some(c) = self.crop_tool.borrow().get_crop() {
            c.draw(canvas, font, bounds)?;
        }

        canvas.flush();
        Ok(())
    }

    fn render_background_image(
        &mut self,
        canvas: &mut femtovg::Canvas<femtovg::renderer::OpenGl>,
        onscreen: bool,
    ) -> Result<()> {
        let background_image_id = match self.background_image_id {
            Some(id) => id,
            None => {
                let id = Self::upload_background_image(canvas, &self.background_image)?;
                self.background_image_id.replace(id);
                id
            }
        };

        let transparency_bg_id = match self.transparent_background_id {
            Some(id) if onscreen => Some(id),
            None => {
                if let Some(id) = Self::create_transparency_bg(canvas) {
                    self.transparent_background_id.replace(id);
                    Some(id)
                } else {
                    None
                }
            }
            _ => None,
        };

        // render the image
        let mut path = Path::new();

        let w = self.background_image.width() as f32;
        let h = self.background_image.height() as f32;

        path.rect(0.0, 0.0, w, h);

        if let Some(id) = transparency_bg_id {
            // fix the transparency grid in place
            let pos = self.abs_canvas_to_image_coordinates(Vec2D::zero(), 1.0);

            canvas.fill_path(
                &path,
                &Paint::image(
                    id,
                    pos.x,
                    pos.y,
                    TRANSPARENCY_SQUARE_SIZE as f32 / self.scale_factor,
                    TRANSPARENCY_SQUARE_SIZE as f32 / self.scale_factor,
                    0f32,
                    1f32,
                ),
            );
        }

        canvas.fill_path(
            &path,
            &Paint::image(background_image_id, 0f32, 0f32, w, h, 0f32, 1f32),
        );

        Ok(())
    }

    fn upload_background_image(
        canvas: &mut femtovg::Canvas<femtovg::renderer::OpenGl>,
        image: &Pixbuf,
    ) -> Result<ImageId> {
        let format = if image.has_alpha() {
            PixelFormat::Rgba8
        } else {
            PixelFormat::Rgb8
        };

        let background_image_id = canvas.create_image_empty(
            image.width() as usize,
            image.height() as usize,
            format,
            ImageFlags::empty(),
        )?;

        // extract values
        let width = image.width() as usize;
        let stride = image.rowstride() as usize; // stride is in bytes per row
        let height = image.height() as usize;
        let bytes_per_pixel = if image.has_alpha() { 4 } else { 3 }; // pixbuf supports rgb or rgba

        unsafe {
            let src_buffer = image.pixels();

            let row_length = width * bytes_per_pixel;
            let mut dst_buffer = if row_length == stride {
                // stride == row_length, there are no additional bytes after the end of each row
                src_buffer.to_vec()
            } else {
                // stride != row_length, there are additional bytes after the end of each row that
                // need to be truncated. We copy row by row..
                let mut dst_buffer = Vec::<u8>::with_capacity(width * height * bytes_per_pixel);

                for row in 0..height {
                    let src_offset = row * stride;
                    dst_buffer.extend_from_slice(&src_buffer[src_offset..src_offset + row_length]);
                }
                dst_buffer
            };

            // in almost all cases, that should be a no-op. Buf we might have additional elements after the
            // end of the buffer, e.g. after width * height * bytes_per_pixel
            dst_buffer.truncate(width * height * bytes_per_pixel);

            if image.has_alpha() {
                let img = Img::new_stride(
                    dst_buffer.align_to::<RGBA<u8>>().1.to_vec(),
                    width,
                    height,
                    width,
                );

                canvas.update_image(background_image_id, ImageSource::Rgba(img.as_ref()), 0, 0)?;
            } else {
                let img = Img::new_stride(
                    dst_buffer.align_to::<RGB<u8>>().1.to_owned(),
                    width,
                    height,
                    width,
                );

                canvas.update_image(background_image_id, ImageSource::Rgb(img.as_ref()), 0, 0)?;
            }
        }

        Ok(background_image_id)
    }

    fn create_transparency_bg(
        canvas: &mut femtovg::Canvas<femtovg::renderer::OpenGl>,
    ) -> Option<femtovg::ImageId> {
        let tile: usize = TRANSPARENCY_SQUARE_SIZE * 2;
        let mut pixels = vec![RGBA8::new(204, 204, 204, 255); tile * tile];

        for y in 0..tile {
            for x in 0..tile {
                if (x / TRANSPARENCY_SQUARE_SIZE + y / TRANSPARENCY_SQUARE_SIZE) % 2 == 1 {
                    pixels[y * tile + x] = RGBA8::new(153, 153, 153, 255);
                }
            }
        }
        let img = Img::new(pixels, tile, tile);

        match canvas.create_image(
            ImageSource::Rgba(img.as_ref()),
            ImageFlags::REPEAT_X | ImageFlags::REPEAT_Y,
        ) {
            Ok(id) => Some(id),
            Err(_) => {
                eprintln!("Could not create transparency background image");
                None
            }
        }
    }

    pub fn update_transformation(
        &mut self,
        canvas: &mut femtovg::Canvas<femtovg::renderer::OpenGl>,
    ) {
        let image_width = self.background_image.width() as f32;
        let image_height = self.background_image.height() as f32;
        let aspect_ratio = image_width / image_height;

        let canvas_width = canvas.width() as f32;
        let canvas_height = canvas.height() as f32;

        let prev_scale = self.scale_factor;
        let mut center_offset = Vec2D::zero();

        // update scale_factor
        if self.zoom_scale != 0.0 {
            if self.zoom_scale != self.last_scale {
                self.last_scale = self.zoom_scale;
                self.scale_factor = self.zoom_scale;

                if !self.is_reset {
                    // calculate offset from pointer
                    let pointer_offset = self.pointer_offset;
                    let zoom_offset = Vec2D::new(
                        (pointer_offset.x - self.offset.x) / prev_scale,
                        (pointer_offset.y - self.offset.y) / prev_scale,
                    );

                    let calculated_offset = pointer_offset - zoom_offset * self.scale_factor;

                    // update drag_offset
                    center_offset = Vec2D::new(
                        (canvas_width - image_width * self.scale_factor) / 2.0,
                        (canvas_height - image_height * self.scale_factor) / 2.0,
                    );

                    self.drag_offset = calculated_offset - center_offset;
                    self.store_last_offset();
                }
            } else {
                self.scale_factor = self.zoom_scale;
            }
        } else {
            self.scale_factor = if canvas_width / aspect_ratio <= canvas_height {
                canvas_width / aspect_ratio / image_height
            } else {
                canvas_height * aspect_ratio / image_width
            };
        }

        // final offset
        if center_offset.is_zero() {
            center_offset = Vec2D::new(
                (canvas_width - image_width * self.scale_factor) / 2.0,
                (canvas_height - image_height * self.scale_factor) / 2.0,
            );
        }

        if self.is_reset {
            // centered
            self.is_reset = false;
            self.offset = center_offset;
        } else {
            // dragged
            self.offset = center_offset + self.drag_offset;
        }
    }

    pub fn abs_canvas_to_image_coordinates(&self, input: Vec2D, dpi_scale_factor: f32) -> Vec2D {
        Vec2D::new(
            (input.x * dpi_scale_factor - self.offset.x) / self.scale_factor,
            (input.y * dpi_scale_factor - self.offset.y) / self.scale_factor,
        )
    }
    pub fn rel_canvas_to_image_coordinates(&self, input: Vec2D, dpi_scale_factor: f32) -> Vec2D {
        Vec2D::new(
            input.x * dpi_scale_factor / self.scale_factor,
            input.y * dpi_scale_factor / self.scale_factor,
        )
    }

    pub fn set_zoom_scale(&mut self, factor: f32, abs: bool) {
        if self.is_drag {
            return;
        }

        if abs {
            self.zoom_scale = factor;
        } else {
            if self.zoom_scale == 0.0 {
                self.zoom_scale = self.scale_factor;
            }

            self.zoom_scale *= factor;
            self.zoom_scale = self.zoom_scale.max(0.);
        }
    }

    pub fn set_pointer_offset(&mut self, offset: Vec2D) {
        self.pointer_offset = offset;
    }

    pub fn set_drag_offset(&mut self, offset: Vec2D) {
        self.drag_offset = self.last_offset + offset;
    }

    pub fn reset_drag_offset(&mut self) {
        self.drag_offset = Vec2D::zero();
        self.store_last_offset();
        self.is_reset = true;
    }

    pub fn store_last_offset(&mut self) {
        self.last_offset = self.drag_offset;
    }

    pub fn set_is_drag(&mut self, is_drag: bool) {
        self.is_drag = is_drag;
    }
}
