use crate::CoreApi;
use gio::resources_lookup_data;
use glib::Object;
use gtk::{
    gdk_pixbuf::{InterpType, Pixbuf},
    prelude::*,
    subclass::prelude::*,
};
use image::io::Reader as ImageReader;
use kifu_core::{
    ui::{BoardElement, IntersectionElement},
    Color,
};
use std::{cell::RefCell, io::Cursor, rc::Rc};

const WIDTH: i32 = 800;
const HEIGHT: i32 = 800;
const MARGIN: i32 = 20;

#[derive(Clone, Default, PartialEq)]
struct Addr {
    row: u8,
    column: u8,
}

pub struct BoardPrivate {
    drawing_area: gtk::DrawingArea,

    current_player: Rc<RefCell<Color>>,
    board: Rc<RefCell<BoardElement>>,
    cursor_location: Rc<RefCell<Option<Addr>>>,

    api: Rc<RefCell<Option<CoreApi>>>,
}

#[glib::object_subclass]
impl ObjectSubclass for BoardPrivate {
    const NAME: &'static str = "Board";
    type Type = Board;
    type ParentType = gtk::Grid;

    fn new() -> BoardPrivate {
        BoardPrivate {
            drawing_area: Default::default(),
            current_player: Rc::new(RefCell::new(Color::Black)),
            board: Default::default(),
            cursor_location: Default::default(),
            api: Default::default(),
        }
    }
}

impl ObjectImpl for BoardPrivate {
    fn constructed(&self) {
        self.drawing_area.set_width_request(WIDTH);
        self.drawing_area.set_height_request(HEIGHT);

        let board = self.board.clone();
        let cursor_location = self.cursor_location.clone();
        let current_player = self.current_player.clone();

        let wood_texture = resources_lookup_data(
            "/com/luminescent-dreams/kifu-gtk/wood_texture.jpg",
            gio::ResourceLookupFlags::NONE,
        )
        .unwrap();
        let background = ImageReader::new(Cursor::new(wood_texture))
            .with_guessed_format()
            .unwrap()
            .decode();
        let background = background.map(|background| {
            Pixbuf::from_bytes(
                &glib::Bytes::from(background.as_bytes()),
                gtk::gdk_pixbuf::Colorspace::Rgb,
                false,
                8,
                background.width() as i32,
                background.height() as i32,
                background.to_rgb8().sample_layout().height_stride as i32,
            )
            .scale_simple(WIDTH, HEIGHT, InterpType::Nearest)
        });

        self.drawing_area
            .set_draw_func(move |_, context, width, height| {
                let render_start = std::time::Instant::now();
                let board = board.borrow();

                match background {
                    Ok(Some(ref background)) => {
                        context.set_source_pixbuf(&background, 0., 0.);
                        context.paint().expect("paint should succeed");
                    }
                    Ok(None) | Err(_) => context.set_source_rgb(0.7, 0.7, 0.7),
                };
                let _ = context.paint();

                context.set_source_rgb(0.1, 0.1, 0.1);
                context.set_line_width(2.);
                let hspace_between = ((width - 40) as f64) / ((board.size.width - 1) as f64);
                let vspace_between = ((height - 40) as f64) / ((board.size.height - 1) as f64);

                let pen = Pen {
                    x_offset: MARGIN as f64,
                    y_offset: MARGIN as f64,
                    hspace_between,
                    vspace_between,
                };

                (0..board.size.width).for_each(|col| {
                    context.move_to(
                        (MARGIN as f64) + (col as f64) * hspace_between,
                        MARGIN as f64,
                    );
                    context.line_to(
                        (MARGIN as f64) + (col as f64) * hspace_between,
                        (height as f64) - (MARGIN as f64),
                    );
                    let _ = context.stroke();
                });
                (0..board.size.height).for_each(|row| {
                    context.move_to(
                        MARGIN as f64,
                        (MARGIN as f64) + (row as f64) * vspace_between,
                    );
                    context.line_to(
                        (width - MARGIN) as f64,
                        (MARGIN as f64) + (row as f64) * vspace_between,
                    );
                    let _ = context.stroke();
                });

                context.set_source_rgb(0.1, 0.1, 0.0);
                vec![3, 9, 15].into_iter().for_each(|col| {
                    vec![3, 9, 15].into_iter().for_each(|row| {
                        pen.star_point(context, col, row);
                    });
                });

                (0..19).for_each(|col| {
                    (0..19).for_each(|row| {
                        match board.stone(row, col) {
                            IntersectionElement::Filled(stone) => {
                                pen.stone(&context, row, col, stone.color, stone.liberties);
                            }
                            _ => {}
                        };
                    })
                });

                let cursor = cursor_location.borrow();
                match *cursor {
                    None => {}
                    Some(ref cursor) => match board.stone(cursor.row, cursor.column) {
                        IntersectionElement::Empty(_) => pen.ghost_stone(
                            context,
                            cursor.row,
                            cursor.column,
                            *current_player.borrow(),
                        ),
                        _ => {}
                    },
                }
                let render_end = std::time::Instant::now();
                println!("board rendering time: {:?}", render_end - render_start);
            });

        let motion_controller = gtk::EventControllerMotion::new();
        {
            let board = self.board.clone();
            let cursor = self.cursor_location.clone();
            let drawing_area = self.drawing_area.clone();
            motion_controller.connect_motion(move |_, x, y| {
                let board = board.borrow();
                let mut cursor = cursor.borrow_mut();
                let hspace_between = ((WIDTH - 40) as f64) / ((board.size.width - 1) as f64);
                let vspace_between = ((HEIGHT - 40) as f64) / ((board.size.height - 1) as f64);

                let addr = if x.round() < MARGIN as f64
                    || x.round() > (WIDTH - MARGIN) as f64
                    || y.round() < MARGIN as f64
                    || y.round() > (HEIGHT - MARGIN) as f64
                {
                    None
                } else {
                    Some(Addr {
                        column: ((x.round() - MARGIN as f64) / hspace_between).round() as u8,
                        row: ((y.round() - MARGIN as f64) / vspace_between).round() as u8,
                    })
                };

                if *cursor != addr.clone() {
                    *cursor = addr;
                    drawing_area.queue_draw();
                }
            });
        }

        let gesture = gtk::GestureClick::new();
        {
            let board = self.board.clone();
            let cursor = self.cursor_location.clone();
            let api = self.api.clone();
            gesture.connect_released(move |_, _, _, _| {
                let board = board.borrow();
                let cursor = cursor.borrow();
                match *cursor {
                    None => {}
                    Some(ref cursor) => match board.stone(cursor.row, cursor.column) {
                        IntersectionElement::Empty(request) => {
                            println!("need to send request: {:?}", request);
                            api.borrow()
                                .as_ref()
                                .expect("API must exist")
                                .dispatch(request);
                        }
                        _ => {}
                    },
                }
            });
        }

        self.drawing_area.add_controller(motion_controller);
        self.drawing_area.add_controller(gesture);
    }
}
impl WidgetImpl for BoardPrivate {}
impl GridImpl for BoardPrivate {}

