#![allow(clippy::type_complexity)]
#![allow(clippy::too_many_arguments)]

mod utils;

use bevy::prelude::*;
use bevy::window::WindowResolution;
use helpers::grid::{Flag, GridPlugin, Revealed, Tile, TileClickEvent, TileOffset, TileType};
use serde::{Deserialize, Serialize};

mod helpers;

#[derive(Deref, Resource)]
pub struct PlayerAlive(bool);

#[derive(Deref, Resource, Reflect, Serialize, Deserialize)]
#[reflect(Resource)]
pub struct Score(usize);

impl FromWorld for PlayerAlive {
    fn from_world(world: &mut World) -> Self {
        let alive = world.resource::<PlayerAlive>();
        Self(**alive)
    }
}

#[derive(Deref, Resource)]
pub struct FontHandle(Handle<Font>);

impl FromWorld for FontHandle {
    fn from_world(world: &mut World) -> Self {
        let asset_server = world.resource::<AssetServer>();
        Self(asset_server.load("fonts/FiraSans-Bold.ttf"))
    }
}

fn setup_camera(mut commands: Commands) {
    let translation = Vec3 {
        x: -(1280.0 / 2.0),
        y: -(720.0 / 2.0),
        z: 1.0,
    };

    let offset_x = (translation.x / 16.0) as isize;
    let offset_y = (translation.y / 16.0) as isize;

    commands.spawn((
        Camera2dBundle {
            projection: OrthographicProjection {
                far: 1000.0,
                near: -1000.0,
                scaling_mode: bevy::render::camera::ScalingMode::Fixed {
                    width: 1280.0,
                    height: 720.0,
                },
                scale: 0.75,
                ..Default::default()
            },
            transform: Transform {
                translation: (translation % 16.0),
                ..default()
            },
            ..default()
        },
        TileOffset {
            x: offset_x,
            y: offset_y,
            translation,
        },
    ));
}

fn tile_clicked(
    mut commands: Commands,
    mut tiles: Query<(Entity, &Tile, &mut TileType)>,
    mut tile_flag: Query<&mut Flag>,
    mut tile_revealed: Query<&mut Revealed>,
    mut ev_click: EventReader<TileClickEvent>,
    mut score: ResMut<Score>,
    mut alive: ResMut<PlayerAlive>,
) {
    if !**alive {
        return;
    }

    for ev in ev_click.read() {
        if reveal(
            &mut commands,
            ev.x,
            ev.y,
            ev.button,
            &mut tiles,
            &mut tile_flag,
            &mut tile_revealed,
            &mut score,
            10,
        ) && ev.button == MouseButton::Left
        {
            *alive = PlayerAlive(false);
            println!("Score: {}", **score);
        }
    }
}

fn reveal(
    commands: &mut Commands,
    x: isize,
    y: isize,
    button: MouseButton,
    tiles: &mut Query<(Entity, &Tile, &mut TileType)>,
    tile_flag: &mut Query<&mut Flag>,
    tile_revealed: &mut Query<&mut Revealed>,
    score: &mut ResMut<Score>,
    n: isize,
) -> bool {
    use std::collections::{HashSet, VecDeque};

    let mut visited = HashSet::new();
    let mut queue = VecDeque::new();
    queue.push_back((x, y));

    while let Some((cx, cy)) = queue.pop_front() {
        if (cx - x).abs() > n || (cy - y).abs() > n {
            continue;
        }

        if visited.contains(&(cx, cy)) {
            continue;
        }
        visited.insert((cx, cy));

        let result = get_tile(cx, cy);
        let mut found = false;
        for (ent, tile, mut kind) in tiles.iter_mut() {
            if tile.x == cx && tile.y == cy {
                // tile found
                *kind = result;

                if button == MouseButton::Right && tile_revealed.get(ent).is_err() {
                    if tile_flag.get(ent).is_ok() {
                        if *kind == TileType::Mine {
                            **score = Score(***score - 1);
                        }
                        commands.entity(ent).remove::<Flag>();
                    } else {
                        if *kind == TileType::Mine {
                            **score = Score(***score + 1);
                        }
                        commands.entity(ent).insert(Flag);
                    }
                } else if tile_flag.get(ent).is_err() {
                    commands.entity(ent).insert(Revealed);
                }

                found = true;
                break;
            }
        }

        if !found {
            let tile_pos = Tile { x: cx, y: cy };

            let mut ent = commands.spawn(tile_pos);
            ent.insert(result);

            if button == MouseButton::Left {
                ent.insert(Revealed);
            }

            if button == MouseButton::Right {
                if result == TileType::Mine {
                    **score = Score(***score + 1);
                }
                ent.insert(Flag);
            }

            if result == TileType::Empty && button == MouseButton::Left {
                for i in cx - 1..=cx + 1 {
                    for j in cy - 1..=cy + 1 {
                        if i != cx || j != cy {
                            queue.push_back((i, j));
                        }
                    }
                }
            }
        }

        if result == TileType::Mine {
            return true;
        }
    }

    false
}

