/*
    Hurry Curry! - a game about cooking
    Copyright (C) 2025 Hurry Curry! Contributors

    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU Affero General Public License as published by
    the Free Software Foundation, version 3 of the License only.

    This program 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 Affero General Public License for more details.

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

*/

use anyhow::Result;
use hurrycurry_data::{Serverdata, index::DataIndex};
use hurrycurry_locale::{
    FALLBACK_LOCALE,
    message::{COLORED, MessageDisplayExt},
    trm,
};
use hurrycurry_protocol::{
    Gamedata, TileIndex,
    glam::{IVec2, ivec2},
};
use std::{
    collections::{BTreeSet, HashMap, HashSet},
    sync::LazyLock,
};

#[derive(PartialEq, Clone, Copy)]
enum TileMode {
    Normal,
    Exclusive,
    Collider,
    Walkable,
}
use TileMode::*;

static NORMAL: &[&str] = &[
    "counter-window",
    "counter",
    "sink",
    "cutting-board",
    "rolling-board",
    "stove",
    "table",
    "oven",
    "freezer",
    "black-hole-counter",
    "white-hole-counter",
    "book",
    "conveyor",
];
static EXCLUSIVE: &[&str] = &[
    "steak-crate",
    "coconut-crate",
    "strawberry-crate",
    "fish-crate",
    "rice-crate",
    "tomato-crate",
    "flour-crate",
    "leek-crate",
    "lettuce-crate",
    "cheese-crate",
    "mushroom-crate",
    "potato-crate",
    "bun-crate",
    "trash",
    "deep-fryer",
];
static COLLIDER: &[&str] = &[
    "wall",
    "wall-window",
    "white-hole",
    "tree",
    "lamp",
    "fence",
    "house-roof",
    "house-roof-chimney",
    "house-side",
    "house-wall",
    "house-balcony",
    "house-oriel",
    "house-door",
];
static WALKABLE: &[&str] = &[
    "floor",
    "grass",
    "door",
    "chair",
    "path",
    "street",
    "chandelier",
    "black-hole",
    "ceiling-lamp",
];
static NEED_ACCESS: &[&str] = &[
    "oven",
    "freezer",
    "steak-crate",
    "coconut-crate",
    "strawberry-crate",
    "fish-crate",
    "rice-crate",
    "tomato-crate",
    "flour-crate",
    "leek-crate",
    "lettuce-crate",
    "cheese-crate",
    "mushroom-crate",
    "potato-crate",
    "bun-crate",
    "trash",
    "deep-fryer",
];
static NEED_EASY_ACCESS: &[&str] = &[
    "sink",
    "cutting-board",
    "rolling-board",
    "stove",
    "deep-fryer",
];

static TILE_MODE: LazyLock<HashMap<String, TileMode>> = LazyLock::new(|| {
    let mut out = HashMap::new();
    for tile in EXCLUSIVE {
        out.insert(tile.to_string(), Exclusive);
    }
    for tile in COLLIDER {
        out.insert(tile.to_string(), Collider);
    }
    for tile in NORMAL {
        out.insert(tile.to_string(), Normal);
    }
    for tile in WALKABLE {
        out.insert(tile.to_string(), Walkable);
    }
    out
});

