use super::{ BuilderChain, BuilderMap, InitialMapBuilder, Position, TileType, FillEdges };
use std::collections::HashSet;
use crate::consts::names::*;
use bracket_lib::prelude::*;

pub fn town_builder(
    new_id: i32,
    _rng: &mut RandomNumberGenerator,
    width: i32,
    height: i32,
    difficulty: i32,
    initial_player_level: i32
) -> BuilderChain {
    console::log(format!("DEBUGINFO: Building town (ID:{}, DIFF:{})", new_id, difficulty));
    let mut chain = BuilderChain::new(
        false,
        new_id,
        width,
        height,
        difficulty,
        NAME_STARTER_TOWN,
        SHORTNAME_STARTER_TOWN,
        0,
        initial_player_level
    );
    chain.start_with(TownBuilder::new());
    chain.with(FillEdges::overmap_transition(new_id));

    return chain;
}

pub struct TownBuilder {}

impl InitialMapBuilder for TownBuilder {
    #[allow(dead_code)]
    fn build_map(&mut self, rng: &mut RandomNumberGenerator, build_data: &mut BuilderMap) {
        self.build_map(rng, build_data);
    }
}

enum BuildingTag {
    Tavern,
    Temple,
    PlayerHouse,
    NPCHouse,
    Mine,
    Abandoned,
    Unassigned,
}

impl TownBuilder {
    pub fn new() -> Box<TownBuilder> {
        return Box::new(TownBuilder {});
    }

    pub fn build_map(&mut self, rng: &mut RandomNumberGenerator, build_data: &mut BuilderMap) {
        // Make visible for snapshot
        for t in build_data.map.visible_tiles.iter_mut() {
            *t = true;
        }

        // Build map
        self.grass_layer(build_data);
        let piers = self.water_and_piers(rng, build_data);
        let (mut available_building_tiles, wall_gap_y) = self.town_walls(rng, build_data);
        let mut buildings = self.buildings(rng, build_data, &mut available_building_tiles);
        let doors = self.add_doors(rng, build_data, &mut buildings, wall_gap_y);
        self.path_from_tiles_to_nearest_tiletype(
            build_data,
            &doors,
            TileType::Road,
            TileType::Road,
            true
        );
        self.path_from_tiles_to_nearest_tiletype(
            build_data,
            &piers,
            TileType::Road,
            TileType::Road,
            false
        );

        // Spawn entities
        let building_size = self.sort_buildings(&buildings);
        self.building_factory(rng, build_data, &buildings, &building_size);
        self.spawn_dockers(build_data, rng);
        self.spawn_townsfolk(build_data, rng, &mut available_building_tiles);

        build_data.starting_position = Some(Position {
            x: build_data.width - 2,
            y: wall_gap_y,
        });

        build_data.take_snapshot();
    }

    fn sort_buildings(
        &mut self,
        buildings: &[(i32, i32, i32, i32)]
    ) -> Vec<(usize, i32, BuildingTag)> {
        // Sort buildings by size, defaulting them to Unassigned buildings
        let mut building_size: Vec<(usize, i32, BuildingTag)> = Vec::new();
        for (i, building) in buildings.iter().enumerate() {
            building_size.push((i, building.2 * building.3, BuildingTag::Unassigned));
        }
        building_size.sort_by(|a, b| b.1.cmp(&a.1));

        // Set individual buildings to their correct tags
        building_size[0].2 = BuildingTag::Tavern;
        building_size[1].2 = BuildingTag::Temple;
        building_size[2].2 = BuildingTag::Mine;
        building_size[3].2 = BuildingTag::PlayerHouse;
        for b in building_size.iter_mut().skip(4) {
            b.2 = BuildingTag::NPCHouse;
        }
        let last_idx = building_size.len() - 1;
        building_size[last_idx].2 = BuildingTag::Abandoned;

        return building_size;
    }

