// engine
use engine::prelude::*;

use super::{async_db::AsyncDBAccess, map::MapObjectType, map_db::EntityDBType, surface::Surface};

use anyhow::Result;
use cgmath::{Rad, vec3};
use cgmath::{Vector2, Vector3};

// std
use std::cmp::Ordering;
use std::collections::HashMap;
use std::path::Path;
use std::sync::Arc;

const BOSS_MARKER_SCALE: f32 = 3.0;

#[derive(Debug, Clone)]
pub struct OrderedElement<T> {
    pub index: u32,
    pub references: u32,
    pub name: String,
    pub element: T,
}

#[derive(PartialEq, Eq, Hash, Clone, Debug)]
pub struct Coordinate {
    pub x: u32,
    pub y: u32,
}

impl From<Vector2<f32>> for Coordinate {
    fn from(v: Vector2<f32>) -> Self {
        Coordinate {
            x: v.x as u32,
            y: v.y as u32,
        }
    }
}

pub enum NPCSpawnParameter {
    MinCount(u32),
    MaxCount(u32),
    EliteNPC(String),
    NormalNPC(String),
    Radius(f32),
}

pub struct SpawnMarkerEntities {
    flag: EntityObject,
    area: EntityObject,
}

impl SpawnMarkerEntities {
    pub fn new(
        world: &mut World,
        entity_manager: &mut impl AssetLoader,
        flag_name: &str,
        area_name: &str,
    ) -> Result<Self> {
        let mut assets = AssetHandler::create(world);

        let mut flag = entity_manager.load_entity(&mut assets, flag_name)?;

        #[cfg(debug_assertions)]
        {
            flag.debug_name = Some("NPC Spawn Flag".to_string());
        }

        Location::new_and_setup(&mut flag)?;

        let mut area = entity_manager.load_entity(&mut assets, area_name)?;

        #[cfg(debug_assertions)]
        {
            area.debug_name = Some("NPC Spawn Area".to_string());
        }

        Location::new_and_setup(&mut area)?;

        Ok(Self { flag, area })
    }

    fn clone_entity(entity: &EntityObject, world: &mut World) -> Result<EntityObject> {
        let mut flag = world.clone_without_components(entity);
        flag.clone_component_from::<Draw>(entity)?;
        flag.clone_component_from::<Location>(entity)?;
        flag.clone_component_from::<BoundingBox>(entity)?;

        Location::setup(flag.multi_mut())?;

        Ok(flag)
    }

    fn clone(&self, world: &mut World) -> Result<Self> {
        let flag = Self::clone_entity(&self.flag, world)?;
        let area = Self::clone_entity(&self.area, world)?;

        Ok(Self { flag, area })
    }

    fn set_position(&mut self, position: Vector3<f32>) {
        self.flag
            .get_component_mut::<Location>()
            .unwrap()
            .set_position(position);

        self.area
            .get_component_mut::<Location>()
            .unwrap()
            .set_position(position);
    }

    fn set_radius(&mut self, radius: f32) {
        self.area
            .get_component_mut::<Location>()
            .unwrap()
            .set_scale(vec3(radius, radius, 1.0));
    }

    pub fn add(self, world: &mut World) -> Result<SpawnMarker> {
        let flag = world.add_entity(self.flag)?;
        let area = world.add_entity(self.area)?;

        Ok(SpawnMarker { flag, area })
    }
}

pub struct SpawnMarker {
    flag: Entity,
    area: Entity,
}

impl SpawnMarker {
    fn set_radius(&self, world: &mut World, radius: f32) -> Result<()> {
        let flag_object = world.entity_mut(self.area)?;

        flag_object
            .get_component_mut::<Location>()?
            .set_scale(vec3(radius, radius, 1.0));

        Ok(())
    }
}

trait AlterEntities {
    fn get(&self, coordinate: &Coordinate) -> Option<&Entity>;
    fn insert(
        &mut self,
        coordinate: Coordinate,
        entity: Entity,
        position: Vector3<f32>,
    ) -> Option<Entity>;
    fn remove(&mut self, coordinate: &Coordinate) -> Option<Entity>;
}

