use super::{
    constants::{DEFAULT_PLANET_ID, KILOMETER},
    player::Player,
    skill::MAX_SKILL,
    world::World,
};
use crate::{
    core::{GamePosition, GamePositionUtils, Resource, Trait, NUM_GAME_POSITIONS},
    image::color_map::SkinColorMap,
    types::{AppResult, HashMapWithResult, PlanetId, SystemTimeTick, TeamId, Tick},
};
use rand::{seq::IteratorRandom, SeedableRng};
use rand_chacha::ChaCha8Rng;
use rand_distr::{weighted::WeightedIndex, Distribution};
use serde::{Deserialize, Serialize};
use serde_repr::{Deserialize_repr, Serialize_repr};
use std::fmt::Display;
use strum::IntoEnumIterator;
use strum::{Display, FromRepr};
use strum_macros::EnumIter;

#[derive(
    Debug, Default, PartialEq, Eq, Clone, Copy, EnumIter, Serialize_repr, Deserialize_repr, Hash,
)]
#[repr(u8)]
pub enum Region {
    #[default]
    Italy,
    Germany,
    Spain,
    Greece,
    Nigeria,
    India,
    Euskadi,
    Kurdistan,
    Palestine,
    Japan,
}

impl From<u8> for Region {
    fn from(value: u8) -> Self {
        match value {
            0 => Self::Italy,
            1 => Self::Germany,
            2 => Self::Spain,
            3 => Self::Greece,
            4 => Self::Nigeria,
            5 => Self::India,
            6 => Self::Euskadi,
            7 => Self::Kurdistan,
            8 => Self::Palestine,
            9 => Self::Japan,
            _ => Self::Italy,
        }
    }
}

#[derive(Debug, PartialEq, Eq, Clone, Copy, EnumIter, Hash)]
pub enum Population {
    Human { region: Region },
    Yardalaim,
    Polpett,
    Juppa,
    Galdari,
    Pupparoll,
    Octopulp,
}

impl Serialize for Population {
    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
        // Serialize the enum as a u8.
        // The Human option is serialized as 100 + (region as u8)
        let value = match self {
            Self::Human { region } => 100 + *region as u8,
            Self::Yardalaim => 1,
            Self::Polpett => 2,
            Self::Juppa => 3,
            Self::Galdari => 4,
            Self::Pupparoll => 5,
            Self::Octopulp => 6,
        };
        value.serialize(serializer)
    }
}

impl<'de> Deserialize<'de> for Population {
    fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
        // Deserialize the enum from a u8.
        // The Human option is deserialized as 100 + (region as u8)
        let value = u8::deserialize(deserializer)?;
        match value {
            1 => Ok(Self::Yardalaim),
            2 => Ok(Self::Polpett),
            3 => Ok(Self::Juppa),
            4 => Ok(Self::Galdari),
            5 => Ok(Self::Pupparoll),
            6 => Ok(Self::Octopulp),
            100..=109 => Ok(Self::Human {
                region: (value - 100).into(),
            }),
            _ => Err(serde::de::Error::custom("Invalid value for Population")),
        }
    }
}

impl Default for Population {
    fn default() -> Self {
        Self::Human {
            region: Region::Italy,
        }
    }
}

impl Display for Population {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::Human { .. } => write!(f, "Human"),
            Self::Yardalaim => write!(f, "Yardalaim"),
            Self::Polpett => write!(f, "Polpett"),
            Self::Juppa => write!(f, "Juppa"),
            Self::Galdari => write!(f, "Galdari"),
            Self::Pupparoll => write!(f, "Pupparoll"),
            Self::Octopulp => write!(f, "Octopulp"),
        }
    }
}

impl Population {
    pub fn random() -> Self {
        let rng = &mut ChaCha8Rng::from_rng(&mut rand::rng());
        Self::iter()
            .choose(rng)
            .expect("There should be at lease one Population to choose from.")
    }

    pub fn relative_age(&self, age: f32) -> f32 {
        (age - self.min_age()) / (self.max_age() - self.min_age())
    }

