/*
Copyright 2024, Savanni D'Gerinel <savanni@luminescent-dreams.com>

This file is part of On the Grid.

On the Grid is free software: you can redistribute it and/or modify it under the terms of
the GNU General Public License as published by the Free Software Foundation, either version 3 of
the License, or (at your option) any later version.

On the Grid is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License along with On the Grid. If not, see <https://www.gnu.org/licenses/>.
*/

// I have an old Board class which I'm going to update. I'll just copy over the rendering code, but
// at the same time I am going to work pretty heavily on the API.
//
// For a game review, the board needs to interact very well with a game record. So I have to keep
// in mind all of that as I work on the API.
//
// Also, this is going to be a cross-platform application. Today it is Gnome + Rust, but as I
// progress I will also need a Progressive Web App so that I can run this on my tablet. Especially
// useful if I'm out socializing and happen to be talking to somebody who would enjoy a relaxing
// game. Anyway, that is going to impact some of my API decisions.
//
// First, though, I need to rename my game record.
//
// Now, let's get the existing code compiling again.
//
// Okay, that wasn't so bad. I'm a little confused that I don't have a code action for renaming a
// symbol, but I'll fix that some other time. Anyway, now let's focus on the goban.

// Now, we know what kind of object we have for the current board representation. Let's make use of
// that.

use crate::{perftrace, Resource, ResourceManager};

use glib::Object;
use gtk::{gdk_pixbuf::Pixbuf, prelude::*, subclass::prelude::*};
use otg_core::{Color, Coordinate};
use std::{cell::RefCell, rc::Rc};

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

// Internal representation of the Goban drawing area.
#[derive(Default)]
pub struct GobanPrivate {
    board_state: Rc<RefCell<otg_core::Goban>>,
    resource_manager: Rc<RefCell<Option<ResourceManager>>>,
}

impl GobanPrivate {}

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

impl ObjectImpl for GobanPrivate {}
impl WidgetImpl for GobanPrivate {}
impl DrawingAreaImpl for GobanPrivate {}

// This Goban, being in the `components` crate, is merely the rendering of a board. This is not
// the primary representation of the board.
//
// In a game of Go, there are certain rules about what are valid moves and what are not.
// Internally, I want to keep track of those, and doing so requires a few things.
//
// - We can never repeat a game state (though I think maybe that is allowed in a few rulesets, but
// I'm coding to the AGA ruleset)
// - We can never play a suicidal move
//
// Finally, updating the board state is non-GUI logic. So, sorry, might be dropping away from GUI
// code for a while to work on the backend representation, some of which already exists.
glib::wrapper! {
    pub struct Goban(ObjectSubclass<GobanPrivate>) @extends gtk::DrawingArea, gtk::Widget;
}

impl Goban {
    pub fn new(board_state: otg_core::Goban, resources: ResourceManager) -> Self {
        let s: Self = Object::builder().build();

        *s.imp().board_state.borrow_mut() = board_state;
        *s.imp().resource_manager.borrow_mut() = Some(resources);
        s.set_width_request(WIDTH);
        s.set_height_request(HEIGHT);

        s.set_draw_func({
            let s = s.clone();
            move |_, ctx, width, height| {
                perftrace("render drawing area", || s.redraw(ctx, width, height));
            }
        });

        s
    }

    pub fn set_board_state(&mut self, board_state: otg_core::Goban) {
        *self.imp().board_state.borrow_mut() = board_state;
        self.queue_draw();
    }

    fn redraw(&self, ctx: &cairo::Context, width: i32, height: i32) {
        let background = self
            .imp()
            .resource_manager
            .borrow()
            .as_ref()
            .and_then(|r| r.resource("/com/luminescent-dreams/otg-gtk/wood_texture.jpg"));

        let black_texture = self
            .imp()
            .resource_manager
            .borrow()
            .as_ref()
            .and_then(|r| r.resource("/com/luminescent-dreams/otg-gtk/black_stone.png"));

        let white_texture = self
            .imp()
            .resource_manager
            .borrow()
            .as_ref()
            .and_then(|r| r.resource("/com/luminescent-dreams/otg-gtk/white_stone.png"));

        match background {
            Some(Resource::Image(ref background)) => {
                ctx.set_source_pixbuf(background, 0., 0.);
                ctx.paint().expect("paint should never fail");
            }
            None => ctx.set_source_rgb(0.7, 0.7, 0.7),
        }

        let board = self.imp().board_state.borrow();

        ctx.set_source_rgb(0.1, 0.1, 0.1);
        ctx.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::new(
            MARGIN as f64,
            MARGIN as f64,
            hspace_between,
            vspace_between,
            black_texture,
            white_texture,
        );

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

        // This doesn't work except on 19x19 boads. This code needs to be adjusted to at least
        // handle 13x13 and 9x9. Non-standard boards are On Their Own (TM).
        vec![3, 9, 15].into_iter().for_each(|col| {
            vec![3, 9, 15].into_iter().for_each(|row| {
                pen.star_point(ctx, col, row);
            });
        });

        (0..board.size.height).for_each(|row| {
            (0..board.size.width).for_each(|column| {
                match board.stone(&Coordinate { row, column }) {
                    None => {}
                    Some(Color::White) => pen.stone(ctx, row, column, Color::White, None),
                    Some(Color::Black) => pen.stone(ctx, row, column, Color::Black, None),
                }
            })
        })
    }
}

struct Pen {
    x_offset: f64,
    y_offset: f64,
    hspace_between: f64,
    vspace_between: f64,
    black_stone: Option<Pixbuf>,
    white_stone: Option<Pixbuf>,
}

impl Pen {
    fn new(
        x_offset: f64,
        y_offset: f64,
        hspace_between: f64,
        vspace_between: f64,
        black_stone: Option<Resource>,
        white_stone: Option<Resource>,
    ) -> Self {
        let black_stone = match black_stone {
            Some(Resource::Image(img)) => Some(img),
            _ => None,
        };
        let white_stone = match white_stone {
            Some(Resource::Image(img)) => Some(img),
            _ => None,
        };
        Pen {
            x_offset,
            y_offset,
            hspace_between,
            vspace_between,
            black_stone,
            white_stone,
        }
    }

    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, ctx: &cairo::Context, row: u8, col: u8, color: Color, _liberties: Option<u8>) {
        let (x_loc, y_loc) = self.stone_location(row, col);
        match color {
            Color::White => match self.white_stone {
                Some(ref white_stone) => ctx.set_source_pixbuf(white_stone, x_loc, y_loc),
                None => ctx.set_source_rgb(0.9, 0.9, 0.9),
            },
            Color::Black => match self.black_stone {
                Some(ref black_stone) => ctx.set_source_pixbuf(black_stone, x_loc, y_loc),
                None => ctx.set_source_rgb(0.0, 0.0, 0.0),
            },
        }
        ctx.paint().expect("paint should never fail");
        /*
        match color {
            Color::White => ctx.set_source_rgb(0.9, 0.9, 0.9),
            Color::Black => ctx.set_source_rgb(0.0, 0.0, 0.0),
        };
        self.draw_stone(ctx, 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));
        }
        */
    }

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

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

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