impl AlterEntities for HashMap<Coordinate, Entity> {
    fn get(&self, coordinate: &Coordinate) -> Option<&Entity> {
        self.get(coordinate)
    }

    fn insert(
        &mut self,
        coordinate: Coordinate,
        entity: Entity,
        _: Vector3<f32>,
    ) -> Option<Entity> {
        self.insert(coordinate, entity)
    }

    fn remove(&mut self, coordinate: &Coordinate) -> Option<Entity> {
        self.remove(coordinate)
    }
}

impl AlterEntities for HashMap<Coordinate, (Entity, Vector3<f32>)> {
    fn get(&self, coordinate: &Coordinate) -> Option<&Entity> {
        self.get(coordinate).map(|(e, _)| e)
    }

    fn insert(
        &mut self,
        coordinate: Coordinate,
        entity: Entity,
        position: Vector3<f32>,
    ) -> Option<Entity> {
        self.insert(coordinate, (entity, position)).map(|(e, _)| e)
    }

    fn remove(&mut self, coordinate: &Coordinate) -> Option<Entity> {
        self.remove(coordinate).map(|(e, _)| e)
    }
}

impl SpawnMarker {
    pub fn remove(&self, world: &mut World) -> Result<()> {
        world.remove_entity(self.flag)?;
        world.remove_entity(self.area)?;

        Ok(())
    }
}

#[derive(Clone, Debug)]
pub struct MapNPCSpawnInfo {
    pub coordinate: Coordinate,
    pub position: Vector2<f32>,
    pub radius: f32,
    pub min_count: u32,
    pub max_count: u32,
    pub normal_npc: Option<String>,
    pub elite_npc: Option<String>,
}

#[derive(Clone, Debug)]
pub struct MapBossSpawnInfo {
    pub coordinate: Coordinate,
    pub position: Vector2<f32>,
    pub name: Option<String>,
}

impl MapBossSpawnInfo {
    fn setup_flag(
        marker: &SpawnMarkerEntities,
        world: &mut World,
        position: Vector3<f32>,
    ) -> Result<EntityObject> {
        let mut flag = SpawnMarkerEntities::clone_entity(&marker.flag, world)?;

        let location = flag.get_component_mut::<Location>()?;
        let scale = location.scale();
        location.set_scale(scale * BOSS_MARKER_SCALE);
        location.set_position(position);

        Ok(flag)
    }
}

pub struct MapData {
    pub width: u32,
    pub height: u32,

    // surface
    pub surface: Surface,

    // entities
    pub entities: HashMap<Coordinate, Entity>,

    // textures
    pub textures_index: HashMap<String, u32>,

    pub textures_info: HashMap<u32, OrderedElement<Arc<Image>>>,

    // <(Coordinate), TextureIndex>
    pub chunk_handles: HashMap<(u32, u32), Entity>,

    pub spawn_locations: HashMap<Coordinate, (Entity, Vector3<f32>)>,
    pub show_spawn_locations: bool,
    pub leave_locations: HashMap<Coordinate, Entity>,

    pub npc_spawn_areas: Vec<(MapNPCSpawnInfo, Option<SpawnMarker>)>,
    pub boss_spawns: Vec<(MapBossSpawnInfo, Option<Entity>)>,
    pub show_npc_spawns: bool,

    pub npc_spawn_marker: Option<SpawnMarkerEntities>,
}

impl MapData {
    pub fn interpolate_height(x: f32, y: f32, surface: &Surface) -> f32 {
        let x_fract = x.fract();
        let y_fract = y.fract();
        let ux = x as u32;
        let uy = y as u32;

        let left_bottom = surface.point(ux, uy);
        let right_bottom = surface.point(ux + 1, uy);
        let left_top = surface.point(ux, uy + 1);
        let right_top = surface.point(ux + 1, uy + 1);

        let bottom_right_diff = right_bottom - left_bottom;
        let top_right_diff = right_top - left_top;

        let bottom_inter = left_bottom + x_fract * bottom_right_diff;
        let top_inter = left_top + x_fract * top_right_diff;

        let inter_diff = top_inter - bottom_inter;

        let middle_inter = bottom_inter + y_fract * inter_diff;

        middle_inter.z
    }