    pub fn min_age(&self) -> f32 {
        match self {
            Self::Human { .. } => 16.0,
            Self::Yardalaim => 35.0,
            Self::Polpett => 14.0,
            Self::Juppa => 50.0,
            Self::Galdari => 80.0,
            Self::Pupparoll => 6.0,
            Self::Octopulp => 3.0,
        }
    }

    pub fn max_age(&self) -> f32 {
        match self {
            Self::Human { .. } => 65.0,
            Self::Yardalaim => 120.0,
            Self::Polpett => 41.0,
            Self::Juppa => 110.0,
            Self::Galdari => 220.0,
            Self::Pupparoll => 45.0,
            Self::Octopulp => 18.0,
        }
    }

    pub fn bonus_game_position_fitness(&self, game_position: GamePosition) -> f32 {
        match self {
            Self::Human { .. } => 3.0,
            Self::Yardalaim => match game_position {
                4 => 6.0,
                3 => 3.0,
                0 => -2.5,
                _ => 0.0,
            },
            Self::Polpett => match game_position {
                0 | 1 => 4.0,
                3 => -2.0,
                4 => -2.0,
                _ => 0.0,
            },
            Self::Juppa => match game_position {
                4 => 1.0,
                2 | 3 => 3.0,
                _ => 0.0,
            },
            Self::Galdari => -1.0,
            Self::Pupparoll => match game_position {
                2..=4 => 1.0,
                _ => -1.0,
            },
            Self::Octopulp => match game_position {
                2 => 4.0,
                1 | 3 => 1.0,
                _ => -1.0,
            },
        }
    }

    pub fn random_skin_map(&self, rng: &mut ChaCha8Rng) -> SkinColorMap {
        let weights = match self {
            Self::Human { region } => match *region {
                Region::Italy => vec![
                    (SkinColorMap::Pale, 0.1),
                    (SkinColorMap::Light, 0.2),
                    (SkinColorMap::Medium, 0.2),
                    (SkinColorMap::Dark, 0.1),
                ],
                Region::Germany => vec![
                    (SkinColorMap::Pale, 0.2),
                    (SkinColorMap::Light, 0.2),
                    (SkinColorMap::Medium, 0.1),
                    (SkinColorMap::Dark, 0.05),
                ],
                Region::Spain => vec![
                    (SkinColorMap::Pale, 0.15),
                    (SkinColorMap::Light, 0.1),
                    (SkinColorMap::Medium, 0.2),
                    (SkinColorMap::Dark, 0.15),
                ],
                Region::Greece => vec![
                    (SkinColorMap::Pale, 0.1),
                    (SkinColorMap::Light, 0.2),
                    (SkinColorMap::Medium, 0.2),
                    (SkinColorMap::Dark, 0.1),
                ],
                Region::Nigeria => vec![
                    (SkinColorMap::Pale, 0.025),
                    (SkinColorMap::Light, 0.05),
                    (SkinColorMap::Medium, 0.1),
                    (SkinColorMap::Dark, 0.3),
                ],
                Region::India => vec![
                    (SkinColorMap::Pale, 0.05),
                    (SkinColorMap::Light, 0.1),
                    (SkinColorMap::Medium, 0.3),
                    (SkinColorMap::Dark, 0.2),
                ],
                Region::Euskadi => vec![
                    (SkinColorMap::Pale, 0.2),
                    (SkinColorMap::Light, 0.2),
                    (SkinColorMap::Medium, 0.15),
                    (SkinColorMap::Dark, 0.05),
                ],
                Region::Kurdistan => vec![
                    (SkinColorMap::Pale, 0.01),
                    (SkinColorMap::Light, 0.1),
                    (SkinColorMap::Medium, 0.5),
                    (SkinColorMap::Dark, 0.1),
                ],
                Region::Palestine => vec![
                    (SkinColorMap::Light, 0.05),
                    (SkinColorMap::Medium, 0.5),
                    (SkinColorMap::Dark, 0.2),
                ],
                Region::Japan => vec![
                    (SkinColorMap::Pale, 0.2),
                    (SkinColorMap::Light, 0.25),
                    (SkinColorMap::Medium, 0.1),
                    (SkinColorMap::Dark, 0.025),
                ],
            },
            Self::Yardalaim => vec![(SkinColorMap::LightGreen, 0.5), (SkinColorMap::Green, 0.5)],
            Self::Polpett => vec![(SkinColorMap::LightRed, 0.75), (SkinColorMap::Red, 0.25)],
            Self::Juppa => vec![
                (SkinColorMap::LightBlue, 0.45),
                (SkinColorMap::Blue, 0.45),
                (SkinColorMap::Purple, 0.1),
            ],
            Self::Galdari => vec![
                (SkinColorMap::LightYellow, 0.55),
                (SkinColorMap::Yellow, 0.43),
                (SkinColorMap::Orange, 0.02),
            ],
            Self::Pupparoll => vec![
                (SkinColorMap::LightGreen, 0.1),
                (SkinColorMap::Green, 0.1),
                (SkinColorMap::LightBlue, 0.1),
                (SkinColorMap::Blue, 0.1),
                (SkinColorMap::LightRed, 0.1),
                (SkinColorMap::Red, 0.1),
                (SkinColorMap::Orange, 0.2),
                (SkinColorMap::LightYellow, 0.1),
                (SkinColorMap::Yellow, 0.1),
                (SkinColorMap::Rainbow, 0.3),
                (SkinColorMap::Dark, 0.05),
                (SkinColorMap::Purple, 0.2),
            ],
            Self::Octopulp => vec![
                (SkinColorMap::LightPurple, 0.45),
                (SkinColorMap::Dark, 0.05),
                (SkinColorMap::LightBlue, 0.5),
                (SkinColorMap::Yellow, 0.02),
            ],
        };

        let dist = WeightedIndex::new(weights.iter().map(|(_, w)| w)).unwrap();
        weights[dist.sample(rng)].0
    }
}

