use crate::application::{Action, State};
use crate::utils;
use crate::widgets::PixbufExt;
use gdk::prelude::*;
use gio::prelude::*;
use glib::Sender;
use gtk::prelude::*;
use std::cell::RefCell;
use std::fs::File;
use std::path::PathBuf;
use std::rc::Rc;

#[derive(PartialEq, Debug, Clone, Copy)]
pub enum Filter {
    Blur,
    Filled,
}

pub struct Drawer {
    pub widget: gtk::ScrolledWindow,
    builder: gtk::Builder,
    area: gtk::DrawingArea,
    surface: RefCell<cairo::ImageSurface>,
    filter: RefCell<Filter>,
    zoom: RefCell<f64>,
    offset: RefCell<(f64, f64)>,
    select_position: RefCell<(f64, f64)>, // (x, y) position of the selection
    history: RefCell<Vec<gdk_pixbuf::Pixbuf>>,
    future: RefCell<Vec<gdk_pixbuf::Pixbuf>>,
    sender: Sender<Action>,
}

impl Drawer {
    pub fn new(sender: Sender<Action>) -> Rc<Self> {
        let builder = gtk::Builder::new_from_resource("/com/belmoussaoui/Obfuscate/drawer.ui");
        get_widget!(builder, gtk::ScrolledWindow, scrolled_window);
        get_widget!(builder, gtk::DrawingArea, drawing_area);

        let surface = cairo::ImageSurface::create(cairo::Format::Rgb24, 300, 300);

        let drawer = Rc::new(Drawer {
            widget: scrolled_window,
            surface: RefCell::new(surface.unwrap()),
            filter: RefCell::new(Filter::Blur),
            zoom: RefCell::new(1.0),
            select_position: RefCell::new((0.0, 0.0)),
            history: RefCell::new(Vec::new()),
            future: RefCell::new(Vec::new()),
            offset: RefCell::new((0.0, 0.0)),
            builder,
            area: drawing_area,
            sender,
        });
        drawer.init(drawer.clone());
        drawer.setup_signals(drawer.clone());
        drawer
    }
    fn init(&self, drawer: Rc<Self>) {
        // Setup drop area
        get_widget!(self.builder, gtk::Adjustment, hadjustment);
        get_widget!(self.builder, gtk::Adjustment, vadjustment);
        hadjustment.connect_value_changed(clone!(@strong drawer => move |hadj| {
            let allocation = drawer.area.get_allocation();
            let scaled_width = drawer.surface.borrow().clone().get_width() as f64 * *drawer.zoom.borrow();
            let max_offset_x = scaled_width - allocation.width as f64;
            let max_value = hadj.get_upper() - hadj.get_page_size();
            let value = hadj.get_value();

            let mut offset_x = -1.0 * value * max_offset_x;
            if max_value != 0.0 {
                offset_x /= max_value;
            }
            let mut offset = drawer.offset.borrow_mut();
            offset.0 = offset_x;
            drawer.area.queue_draw()
        }));
        vadjustment.connect_value_changed(clone!(@strong drawer => move |adj| {
            let allocation = drawer.area.get_allocation();
            let scaled_height = drawer.surface.borrow().clone().get_height() as f64 * *drawer.zoom.borrow();
            let max_offset_y = scaled_height - allocation.height as f64;
            let max_value = adj.get_upper() - adj.get_page_size();
            let value = adj.get_value();

            let mut offset_y = -1.0 * value * max_offset_y;
            if max_value != 0.0 {
                offset_y /= max_value;
            }
            let mut offset = drawer.offset.borrow_mut();
            offset.1 = offset_y;
            drawer.area.queue_draw()
        }));
        self.widget.show();
    }
    pub fn set_filter(&self, filter: Filter) {
        self.filter.replace(filter);
    }

    pub fn get_filter(&self) -> Filter {
        *self.filter.borrow()
    }