    pub fn add_height(
        &mut self,
        x: u32,
        y: u32,
        height: f32,
        async_db: &AsyncDBAccess,
    ) -> Result<()> {
        // (height, x, y, z)
        let mut vec = vec![
            (height, x, y, self.surface.point(x, y).z),
            (height, x, y + 1, self.surface.point(x, y + 1).z),
            (height, x + 1, y, self.surface.point(x + 1, y).z),
            (height, x + 1, y + 1, self.surface.point(x + 1, y + 1).z),
        ];

        vec.sort_by(|(_h1, _x1, _y1, z1), (_h2, _x2, _y2, z2)| {
            z1.partial_cmp(z2).unwrap_or(Ordering::Equal)
        });

        if height < 0.0 {
            // take highest point
            let (_, _, _, zv) = vec[3];
            let target_height = zv + height;

            for (h, _, _, z) in &mut vec {
                let point_height = target_height - *z;

                if point_height >= 0.0 {
                    *h = 0.0;
                } else {
                    *h = point_height;
                }
            }
        } else {
            // take lowest point
            let (_, _, _, zv) = vec[0];
            let target_height = zv + height;

            for (h, _, _, z) in &mut vec {
                let point_height = target_height - *z;

                if point_height <= 0.0 {
                    *h = 0.0;
                } else {
                    *h = point_height;
                }
            }
        }

        for (height, x, y, _z) in vec {
            self.add_height_to_point(x, y, height, async_db)?;
        }

        Ok(())

        // TODO: check if tiles at the corners still are plains
    }

    #[inline]
    fn _get_height(x: f32, y: f32, width: u32, height: u32, surface: &Surface) -> f32 {
        if x >= 0.0 && y >= 0.0 && x <= width as f32 && y <= height as f32 {
            Self::interpolate_height(x, y, surface)
        } else {
            0.0
        }
    }

    pub fn get_height(&self, x: f32, y: f32) -> f32 {
        Self::_get_height(x, y, self.width, self.height, &self.surface)
    }

    pub fn find_entity(&self, x: u32, y: u32) -> Option<Entity> {
        self.entities.get(&Coordinate { x, y }).copied()
    }

    pub fn find_spawn_marker(&self, x: u32, y: u32) -> Option<Entity> {
        self.spawn_locations
            .get(&Coordinate { x, y })
            .map(|(e, _)| *e)
    }

    pub fn find_leave_marker(&self, x: u32, y: u32) -> Option<Entity> {
        self.leave_locations.get(&Coordinate { x, y }).copied()
    }

    pub fn find_npc_spawn_marker(&self, x: u32, y: u32) -> Option<&SpawnMarker> {
        self.npc_spawn_areas
            .iter()
            .find(|(spawn_info, _)| Coordinate { x, y } == spawn_info.coordinate)
            .and_then(|(_, marker)| marker.as_ref())
    }

    pub fn find_boss_spawn_marker(&self, x: u32, y: u32) -> Option<Entity> {
        self.boss_spawns
            .iter()
            .find(|(spawn_info, _)| Coordinate { x, y } == spawn_info.coordinate)
            .and_then(|(_, flag)| *flag)
    }

    pub fn object_type_of(&self, object: Entity) -> MapObjectType {
        if self
            .spawn_locations
            .values()
            .any(|(entity, _)| *entity == object)
        {
            return MapObjectType::PlayerSpawn;
        }

        if self
            .leave_locations
            .values()
            .find(|&entity| *entity == object)
            .is_some()
        {
            return MapObjectType::PlayerDespawn;
        }

        if self
            .npc_spawn_areas
            .iter()
            .find(|(_, marker_opt)| match marker_opt {
                Some(marker) => object == marker.flag,
                None => false,
            })
            .is_some()
        {
            return MapObjectType::NPCSpawn;
        }

        if self
            .boss_spawns
            .iter()
            .find(|(_, flag_opt)| match flag_opt {
                Some(flag) => object == *flag,
                None => false,
            })
            .is_some()
        {
            return MapObjectType::BossSpawn;
        }

        if self.entities.values().any(|entity| *entity == object) {
            return MapObjectType::Object;
        }

        MapObjectType::Unknown
    }