#[derive(Debug, Clone, Copy, Display, Serialize, Deserialize)]
pub enum PlayerLocation {
    WithTeam,
    OnPlanet { planet_id: PlanetId },
}

#[derive(Debug, Clone, Copy, Display, Serialize, Deserialize, PartialEq)]
pub enum KartoffelLocation {
    WithTeam { team_id: TeamId },
    OnPlanet { planet_id: PlanetId },
}

impl PartialEq for PlayerLocation {
    fn eq(&self, other: &Self) -> bool {
        match (self, other) {
            (Self::OnPlanet { planet_id: p1 }, Self::OnPlanet { planet_id: p2 }) => p1 == p2,
            _ => false,
        }
    }
}

impl Default for PlayerLocation {
    fn default() -> Self {
        Self::OnPlanet {
            planet_id: *DEFAULT_PLANET_ID,
        }
    }
}

#[derive(Debug, Clone, Copy, Display, Serialize, Deserialize)]
pub enum TeamLocation {
    Travelling {
        from: PlanetId,
        to: PlanetId,
        started: Tick,
        duration: Tick,
        distance: KILOMETER,
    },
    OnPlanet {
        planet_id: PlanetId,
    },
    Exploring {
        around: PlanetId,
        started: Tick,
        duration: Tick,
    },
    OnSpaceAdventure {
        around: PlanetId,
    },
}

impl PartialEq for TeamLocation {
    fn eq(&self, other: &Self) -> bool {
        match (self, other) {
            (Self::OnPlanet { planet_id: p1 }, Self::OnPlanet { planet_id: p2 }) => p1 == p2,
            _ => false,
        }
    }
}

impl Default for TeamLocation {
    fn default() -> Self {
        Self::OnPlanet {
            planet_id: *DEFAULT_PLANET_ID,
        }
    }
}

#[derive(Debug, Default, Clone, Copy, Serialize_repr, Deserialize_repr, PartialEq, FromRepr)]
#[repr(u8)]
pub enum Pronoun {
    He,
    She,
    #[default]
    They,
}