    pub fn save_to(&self, file: PathBuf) {
        match File::create(file.to_str().unwrap()) {
            Ok(mut buffer) => {
                let surface = self.surface.borrow().clone();
                surface.write_to_png(&mut buffer).expect("Failed to save file");
            }
            Err(err) => error!("Failed to save the PNG {}", err),
        }
    }

    pub fn set_zoom(&self, zoom: f64) {
        self.zoom.replace(zoom);
        self.area.queue_draw();
    }

    pub fn get_zoom(&self) -> f64 {
        *self.zoom.borrow()
    }

    pub fn undo(&self) {
        let mut history = self.history.borrow_mut();
        if history.len() == 1 {
            // don't clean everything
            return;
        }
        if let Some(dropped_pixbuf) = history.pop() {
            // drop the latest element
            self.future.borrow_mut().push(dropped_pixbuf);
        }
        if let Some(pixbuf) = history.last() {
            let surface = PixbufExt::create_surface(pixbuf, self.get_zoom() as i32, None).unwrap();
            self.surface.replace(surface);
            self.area.queue_draw();
        }
        send!(self.sender, Action::SetState(State::CanUndo(history.len() > 1)));
        send!(self.sender, Action::SetState(State::CanRedo(true)));
    }
    pub fn redo(&self) {
        let mut future = self.future.borrow_mut();
        if let Some(pixbuf) = future.pop() {
            let surface = PixbufExt::create_surface(&pixbuf, self.get_zoom() as i32, None).unwrap();
            self.history.borrow_mut().push(pixbuf); // push back to history
            self.surface.replace(surface);
            self.area.queue_draw();
        }
        send!(self.sender, Action::SetState(State::CanUndo(true)));
        send!(self.sender, Action::SetState(State::CanRedo(future.len() != 0)));
    }

    pub fn get_pixbuf(&self) -> gdk_pixbuf::Pixbuf {
        let latest_item = self.history.borrow();
        latest_item.last().unwrap().clone()
    }

    pub fn paste(&self, s: Rc<Self>) {
        // Await for any pixbuf content
        let display = gdk::Display::get_default().unwrap();
        let clipboard = gtk::Clipboard::get_default(&display).unwrap();
        if clipboard.wait_is_image_available() {
            if let Some(pixbuf) = clipboard.wait_for_image() {
                s.load_pixbuf(pixbuf);
                s.area.queue_draw();
            }
        }
        send!(self.sender, Action::ViewPhoto);
    }

    pub fn copy(&self) {
        if self.history.borrow().len() != 0 {
            let display = gdk::Display::get_default().unwrap();
            let clipboard = gtk::Clipboard::get_default(&display).unwrap();

            let surface = self.surface.borrow().clone();
            let pixbuf = gdk::pixbuf_get_from_surface(&surface, 0, 0, surface.get_width(), surface.get_height()).unwrap();

            clipboard.set_image(&pixbuf);
        }
    }

    fn get_center(&self) -> (f64, f64) {
        let surface = self.surface.borrow().clone();
        let zoom = *self.zoom.borrow();
        let alloc = self.area.get_allocation();
        let offset = *self.offset.borrow();

        let scaled_width = surface.get_width() as f64 * zoom;
        let scaled_height = surface.get_height() as f64 * zoom;

        let mut offset_x = offset.0;
        let mut offset_y = offset.1;

        let center_x = (alloc.width as f64 - scaled_width) / 2.0;
        if center_x > 0.0 {
            offset_x += center_x;
        }
        let center_y = (alloc.height as f64 - scaled_height) / 2.0;
        if center_y > 0.0 {
            offset_y += center_y;
        }
        (offset_x, offset_y)
    }