    #[inline]

    fn execute_on_entity<F>(&self, x: u32, y: u32, mut f: F) -> Result<()>
    where
        F: FnMut(Entity) -> Result<()>,
    {
        if let Some(managed_entity) = self.find_entity(x, y) {
            f(managed_entity)?;
        }

        if self.show_spawn_locations {
            if let Some(managed_entity) = self.find_spawn_marker(x, y) {
                f(managed_entity)?;
            }
        }

        if let Some(managed_entity) = self.find_leave_marker(x, y) {
            f(managed_entity)?;
        }

        if self.show_npc_spawns {
            if let Some(managed_entity) = self.find_npc_spawn_marker(x, y) {
                f(managed_entity.flag)?;
                f(managed_entity.area)?;
            }

            if let Some(flag) = self.find_boss_spawn_marker(x, y) {
                f(flag)?;
            }
        }

        Ok(())
    }

    pub fn check_height_at(&mut self, x: u32, y: u32, world: &mut World) -> Result<()> {
        self.execute_on_entity(x, y, |entity| {
            let entity_object = world.entity_mut(entity)?;

            let location = entity_object.get_component_mut::<Location>()?;

            let pos = location.position();
            location.set_position(cgmath::Vector3::new(
                pos.x,
                pos.y,
                self.get_height(pos.x, pos.y),
            ));

            Ok(())
        })?;

        Ok(())
    }

    pub fn set_spawn_location(
        &mut self,
        world: &mut World,
        entity_opt: Option<(String, EntityObject)>,
        position: cgmath::Vector2<f32>,
        rotation: impl Into<Rad<f32>>,
        async_db: &AsyncDBAccess,
    ) -> Result<()> {
        Self::set_entity_alike(
            entity_opt,
            position,
            rotation,
            async_db,
            &mut self.spawn_locations,
            (self.width, self.height),
            &self.surface,
            EntityDBType::spawn_location(),
            world,
        )?;

        Ok(())
    }

    pub fn change_boss(
        &mut self,
        async_db: &AsyncDBAccess,
        position: Vector2<f32>,
        name: impl ToString,
    ) -> Result<()> {
        if let Some((spawn_info, _)) = self
            .boss_spawns
            .iter_mut()
            .find(|(spawn_info, _)| spawn_info.coordinate == Coordinate::from(position))
        {
            let coordinate = spawn_info.coordinate.clone();
            let name = name.to_string();

            spawn_info.name = Some(name.clone());

            async_db.add(move |sql| sql.update_boss_name(&coordinate, &name))?;
        }

        Ok(())
    }

    pub fn change_npc_spawn(
        &mut self,
        async_db: &AsyncDBAccess,
        world: &mut World,
        position: Vector2<f32>,
        npc_spawn_parameter: NPCSpawnParameter,
    ) -> Result<()> {
        if let Some((spawn_info, marker)) = self
            .npc_spawn_areas
            .iter_mut()
            .find(|(spawn_info, _)| spawn_info.coordinate == Coordinate::from(position))
        {
            let coordinate = spawn_info.coordinate.clone();

            match npc_spawn_parameter {
                NPCSpawnParameter::MinCount(min_count) => {
                    spawn_info.min_count = min_count;

                    async_db.add(move |sql| {
                        sql.update_npc_spawn_min_npc_count(&coordinate, min_count)
                    })?;
                }
                NPCSpawnParameter::MaxCount(max_count) => {
                    spawn_info.max_count = max_count;

                    async_db.add(move |sql| {
                        sql.update_npc_spawn_max_npc_count(&coordinate, max_count)
                    })?;
                }
                NPCSpawnParameter::EliteNPC(elite_npc) => {
                    spawn_info.elite_npc = Some(elite_npc.clone());

                    async_db
                        .add(move |sql| sql.update_npc_spawn_elite_npc(&coordinate, elite_npc))?;
                }
                NPCSpawnParameter::NormalNPC(normal_npc) => {
                    spawn_info.normal_npc = Some(normal_npc.clone());

                    async_db
                        .add(move |sql| sql.update_npc_spawn_normal_npc(&coordinate, normal_npc))?;
                }
                NPCSpawnParameter::Radius(radius) => {
                    spawn_info.radius = radius;

                    marker.as_ref().unwrap().set_radius(world, radius)?;

                    async_db.add(move |sql| sql.update_npc_spawn_radius(&coordinate, radius))?;
                }
            }
        }

        Ok(())
    }

