#![allow(clippy::needless_pass_by_value)]

use enum_iterator::Sequence;
use log::{error, info};
use rocket::{
    fairing::AdHoc,
    get,
    http::Status,
    launch, post, routes,
    serde::{json::Json, Deserialize},
    tokio::task,
};
use serde::Serialize;
use serde_json::Value;
use std::env;

mod logic;
mod simulation;

const MAX_HEALTH: i32 = 100;

// API and Response Objects
// See https://docs.battlesnake.com/api

#[derive(
    Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, Deserialize, Serialize, Sequence,
)]
#[serde(rename_all = "lowercase")]
enum Direction {
    /// Move left (-x)
    Left,
    /// Move up (+y)
    Up,
    /// Move right (+x)
    Right,
    /// Move down (-y)
    Down,
}

#[derive(Debug, Deserialize, Serialize)]
struct Action {
    /// In which direction the snake should move
    r#move: Direction,
    /// Say something to the other snakes
    #[serde(default, skip_serializing_if = "is_default")]
    shout: Option<String>,
}

fn is_default<T: Default + PartialEq>(value: &T) -> bool {
    *value == T::default()
}

#[derive(Deserialize, Serialize, Debug)]
struct Game {
    /// A unique identifier for this Game
    id: String,
    /// Information about the ruleset being used to run this game
    ruleset: Ruleset,
    /// The name of the map being played on.
    map: String,
    /// How much time your snake has to respond to requests for this Game
    timeout: u32,
    /// The source of this game.
    ///
    /// One of:
    /// - "tournament"
    /// - "league"
    /// - "arena"
    /// - "challenge"
    /// - "custom"
    ///
    /// The values may change.
    source: String,
}

#[derive(Debug, Deserialize, Serialize)]
struct Ruleset {
    /// Name of the ruleset being used to run this game.
    name: String,
    /// The release version of the [Rules](https://github.com/BattlesnakeOfficial/rules) module used in this game.
    version: String,
    /// A collection of specific settings being used by the current game that control how the rules
    /// are applied.
    settings: RulesetSettings,
}

#[derive(Debug, Deserialize, Serialize)]
struct RulesetSettings {
    /// Percentage chance of spawning a new food every round.
    #[serde(rename = "foodSpawnChance")]
    food_spawn_chance: u8,
    /// Minimum food to keep on the board every turn.
    #[serde(rename = "minimumFood")]
    minimum_food: u8,
    /// Health damage a snake will take when ending its turn in a hazard. This stacks on top of the
    /// regular 1 damage a snake takes per turn.
    #[serde(rename = "hazardDamagePerTurn")]
    hazard_damage_per_turn: u8,
    /// rules for the royale mode
    royale: RulesetRoyale,
    /// rules for the squad mode
    squad: RulesetSquad,
}

#[derive(Debug, Deserialize, Serialize)]
struct RulesetRoyale {
    /// The number of turns between generating new hazards (shrinking the safe board space).
    #[serde(rename = "shrinkEveryNTurns")]
    shrink_every_n_turns: i32,
}

#[allow(clippy::struct_excessive_bools)]
#[derive(Debug, Deserialize, Serialize)]
struct RulesetSquad {
    /// Allow members of the same squad to move over each other without dying.
    #[serde(rename = "allowBodyCollisions")]
    allow_body_collisions: bool,
    /// All squad members are eliminated when one is eliminated.
    #[serde(rename = "sharedElimination")]
    shared_elimination: bool,
    /// All squad members share health.
    #[serde(rename = "sharedHealth")]
    shared_health: bool,
    /// All squad members share length.
    #[serde(rename = "sharedLength")]
    shared_length: bool,
}

#[derive(Deserialize, Serialize, Debug, Clone)]
struct Board {
    /// The number of rows in the y-axis of the game board.
    height: i32,
    /// The number of columns in the x-axis of the game board.
    width: i32,
    /// Array of coordinates representing food locations on the game board.
    food: Vec<Coord>,
    /// Array of Battlesnakes representing all Battlesnakes remaining on the game board (including
    /// yourself if you haven't been eliminated).
    snakes: Vec<Battlesnake>,
    /// Array of coordinates representing hazardous locations on the game board.
    hazards: Vec<Coord>,
}

