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

This file is part of the Luminescent Dreams Tools.

Luminescent Dreams Tools 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.

Luminescent Dreams Tools 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 Lumeto. If not, see <https://www.gnu.org/licenses/>.
*/

use coordinates::{hex_map::parse_data, AxialAddr};
use gio::resources_lookup_data;
use glib::Object;
use gtk::{gio, prelude::*, subclass::prelude::*, Application, DrawingArea};
use image::io::Reader as ImageReader;
use std::{cell::RefCell, io::Cursor, rc::Rc};

mod labeled_field;
mod palette_entry;
mod tile;
mod utilities;

const APP_ID: &str = "com.luminescent-dreams.hex-grid";
const HEX_RADIUS: f64 = 50.;
const MAP_RADIUS: usize = 3;
const DRAWING_ORIGIN: (f64, f64) = (1024. / 2., 768. / 2.);

fn main() {
    gio::resources_register_include!("com.luminescent-dreams.hex-grid.gresource")
        .expect("Failed to register resources");

    let app = Application::builder().application_id(APP_ID).build();

    app.connect_activate(|app| {
        let window = HexGridWindow::new(app);
        window.present();
    });

    app.run();
}

pub struct HexGridWindowPrivate {
    layout: gtk::Box,
    palette: gtk::Box,

    drawing_area: DrawingArea,
    hex_address: labeled_field::LabeledField,
    canvas_address: labeled_field::LabeledField,

    current_coordinate: Rc<RefCell<Option<AxialAddr>>>,
}

#[glib::object_subclass]
impl ObjectSubclass for HexGridWindowPrivate {
    const NAME: &'static str = "HexGridWindow";
    type Type = HexGridWindow;
    type ParentType = gtk::ApplicationWindow;

    fn new() -> Self {
        println!("hexGridWindowPrivate::new()");
        let current_coordinate = Rc::new(RefCell::new(None));

        let drawing_area = DrawingArea::builder()
            .width_request(1024)
            .height_request(768)
            .margin_start(8)
            .margin_end(8)
            .margin_top(8)
            .margin_bottom(8)
            .hexpand(true)
            .build();

        let layout = gtk::Box::builder()
            .homogeneous(false)
            .spacing(8)
            .can_focus(false)
            .visible(true)
            .build();

        let sidebar = gtk::Box::builder()
            .orientation(gtk::Orientation::Vertical)
            .width_request(100)
            .visible(true)
            .can_focus(false)
            .margin_start(4)
            .margin_end(4)
            .margin_top(4)
            .margin_bottom(4)
            .spacing(8)
            .build();

        let canvas_address = labeled_field::LabeledField::new("Canvas Address", "-----");
        let hex_address = labeled_field::LabeledField::new("Hex Address", "-----");

        let palette = gtk::Box::builder()
            .spacing(8)
            .orientation(gtk::Orientation::Vertical)
            .hexpand(true)
            .build();

        sidebar.append(&canvas_address);
        sidebar.append(&hex_address);
        sidebar.append(&palette);

        layout.append(&drawing_area);
        layout.append(&sidebar);

        layout.show();

        Self {
            drawing_area,
            hex_address,
            canvas_address,
            current_coordinate,
            layout,
            palette,
        }
    }
}

