// Copyright (C) Call of Nil contributors
// SPDX-License-Identifier: AGPL-3.0-only

mod coord;
mod field;
mod index;
mod size;

#[cfg(test)]
mod tests;

use crate::city::{City, CitySearch};
use crate::error::{Error, Result};
use crate::ruler::Ruler;
use serde::{Deserialize, Serialize};

pub use coord::{Coord, Distance};
pub use field::{Field, PublicField};
pub use index::{ContinentIndex, ContinentKey};
pub use size::ContinentSize;

#[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
pub struct Continent {
  fields: Box<[Field]>,
  size: ContinentSize,
}

impl Continent {
  pub(crate) fn new(size: u8) -> Self {
    let size = ContinentSize::new(size);
    let capacity = usize::from(size.get()).pow(2);
    let mut fields = Vec::with_capacity(capacity);
    fields.resize_with(capacity, Field::default);

    Self {
      fields: fields.into_boxed_slice(),
      size,
    }
  }

  #[inline]
  pub fn size(&self) -> ContinentSize {
    self.size
  }

  #[inline]
  pub fn radius(&self) -> u8 {
    self.size.get().div_ceil(2)
  }

  #[inline]
  pub fn center(&self) -> Coord {
    Coord::splat(self.radius())
  }

  pub fn field(&self, key: impl ContinentKey) -> Result<&Field> {
    let index = key.into_index(self.size);
    self
      .fields
      .get(usize::from(index))
      .ok_or(Error::IndexOutOfBounds(index))
  }

  pub(crate) fn field_mut(&mut self, key: impl ContinentKey) -> Result<&mut Field> {
    let index = key.into_index(self.size);
    self
      .fields
      .get_mut(usize::from(index))
      .ok_or(Error::IndexOutOfBounds(index))
  }

  pub fn fields(&self) -> impl Iterator<Item = &Field> {
    self.fields.iter()
  }

  fn fields_mut(&mut self) -> impl Iterator<Item = &mut Field> {
    self.fields.iter_mut()
  }

  pub fn enumerate_fields(&self) -> impl Iterator<Item = (ContinentIndex, &Field)> {
    self
      .fields()
      .enumerate()
      .map(|(idx, field)| (ContinentIndex::new(idx), field))
  }

  pub fn city(&self, key: impl ContinentKey) -> Result<&City> {
    let index = key.into_index(self.size);
    if let Some(city) = self.field(index)?.city() {
      Ok(city)
    } else {
      let coord = index.to_coord(self.size)?;
      Err(Error::CityNotFound(coord))
    }
  }

  pub fn city_mut(&mut self, key: impl ContinentKey) -> Result<&mut City> {
    let size = self.size;
    let index = key.into_index(size);
    if let Some(city) = self.field_mut(index)?.city_mut() {
      Ok(city)
    } else {
      let coord = index.to_coord(size)?;
      Err(Error::CityNotFound(coord))
    }
  }

  pub fn cities(&self) -> impl Iterator<Item = &City> {
    self.fields().filter_map(Field::city)
  }

  pub fn cities_mut(&mut self) -> impl Iterator<Item = &mut City> {
    self.fields_mut().filter_map(Field::city_mut)
  }

  pub fn cities_by<F>(&self, f: F) -> impl Iterator<Item = &City>
  where
    F: Fn(&City) -> bool,
  {
    self.cities().filter(move |city| f(city))
  }

  pub fn cities_of<R>(&self, owner: R) -> impl Iterator<Item = &City>
  where
    R: Into<Ruler>,
  {
    let owner: Ruler = owner.into();
    self.cities_by(move |city| city.owner() == &owner)
  }

  pub fn cities_within(&self, origin: Coord, distance: Distance) -> impl Iterator<Item = &City> {
    self.cities_by(move |city| origin.is_within_distance(city.coord(), distance))
  }

  pub fn coords_by<F>(&self, f: F) -> impl Iterator<Item = Coord>
  where
    F: Fn(&City) -> bool,
  {
    self.cities_by(f).map(City::coord)
  }

  pub fn coords_of<R>(&self, owner: R) -> impl Iterator<Item = Coord>
  where
    R: Into<Ruler>,
  {
    let owner: Ruler = owner.into();
    self.coords_by(move |city| city.owner() == &owner)
  }

  pub fn city_coords(&self) -> impl Iterator<Item = Coord> {
    self.cities().map(City::coord)
  }

  pub fn owner_of(&self, key: impl ContinentKey) -> Result<&Ruler> {
    self.city(key).map(City::owner)
  }

  pub fn search<S>(&self, search: S) -> Result<Vec<&City>>
  where
    S: Into<CitySearch>,
  {
    let mut found = Vec::new();
    let mut search: CitySearch = search.into();
    search.name = search
      .name
      .into_iter()
      .map(|name| name.trim().to_lowercase().into())
      .collect();

    'outer: for city in self.cities() {
      if search.coord.contains(&city.coord()) {
        found.push(city);
        continue;
      }

      if !search.name.is_empty() {
        let city_name = city.name().to_lowercase();
        for name in &search.name {
          if city_name.contains(name.as_str()) {
            found.push(city);
            continue 'outer;
          }
        }
      }
    }

    Ok(found)
  }
}

impl Default for Continent {
  fn default() -> Self {
    Self::new(ContinentSize::default().get())
  }
}
