use super::{ spawner, Map, Position, Rect, TileType };
use bracket_lib::prelude::*;

mod room_accretion;
use room_accretion::RoomAccretionBuilder;
mod bsp_dungeon;
use bsp_dungeon::BspDungeonBuilder;
mod bsp_interior;
use bsp_interior::BspInteriorBuilder;
mod cellular_automata;
use cellular_automata::CellularAutomataBuilder;
mod common;
mod dla;
use dla::DLABuilder;
mod drunkard;
use drunkard::DrunkardsWalkBuilder;
mod maze;
use maze::MazeBuilder;
mod simple_map;
use simple_map::SimpleMapBuilder;
mod voronoi;
use voronoi::VoronoiBuilder;
mod prefab_builder;
use prefab_builder::PrefabBuilder;
mod room_based_spawner;
mod wfc;
use room_based_spawner::*;
mod room_based_stairs;
use room_based_stairs::*;
mod room_based_starting_position;
use room_based_starting_position::*;
mod area_starting_points;
use area_starting_points::{ AreaStartingPosition, XStart, YStart };
mod cull_unreachable;
use cull_unreachable::CullUnreachable;
mod distant_exit;
use distant_exit::DistantExit;
mod voronoi_spawning;
use common::*;
use specs::prelude::*;
use voronoi_spawning::VoronoiSpawning;
use super::config::CONFIG;
use super::consts::ids::*;
use super::consts::names::*;
//use wfc::WaveFunctionCollapseBuilder;
mod room_exploder;
use room_exploder::RoomExploder;
mod room_corner_rounding;
use room_corner_rounding::RoomCornerRounder;
mod rooms_corridors_dogleg;
use rooms_corridors_dogleg::DoglegCorridors;
mod rooms_corridors_bsp;
use rooms_corridors_bsp::BspCorridors;
mod room_sorter;
use room_sorter::{ RoomSort, RoomSorter };
mod room_draw;
use room_draw::RoomDrawer;
mod rooms_corridors_nearest;
use rooms_corridors_nearest::NearestCorridors;
mod rooms_corridors_bresenham;
use rooms_corridors_bresenham::BresenhamCorridors;
mod rooms_corridors_spawner;
use rooms_corridors_spawner::CorridorSpawner;
mod door_placement;
use door_placement::DoorPlacement;
mod fill_edges;
use fill_edges::FillEdges;
mod town;
use town::town_builder;
mod forest;
use forest::forest_builder;
mod foliage;
use foliage::Foliage;
mod room_themer;
use room_themer::ThemeRooms;

// Shared data to be passed around build chain
pub struct BuilderMap {
    pub spawn_list: Vec<(usize, String)>,
    pub map: Map,
    pub starting_position: Option<Position>,
    pub rooms: Option<Vec<Rect>>,
    pub corridors: Option<Vec<Vec<usize>>>,
    pub history: Vec<Map>,
    pub width: i32,
    pub height: i32,
    pub initial_player_level: i32,
}

impl BuilderMap {
    fn take_snapshot(&mut self) {
        if CONFIG.logging.show_mapgen {
            let mut snapshot = self.map.clone();
            for v in snapshot.revealed_tiles.iter_mut() {
                *v = true;
            }
            self.history.push(snapshot);
        }
    }
}

pub struct BuilderChain {
    starter: Option<Box<dyn InitialMapBuilder>>,
    builders: Vec<Box<dyn MetaMapBuilder>>,
    pub build_data: BuilderMap,
}

impl BuilderChain {
    pub fn new<S: ToString>(
        overmap: bool,
        new_id: i32,
        width: i32,
        height: i32,
        difficulty: i32,
        name: S,
        short_name: S,
        depth: i32,
        initial_player_level: i32
    ) -> BuilderChain {
        BuilderChain {
            starter: None,
            builders: Vec::new(),
            build_data: BuilderMap {
                spawn_list: Vec::new(),
                map: Map::new(overmap, new_id, width, height, difficulty, name, short_name, depth),
                starting_position: None,
                rooms: None,
                corridors: None,
                history: Vec::new(),
                width: width,
                height: height,
                initial_player_level: initial_player_level,
            },
        }
    }