    fn building_factory(
        &mut self,
        rng: &mut RandomNumberGenerator,
        build_data: &mut BuilderMap,
        buildings: &[(i32, i32, i32, i32)],
        building_index: &[(usize, i32, BuildingTag)]
    ) {
        for (i, building) in buildings.iter().enumerate() {
            let build_tag = &building_index[i].2;
            match build_tag {
                BuildingTag::Tavern => self.build_tavern(&building, build_data, rng),
                BuildingTag::Temple => self.build_temple(&building, build_data, rng),
                BuildingTag::Mine => self.build_mine(&building, build_data, rng),
                BuildingTag::PlayerHouse => self.build_playerhouse(&building, build_data, rng),
                BuildingTag::NPCHouse => self.build_npchouse(&building, build_data, rng),
                BuildingTag::Abandoned => self.build_abandoned(&building, build_data, rng),
                _ => {}
            }
        }
    }

    fn spawn_dockers(&mut self, build_data: &mut BuilderMap, rng: &mut RandomNumberGenerator) {
        for (idx, tt) in build_data.map.tiles.iter().enumerate() {
            if *tt == TileType::Bridge && rng.roll_dice(1, 20) == 1 {
                let roll = rng.roll_dice(1, 2);
                match roll {
                    1 => build_data.spawn_list.push((idx, "npc_fisher".to_string())),
                    _ => build_data.spawn_list.push((idx, "npc_dockworker".to_string())),
                }
            }
        }
    }

    fn spawn_townsfolk(
        &mut self,
        build_data: &mut BuilderMap,
        rng: &mut RandomNumberGenerator,
        available_building_tiles: &mut HashSet<usize>
    ) {
        for idx in available_building_tiles.iter() {
            if rng.roll_dice(1, 40) == 1 {
                let roll = rng.roll_dice(1, 7);
                match roll {
                    1 => build_data.spawn_list.push((*idx, "npc_fisher".to_string())),
                    2 => build_data.spawn_list.push((*idx, "npc_dockworker".to_string())),
                    3 => build_data.spawn_list.push((*idx, "npc_drunk".to_string())),
                    4 => build_data.spawn_list.push((*idx, "npc_townsperson".to_string())),
                    5 => build_data.spawn_list.push((*idx, "npc_guard".to_string())),
                    6 => {
                        let animal_roll = rng.roll_dice(1, 3);
                        match animal_roll {
                            1 => build_data.spawn_list.push((*idx, "chicken_little".to_string())),
                            2 => build_data.spawn_list.push((*idx, "chicken".to_string())),
                            _ => build_data.spawn_list.push((*idx, "dog_little".to_string())),
                        }
                    }
                    _ => {
                        let prop_roll = rng.roll_dice(1, 3);
                        match prop_roll {
                            1 => build_data.spawn_list.push((*idx, "prop_hay".to_string())),
                            2 => build_data.spawn_list.push((*idx, "prop_statue".to_string())),
                            _ => {}
                        }
                    }
                }
            }
        }
    }

    fn random_building_spawn(
        &mut self,
        building: &(i32, i32, i32, i32),
        build_data: &mut BuilderMap,
        rng: &mut RandomNumberGenerator,
        to_place: &mut Vec<&str>,
        avoid_tile: usize
    ) {
        for y in building.1..building.1 + building.3 {
            for x in building.0..building.0 + building.2 {
                let idx = build_data.map.xy_idx(x, y);
                if
                    build_data.map.tiles[idx] == TileType::WoodFloor &&
                    idx != avoid_tile &&
                    rng.roll_dice(1, 3) == 1 &&
                    !to_place.is_empty()
                {
                    let entity_tag = to_place[0];
                    to_place.remove(0);
                    build_data.spawn_list.push((idx, entity_tag.to_string()));
                }
            }
        }
    }