impl ObjectImpl for HexGridWindowPrivate {
    fn constructed(&self) {
        println!("HexGridWindowPrivate::constructed()");
        self.parent_constructed();

        let map_text_resource = resources_lookup_data(
            "/com/luminescent-dreams/hex-grid/map.txt",
            gio::ResourceLookupFlags::NONE,
        )
        .expect("map should be in the bundle")
        .to_vec();

        let hex_map =
            parse_data::<tile::Terrain>(String::from_utf8(map_text_resource).unwrap().lines());

        let terrain_data = resources_lookup_data(
            "/com/luminescent-dreams/hex-grid/terrain.ppm",
            gio::ResourceLookupFlags::NONE,
        )
        .unwrap();
        let reader = ImageReader::new(Cursor::new(terrain_data))
            .with_guessed_format()
            .unwrap();

        let image = reader.decode().unwrap();
        let badlands = tile::Tile::new(&image, tile::Terrain::Badlands);
        let deep_water = tile::Tile::new(&image, tile::Terrain::DeepWater);
        let desert = tile::Tile::new(&image, tile::Terrain::Desert);
        let grasslands = tile::Tile::new(&image, tile::Terrain::Grasslands);
        let mountain = tile::Tile::new(&image, tile::Terrain::Mountain);
        let shallow_water = tile::Tile::new(&image, tile::Terrain::ShallowWater);
        let swamp = tile::Tile::new(&image, tile::Terrain::Swamp);

        self.palette
            .append(&palette_entry::PaletteEntry::new(&badlands));
        self.palette
            .append(&palette_entry::PaletteEntry::new(&deep_water));
        self.palette
            .append(&palette_entry::PaletteEntry::new(&desert));
        self.palette
            .append(&palette_entry::PaletteEntry::new(&grasslands));
        self.palette
            .append(&palette_entry::PaletteEntry::new(&mountain));
        self.palette
            .append(&palette_entry::PaletteEntry::new(&shallow_water));
        self.palette
            .append(&palette_entry::PaletteEntry::new(&swamp));

        let motion_controller = gtk::EventControllerMotion::new();
        {
            let canvas_address = self.canvas_address.clone();
            let hex_address = self.hex_address.clone();
            let c = self.current_coordinate.clone();
            motion_controller.connect_motion(move |_, x, y| {
                let norm_x = x - DRAWING_ORIGIN.0;
                let norm_y = y - DRAWING_ORIGIN.1;
                let q = (2. / 3. * norm_x) / HEX_RADIUS;
                let r = (-1. / 3. * norm_x + (3_f64).sqrt() / 3. * norm_y) / HEX_RADIUS;

                let (q, r) = axial_round(q, r);
                let coordinate = AxialAddr::new(q, r);
                canvas_address.set_value(&format!("{:.0} {:.0}", x, y));

                if coordinate.distance(&AxialAddr::origin()) > MAP_RADIUS {
                    hex_address.set_value("-----");
                    *c.borrow_mut() = None;
                } else {
                    hex_address.set_value(&format!("{:.0} {:.0}", coordinate.q(), coordinate.r()));
                    *c.borrow_mut() = Some(coordinate);
                }
            });
        }

        {
            self.drawing_area.set_draw_func(move |_, context, _, _| {
                context.set_source_rgb(0., 0., 0.);
                let _ = context.paint();

                context.set_line_width(2.);

                for coordinate in vec![AxialAddr::origin()]
                    .into_iter()
                    .chain(AxialAddr::origin().addresses(MAP_RADIUS))
                {
                    let center_x =
                        DRAWING_ORIGIN.0 + HEX_RADIUS * (3. / 2. * (coordinate.q() as f64));
                    let center_y = DRAWING_ORIGIN.1
                        + HEX_RADIUS
                            * ((3_f64).sqrt() / 2. * (coordinate.q() as f64)
                                + (3_f64).sqrt() * (coordinate.r() as f64));
                    let translate_x = center_x - HEX_RADIUS;
                    let translate_y = center_y - (3_f64).sqrt() * HEX_RADIUS / 2.;

                    let tile = match hex_map.get(&coordinate).unwrap() {
                        tile::Terrain::Mountain => &mountain,
                        tile::Terrain::Grasslands => &grasslands,
                        tile::Terrain::ShallowWater => &shallow_water,
                        tile::Terrain::DeepWater => &deep_water,
                        tile::Terrain::Badlands => &badlands,
                        tile::Terrain::Desert => &desert,
                        tile::Terrain::Swamp => &swamp,
                        _ => panic!("unhandled terrain type"),
                    };
                    tile.render_on_context(context, translate_x, translate_y);
                }
            });
        }
        self.drawing_area.add_controller(motion_controller);
    }
}

impl WidgetImpl for HexGridWindowPrivate {}
impl WindowImpl for HexGridWindowPrivate {}
impl ApplicationWindowImpl for HexGridWindowPrivate {}

glib::wrapper! {
    pub struct HexGridWindow(ObjectSubclass<HexGridWindowPrivate>) @extends gtk::ApplicationWindow, gtk::Window, gtk::Widget,
        @implements gtk::Accessible, gtk::Actionable, gtk::Buildable, gtk::ConstraintTarget;
}

impl HexGridWindow {
    pub fn new(app: &Application) -> Self {
        let window: Self = Object::builder().property("application", app).build();
        window.set_child(Some(&window.imp().layout));
        window
    }
}

/*
fn draw_hexagon(context: &Context, center_x: f64, center_y: f64, radius: f64) {
    let ul_x = center_x - radius;
    let ul_y = center_y - (3_f64).sqrt() * radius / 2.;
    let points: Vec<(f64, f64)> = utilities::hexagon(radius * 2., (3_f64).sqrt() * radius);
    context.new_path();
    context.move_to(ul_x + points[0].0, ul_y + points[0].1);
    context.line_to(ul_x + points[1].0, ul_y + points[1].1);
    context.line_to(ul_x + points[2].0, ul_y + points[2].1);
    context.line_to(ul_x + points[3].0, ul_y + points[3].1);
    context.line_to(ul_x + points[4].0, ul_y + points[4].1);
    context.line_to(ul_x + points[5].0, ul_y + points[5].1);
    context.close_path();
}
*/

fn axial_round(q_f64: f64, r_f64: f64) -> (i32, i32) {
    let s_f64 = -q_f64 - r_f64;
    let mut q = q_f64.round();
    let mut r = r_f64.round();
    let s = s_f64.round();

    let q_diff = (q - q_f64).abs();
    let r_diff = (r - r_f64).abs();
    let s_diff = (s - s_f64).abs();

    if q_diff > r_diff && q_diff > s_diff {
        q = -r - s;
    } else if r_diff > s_diff {
        r = -q - s;
    }
    unsafe { (q.to_int_unchecked::<i32>(), r.to_int_unchecked::<i32>()) }
}