#include "world.h"
#include "core/character.h"
#include "core/renderer.h"
#include "core/roguedefs.h"
#include <SDL2/SDL_log.h>
#include <algorithm>
#include <cassert>
#include <cstdlib>
#include <memory>

namespace rogue {
void Room::draw(Tile world_pivot) {
	for(Tile local_tile{0, 0}; local_tile.y < this->chunk_size;) {
		if(this->tile_is_wall({local_tile.x, local_tile.y})) {
			Render::draw({
				world_pivot.x + local_tile.x,
				world_pivot.y + local_tile.y
			}, 1);
		}
		if((++local_tile.x %= this->chunk_size) == 0) {
			++local_tile.y;
		}
	}
}

bool Room::tile_is_wall(Tile local_tile) const {
	bool const is_outside_room{
		local_tile.x <= this->rect.x
		|| local_tile.y <= this->rect.y
		|| local_tile.x >= this->rect.x + this->rect.w-1
		|| local_tile.y >= this->rect.y + this->rect.h-1};
	int chunk_size2 = chunk_size/2;
	bool const is_outside_hallways{
		((this->hallway_paths & NORTH) == 0 || local_tile.x != chunk_size / 2 || local_tile.y > chunk_size2)
		&& ((this->hallway_paths & SOUTH) == 0 || local_tile.x != chunk_size / 2 || local_tile.y < chunk_size2)
		&& ((this->hallway_paths & WEST) == 0 || local_tile.y != chunk_size / 2 || local_tile.x > chunk_size2)
		&& ((this->hallway_paths & EAST) == 0 || local_tile.y != chunk_size / 2 || local_tile.x < chunk_size2)
	};
	return is_outside_room && is_outside_hallways;
}

World::World()
: direction_pressed{{
		SDL_SCANCODE_H, SDL_SCANCODE_J, SDL_SCANCODE_K, SDL_SCANCODE_L,
		SDL_SCANCODE_LEFT, SDL_SCANCODE_RIGHT, SDL_SCANCODE_UP, SDL_SCANCODE_DOWN
	},
	std::bind(&World::on_input, this, std::placeholders::_1)
} {}

void World::act() {
	player.act();
	for(Character &character : this->characters) {
		character.act();
	}
}

void World::render() {
	Render::clear(this->player.location, 15);
	for(size_t i{0}; i < this->rooms.size(); ++i) {
		this->rooms[i].draw({
			.x = int(i % this->shear * this->chunk_size),
			.y = int(i / this->shear * this->chunk_size)
		});
	}
	for(Character &character : this->characters) {
		character.draw();
	}
	player.draw();
}

Room &World::get_room(Chunk chunk) {
	return rooms[chunk.x + chunk.y * this->shear];
}

Character *World::get_player() {
	return &this->player;
}

Chunk World::get_chunk(Tile tile) {
	return {tile.x / this->chunk_size, tile.y / this->chunk_size};
}

TileData World::query_tile(Tile tile) {
	Room &room{this->get_room({tile.x / this->chunk_size, tile.y / this->chunk_size})};
	TileData out{
		.character = nullptr,
		.is_wall = room.tile_is_wall({tile.x % this->chunk_size, tile.y % this->chunk_size})
	};
	if(player.location == tile) {
		out.character = &player;
		return out;
	}
	for(Character &character : this->characters) {
		if(character.location == tile && character.health > 0) {
			out.character = &character;
			return out;
		}
	}
	return out;
}

void World::on_input(bool pressed) {
	if(!pressed) {
		this->act();
	}
}

WorldGenerator &WorldGenerator::with_chunk_size(unsigned side_length) {
	assert(this->chunk_side_length == 0);
	assert(side_length > 0);
	this->chunk_side_length = side_length;
	return *this;
}

WorldGenerator &WorldGenerator::with_world_size(unsigned side_length) {
	assert(this->world_side_length == 0);
	assert(side_length > 0);
	this->world_side_length = side_length;
	return *this;
}

WorldGenerator &WorldGenerator::with_room_size(unsigned min_side_length, unsigned max_side_length) {
	assert(chunk_side_length > 0 && "Chunk size needs to be initialized before room size");
	assert(min_side_length < max_side_length);
	assert(min_side_length > 0);
	this->min_room_size = min_side_length;
	this->max_room_size = max_side_length;
	return *this;
}

WorldGenerator &WorldGenerator::with_player(Character character) {
	this->player.emplace(character);
	return *this;
}

WorldGenerator &WorldGenerator::with_enemy(std::function<Character(Tile)> spawner, int inv_frequency) {
	this->enemy_spawners.push_back({spawner, inv_frequency});
	return *this;
}

WorldGenerator &WorldGenerator::with_connecting_chance(unsigned num) {
	this->connecting_chance = num;
	return *this;
}

std::unique_ptr<World> WorldGenerator::generate() {
	assert(this->chunk_side_length > 0 && "Chunk size is required to generate world");
	assert(this->min_room_size > 0 && "Room size is required to generate world");
	assert(this->min_room_size < this->max_room_size && "Room size is required to generate world");
	assert(this->world_side_length > 0 && "World size is required to generate world");
	assert(this->player.has_value() && "World cannot be generated without a player"); 

	std::unique_ptr<World> world{new World()};
	Chunk start_room{this->world_side_length / 2, this->world_side_length / 2};
	this->setup_player(world, start_room);
	this->generate_world(world, start_room);
	return world;
}

void WorldGenerator::generate_world(std::unique_ptr<World> &world, Chunk start) {
	world->chunk_size = this->chunk_side_length;
	world->shear = this->world_side_length;
	world->rooms.resize(this->world_side_length * this->world_side_length);
	for(Room &room : world->rooms) {
		room = Room{
			.hallway_paths = Directions(0x0),
			.chunk_size = this->chunk_side_length,
			.rect = {
				0,
				0,
				this->chunk_side_length,
				this->chunk_side_length
			}
		};
	}
	world->get_room(start).rect = SDL_Rect {
		.x = this->chunk_side_length/2-2,
		.y = this->chunk_side_length/2-2,
		.w = 5,
		.h = 5
	};
	world->get_room(start).initialized = true;
	this->generate_room(world, start);
}

void WorldGenerator::generate_room(std::unique_ptr<World> &world, Chunk const &last) {
	Room &last_room{world->get_room(last)};
	last_room.initialized = true;
	// generate a room that encloses center tile of this chunk
	last_room.rect.w = WRAP(std::rand(), this->min_room_size, this->max_room_size);
	last_room.rect.x = WRAP(std::rand(),
		std::max(this->chunk_side_length/2 - last_room.rect.w + 2, 0),
		1 + std::min(this->chunk_side_length - last_room.rect.w - 1, this->chunk_side_length/2));
	last_room.rect.h = WRAP(std::rand(), this->min_room_size, this->max_room_size);
	last_room.rect.y = WRAP(std::rand(),
		std::max(this->chunk_side_length/2 - last_room.rect.h + 2, 0),
		1 + std::min(this->chunk_side_length - last_room.rect.h - 1, this->chunk_side_length/2));
	const Chunk options[]{
		{last.x + 1, last.y},
		{last.x - 1, last.y},
		{last.x, last.y + 1},
		{last.x, last.y - 1}
	};
	this->add_enemies(world, last, last_room);
	static const Directions directions[]{EAST, WEST, SOUTH, NORTH};
	static const Directions inv_directions[] {WEST, EAST, NORTH, SOUTH};
	int const rand_offset{std::rand() % 4};
	for(size_t i{0}; i < 4; ++i) {
		size_t const true_offset{(i + rand_offset) % 4};
		Chunk const &next{options[true_offset]};
		if(next.x < 0 || next.x >= this->world_side_length
		|| next.y < 0 || next.y >= this->world_side_length) {
			continue;
		}
		Room &option{world->get_room(next)};
		if(this->connecting_chance > 0 && !option.initialized || WRAP(std::rand(), 0, this->connecting_chance) == 0) {
			// register hallway with the last chunk
			last_room.hallway_paths |= directions[true_offset];
			// add returning hallway from the new chunk
			option.hallway_paths |= inv_directions[true_offset];
		}
		if(option.initialized) {
			continue;
		}
		// generate surrounding rooms
		this->generate_room(world, next);
	}
}

void WorldGenerator::add_enemies(std::unique_ptr<World> &world, Chunk const &chunk, Room &room) {
	for(std::pair<std::function<Character(Tile)>, int> const &pair : this->enemy_spawners) {
		if(WRAP(std::rand(), 0, pair.second) == 0) {
			Tile offset{
				WRAP(std::rand(), 1, room.rect.w - 2) + (chunk.x * this->chunk_side_length),
				WRAP(std::rand(), 1, room.rect.h - 2) + (chunk.y * this->chunk_side_length)
			};
			Character character{pair.first(offset)};
			character.world = world.get();
			world->characters.push_back(character);
		}
	}
}

void WorldGenerator::setup_player(std::unique_ptr<World> &world, Chunk spawn_chunk) {
	world->player = this->player.value();
	world->player.world = world.get();
	world->player.location = {
		(spawn_chunk.x * this->chunk_side_length) + (this->chunk_side_length / 2),
		(spawn_chunk.y * this->chunk_side_length) + (this->chunk_side_length / 2),
	};
}
}