    pub fn disable_spawns(&mut self, world: &mut World) -> Result<()> {
        if self.show_spawn_locations {
            self.show_spawn_locations = false;

            for (spawn_entity, _) in self.spawn_locations.values() {
                world.remove_entity(*spawn_entity)?;
            }
        }

        Ok(())
    }

    pub fn spawn_locations(&self) -> Vec<Vector3<f32>> {
        let locations = self
            .spawn_locations
            .values()
            .map(|(_, location)| *location)
            .collect();

        locations
    }

    pub fn leave_markers(&self) -> Vec<Entity> {
        self.leave_locations.values().copied().collect()
    }

    pub fn set_leave_location(
        &mut self,
        world: &mut World,
        entity_opt: Option<(String, EntityObject)>,
        position: cgmath::Vector2<f32>,
        rotation: impl Into<Rad<f32>>,
        async_db: &AsyncDBAccess,
    ) -> Result<()> {
        Self::set_entity_alike(
            entity_opt,
            position,
            rotation,
            async_db,
            &mut self.leave_locations,
            (self.width, self.height),
            &self.surface,
            EntityDBType::leave_location(),
            world,
        )
    }

    pub fn toggle_npc_spawns(&mut self, world: &mut World) -> Result<()> {
        self.show_npc_spawns = !self.show_npc_spawns;

        for (spawn_info, marker) in self.npc_spawn_areas.iter_mut() {
            let height = Self::_get_height(
                spawn_info.position.x,
                spawn_info.position.y,
                self.width,
                self.height,
                &self.surface,
            );

            let marker: Option<&mut SpawnMarker> = match marker {
                Some(marker) => Some(marker),
                None => match &self.npc_spawn_marker {
                    Some(global_marker) => {
                        let mut new_marker = global_marker.clone(world)?;

                        // set position
                        new_marker.set_position(spawn_info.position.extend(height));
                        new_marker.set_radius(spawn_info.radius);

                        *marker = Some(new_marker.add(world)?);

                        marker.as_mut()
                    }
                    None => None,
                },
            };

            if let Some(marker) = marker {
                // let coordinate = Coordinate::from(*position);

                if self.show_npc_spawns {
                    // marker.add(scene)?;
                } else {
                    marker.remove(world)?;
                }
            }
        }

        for (spawn_info, flag_marker) in self.boss_spawns.iter_mut() {
            let height = Self::_get_height(
                spawn_info.position.x,
                spawn_info.position.y,
                self.width,
                self.height,
                &self.surface,
            );

            let marker: Option<Entity> = match flag_marker {
                Some(marker) => Some(*marker),
                None => match &self.npc_spawn_marker {
                    Some(global_marker) => {
                        let flag = MapBossSpawnInfo::setup_flag(
                            global_marker,
                            world,
                            spawn_info.position.extend(height),
                        )?;

                        *flag_marker = Some(world.add_entity(flag)?);

                        *flag_marker
                    }
                    None => None,
                },
            };

            if let Some(marker) = marker {
                // let coordinate = Coordinate::from(*position);

                if self.show_npc_spawns {
                    // marker.add(scene)?;
                } else {
                    world.remove_entity(marker)?;
                }
            }
        }

        Ok(())
    }