impl Pronoun {
    pub fn random(rng: &mut ChaCha8Rng) -> Self {
        if let Ok(dist) = WeightedIndex::new([10, 10, 1]) {
            return Self::from_repr(dist.sample(rng) as u8).unwrap_or_default();
        }

        Self::default()
    }

    pub fn as_subject(&self) -> &'static str {
        match self {
            Self::He => "He",
            Self::She => "She",
            Self::They => "They",
        }
    }

    pub fn as_object(&self) -> &'static str {
        match self {
            Self::He => "him",
            Self::She => "her",
            Self::They => "them",
        }
    }

    pub fn as_possessive(&self) -> &'static str {
        match self {
            Self::He => "his",
            Self::She => "her",
            Self::They => "their",
        }
    }

    pub fn to_be(&self) -> &'static str {
        match self {
            Self::He | Self::She => "is",
            Self::They => "are",
        }
    }

    pub fn to_have(&self) -> &'static str {
        match self {
            Self::He | Self::She => "has",
            Self::They => "have",
        }
    }
}

#[derive(Debug, Clone, Copy, Default, PartialEq, EnumIter)]
pub enum TrainingFocus {
    #[default]
    Athletics,
    Offense,
    Defense,
    Technical,
    Mental,
    GamePosition(GamePosition),
}

impl Display for TrainingFocus {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::Athletics => write!(f, "Athletics"),
            Self::Offense => write!(f, "Offense"),
            Self::Defense => write!(f, "Defense"),
            Self::Technical => write!(f, "Technical"),
            Self::Mental => write!(f, "Mental"),
            Self::GamePosition(position) => match position {
                0 => write!(f, "PG"),
                1 => write!(f, "SG"),
                2 => write!(f, "SF"),
                3 => write!(f, "PF"),
                4 => write!(f, "C"),
                _ => unreachable!(),
            },
        }
    }
}

impl TrainingFocus {
    pub fn bonus_for_skill(&self, skill_index: usize) -> f32 {
        match self {
            Self::Athletics => {
                if skill_index < 4 {
                    1.5
                } else {
                    0.75
                }
            }
            Self::Offense => {
                if (4..8).contains(&skill_index) {
                    1.4
                } else {
                    0.775
                }
            }
            Self::Defense => {
                if (8..12).contains(&skill_index) {
                    1.55
                } else {
                    0.8
                }
            }
            Self::Technical => {
                if (12..16).contains(&skill_index) {
                    1.55
                } else {
                    0.75
                }
            }
            Self::Mental => {
                if skill_index >= 16 {
                    1.7
                } else {
                    0.8
                }
            }
            Self::GamePosition(position) => 0.55 * position.weights()[skill_index].sqrt(),
        }
    }

    pub fn next(&self) -> Option<Self> {
        match self {
            Self::Athletics => Some(Self::Offense),
            Self::Offense => Some(Self::Defense),
            Self::Defense => Some(Self::Technical),
            Self::Technical => Some(Self::Mental),
            Self::Mental => Some(Self::GamePosition(0)),
            Self::GamePosition(position) => {
                if *position < NUM_GAME_POSITIONS - 1 {
                    Some(Self::GamePosition(*position + 1))
                } else {
                    None
                }
            }
        }
    }
}

impl Serialize for TrainingFocus {
    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
        // Serialize the enum as a u8.
        // The GamePosition option is serialized as 10 + (position as u8)
        let value = match self {
            Self::Athletics => 0,
            Self::Offense => 1,
            Self::Defense => 2,
            Self::Technical => 3,
            Self::Mental => 4,
            Self::GamePosition(position) => 10 + position,
        };
        value.serialize(serializer)
    }
}

impl<'de> Deserialize<'de> for TrainingFocus {
    fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
        // Deserialize the enum from a u8.
        // The GamePosition option is deserialized as 100 + (position as u8)
        let value = u8::deserialize(deserializer)?;
        match value {
            0 => Ok(Self::Athletics),
            1 => Ok(Self::Offense),
            2 => Ok(Self::Defense),
            3 => Ok(Self::Technical),
            4 => Ok(Self::Mental),
            10..=14 => Ok(Self::GamePosition(value - 10)),
            _ => Err(serde::de::Error::custom("Invalid value for TrainingFocus")),
        }
    }
}