#[derive(Deserialize, Serialize, Debug, Clone)]
struct Battlesnake {
    /// Unique identifier for this Battlesnake in the context of the current Game
    id: String,
    /// Name given to this Battlesnake by its author
    name: String,
    /// Health value of this Battlesnake, between 0 and 100
    health: i32,
    /// Array of coordinates representing this Battlesnake's location on the game board. This array
    /// is ordered from head to tail
    body: Vec<Coord>,
    /// Coordinates for this Battlesnake's head. Equivalent to the first element of the body array.
    head: Coord,
    /// Length of this Battlesnake from head to tail. Equivalent to the length of the body array.
    length: i32,
    /// The previous response time of this Battlesnake, in milliseconds. If the Battlesnake timed
    /// out and failed to respond, the game timeout will be returned.
    latency: String,
    /// Message shouted by this Battlesnake on the previous turn.
    shout: Option<String>,
    /// The squad that the Battlesnake belongs to. Used to identify squad members in Squad Mode
    /// games.
    squad: String,
    // /// The collection of customizations that control how this Battlesnake is displayed.
    // customizations: {color, head, tail}
}

#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone, Copy, Deserialize, Serialize)]
struct Coord {
    x: i32,
    y: i32,
}

impl Coord {
    const fn move_to(mut self, direction: Direction) -> Self {
        match direction {
            Direction::Left => self.x -= 1,
            Direction::Up => self.y += 1,
            Direction::Right => self.x += 1,
            Direction::Down => self.y -= 1,
        }
        self
    }
}

#[derive(Deserialize, Serialize, Debug)]
struct GameState {
    game: Game,
    turn: i32,
    board: Board,
    you: Battlesnake,
}

#[get("/")]
fn handle_index() -> Json<Value> {
    Json(logic::info())
}

#[post("/start", format = "json", data = "<start_req>")]
fn handle_start(start_req: Json<GameState>) -> Status {
    logic::start(
        &start_req.game,
        start_req.turn,
        &start_req.board,
        &start_req.you,
    );

    Status::Ok
}

#[post("/move", format = "json", data = "<move_req>")]
async fn handle_move(move_req: Json<GameState>) -> Option<Json<Action>> {
    let response = task::spawn_blocking(move || {
        logic::get_move(
            &move_req.game,
            move_req.turn,
            &move_req.board,
            &move_req.you,
        )
    })
    .await
    .inspect_err(|e| error!("failed to join compute thread: {e}"))
    .ok()
    .flatten()?;

    Some(Json(response))
}

#[post("/end", format = "json", data = "<end_req>")]
fn handle_end(end_req: Json<GameState>) -> Status {
    logic::end(&end_req.game, end_req.turn, &end_req.board, &end_req.you);

    Status::Ok
}

#[launch]
fn rocket() -> _ {
    // Lots of web hosting services expect you to bind to the port specified by the `PORT`
    // environment variable. However, Rocket looks at the `ROCKET_PORT` environment variable.
    // If we find a value for `PORT`, we set `ROCKET_PORT` to that value.
    if let Ok(port) = env::var("PORT") {
        env::set_var("ROCKET_PORT", &port);
    }

    // We default to 'info' level logging. But if the `RUST_LOG` environment variable is set,
    // we keep that value instead.
    if env::var("RUST_LOG").is_err() {
        env::set_var("RUST_LOG", "info");
    }

    env_logger::init();

    info!("Starting Battlesnake Server...");

    rocket::build()
        .attach(AdHoc::on_response("Server ID Middleware", |_, res| {
            Box::pin(async move {
                res.set_raw_header("Server", "battlesnake/github/starter-snake-rust");
            })
        }))
        .mount(
            "/",
            routes![handle_index, handle_start, handle_move, handle_end],
        )
}