    fn setup_signals(&self, drawer: Rc<Self>) {
        self.area.add_events(
            gdk::EventMask::BUTTON_PRESS_MASK
                | gdk::EventMask::BUTTON_RELEASE_MASK
                | gdk::EventMask::ENTER_NOTIFY_MASK
                | gdk::EventMask::POINTER_MOTION_MASK
                | gdk::EventMask::LEAVE_NOTIFY_MASK
                | gdk::EventMask::SCROLL_MASK,
        );

        get_widget!(self.builder, gtk::Adjustment, hadjustment);
        get_widget!(self.builder, gtk::Adjustment, vadjustment);
        self.area.connect_draw(clone!(@strong drawer => move |_, ctx| {
            let surface = drawer.surface.borrow().clone();
            let zoom = *drawer.zoom.borrow();

            let alloc = drawer.area.get_allocation();
            let scaled_width = surface.get_width() as f64 * zoom;
            let scaled_height = surface.get_height() as f64 * zoom;

            let page_size_x = alloc.width as f64 / scaled_width;
            hadjustment.set_lower(0.0);
            hadjustment.set_page_size(page_size_x);
            hadjustment.set_upper(1.0);
            hadjustment.set_step_increment(0.1);
            hadjustment.set_page_increment(0.5);

            let page_size_y = alloc.height as f64 / scaled_height;
            vadjustment.set_lower(0.0);
            vadjustment.set_page_size(page_size_y);
            vadjustment.set_upper(1.0);
            vadjustment.set_step_increment(0.1);
            vadjustment.set_page_increment(0.5);

            let (center_x, center_y) = drawer.get_center();
            ctx.translate(center_x, center_y);
            ctx.scale(zoom, zoom);
            ctx.set_source_surface(&surface, 0.0, 0.0);
            ctx.paint();
            gtk::Inhibit(false)
        }));

        // Selection
        self.area.connect_button_press_event(clone!(@strong drawer => move |_, event| {
            let position = event.get_position();
            drawer.select_position.replace(position);
            gtk::Inhibit(false)
        }));

        self.area.connect_button_release_event(clone!(@strong drawer => move |_, event| {
            let start_position = *drawer.select_position.borrow();
            let end_position = event.get_position();

            drawer.apply_filter(start_position, end_position);
            drawer.select_position.replace((0.0, 0.0));

            gtk::Inhibit(false)
        }));
        self.area.connect_enter_notify_event(move |widget, _| {
            let display = gdk::Display::get_default().unwrap();
            if let Some(cursor) = gdk::Cursor::new_from_name(&display, "cell") {
                let top_level = widget.get_window().unwrap();
                top_level.set_cursor(Some(&cursor));
            }
            gtk::Inhibit(false)
        });

        self.area.connect_leave_notify_event(move |widget, _| {
            let display = gdk::Display::get_default().unwrap();
            if let Some(cursor) = gdk::Cursor::new_from_name(&display, "default") {
                let top_level = widget.get_window().unwrap();
                top_level.set_cursor(Some(&cursor));
            }
            gtk::Inhibit(false)
        });
        self.area.connect_motion_notify_event(clone!(@strong drawer => move |widget, event| {
            if *drawer.select_position.borrow() != (0.0, 0.0) {
                let pixbuf = drawer.get_pixbuf();
                let surface = PixbufExt::create_surface(&pixbuf, drawer.get_zoom() as i32, None).unwrap();
                let position = event.get_position();
                let (center_x, center_y) = drawer.get_center();

                let start_position = *drawer.select_position.borrow();
                let ctx = cairo::Context::new(&surface);
                let scale = drawer.get_zoom();

                ctx.set_operator(cairo::Operator::Over);
                ctx.set_line_width(0.3);
                ctx.rectangle(
                    (start_position.0 - center_x) / scale,
                    (start_position.1 - center_y) / scale,
                    (position.0 - start_position.0) / scale,
                    (position.1 - start_position.1) / scale,
                );

                ctx.set_source_rgb(36.0 / 255.0, 31.0 / 255.0, 49.0 / 255.0);
                ctx.stroke();
                widget.queue_draw();
                drawer.surface.replace(surface);
            }
            gtk::Inhibit(false)
        }));

        // Ctrl + scroll up/down to zoom/unzoom
        self.area.connect_scroll_event(clone!(@strong drawer => move |_, event| {
            if event.get_state() == gdk::ModifierType::CONTROL_MASK {
                if event.get_direction() == gdk::ScrollDirection::Up {
                    send!(drawer.sender, Action::SetState(State::ZoomUp));
                } else {
                    send!(drawer.sender, Action::SetState(State::ZoomDown));
                }
            }
            gtk::Inhibit(false)
        }));
    }