    fn build_tavern(
        &mut self,
        building: &(i32, i32, i32, i32),
        build_data: &mut BuilderMap,
        rng: &mut RandomNumberGenerator
    ) {
        let player_idx = build_data.map.xy_idx(
            building.0 + building.2 / 2,
            building.1 + building.3 / 2
        );

        // Place other items
        let mut to_place: Vec<&str> = vec![
            "npc_barkeep",
            "npc_townsperson",
            "npc_drunk",
            "npc_drunk",
            "prop_keg",
            "prop_table",
            "prop_table",
            "prop_chair",
            "prop_chair"
        ];
        self.random_building_spawn(building, build_data, rng, &mut to_place, player_idx);
    }

    fn build_temple(
        &mut self,
        building: &(i32, i32, i32, i32),
        build_data: &mut BuilderMap,
        rng: &mut RandomNumberGenerator
    ) {
        let mut to_place: Vec<&str> = vec![
            "npc_priest",
            "prop_altar",
            "prop_chair",
            "prop_chair",
            "prop_chair",
            "prop_table",
            "prop_table",
            "prop_candle",
            "prop_candle"
        ];
        self.random_building_spawn(building, build_data, rng, &mut to_place, 0)
    }

    fn build_mine(
        &mut self,
        building: &(i32, i32, i32, i32),
        build_data: &mut BuilderMap,
        rng: &mut RandomNumberGenerator
    ) {
        // Place exit
        let exit_idx = build_data.map.xy_idx(
            building.0 + building.2 / 2,
            building.1 + building.3 / 2
        );
        build_data.map.tiles[exit_idx] = TileType::DownStair;
        build_data.spawn_list.push((exit_idx, "trapdoor".to_string()));
        let mut to_place: Vec<&str> = vec!["npc_miner", "npc_miner", "npc_guard", "prop_chair"];
        self.random_building_spawn(building, build_data, rng, &mut to_place, exit_idx)
    }

    fn build_playerhouse(
        &mut self,
        building: &(i32, i32, i32, i32),
        build_data: &mut BuilderMap,
        rng: &mut RandomNumberGenerator
    ) {
        let mut to_place: Vec<&str> = vec![
            "prop_bed",
            "prop_table",
            "dog_little",
            "prop_chair",
            "prop_chair"
        ];
        self.random_building_spawn(building, build_data, rng, &mut to_place, 0);
    }

    fn build_npchouse(
        &mut self,
        building: &(i32, i32, i32, i32),
        build_data: &mut BuilderMap,
        rng: &mut RandomNumberGenerator
    ) {
        let mut to_place: Vec<&str> = vec![
            "npc_townsperson",
            "prop_bed",
            "prop_table",
            "prop_chair"
        ];
        self.random_building_spawn(building, build_data, rng, &mut to_place, 0);
    }

    fn build_abandoned(
        &mut self,
        building: &(i32, i32, i32, i32),
        build_data: &mut BuilderMap,
        rng: &mut RandomNumberGenerator
    ) {
        let mut to_place: Vec<&str> = vec!["rat", "rat", "rat", "prop_table", "prop_chair"];
        self.random_building_spawn(building, build_data, rng, &mut to_place, 0);
    }

    fn grass_layer(&mut self, build_data: &mut BuilderMap) {
        // Grass everywhere
        for t in build_data.map.tiles.iter_mut() {
            *t = TileType::Grass;
        }
        build_data.take_snapshot();
    }