fn is_mine(x: isize, y: isize) -> bool {
    let value = get_mine_value(x, y);

    let x = x as f64;
    let y = y as f64;
    let distance = (x.powf(2.0) + y.powf(2.0)).sqrt();

    value > (1.0 - (distance / 100.0)).min(0.7)
}

const SEED: u32 = 65536;
fn get_mine_value(x: isize, y: isize) -> f64 {
    use noise::{NoiseFn, Perlin};

    let x = x as f64;
    let y = y as f64;

    const SCALE: f64 = 2.0;

    let result2 = Perlin::new(SEED).get([x / (SCALE * 2.0), y / (SCALE * 2.0), 3.8]);
    let result1 = Perlin::new(SEED + 1).get([x / (SCALE / 2.0), y / (SCALE / 2.0), 1.5]);

    let result1 = 1.0 - result1.powf(2.5);

    let p = 4.0;

    result1 * (1.0 - (((1.0 - result2) * 2.0).powf(p)) / 2.0)
}

fn get_tile(x: isize, y: isize) -> TileType {
    if is_mine(x, y) {
        return TileType::Mine;
    }

    let mut surrounding = 0;
    for i in x - 1..x + 2 {
        for j in y - 1..y + 2 {
            if is_mine(i, j) {
                surrounding += 1;
            }
        }
    }

    surrounding.into()
}

fn load_game(mut commands: Commands, asset_server: Res<AssetServer>) {
    // "Spawning" a scene bundle creates a new entity and spawns new instances
    // of the given scene's entities as children of that entity.
    commands.spawn(DynamicSceneBundle {
        // Scenes are loaded just like any other asset.
        scene: asset_server.load("save.sav"),
        ..default()
    });
}

fn save_and_exit(
    input: Res<ButtonInput<KeyCode>>,
    world: &World,
    type_registry: Res<AppTypeRegistry>,
) {
    if !input.just_pressed(KeyCode::Escape) {
        return;
    }

    let type_registry = type_registry.read();

    let dscene = DynamicSceneBuilder::from_world(world)
        .deny_all()
        .deny_all_resources()
        .allow::<Tile>()
        .allow::<Flag>()
        .allow::<Revealed>()
        .allow::<TileType>()
        .allow::<Transform>()
        .allow::<TileOffset>()
        .allow::<OrthographicProjection>()
        .allow_resource::<Score>()
        .extract_entities(world.iter_entities().filter_map(|entity| {
            world.entity(entity.id()).get::<Tile>()?;
            Some(entity.id())
        }))
        // .extract_entities(world.iter_entities().filter_map(|entity| {
        //     world.entity(entity.id()).get::<TileOffset>()?;
        //     Some(entity.id())
        // }))
        .extract_resources()
        .build();

    let serialized = match dscene.serialize(&type_registry) {
        Ok(s) => s,
        Err(e) => {
            eprintln!("Could not serialize scene: {:?}", e);
            return;
        }
    };

    if let Err(e) = std::fs::write("./assets/save.sav", serialized) {
        eprintln!("Unable to write save file: {:?}", e);
    }
}

fn main() {
    // use imageproc::drawing::Canvas;
    // // prepare an image
    // let width = 2048;
    // let height = 2048;

    // let mut canvas = image::DynamicImage::new(width, height, image::ColorType::Rgb8);

    // for x in 0..width {
    //     for y in 0..height {
    //         // let c = (get_mine_value(
    //         //     x as isize - (width as isize / 2_isize),
    //         //     y as isize - (height as isize / 2_isize),
    //         // ) * 255.0) as u8;
    //         // let color = [c, c, c, 1];
    //         let color = if is_mine(
    //             x as isize - (width as isize / 2_isize),
    //             y as isize - (height as isize / 2_isize),
    //         ) {
    //             [255, 255, 255, 1]
    //         } else {
    //             [0, 0, 0, 1]
    //         };
    //         canvas.draw_pixel(
    //             x,
    //             (-(y as isize) + (height - 1) as isize) as u32,
    //             color.into(),
    //         );
    //     }
    // }

    // if let Err(e) = canvas.save("rawr.png") {
    //     eprintln!("Error saving image: {:?}", e);
    // }

    App::new()
        .add_plugins((
            DefaultPlugins.set(WindowPlugin {
                primary_window: Some(Window {
                    title: String::from("Infinite Minesweeper"),
                    resolution: WindowResolution::new(1280.0, 720.0),
                    ..Default::default()
                }),
                ..default()
            }),
            // bevy_inspector_egui::quick::WorldInspectorPlugin::new(),
        ))
        .insert_resource(PlayerAlive(true))
        .register_type::<Score>()
        .register_type::<TileOffset>()
        .insert_resource(Score(0))
        .init_resource::<FontHandle>()
        .add_plugins(GridPlugin)
        .add_systems(Startup, (load_game, setup_camera))
        .add_systems(Update, (tile_clicked, save_and_exit))
        .run();
}