    pub fn start_with(&mut self, starter: Box<dyn InitialMapBuilder>) {
        match self.starter {
            None => {
                self.starter = Some(starter);
            }
            Some(_) => unreachable!("You can only have one starting builder."),
        };
    }

    pub fn with(&mut self, metabuilder: Box<dyn MetaMapBuilder>) {
        self.builders.push(metabuilder);
    }

    pub fn build_map(&mut self, rng: &mut RandomNumberGenerator) {
        match &mut self.starter {
            None => unreachable!("Cannot run a map builder chain without a starting build system"),
            Some(starter) => {
                // Build the starting map
                starter.build_map(rng, &mut self.build_data);
            }
        }

        // Build additional layers in turn
        for metabuilder in self.builders.iter_mut() {
            metabuilder.build_map(rng, &mut self.build_data);
        }
    }

    pub fn spawn_entities(&mut self, ecs: &mut World) {
        let mut spawned_entities = Vec::new();
        for entity in self.build_data.spawn_list.iter() {
            spawned_entities.push(&entity.1);
            spawner::spawn_entity(ecs, &(&entity.0, &entity.1));
        }
        if CONFIG.logging.log_spawning {
            console::log(format!("DEBUGINFO: SPAWNED ENTITIES = {:?}", spawned_entities));
        }
    }
}

pub trait InitialMapBuilder {
    fn build_map(&mut self, rng: &mut RandomNumberGenerator, build_data: &mut BuilderMap);
}

pub trait MetaMapBuilder {
    fn build_map(&mut self, rng: &mut RandomNumberGenerator, build_data: &mut BuilderMap);
}

fn random_start_position(rng: &mut RandomNumberGenerator) -> (XStart, YStart) {
    let x;
    let xroll = rng.roll_dice(1, 3);
    match xroll {
        1 => {
            x = XStart::LEFT;
        }
        2 => {
            x = XStart::CENTRE;
        }
        _ => {
            x = XStart::RIGHT;
        }
    }

    let y;
    let yroll = rng.roll_dice(1, 3);
    match yroll {
        1 => {
            y = YStart::BOTTOM;
        }
        2 => {
            y = YStart::CENTRE;
        }
        _ => {
            y = YStart::TOP;
        }
    }

    (x, y)
}