    fn water_and_piers(
        &mut self,
        rng: &mut RandomNumberGenerator,
        build_data: &mut BuilderMap
    ) -> Vec<usize> {
        let mut n = (rng.roll_dice(1, 65535) as f32) / 65535f32;
        let mut water_width: Vec<i32> = Vec::new();
        let variance = 5;
        let minimum_width = variance + 5;
        let shallow_width = 6;
        let sand_width = shallow_width + 4;

        for y in 0..build_data.height {
            let n_water =
                ((f32::sin(n) * (variance as f32)) as i32) + minimum_width + rng.roll_dice(1, 2);
            water_width.push(n_water);
            n += 0.1;
            for x in 0..n_water {
                let idx = build_data.map.xy_idx(x, y);
                build_data.map.tiles[idx] = TileType::DeepWater;
            }
            for x in n_water..n_water + shallow_width {
                let idx = build_data.map.xy_idx(x, y);
                build_data.map.tiles[idx] = TileType::ShallowWater;
            }
            for x in n_water + shallow_width..n_water + sand_width {
                let idx = build_data.map.xy_idx(x, y);
                build_data.map.tiles[idx] = TileType::Sand;
            }
        }
        build_data.take_snapshot();

        // Add piers
        let mut placed_piers: Vec<i32> = Vec::new();
        let pier_width = 4;
        let mut pier_idxs: Vec<usize> = Vec::new();

        for _i in 0..rng.roll_dice(1, 3) + 2 {
            let mut y;
            loop {
                y = rng.roll_dice(1, build_data.height - 3) - 1;
                if !(placed_piers.contains(&y) || placed_piers.contains(&(y + pier_width))) {
                    break;
                }
            }

            for i in 0..=pier_width {
                placed_piers.push(y + i);
            }

            let start_roll = rng.roll_dice(1, 4);
            let largest_water_width;
            if water_width[y as usize] > water_width[(y as usize) + 1] {
                largest_water_width = water_width[y as usize];
            } else {
                largest_water_width = water_width[(y as usize) + 1];
            }

            // Make pier length
            for x in 2 + start_roll..largest_water_width + sand_width {
                let idx = build_data.map.xy_idx(x, y);
                build_data.map.tiles[idx] = TileType::Fence;
                let idx = build_data.map.xy_idx(x, y + 1);
                build_data.map.tiles[idx] = TileType::Bridge;
                let idx = build_data.map.xy_idx(x, y + 2);
                build_data.map.tiles[idx] = TileType::Bridge;
                let idx = build_data.map.xy_idx(x, y + 3);
                build_data.map.tiles[idx] = TileType::Fence;
            }

            // Set end of pier to fences
            for y in y + 1..y + pier_width - 1 {
                let idx = build_data.map.xy_idx(2 + start_roll, y);
                build_data.map.tiles[idx] = TileType::Fence;
            }
            build_data.take_snapshot();

            pier_idxs.push(build_data.map.xy_idx(largest_water_width + sand_width, y));
            pier_idxs.push(build_data.map.xy_idx(largest_water_width + sand_width, y + 1));
            pier_idxs.push(build_data.map.xy_idx(largest_water_width + sand_width, y + 2));
            pier_idxs.push(build_data.map.xy_idx(largest_water_width + sand_width, y + 3));
        }

        return pier_idxs;
    }