    pub fn set_npc_spawn(
        &mut self,
        world: &mut World,
        position: cgmath::Vector2<f32>,
        async_db: &AsyncDBAccess,
        radius: f32,
        min: u32,
        max: u32,
    ) -> Result<()> {
        let coordinate = Coordinate::from(position);

        if self
            .npc_spawn_areas
            .iter()
            .any(|(spawn_info, _)| spawn_info.coordinate == coordinate)
        {
            // if there is already a spawn at position, ignore request
            return Ok(());
        }

        let marker = match self.npc_spawn_marker.as_ref() {
            Some(managed_entity) => {
                let mut marker = managed_entity.clone(world)?;

                // set position
                marker.set_position(position.extend(self.get_height(position.x, position.y)));
                marker.set_radius(radius);

                Some(marker.add(world)?)
            }
            None => None,
        };

        self.npc_spawn_areas.push((
            MapNPCSpawnInfo {
                coordinate: coordinate.clone(),
                position,
                radius,
                min_count: min,
                max_count: max,
                normal_npc: None,
                elite_npc: None,
            },
            marker,
        ));

        async_db
            .add(move |sql| sql.insert_npc_spawn((coordinate.x, coordinate.y), radius, min, max))?;

        Ok(())
    }

    pub fn unset_npc_spawn(
        &mut self,
        world: &mut World,
        position: cgmath::Vector2<f32>,
        async_db: &AsyncDBAccess,
    ) -> Result<()> {
        let coordinate = Coordinate::from(position);

        if let Some(index) = self
            .npc_spawn_areas
            .iter()
            .position(|(spawn_info, _)| spawn_info.coordinate == coordinate)
        {
            let (_, marker) = self.npc_spawn_areas.remove(index);

            async_db.add(move |sql| sql.remove_npc_spawn((coordinate.x, coordinate.y)))?;

            if let Some(marker) = marker {
                marker.remove(world)?;
            }
        }

        Ok(())
    }

    pub fn set_boss_spawn(
        &mut self,
        world: &mut World,
        position: cgmath::Vector2<f32>,
        async_db: &AsyncDBAccess,
    ) -> Result<()> {
        let coordinate = Coordinate::from(position);

        if self
            .boss_spawns
            .iter()
            .any(|(spawn_info, _)| spawn_info.coordinate == coordinate)
        {
            // if there is already a spawn at position, ignore request
            return Ok(());
        }

        let marker = match self.npc_spawn_marker.as_ref() {
            Some(managed_entity) => {
                let flag = MapBossSpawnInfo::setup_flag(
                    managed_entity,
                    world,
                    position.extend(self.get_height(position.x, position.y)),
                )?;

                Some(world.add_entity(flag)?)
            }
            None => None,
        };

        self.boss_spawns.push((
            MapBossSpawnInfo {
                coordinate: coordinate.clone(),
                position,
                name: None,
            },
            marker,
        ));

        async_db.add(move |sql| sql.insert_boss_spawn((coordinate.x, coordinate.y)))?;

        Ok(())
    }

    pub fn unset_boss_spawn(
        &mut self,
        world: &mut World,
        position: cgmath::Vector2<f32>,
        async_db: &AsyncDBAccess,
    ) -> Result<()> {
        let coordinate = Coordinate::from(position);

        if let Some(index) = self
            .boss_spawns
            .iter()
            .position(|(spawn_info, _)| spawn_info.coordinate == coordinate)
        {
            let (_, marker) = self.boss_spawns.remove(index);

            async_db.add(move |sql| sql.remove_boss_spawn((coordinate.x, coordinate.y)))?;

            if let Some(marker) = marker {
                world.remove_entity(marker)?;
            }
        }

        Ok(())
    }