fn random_room_builder(rng: &mut RandomNumberGenerator, builder: &mut BuilderChain, end: bool) {
    let build_roll = rng.roll_dice(1, 2); // TODO: BSP Interiors, change this to 1d3 and uncomment.
    // Start with a room builder.
    match build_roll {
        1 => builder.start_with(SimpleMapBuilder::new(None)),
        _ => builder.start_with(BspDungeonBuilder::new()),
        //_ => builder.start_with(BspInteriorBuilder::new()),
    }

    // BspInterior makes its own doorways. If we're not using that one,
    // select a sorting method, a type of corridor, and modifiers.
    if build_roll != 3 {
        // Sort by one of the 5 available algorithms
        let sort_roll = rng.roll_dice(1, 5);
        match sort_roll {
            1 => builder.with(RoomSorter::new(RoomSort::LEFTMOST)),
            2 => builder.with(RoomSorter::new(RoomSort::RIGHTMOST)),
            3 => builder.with(RoomSorter::new(RoomSort::TOPMOST)),
            4 => builder.with(RoomSorter::new(RoomSort::BOTTOMMOST)),
            _ => builder.with(RoomSorter::new(RoomSort::CENTRAL)),
        }

        builder.with(RoomDrawer::new());

        let corridor_roll = rng.roll_dice(1, 2);
        match corridor_roll {
            1 => builder.with(DoglegCorridors::new()),
            _ => builder.with(BspCorridors::new()),
        }

        let corridor_roll = rng.roll_dice(1, 4);
        match corridor_roll {
            1 => builder.with(DoglegCorridors::new()),
            2 => builder.with(NearestCorridors::new()),
            3 => builder.with(BresenhamCorridors::new()),
            _ => builder.with(BspCorridors::new()),
        }

        let cspawn_roll = rng.roll_dice(1, 2);
        if cspawn_roll == 1 {
            builder.with(CorridorSpawner::new());
        }

        let modifier_roll = rng.roll_dice(1, 6);
        match modifier_roll {
            1 => builder.with(RoomExploder::new()),
            2 => builder.with(RoomCornerRounder::new()),
            _ => {}
        }
    }

    // Pick a starting position, in a room or elsewhere.
    let start_roll = rng.roll_dice(1, 2);
    match start_roll {
        1 => builder.with(RoomBasedStartingPosition::new()),
        _ => {
            let (start_x, start_y) = random_start_position(rng);
            builder.with(AreaStartingPosition::new(start_x, start_y));
        }
    }

    // Decide where to put the exit - in a room or far away, anywhere.
    if !end {
        let exit_roll = rng.roll_dice(1, 2);
        match exit_roll {
            1 => builder.with(RoomBasedStairs::new()),
            _ => builder.with(DistantExit::new()),
        }
    }

    // Decide whether to spawn entities only in rooms, or with voronoi noise.
    let spawn_roll = rng.roll_dice(1, 2);
    match spawn_roll {
        1 => builder.with(RoomBasedSpawner::new()),
        _ => builder.with(VoronoiSpawning::new()),
    }

    builder.with(ThemeRooms::grass(5, 5 * 5)); // 5% chance of an overgrown treant room. Must be 5*5 tiles minimum.
    builder.with(ThemeRooms::barracks(5, 6 * 6)); // 5% chance of a squad barracks. Must be 6*6 tiles minimum.
}

fn random_shape_builder(
    rng: &mut RandomNumberGenerator,
    builder: &mut BuilderChain,
    end: bool
) -> bool {
    // Pick an initial builder
    let builder_roll = rng.roll_dice(1, 13);
    let mut want_doors = true;
    match builder_roll {
        1 => builder.start_with(CellularAutomataBuilder::new()),
        2 => builder.start_with(DrunkardsWalkBuilder::open_area()),
        3 => builder.start_with(DrunkardsWalkBuilder::open_halls()),
        4 => builder.start_with(DrunkardsWalkBuilder::winding_passages()),
        5 => builder.start_with(DrunkardsWalkBuilder::fat_passages()),
        6 => builder.start_with(DrunkardsWalkBuilder::fearful_symmetry()),
        7 => {
            builder.start_with(MazeBuilder::new());
            want_doors = false;
        }
        8 => builder.start_with(DLABuilder::walk_inwards()),
        9 => builder.start_with(DLABuilder::walk_outwards()),
        10 => builder.start_with(DLABuilder::central_attractor()),
        11 => builder.start_with(DLABuilder::insectoid()),
        12 => builder.start_with(VoronoiBuilder::pythagoras()),
        _ => builder.start_with(VoronoiBuilder::manhattan()),
    }

    // 'Select' the centre by placing a starting position, and cull everywhere unreachable.
    builder.with(AreaStartingPosition::new(XStart::CENTRE, YStart::CENTRE));
    builder.with(CullUnreachable::new());

    // Now set the start to a random spot in our remaining area.
    let (start_x, start_y) = random_start_position(rng);
    builder.with(AreaStartingPosition::new(start_x, start_y));

    // Place the exit and spawn mobs
    builder.with(VoronoiSpawning::new());
    if !end {
        builder.with(DistantExit::new());
    }

    return want_doors;
}

fn overmap_builder() -> BuilderChain {
    let mut builder = BuilderChain::new(
        true,
        ID_OVERMAP,
        69,
        41,
        0,
        NAME_OVERMAP,
        SHORTNAME_OVERMAP,
        0,
        1
    );
    builder.start_with(PrefabBuilder::overmap());
    builder.with(Foliage::percent(TileType::Grass, 30));
    return builder;
}