#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq)]
pub struct AutonomousStrategy {
    pub challenge_local: bool,   // controls whether to challenge local teams
    pub challenge_network: bool, // controls whether to accept challenges from network teams
}

impl Default for AutonomousStrategy {
    fn default() -> Self {
        Self {
            challenge_local: true,
            challenge_network: false,
        }
    }
}

impl AutonomousStrategy {
    pub fn new_for_own_team() -> Self {
        Self {
            challenge_local: false,
            challenge_network: false,
        }
    }
}

#[derive(Clone, Copy, Debug)]
pub enum TeamBonus {
    Exploration,       //pilot
    SpaceshipSpeed,    //pilot
    Training,          //doctor
    TirednessRecovery, //doctor
    TradePrice,        //captain
    Reputation,        //captain
    Weapons,           //engineer
    Upgrades,          //engineer
}

impl Display for TeamBonus {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::Exploration => write!(f, "Exploration"),
            Self::Reputation => write!(f, "Reputation"),
            Self::SpaceshipSpeed => write!(f, "Ship speed"),
            Self::TirednessRecovery => write!(f, "Recovery"),
            Self::TradePrice => write!(f, "Trading"),
            Self::Training => write!(f, "Training"),
            Self::Weapons => write!(f, "Weapons"),
            Self::Upgrades => write!(f, "Upgrades"),
        }
    }
}

impl TeamBonus {
    pub const BASE_BONUS: f32 = 1.0;
    const BONUS_PER_SKILL: f32 = 1.0 / MAX_SKILL;
    pub fn current_team_bonus(&self, world: &World, team_id: &TeamId) -> AppResult<f32> {
        let team = world.teams.get_or_err(team_id)?;
        let player_id = match self {
            Self::Exploration => team.crew_roles.pilot,
            Self::SpaceshipSpeed => team.crew_roles.pilot,
            Self::Training => team.crew_roles.doctor,
            Self::TirednessRecovery => team.crew_roles.doctor,
            Self::TradePrice => team.crew_roles.captain,
            Self::Reputation => team.crew_roles.captain,
            Self::Weapons => team.crew_roles.engineer,
            Self::Upgrades => team.crew_roles.engineer,
        };

        let skill = if let Some(id) = player_id {
            let player = world.players.get_or_err(&id)?;
            self.as_skill(player)
        } else {
            0.0
        };

        Ok(Self::BASE_BONUS + Self::BONUS_PER_SKILL * skill)
    }

    pub fn current_player_bonus(&self, player: &Player) -> f32 {
        let skill = self.as_skill(player);
        Self::BASE_BONUS + Self::BONUS_PER_SKILL * skill
    }

    pub fn as_skill(&self, player: &Player) -> f32 {
        if player.is_knocked_out() {
            return 0.0;
        }

        match self {
            TeamBonus::Exploration => {
                0.15 * player.athletics.stamina
                    + 0.15 * player.defense.steal
                    + 0.7 * player.mental.vision
            }
            TeamBonus::Reputation => {
                let mut bonus = 0.75 * player.mental.charisma
                    + 0.15 * player.mental.aggression
                    + 0.1 * player.athletics.strength;
                if matches!(player.special_trait, Some(Trait::Showpirate)) {
                    bonus *= 1.25; // Can exceed 2.0
                }
                bonus
            }
            TeamBonus::SpaceshipSpeed => {
                0.75 * player.athletics.quickness + 0.25 * player.mental.vision
            }
            TeamBonus::TirednessRecovery => {
                let mut bonus = 0.75 * player.athletics.stamina + 0.25 * player.mental.intuition;
                if matches!(player.special_trait, Some(Trait::Relentless)) {
                    bonus *= 1.25; // Can exceed 2.0
                }
                bonus
            }
            TeamBonus::TradePrice => {
                0.5 * player.mental.charisma
                    + 0.25 * player.mental.aggression
                    + 0.25 * player.mental.intuition
            }
            TeamBonus::Training => {
                0.25 * player.athletics.strength
                    + 0.25 * player.athletics.vertical
                    + 0.5 * player.mental.intuition
            }
            TeamBonus::Weapons => {
                let mut bonus = 0.25 * player.technical.ball_handling
                    + 0.25 * player.mental.aggression
                    + 0.5 * player.offense.brawl;

                if matches!(player.special_trait, Some(Trait::Killer)) {
                    bonus *= 1.25; // Can exceed 2.0
                }

                bonus
            }
            TeamBonus::Upgrades => {
                0.25 * player.athletics.stamina
                    + 0.25 * player.mental.vision
                    + 0.5 * player.mental.intuition
            }
        }
    }
}

