use std::path::Path;

use anyhow::Result;
use image::{Bgra, DynamicImage, ImageBuffer};
use xcb::{
    base::Connection,
    xproto::{self, Rectangle, Screen},
};
use xcb_util::image as xcb_image;

pub struct Gui {
    conn: Connection,
}

impl Gui {
    pub fn new() -> Result<Self> {
        let (conn, _) = Connection::connect(None)?;
        Ok(Gui { conn })
    }

    fn get_default_screen(&self) -> Screen {
        // TODO: multiple screens
        let setup = self.conn.get_setup();
        let mut iter = setup.roots();
        iter.next().unwrap()
    }

    pub fn capture_entire_screen(&self) -> Result<ScreenCapture> {
        // get the dimensions of the screen
        let screen = self.get_default_screen();
        let (width, height) = (screen.width_in_pixels(), screen.height_in_pixels());

        let image = xcb_image::get(
            &self.conn,
            screen.root(),
            0,
            0,
            width,
            height,
            u32::MAX,
            xcb::IMAGE_FORMAT_Z_PIXMAP,
        )
        .unwrap();

        Ok(ScreenCapture { image })
    }

    pub fn interactive_select(&self, image: &ScreenCapture) -> Result<Option<Rectangle>> {
        let window_id = self.conn.generate_id();
        let screen = self.get_default_screen();
        let root_window = screen.root();
        let (width, height) = (screen.width_in_pixels(), screen.height_in_pixels());

        let evt_mask = xcb::EVENT_MASK_BUTTON_PRESS
            | xcb::EVENT_MASK_BUTTON_RELEASE
            | xcb::EVENT_MASK_POINTER_MOTION;
        let cursor = xcb_util::cursor::create_font_cursor(&self.conn, xcb_util::cursor::CROSSHAIR);

        xproto::create_window(
            &self.conn,
            xcb::COPY_FROM_PARENT as u8,
            window_id,
            root_window,
            0,
            0,
            width,
            height,
            0,
            xcb::WINDOW_CLASS_INPUT_OUTPUT as u16,
            screen.root_visual(),
            &[
                (xcb::CW_OVERRIDE_REDIRECT, 1),
                (
                    xcb::CW_EVENT_MASK,
                    evt_mask | xcb::EVENT_MASK_KEY_PRESS | xcb::EVENT_MASK_KEY_RELEASE,
                ),
                (xcb::CW_CURSOR, cursor),
            ],
        )
        .request_check()?;

        xproto::map_window(&self.conn, window_id).request_check()?;
        self.conn.flush();

        let wm_state = xproto::intern_atom(&self.conn, true, "_NET_WM_STATE")
            .get_reply()?
            .atom();
        let wm_fullscreen = xproto::intern_atom(&self.conn, true, "_NET_WM_STATE_FULLSCREEN")
            .get_reply()?
            .atom();
        xproto::change_property(
            &self.conn,
            xcb::PROP_MODE_REPLACE as u8,
            window_id,
            wm_state,
            4,
            32,
            &[wm_fullscreen],
        );

        info!("Setting input focus...");
        xproto::set_input_focus(
            &self.conn,
            xcb::INPUT_FOCUS_POINTER_ROOT as u8,
            window_id,
            xcb::CURRENT_TIME,
        )
        .request_check()?;

        info!("Grabbing keyboard...");
        xproto::grab_keyboard(
            &self.conn,
            true,
            window_id,
            xcb::CURRENT_TIME,
            xcb::GRAB_MODE_ASYNC as u8,
            xcb::GRAB_MODE_ASYNC as u8,
        )
        .get_reply()?;

        info!("Grabbing pointer...");
        xproto::grab_pointer(
            &self.conn,
            false,
            window_id,
            evt_mask as u16,
            xcb::GRAB_MODE_ASYNC as u8,
            xcb::GRAB_MODE_ASYNC as u8,
            xcb::NONE,
            cursor,
            xcb::CURRENT_TIME,
        )
        .get_reply()?;

        let window_gc = self.conn.generate_id();
        xproto::create_gc(
            &self.conn,
            window_gc,
            window_id,
            &[
                (xcb::GC_FOREGROUND, screen.white_pixel()),
                (xcb::GC_BACKGROUND, screen.white_pixel()),
                (xcb::GC_LINE_WIDTH, 1),
            ],
        );

        #[derive(Default)]
        struct State {
            dragging: bool,

            // where did we start dragging from
            dx: i16,
            dy: i16,

            // where is the mouse right now
            mx: i16,
            my: i16,
        }

        impl State {
            pub fn rect(&self) -> Option<Rectangle> {
                if self.dx < 0 || self.dy < 0 {
                    return None;
                }

                let tlx = self.dx.min(self.mx);
                let tly = self.dy.min(self.my);

                let brx = self.dx.max(self.mx);
                let bry = self.dy.max(self.my);

                let rw = (brx - tlx) as u16;
                let rh = (bry - tly) as u16;

                if rw <= 2 || rh <= 2 {
                    return None;
                }

                Some(Rectangle::new(tlx, tly, rw, rh))
            }
        }

        let mut state = State::default();
        state.dx = -1;
        state.dy = -1;

        let mut cancelled = false;

        let redraw = |state: &State| -> Result<()> {
            xcb_image::put(&self.conn, window_id, window_gc, &image.image, 0, 0);

            if let Some(rect) = state.rect() {
                if state.dragging {
                    xproto::change_gc(
                        &self.conn,
                        window_gc,
                        &[(xcb::GC_FOREGROUND, screen.black_pixel())],
                    );
                    xproto::poly_rectangle(&self.conn, window_id, window_gc, &[rect]);

                    let rect2 = Rectangle::new(
                        rect.x() + 1,
                        rect.y() + 1,
                        rect.width() - 2,
                        rect.height() - 2,
                    );
                    xproto::change_gc(
                        &self.conn,
                        window_gc,
                        &[(xcb::GC_FOREGROUND, screen.white_pixel())],
                    );
                    xproto::poly_rectangle(&self.conn, window_id, window_gc, &[rect2]);
                }
            }

            self.conn.flush();
            Ok(())
        };

        redraw(&state)?;

        while let Some(evt) = self.conn.wait_for_event() {
            match evt.response_type() {
                xcb::KEY_RELEASE => {
                    let key_evt = unsafe { xcb::cast_event::<xcb::KeyReleaseEvent>(&evt) };
                    info!("released key {:?}", key_evt.detail());

                    if key_evt.detail() == 9 {
                        if state.dragging {
                            state.dragging = false;
                        } else {
                            cancelled = true;
                            break;
                        }
                    }
                }
                xcb::KEY_PRESS => {}
                xcb::BUTTON_PRESS => {
                    let button_evt = unsafe { xcb::cast_event::<xcb::ButtonPressEvent>(&evt) };
                    info!("pressed mouse button {:?}", button_evt.detail());
                    state.mx = button_evt.root_x();
                    state.my = button_evt.root_y();
                    state.dx = button_evt.root_x();
                    state.dy = button_evt.root_y();

                    // left mouse button
                    if button_evt.detail() == 1 {
                        state.dragging = true;
                    }
                }
                xcb::BUTTON_RELEASE => {
                    let button_evt = unsafe { xcb::cast_event::<xcb::ButtonReleaseEvent>(&evt) };
                    info!("released mouse button {:?}", button_evt.detail());

                    // left mouse button
                    if state.dragging && button_evt.detail() == 1 {
                            state.dragging = false;
                        if let Some(_) = state.rect() {
                            break;
                        }
                    }
                }
                xcb::MOTION_NOTIFY => {
                    let motion_evt = unsafe { xcb::cast_event::<xcb::MotionNotifyEvent>(&evt) };
                    state.mx = motion_evt.root_x();
                    state.my = motion_evt.root_y();
                }
                v => {
                    info!("event type: {:?}", v);
                }
            }

            redraw(&state)?;
        }

        info!("Loop exited, cleaning up...");
        xproto::ungrab_keyboard(&self.conn, xcb::CURRENT_TIME).request_check()?;
        xproto::ungrab_pointer(&self.conn, xcb::CURRENT_TIME).request_check()?;

        Ok(if cancelled { None } else { state.rect() })
    }
}

pub struct ScreenCapture {
    image: xcb_image::Image,
}

impl ScreenCapture {
    pub fn save_to(&self, to: impl AsRef<Path>) -> Result<()> {
        self.save_cropped_to(to, None)
    }

    pub fn save_cropped_to(&self, to: impl AsRef<Path>, section: Option<Rectangle>) -> Result<()> {
        let to = to.as_ref();
        let image = ImageBuffer::<Bgra<u8>, _>::from_raw(
            self.image.width() as u32,
            self.image.height() as u32,
            self.image.data().to_vec(),
        )
        .unwrap();
        let image = DynamicImage::ImageBgra8(image);
        let mut image = DynamicImage::ImageRgb8(image.into_rgb8());

        let image = if let Some(section) = section {
            // crop the image
            image.crop(
                section.x() as u32,
                section.y() as u32,
                section.width() as u32,
                section.height() as u32,
            )
        } else {
            image
        };

        image.save(to)?;
        Ok(())
    }
}