pub enum BuildType {
    Room = 1,
    Shape = 2,
    Any = 3,
}

pub fn random_builder(
    new_id: i32,
    rng: &mut RandomNumberGenerator,
    width: i32,
    height: i32,
    difficulty: i32,
    depth: i32,
    initial_player_level: i32,
    end: bool,
    build_type: BuildType
) -> BuilderChain {
    console::log(format!("DEBUGINFO: Building random (ID:{}, DIFF:{})", new_id, difficulty));
    let mut builder = BuilderChain::new(
        false,
        new_id,
        width,
        height,
        difficulty,
        NAME_DUNGEON_RANDOM,
        SHORTNAME_DUNGEON_RANDOM,
        depth,
        initial_player_level
    );
    let mut want_doors = true;
    match build_type {
        BuildType::Room => random_room_builder(rng, &mut builder, end),
        BuildType::Shape => {
            want_doors = random_shape_builder(rng, &mut builder, end);
        }
        BuildType::Any => {
            let roll = rng.roll_dice(1, 2);
            match roll {
                1 => random_room_builder(rng, &mut builder, end),
                _ => {
                    want_doors = random_shape_builder(rng, &mut builder, end);
                }
            }
        }
    }

    /*
    WFC needs polishing up before it makes good maps. Right now it leaves too much unusable area,
    by making disconnected sections and having no methods to connect them.

    if rng.roll_dice(1, 1) == 1 {
        builder.with(WaveFunctionCollapseBuilder::new());

        // Now set the start to a random starting area
        let (start_x, start_y) = random_start_position(rng);
        builder.with(AreaStartingPosition::new(start_x, start_y));

        // Setup an exit and spawn mobs
        builder.with(VoronoiSpawning::new());
        builder.with(DistantExit::new());
    }
    */

    if want_doors {
        builder.with(DoorPlacement::new());
    }

    if rng.roll_dice(1, 20) == 1 {
        builder.with(PrefabBuilder::sectional(prefab_builder::prefab_sections::UNDERGROUND_FORT));
    }

    builder.with(PrefabBuilder::vaults());
    // Regardless of anything else, fill the edges back in with walls. We can't walk
    // there anyway, and we don't want an open line of sight into the unmapped void.
    builder.with(FillEdges::wall());

    builder
}

use crate::consts::prelude::*;

pub fn level_builder(
    id: i32,
    rng: &mut RandomNumberGenerator,
    width: i32,
    height: i32,
    initial_player_level: i32
) -> BuilderChain {
    match id {
        ID_OVERMAP => overmap_builder(),
        ID_TOWN => town_builder(id, rng, width, height, 0, initial_player_level),
        ID_TOWN2 => forest_builder(id, rng, width, height, 1, initial_player_level),
        ID_TOWN3 =>
            random_builder(
                id,
                rng,
                VIEWPORT_W,
                VIEWPORT_H,
                2,
                1,
                initial_player_level,
                true,
                BuildType::Room
            ),
        _ if id >= ID_INFINITE =>
            random_builder(
                id,
                rng,
                VIEWPORT_W,
                VIEWPORT_H,
                4 + diff(ID_INFINITE, id),
                1 + diff(ID_INFINITE, id),
                initial_player_level,
                false,
                BuildType::Room
            ),
        _ => // This should be unreachable!() eventually. Right now it's reachable with the debug/cheat menu. It should not be in normal gameplay.
            random_builder(
                id,
                rng,
                VIEWPORT_W,
                VIEWPORT_H,
                1,
                404,
                initial_player_level,
                false,
                BuildType::Room
            ),
    }
}

fn diff(branch_id: i32, lvl_id: i32) -> i32 {
    return lvl_id - branch_id;
}

fn room_accretion() -> BuilderChain {
    let mut builder = BuilderChain::new(false, 110, 64, 64, 0, "room_accretion", "accretion", 0, 1);
    builder.start_with(RoomAccretionBuilder::new());
    builder.with(AreaStartingPosition::new(XStart::CENTRE, YStart::CENTRE));
    builder
}