pub fn check_map(map: &str) -> Result<()> {
    let style = &COLORED;
    let locale = &*FALLBACK_LOCALE;
    let index = DataIndex::new("data".into())?;
    let (data, serverdata) = index.generate(map)?;

    let mut warnings = Vec::new();

    let tiles_used = serverdata
        .initial_map
        .values()
        .map(|t| t.0)
        .collect::<BTreeSet<_>>();

    for tile in tiles_used {
        let tile_name = data.tile_name(tile);
        let tile_name_base = tile_name.split(":").next().unwrap();
        let Some(&mode) = TILE_MODE.get(tile_name_base) else {
            warnings.push(trm!("s.tool.map_linter.unknown_tile", t = tile));
            continue;
        };
        match mode {
            Normal => {
                if data.tile_walkable.contains(&tile) {
                    warnings.push(trm!("s.tool.map_linter.walkable", t = tile));
                }
            }
            Exclusive => {
                if !data.tile_placeable_items.contains_key(&tile) {
                    warnings.push(trm!("s.tool.map_linter.not_exclusive", t = tile));
                }
                if data
                    .tile_placeable_items
                    .get(&tile)
                    .is_some_and(|e| e.is_empty())
                {
                    warnings.push(trm!("s.tool.map_linter.exclusive_no_recipe", t = tile));
                }
            }
            Collider => {
                if !data.tile_placeable_items.contains_key(&tile) {
                    warnings.push(trm!("s.tool.map_linter.not_collider", t = tile));
                }
            }
            Walkable => {
                if !data.tile_walkable.contains(&tile) {
                    warnings.push(trm!("s.tool.map_linter.not_walkable", t = tile));
                }
            }
        }
    }

    let chef_access = fill_walkable(&data, &serverdata, serverdata.chef_spawn.as_ivec2());
    let customer_access = fill_walkable(
        &data,
        &serverdata,
        serverdata
            .customer_spawn
            .unwrap_or(serverdata.chef_spawn)
            .as_ivec2(),
    );

    for (&pos, &(tile, _item)) in &serverdata.initial_map {
        let tile_name = data.tile_name(tile);
        if (NEED_EASY_ACCESS.contains(&tile_name) || NEED_ACCESS.contains(&tile_name))
            && !has_neighbour_diag(pos, |t| chef_access.contains(&t))
        {
            warnings.push(trm!(
                "s.tool.map_linter.no_chef_access",
                t = tile,
                s = pos.to_string()
            ));
        }
        if NEED_EASY_ACCESS.contains(&tile_name)
            && !has_neighbour(&serverdata, pos, |t| data.tile_walkable.contains(&t))
        {
            warnings.push(trm!(
                "s.tool.map_linter.no_easy_access",
                t = tile,
                s = pos.to_string()
            ));
        }
        if tile_name == "chair" {
            if !has_neighbour(&serverdata, pos, |t| {
                !data.tile_placeable_items.contains_key(&t)
            }) {
                warnings.push(trm!(
                    "s.tool.map_linter.chair_no_table",
                    s = pos.to_string()
                ));
            }

            if !customer_access.contains(&pos) {
                warnings.push(trm!(
                    "s.tool.map_linter.chair_no_customer_access",
                    s = pos.to_string()
                ));
            }
        }
    }

    if data.demands.is_empty() {
        warnings.push(trm!("s.tool.map_linter.no_demands"));
    }

    if warnings.is_empty() {
        println!(
            "{}",
            trm!("s.tool.map_linter.ok", s = map.to_string()).display_message(locale, &data, style)
        );
        return Ok(());
    }
    println!(
        "{}",
        trm!(
            "s.tool.map_linter.title",
            s = map.to_string(),
            s = warnings.len().to_string()
        )
        .display_message(locale, &data, style)
    );
    for warning in warnings {
        println!("- {}", warning.display_message(locale, &data, style));
    }

    Ok(())
}

fn has_neighbour(sd: &Serverdata, pos: IVec2, pred: impl Fn(TileIndex) -> bool) -> bool {
    [IVec2::X, IVec2::Y, IVec2::NEG_X, IVec2::NEG_Y]
        .into_iter()
        .any(|off| {
            sd.initial_map
                .get(&(pos + off))
                .is_some_and(|(tile, _)| pred(*tile))
        })
}
fn has_neighbour_diag(pos: IVec2, pred: impl Fn(IVec2) -> bool) -> bool {
    [
        ivec2(-1, -1),
        ivec2(0, -1),
        ivec2(1, -1),
        ivec2(-1, 0),
        ivec2(1, 0),
        ivec2(-1, 1),
        ivec2(0, 1),
        ivec2(1, 1),
    ]
    .into_iter()
    .any(|off| pred(pos + off))
}

fn fill_walkable(d: &Gamedata, sd: &Serverdata, start: IVec2) -> HashSet<IVec2> {
    let mut visited = HashSet::new();
    let mut open = HashSet::new();
    open.insert(start);

    while let Some(pos) = open.iter().next().copied() {
        open.remove(&pos);
        visited.insert(pos);
        for off in [IVec2::X, IVec2::Y, IVec2::NEG_X, IVec2::NEG_Y] {
            let npos = pos + off;
            if sd
                .initial_map
                .get(&npos)
                .is_some_and(|(x, _)| d.tile_walkable.contains(x))
                && !visited.contains(&npos)
            {
                open.insert(npos);
            }
        }
    }
    visited
}