    fn set_entity_alike<T: AlterEntities>(
        entity_opt: Option<(String, EntityObject)>,
        position: cgmath::Vector2<f32>,
        rotation: impl Into<Rad<f32>>,
        async_db: &AsyncDBAccess,
        entities: &mut T,
        (width, height): (u32, u32),
        surface: &Surface,
        table_name: EntityDBType,
        world: &mut World,
    ) -> Result<()> {
        let coordinate = Coordinate {
            x: position.x as u32,
            y: position.y as u32,
        };

        let position_3d = position.extend(Self::_get_height(
            position.x, position.y, width, height, surface,
        ));
        let rad_rotation = rotation.into();

        match entity_opt {
            // set entity to current location or dismiss
            Some((entity_name, mut entity)) => {
                // check if tile is used by an entity
                if entities.get(&coordinate).is_some() {
                    println!(
                        "tile ({}, {}) has already on entity, consider erasing first",
                        coordinate.x, coordinate.y
                    );

                    return Ok(());
                }

                let x = coordinate.x;
                let y = coordinate.y;

                async_db.add(move |sql| {
                    sql.insert_entity(
                        table_name,
                        (x, y),
                        entity_name,
                        position_3d.truncate(),
                        rad_rotation,
                    )
                })?;

                // set position
                entity
                    .get_component_mut::<Location>()
                    .unwrap()
                    .update_raw(position_3d, rad_rotation);

                let handle = world.add_entity(entity)?;

                entities.insert(coordinate, handle, position_3d);
            }
            // erase entity object on current location
            None => {
                if let Some(entity) = entities.remove(&coordinate) {
                    async_db.add(move |sql| {
                        sql.remove_entity(table_name, (coordinate.x, coordinate.y))
                    })?;

                    world.remove_entity(entity)?;

                    return Ok(());
                }
            }
        }

        Ok(())
    }

    pub fn set_entity(
        &mut self,
        world: &mut World,
        entity_opt: Option<(String, EntityObject)>,
        position: cgmath::Vector2<f32>,
        rotation: impl Into<Rad<f32>>,
        async_db: &AsyncDBAccess,
    ) -> Result<()> {
        Self::set_entity_alike(
            entity_opt,
            position,
            rotation,
            async_db,
            &mut self.entities,
            (self.width, self.height),
            &self.surface,
            EntityDBType::entity(),
            world,
        )
    }

    /// Updates the texture used by the specified tile
    /// Returns a bool which indicates if the used textures or their
    /// indices have changed
    ///
    /// # Arguments
    ///
    /// * `x`               x coordinate of the tile
    /// * `y`               y coordinate of the tile
    /// * `texture_name`    name of the texture which should be used
    /// * `texture`         texture as `Arc<Image>`

    pub fn set_texture(
        &mut self,
        x: u32,
        y: u32,
        texture: &Arc<Image>,
        async_db: &AsyncDBAccess,
    ) -> Result<bool> {
        let mut map_operations_needed = false;
        let mut first_return = false;
        let mut remove_index = 0;

        let texture_path = (*texture.file_name().as_ref().unwrap()).clone();
        let texture_name = Path::new(&texture_path.full_path())
            .file_stem()
            .unwrap()
            .to_str()
            .unwrap()
            .to_string();

        // texture is already used in the map
        if let Some(texture_index) = self.textures_index.get(&texture_name) {
            let texture_id = self.surface.texture_id(x, y);

            if texture_id == *texture_index {
                // the texture is already used in this tile -> nothing has to be changed
                return Ok(false);
            } else {
                if let Some(old_texture_info) = self.textures_info.get_mut(&texture_id) {
                    old_texture_info.references -= 1;

                    if old_texture_info.references == 0 {
                        map_operations_needed = true;
                        remove_index = old_texture_info.index;
                    }
                }

                if let Some(new_texture_info) = self.textures_info.get_mut(texture_index) {
                    new_texture_info.references += 1;
                }

                self.surface.set_texture_id(x, y, *texture_index);

                let tile_index = Self::tile_index(x, y, self.width);
                let sql_texture_index = texture_index + 1;

                async_db.add(move |sql| sql.set_texture(tile_index, sql_texture_index))?;

                first_return = true;
            }
        }

        if map_operations_needed {
            self.check_texture_clean_up(remove_index, async_db)?;
        }

        if first_return {
            // returns if the tile got a new texture,
            // but this texture was already present in the map
            return Ok(true);
        }

        // else - texture is new on this map
        // commented because of borrowing
        let texture_index = self.textures_index.len() as u32;

        self.textures_index
            .insert(texture_name.clone(), texture_index);

        self.textures_info.insert(
            texture_index,
            OrderedElement {
                name: texture_name.clone(),
                index: texture_index,
                references: 1,
                element: texture.clone(),
            },
        );

        if let Some(info) = self.textures_info.get_mut(&self.surface.texture_id(x, y)) {
            info.references -= 1;

            if info.references == 0 {
                map_operations_needed = true;
                remove_index = info.index;
            }
        }

        self.surface.set_texture_id(x, y, texture_index);

        if map_operations_needed {
            self.check_texture_clean_up(remove_index, async_db)?;
        }

        // sql persistency
        let i = Self::tile_index(x, y, self.width);
        let sql_texture_index = texture_index + 1;
        let texture_name_clone = texture_name.clone();

        async_db.add(move |sql| {
            sql.set_texture(i, sql_texture_index)?;
            sql.insert_new_texture(sql_texture_index, texture_name_clone)?;

            Ok(())
        })?;

        Ok(true)
    }
}