pub trait UpgradeableElement: Sized + Display {
    fn next(&self) -> Option<Self>;
    fn previous(&self) -> Option<Self>;
    fn upgrade_cost(&self) -> Vec<(Resource, u32)>;
    fn upgrade_duration(&self) -> Tick;
    fn can_be_upgraded(&self) -> bool {
        self.next().is_some()
    }
    fn description(&self) -> &str;
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default, Hash)]
pub struct Upgrade<U>
where
    U: UpgradeableElement,
{
    pub target: U,
    pub started: Tick,
    pub duration: Tick,
}

impl<U> Upgrade<U>
where
    U: UpgradeableElement,
{
    pub fn new(target: U, bonus: f32) -> Self {
        let duration = (target.upgrade_duration() as f32 / bonus) as Tick;
        Self {
            started: Tick::now(),
            duration,
            target,
        }
    }

    pub fn with_duration(mut self, duration: Tick) -> Self {
        self.duration = duration;
        self
    }

    pub fn upgrade_cost(&self) -> Vec<(Resource, u32)> {
        self.target.upgrade_cost()
    }
}

// tests
#[cfg(test)]

mod tests {
    use crate::core::{Player, TeamBonus};

    #[test]
    fn test_team_location_eq() {
        use super::TeamLocation;
        use crate::types::PlanetId;
        let planet_id = PlanetId::new_v4();
        let team_location = TeamLocation::OnPlanet { planet_id };
        let team_location2 = TeamLocation::OnPlanet { planet_id };
        assert_eq!(team_location, team_location2);
    }

    #[test]
    fn test_team_location_ne() {
        use super::TeamLocation;
        use crate::types::PlanetId;
        let planet_id = PlanetId::new_v4();
        let planet_id2 = PlanetId::new_v4();
        let team_location = TeamLocation::OnPlanet { planet_id };
        let team_location2 = TeamLocation::OnPlanet {
            planet_id: planet_id2,
        };
        let team_location3 = TeamLocation::Travelling {
            from: planet_id,
            to: planet_id2,
            started: 0,
            duration: 0,
            distance: 0,
        };
        assert_ne!(team_location, team_location2);
        assert_ne!(team_location, team_location3);
    }

    #[test]
    fn test_team_bonus() {
        let mut player = Player::default();

        assert!(TeamBonus::Exploration.as_skill(&player) == 0.0);
        player.athletics.stamina = 10.0;
        player.defense.steal = 6.0;
        player.mental.vision = 2.0;
        assert!(TeamBonus::Exploration.as_skill(&player) == 0.15 * 10.0 + 0.15 * 6.0 + 0.7 * 2.0);

        assert!(TeamBonus::Weapons.as_skill(&player) == 0.0);
        player.technical.ball_handling = 8.5;
        player.mental.aggression = 13.0;
        player.offense.brawl = 2.0;
        assert!(TeamBonus::Weapons.as_skill(&player) == 0.25 * 8.5 + 0.25 * 13.0 + 0.5 * 2.0);
        player.special_trait = Some(crate::core::Trait::Killer);
        assert!(
            TeamBonus::Weapons.as_skill(&player) == (0.25 * 8.5 + 0.25 * 13.0 + 0.5 * 2.0) * 1.25
        );
    }
}