    fn town_walls(
        &mut self,
        rng: &mut RandomNumberGenerator,
        build_data: &mut BuilderMap
    ) -> (HashSet<usize>, i32) {
        let mut available_building_tiles: HashSet<usize> = HashSet::new();

        const BORDER: i32 = 4;
        const OFFSET_FROM_LEFT: i32 = 25 + BORDER;
        const PATH_OFFSET_FROM_CENTRE: i32 = 10;
        const HALF_PATH_THICKNESS: i32 = 3;

        let wall_gap_y =
            build_data.height / 2 +
            rng.roll_dice(1, PATH_OFFSET_FROM_CENTRE * 2) -
            1 -
            PATH_OFFSET_FROM_CENTRE;

        for y in BORDER..build_data.height - BORDER {
            if !(y > wall_gap_y - HALF_PATH_THICKNESS && y < wall_gap_y + HALF_PATH_THICKNESS) {
                let idx = build_data.map.xy_idx(OFFSET_FROM_LEFT, y);
                build_data.map.tiles[idx] = TileType::Wall;

                let idx_right = build_data.map.xy_idx(build_data.width - BORDER, y);
                build_data.map.tiles[idx_right] = TileType::Wall;

                for x in OFFSET_FROM_LEFT + 1..build_data.width - BORDER {
                    let gravel_idx = build_data.map.xy_idx(x, y);
                    let roll = rng.roll_dice(1, 6);
                    match roll {
                        1 => {
                            build_data.map.tiles[gravel_idx] = TileType::Foliage;
                        }
                        2 => {
                            build_data.map.tiles[gravel_idx] = TileType::HeavyFoliage;
                        }
                        _ => {}
                    }
                    if
                        y > BORDER + 1 &&
                        y < build_data.height - BORDER - 1 &&
                        x > OFFSET_FROM_LEFT + 2 &&
                        x < build_data.width - BORDER - 1
                    {
                        available_building_tiles.insert(gravel_idx);
                    }
                }
            } else {
                for x in OFFSET_FROM_LEFT - 3..build_data.width {
                    let road_idx = build_data.map.xy_idx(x, y);
                    build_data.map.tiles[road_idx] = TileType::Road;
                }
            }
        }
        build_data.take_snapshot();

        for x in OFFSET_FROM_LEFT..build_data.width - BORDER + 1 {
            let idx_top = build_data.map.xy_idx(x, BORDER - 1);
            build_data.map.tiles[idx_top] = TileType::Wall;
            let idx_bottom = build_data.map.xy_idx(x, build_data.height - BORDER);
            build_data.map.tiles[idx_bottom] = TileType::Wall;
        }
        build_data.take_snapshot();

        (available_building_tiles, wall_gap_y)
    }

    fn buildings(
        &mut self,
        rng: &mut RandomNumberGenerator,
        build_data: &mut BuilderMap,
        available_building_tiles: &mut HashSet<usize>
    ) -> Vec<(i32, i32, i32, i32)> {
        let mut buildings: Vec<(i32, i32, i32, i32)> = Vec::new();
        let mut n_buildings = 0;

        const BORDER: i32 = 2;
        const REQUIRED_BUILDINGS: i32 = 8;
        const OFFSET_FROM_LEFT: i32 = 25;
        const MIN_BUILDING_SIZE: i32 = 6;
        const MAX_BUILDING_SIZE: i32 = 10;

        while n_buildings < REQUIRED_BUILDINGS {
            let bx =
                rng.roll_dice(1, build_data.map.width - OFFSET_FROM_LEFT - BORDER) +
                OFFSET_FROM_LEFT;
            let by = rng.roll_dice(1, build_data.map.height) - BORDER;
            let bw = rng.roll_dice(1, MAX_BUILDING_SIZE - MIN_BUILDING_SIZE) + MIN_BUILDING_SIZE;
            let bh = rng.roll_dice(1, MAX_BUILDING_SIZE - MIN_BUILDING_SIZE) + MIN_BUILDING_SIZE;
            let mut possible = true;
            for y in by..by + bh {
                for x in bx..bx + bw {
                    if x < 0 || x > build_data.width - 1 || y < 0 || y > build_data.height - 1 {
                        possible = false;
                    } else {
                        let idx = build_data.map.xy_idx(x, y);
                        if !available_building_tiles.contains(&idx) {
                            possible = false;
                        }
                    }
                }
            }
            if possible {
                n_buildings += 1;
                buildings.push((bx, by, bw, bh));
                for y in by..by + bh {
                    for x in bx..bx + bw {
                        let idx = build_data.map.xy_idx(x, y);
                        build_data.map.tiles[idx] = TileType::WoodFloor;
                        available_building_tiles.remove(&idx);
                        available_building_tiles.remove(&(idx + 1));
                        available_building_tiles.remove(&(idx + (build_data.width as usize)));
                        available_building_tiles.remove(&(idx - 1));
                        available_building_tiles.remove(&(idx - (build_data.width as usize)));
                    }
                }
                build_data.take_snapshot();
            }
        }

        // Outlines
        let mut mapclone = build_data.map.clone();
        for y in BORDER..build_data.height - BORDER {
            for x in OFFSET_FROM_LEFT + BORDER..build_data.width - BORDER {
                let idx = build_data.map.xy_idx(x, y);
                if build_data.map.tiles[idx] == TileType::WoodFloor {
                    let mut neighbours = 0;
                    if build_data.map.tiles[idx - 1] != TileType::WoodFloor {
                        neighbours += 1;
                    }
                    if build_data.map.tiles[idx + 1] != TileType::WoodFloor {
                        neighbours += 1;
                    }
                    if
                        build_data.map.tiles[idx - (build_data.width as usize)] !=
                        TileType::WoodFloor
                    {
                        neighbours += 1;
                    }
                    if
                        build_data.map.tiles[idx + (build_data.width as usize)] !=
                        TileType::WoodFloor
                    {
                        neighbours += 1;
                    }
                    if neighbours > 0 {
                        mapclone.tiles[idx] = TileType::Wall;
                    }
                }
            }
        }
        build_data.map = mapclone;
        build_data.take_snapshot();

        buildings
    }