// private
impl MapData {
    fn add_height_to_point(
        &mut self,
        x: u32,
        y: u32,
        height: f32,
        async_db: &AsyncDBAccess,
    ) -> Result<()> {
        let new_height = self.surface.change_height(x, y, height);

        let i = Self::tile_index(x, y, self.width + 1);

        async_db.add(move |sql| sql.update_height(i, new_height))?;

        Ok(())
    }

    fn tile_index(x: u32, y: u32, width: u32) -> u32 {
        (x + 1) + (y * width)
    }

    fn check_texture_clean_up(
        &mut self,
        remove_index: u32,
        async_db: &AsyncDBAccess,
    ) -> Result<()> {
        let info = match self.textures_info.get(&remove_index) {
            Some(info) => info.clone(),
            None => {
                return Err(anyhow::Error::msg(format!(
                    "Index ({}) in textures_info (MapData) not found",
                    remove_index
                )));
            }
        };

        let index = match self.textures_index.remove(&info.name) {
            Some(index) => index,
            None => {
                return Err(anyhow::Error::msg(format!(
                    "Could not remove key ({}) from textures_index",
                    info.name
                )));
            }
        };

        if self.textures_info.remove(&index).is_none() {
            return Err(anyhow::Error::msg(format!(
                "Could not remove key ({}) from textures_info",
                index
            )));
        }

        let sql_index = remove_index + 1;

        async_db.add(move |sql| sql.remove_texture(sql_index))?;

        self.tighten_indices(async_db)?;

        Ok(())
    }

    fn tighten_indices(&mut self, async_db: &AsyncDBAccess) -> Result<()> {
        // check if indices are continuous
        let mut operations_needed = false;
        let mut broken_index = 0;

        for i in 0..self.textures_info.len() {
            if self.textures_info.get(&(i as u32)).is_none() {
                broken_index = i as u32;
                operations_needed = true;
                break;
            }
        }

        if !operations_needed {
            return Ok(());
        }

        let index_of_last_one = self.textures_info.len() as u32;

        // update the indices for every tile: last_index -> borken_index
        for x in 0..self.width {
            for y in 0..self.height {
                if self.surface.texture_id(x, y) == index_of_last_one {
                    self.surface.set_texture_id(x, y, broken_index);
                }
            }
        }

        // (1) get the info of the last index and update its index
        let corrected_info = match self.textures_info.get_mut(&index_of_last_one) {
            Some(info) => {
                info.index = broken_index;
                info.clone()
            }
            None => return Ok(()),
        };

        // use name of info to update the other map
        if let Some(i) = self.textures_index.get_mut(&corrected_info.name) {
            *i = broken_index;
        }

        // (2) remove the last index
        self.textures_info.remove(&index_of_last_one);

        // (3) add the info at the broken index
        self.textures_info.insert(broken_index, corrected_info);

        // sql normalization, since its indexing at 1
        let sql_broken_index = broken_index + 1;
        let sql_last_index = index_of_last_one + 1;

        async_db.add(move |sql| sql.update_texture_index(sql_broken_index, sql_last_index))?;

        Ok(())
    }
}