use crate::types::{
    Direction,
    simulation::Board,
    wire::{self, Settings},
};

use super::Ruleset;

#[derive(Debug, Clone, Copy)]
pub struct Standard {
    damage_hazards: DamageHazards,
}

impl From<&wire::Ruleset> for Standard {
    fn from(value: &wire::Ruleset) -> Self {
        Self {
            damage_hazards: (&value.settings).into(),
        }
    }
}

impl Ruleset for Standard {
    fn execute(&self, board: &mut Board, actions: &[(u8, Direction)]) -> bool {
        let game_over = game_over(board);
        move_snakes(board, actions);
        reduce_snake_health(board);
        self.damage_hazards.execute(board);
        feed_snakes(board);
        eliminate_snakes(board);
        board.update_free_map();
        game_over
    }
}

pub fn move_snakes(board: &mut Board, actions: &[(u8, Direction)]) {
    for i in 0..board.snakes.len() {
        let snake = &board.snakes[i];
        let action = actions.iter().find(|(id, _)| *id == snake.id).map_or_else(
            || {
                let head = snake.body[0];
                let previous_head = snake.body[1];
                let delta_x = i16::from(head.x) - i16::from(previous_head.x);
                let delta_y = i16::from(head.y) - i16::from(previous_head.y);
                if delta_x == 0 && delta_y == 0 {
                    Direction::Up
                } else if delta_x.abs() > delta_y.abs() {
                    if delta_x < 0 {
                        Direction::Left
                    } else {
                        Direction::Right
                    }
                } else if delta_y < 0 {
                    Direction::Down
                } else {
                    Direction::Up
                }
            },
            |(_, action)| *action,
        );
        let new_head = snake.head().wrapping_apply(action);
        board.snakes[i].advance(new_head);
    }
}

pub fn reduce_snake_health(board: &mut Board) {
    for snake in &mut board.snakes {
        snake.health = snake.health.saturating_sub(1);
    }
}

#[derive(Debug, Clone, Copy)]
pub struct DamageHazards {
    damage: u8,
}

impl DamageHazards {
    pub fn execute(self, board: &mut Board) {
        let mut i = 0;
        while i < board.snakes.len() {
            let head = board.snakes[i].head();
            if board.is_in_bounds(head) {
                let head_index = board.coord_to_linear(head);
                if board.hazard[head_index] && !board.food[head_index] {
                    let health = &mut board.snakes[i].health;
                    *health = health.saturating_sub(self.damage);
                    if *health == 0 {
                        let snake = board.snakes.remove(i);
                        for tile in snake.body {
                            let index = board.coord_to_linear(tile);
                            board.free.set(index, true);
                        }
                        continue;
                    }
                }
            }
            i += 1;
        }
    }
}

impl From<&Settings> for DamageHazards {
    fn from(value: &Settings) -> Self {
        let damage = value.hazard_damage_per_turn;
        Self { damage }
    }
}

pub fn eliminate_snakes(board: &mut Board) {
    // eliminate out of health and out of bounds
    let mut i = 0;
    while i < board.snakes.len() {
        let snake = &board.snakes[i];
        if snake.health == 0 || !board.is_in_bounds(snake.head()) {
            let snake = board.snakes.remove(i);
            for tile in snake.body.iter().skip(1) {
                if board.is_in_bounds(*tile) {
                    let index = board.coord_to_linear(*tile);
                    board.free.set(index, true);
                }
            }
            continue;
        }
        i += 1;
    }

    // look for collisions
    let mut collisions = vec![];
    for snake in &board.snakes {
        let head = snake.head();
        let head_index = board.coord_to_linear(head);
        if !board.free[head_index] {
            collisions.push(snake.id);
            continue;
        }
        for snake2 in &board.snakes {
            if snake.id != snake2.id
                && snake.head() == snake2.head()
                && snake.body.len() <= snake2.body.len()
            {
                collisions.push(snake.id);
                break;
            }
        }
    }

    // apply collisions
    let mut i = 0;
    while i < board.snakes.len() {
        if collisions.contains(&board.snakes[i].id) {
            let snake = board.snakes.remove(i);
            for tile in snake.body {
                let index = board.coord_to_linear(tile);
                board.free.set(index, true);
            }
            continue;
        }
        i += 1;
    }
}

pub fn feed_snakes(board: &mut Board) {
    let mut eaten_food = vec![];
    for i in 0..board.snakes.len() {
        let head = board.snakes[i].head();
        if board.is_in_bounds(head) {
            let head_index = board.coord_to_linear(head);
            if board.food[head_index] {
                eaten_food.push(head_index);
                board.snakes[i].feed();
            }
        }
    }
    for food_index in eaten_food {
        board.food.set(food_index, false);
    }
}

pub fn game_over(board: &Board) -> bool {
    board.num_snakes() <= 1
}