    fn add_doors(
        &mut self,
        rng: &mut RandomNumberGenerator,
        build_data: &mut BuilderMap,
        buildings: &mut Vec<(i32, i32, i32, i32)>,
        wall_gap_y: i32
    ) -> Vec<usize> {
        let mut doors = Vec::new();
        for building in buildings.iter() {
            let door_x = building.0 + 1 + rng.roll_dice(1, building.2 - 3);
            let cy = building.1 + building.3 / 2;
            let idx = if cy > wall_gap_y {
                // Door on north wall
                build_data.map.xy_idx(door_x, building.1)
            } else {
                build_data.map.xy_idx(door_x, building.1 + building.3 - 1)
            };
            build_data.map.tiles[idx] = TileType::Floor;
            build_data.spawn_list.push((idx, "door".to_string()));
            doors.push(idx);
        }
        build_data.take_snapshot();
        doors
    }

    fn path_from_tiles_to_nearest_tiletype(
        &mut self,
        build_data: &mut BuilderMap,
        tiles: &[usize],
        tiletype: TileType,
        new_road_tiletype: TileType,
        include_new_tiles: bool
    ) {
        let mut roads = self.find_tiletype(build_data, tiletype);

        build_data.map.populate_blocked();
        for tile_idx in tiles.iter() {
            let mut nearest_tiletype: Vec<(usize, f32)> = Vec::new();
            let tile_pt = Point::new(
                (*tile_idx as i32) % (build_data.map.width as i32),
                (*tile_idx as i32) / (build_data.map.width as i32)
            );
            for r in roads.iter() {
                nearest_tiletype.push((
                    *r,
                    DistanceAlg::Manhattan.distance2d(
                        tile_pt,
                        Point::new(
                            (*r as i32) % build_data.map.width,
                            (*r as i32) / build_data.map.width
                        )
                    ),
                ));
            }
            nearest_tiletype.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap());

            let destination = nearest_tiletype[0].0;
            let path = a_star_search(*tile_idx, destination, &mut build_data.map);
            if path.success {
                for step in path.steps.iter() {
                    let idx = *step as usize;
                    build_data.map.tiles[idx] = new_road_tiletype;
                    if include_new_tiles {
                        roads.push(idx);
                    }
                }
            }
            build_data.take_snapshot();
        }
    }

    fn find_tiletype(&mut self, build_data: &mut BuilderMap, tile: TileType) -> Vec<usize> {
        let mut found_tiles = Vec::new();
        for y in 0..build_data.height {
            for x in 0..build_data.width {
                let idx = build_data.map.xy_idx(x, y);
                if build_data.map.tiles[idx] == tile {
                    found_tiles.push(idx);
                }
            }
        }

        found_tiles
    }
}