    fn apply_filter(&self, start_position: (f64, f64), end_position: (f64, f64)) {
        let surface = self.surface.borrow().clone();
        let scale = self.get_zoom();
        let (center_x, center_y) = self.get_center();
        match self.get_filter() {
            Filter::Filled => {
                let ctx = cairo::Context::new(&surface);
                ctx.set_operator(cairo::Operator::Over);
                ctx.set_source_rgb(0.0, 0.0, 0.0);
                ctx.rectangle(
                    (start_position.0 - center_x) / scale,
                    (start_position.1 - center_y) / scale,
                    (end_position.0 - start_position.0) / scale,
                    (end_position.1 - start_position.1) / scale,
                );
                ctx.fill();
                self.area.queue_draw();
            }
            Filter::Blur => {
                if let Some(pixbuf) = gdk::pixbuf_get_from_surface(
                    &surface,
                    ((start_position.0.min(end_position.0) - center_x) / scale) as i32,
                    ((start_position.1.min(end_position.1) - center_y) / scale) as i32,
                    ((end_position.0 - start_position.0).abs() / scale) as i32,
                    ((end_position.1 - start_position.1).abs() / scale) as i32,
                ) {
                    // This might fail if the user selects a very small area.
                    let mut surface_to_blur = PixbufExt::create_surface(&pixbuf, scale as i32, None).unwrap();
                    match utils::blur_surface(&mut surface_to_blur) {
                        Ok(blurred_surface) => {
                            let ctx = cairo::Context::new(&surface);
                            ctx.set_operator(cairo::Operator::Over);
                            ctx.set_source_surface(
                                &blurred_surface,
                                (start_position.0.min(end_position.0) - center_x as f64) / scale,
                                (start_position.1.min(end_position.1) - center_y as f64) / scale,
                            );
                            ctx.paint();
                            self.area.queue_draw();
                        }
                        Err(err) => error!("Failed to create a blurred surface, {}", err),
                    }
                }
            }
        }
        // let the user undo now
        send!(self.sender, Action::SetState(State::CanUndo(true)));
        // Save state
        let pixbuf = gdk::pixbuf_get_from_surface(&surface, 0, 0, surface.get_width(), surface.get_height()).unwrap();
        self.history.borrow_mut().push(pixbuf);
    }

    pub fn draw_from_file(&self, file: gio::File) -> Result<(), failure::Error> {
        let stream = file.read(gio::NONE_CANCELLABLE)?;

        let pixbuf = gdk_pixbuf::Pixbuf::new_from_stream(&stream, gio::NONE_CANCELLABLE)?;
        self.load_pixbuf(pixbuf);
        Ok(())
    }

    fn load_pixbuf(&self, pixbuf: gdk_pixbuf::Pixbuf) {
        self.history.replace(Vec::new()); // clear the history
        self.future.replace(Vec::new()); // clear the future
        send!(self.sender, Action::SetState(State::CanUndo(false)));
        send!(self.sender, Action::SetState(State::CanRedo(false)));

        // Ask the window to fit that size if possible.
        send!(self.sender, Action::ResizeWindow(pixbuf.get_width(), pixbuf.get_height()));

        let new_surface = PixbufExt::create_surface(&pixbuf, self.get_zoom() as i32, None).unwrap();
        self.surface.replace(new_surface);
        let ctx = cairo::Context::new(&self.surface.borrow());
        ctx.set_source_pixbuf(&pixbuf, 0.0, 0.0);
        self.history.borrow_mut().push(pixbuf);
        ctx.paint();
        ctx.fill();
    }
}