glib::wrapper! {
    pub struct Board(ObjectSubclass<BoardPrivate>) @extends gtk::Grid, gtk::Widget;
}

impl Board {
    pub fn new(api: CoreApi) -> Self {
        let s: Self = Object::builder().build();

        *s.imp().api.borrow_mut() = Some(api);
        s.attach(&s.imp().drawing_area, 1, 1, 1, 1);

        s
    }

    pub fn set_board(&self, board: BoardElement) {
        *self.imp().board.borrow_mut() = board;
        self.imp().drawing_area.queue_draw();
    }

    pub fn set_current_player(&self, color: Color) {
        *self.imp().current_player.borrow_mut() = color;
    }
}

struct Pen {
    x_offset: f64,
    y_offset: f64,
    hspace_between: f64,
    vspace_between: f64,
}

impl Pen {
    fn star_point(&self, context: &cairo::Context, row: u8, col: u8) {
        context.arc(
            self.x_offset + (col as f64) * self.hspace_between,
            self.y_offset + (row as f64) * self.vspace_between,
            5.,
            0.,
            2. * std::f64::consts::PI,
        );
        let _ = context.fill();
    }

    fn stone(
        &self,
        context: &cairo::Context,
        row: u8,
        col: u8,
        color: Color,
        liberties: Option<u8>,
    ) {
        match color {
            Color::White => context.set_source_rgb(0.9, 0.9, 0.9),
            Color::Black => context.set_source_rgb(0.0, 0.0, 0.0),
        };
        self.draw_stone(context, row, col);

        if let Some(liberties) = liberties {
            let stone_location = self.stone_location(row, col);
            context.set_source_rgb(1., 0., 1.);
            context.set_font_size(32.);
            context.move_to(stone_location.0 - 10., stone_location.1 + 10.);
            let _ = context.show_text(&format!("{}", liberties));
        }
    }

    fn ghost_stone(&self, context: &cairo::Context, row: u8, col: u8, color: Color) {
        match color {
            Color::White => context.set_source_rgba(0.9, 0.9, 0.9, 0.5),
            Color::Black => context.set_source_rgba(0.0, 0.0, 0.0, 0.5),
        };
        self.draw_stone(context, row, col);
    }

    fn draw_stone(&self, context: &cairo::Context, row: u8, col: u8) {
        let radius = self.hspace_between / 2. - 2.;
        let (x_loc, y_loc) = self.stone_location(row, col);
        context.arc(x_loc, y_loc, radius, 0.0, 2.0 * std::f64::consts::PI);
        let _ = context.fill();
    }

    fn stone_location(&self, row: u8, col: u8) -> (f64, f64) {
        (
            self.x_offset + (col as f64) * self.hspace_between,
            self.y_offset + (row as f64) * self.vspace_between,
        )
    }
}