#include <world.hpp>

// standard library headers
#include <algorithm>
#include <fstream>
#include <memory>
#include <sstream>

#include <chrono>
using namespace std::literals::chrono_literals;

// local library headers
#include <tinyxml2.h>
using namespace tinyxml2;

// game headers
#include <common.hpp>
#include <components.hpp>
#include <debug.hpp>
#include <engine.hpp>
#include <error.hpp>
#include <fileio.hpp>
#include <gametime.hpp>
#include <player.hpp>
#include <particle.hpp>
#include <render.hpp>
#include <ui.hpp>
#include <vector3.hpp>
#include <weather.hpp>

WorldData2               WorldSystem::world;
Mix_Music*               WorldSystem::bgmObj;
std::string              WorldSystem::bgmCurrent;
std::vector<std::string> WorldSystem::bgFiles;
TextureIterator          WorldSystem::bgTex;
XMLDocument              WorldSystem::xmlDoc;
std::string              WorldSystem::currentXMLFile;
std::thread              WorldSystem::thAmbient;
std::vector<vec2>        WorldSystem::stars;

extern std::string xmlFolder;

// wait
static bool waitToSwap = false;

// externally referenced in main.cpp
int worldShade = 0;

// ground-generating constants
constexpr const float GROUND_HEIGHT_INITIAL =  80.0f;
constexpr const float GROUND_HEIGHT_MINIMUM =  60.0f;
constexpr const float GROUND_HEIGHT_MAXIMUM = 110.0f;
constexpr const float GROUND_HILLINESS      =  10.0f;

// defines grass height in HLINEs
const unsigned int GRASS_HEIGHT = HLINES(4);

// the path of the currently loaded XML file, externally referenced in places
static std::string currentXML;

// pathnames of images for world themes
//using StyleList = std::string[8];
constexpr const char* bgPaths[1][8] = {
	{ // Forest
		"bg.png", 				// sky/background
		"bgFarMountain.png",	// layer 1 (furthest)
		"forestTileFar.png",	// layer 2
		"forestTileBack.png",	// layer 3
		"forestTileMid.png",	// layer 4
		"forestTileFront.png",	// layer 5 (closest)
		"dirt.png",				// ground texture
		"grass.png"				// grass (ground-top) texture
	}
};

// pathnames of structure textures
constexpr const char* buildPaths[] = {
    "townhall.png",
	"house1.png",
    "house2.png",
    "house1.png",
    "house1.png",
    "fountain1.png",
    "lampPost1.png",
	"brazzier.png"
};

/* ----------------------------------------------------------------------------
** Functions section
** --------------------------------------------------------------------------*/

void WorldSystem::generate(int width)
{
	float geninc = 0;

    // allocate space for world
    world.data = std::vector<WorldData> (width + GROUND_HILLINESS);

    // prepare for generation
    world.data[0].groundHeight = GROUND_HEIGHT_INITIAL;
    auto wditer = std::begin(world.data) + GROUND_HILLINESS;

	if (world.indoor) {
		std::fill(world.data.begin(), world.data.end(), WorldData {true, {0, 0}, GROUND_HEIGHT_MINIMUM + 5, 4});
	} else {
	    // give every GROUND_HILLINESSth entry a groundHeight value
	    for (; wditer < std::end(world.data); wditer += GROUND_HILLINESS)
	        wditer[-static_cast<int>(GROUND_HILLINESS)].groundHeight = wditer[0].groundHeight + (randGet() % 8 - 4);

    	// create slopes from the points that were just defined, populate the rest of the WorldData structure
	    for (wditer = std::begin(world.data) + 1; wditer < std::end(world.data); wditer++){
	        auto w = &*(wditer);

    	    if (w->groundHeight != 0)
	            geninc = (w[static_cast<int>(GROUND_HILLINESS)].groundHeight - w->groundHeight) / GROUND_HILLINESS;

    	    w->groundHeight   = std::clamp(w[-1].groundHeight + geninc, GROUND_HEIGHT_MINIMUM, GROUND_HEIGHT_MAXIMUM);
	        w->groundColor    = randGet() % 32 / 8;
	        w->grassUnpressed = true;
	        w->grassHeight[0] = (randGet() % 16) / 3 + HLINES(2);
	        w->grassHeight[1] = (randGet() % 16) / 3 + HLINES(2);
	    }
	}

    // define x-coordinate of world's leftmost 'line'
    world.startX = HLINES(width * -0.5);

	// gen. star coordinates
	if (stars.empty()) {
		stars.resize(game::SCREEN_WIDTH / 30);
		for (auto& s : stars) {
			s.x = world.startX + (randGet() % (int)HLINES(width));
			s.y = game::SCREEN_HEIGHT - (randGet() % (int)HLINES(game::SCREEN_HEIGHT / 1.3f));
		}
	}
}

float WorldSystem::isAboveGround(const vec2& p) 
{
	int line = std::clamp(static_cast<int>((p.x - world.startX) / game::HLINE),
		0, static_cast<int>(world.data.size()));

	const auto& gh = world.data[line].groundHeight;
	return p.y >= gh ? 0 : gh;
}

static Color ambient;

bool WorldSystem::save(void)
{	
	if (world.indoor)
		return false;

	std::ofstream save (xmlFolder + currentXMLFile + ".dat");

	// signature?
	save << "831998 ";

	game::entities.each<Position>([&](entityx::Entity entity, Position& pos) {
		// save position
		save << "p " << pos.x << ' ' << pos.y << ' ';

		// save dialog, if exists
		if (entity.has_component<Dialog>())
			save << "d " << entity.component<Dialog>()->index << ' ';

		if (entity.has_component<Health>())
			save << "h " << entity.component<Health>()->health << ' ';
	}, true);

	save.close();
	return true;
}

static std::vector<entityx::Entity::Id> savedEntities;

void WorldSystem::fight(entityx::Entity entity)
{
	std::string exit = currentXMLFile;

	savedEntities.emplace_back(entity.id());
	load(entity.component<Aggro>()->arena);
	savedEntities.clear();

	entity.component<Health>()->health = entity.component<Health>()->maxHealth;
	entity.remove<Aggro>();

	auto door = game::entities.create();
	door.assign<Position>(0, 100);
	door.assign<Grounded>();
	door.assign<Visible>(-5);
	door.assign<Portal>(exit);

	auto sprite = door.assign<Sprite>();
	auto dtex = game::engine.getSystem<RenderSystem>()->loadTexture("assets/style/classic/door.png");
	sprite->addSpriteSegment(SpriteData(dtex), 0);

	auto dim = HLINES(sprite->getSpriteSize());
	door.assign<Solid>(dim.x, dim.y);
}

void WorldSystem::load(const std::string& file)
{
	auto& render = *game::engine.getSystem<RenderSystem>();

	entityx::Entity entity;

	// check for empty file name
	if (file.empty())
		return;

	// save the current world's data
	if (!currentXMLFile.empty())
		save();

	// load file data to string
	auto xmlPath = xmlFolder + file;
	auto xmlRaw = readFile(xmlPath);

	// let tinyxml parse the file
	UserAssert(xmlDoc.Parse(xmlRaw.data()) == XML_NO_ERROR,
		"XML Error: Failed to parse file (not your fault though..?)");

	// include headers
	std::vector<std::string> toAdd;
	auto ixml = xmlDoc.FirstChildElement("include");
	while (ixml != nullptr) {
		auto file = ixml->Attribute("file");
		UserAssert(file != nullptr, "XML Error: <include> tag file not given");

		if (file != nullptr) {
			DEBUG_printf("Including file: %s\n", file);
			toAdd.emplace_back(xmlFolder + file);
			//xmlRaw.append(readFile(xmlFolder + file));
		} else {
			UserError("XML Error: <include> tag file not given");
		}

		ixml = ixml->NextSiblingElement("include");
	}

	for (const auto& f : toAdd)
		xmlRaw.append(readFile(f)); 

	UserAssert(xmlDoc.Parse(xmlRaw.data()) == XML_NO_ERROR,
		"XML Error: failed to append includes");

	// look for an opening world tag
	auto wxml = xmlDoc.FirstChildElement("World");
	if (wxml != nullptr) {
		wxml = wxml->FirstChildElement();
		world.indoor = false;
	} else {
		wxml = xmlDoc.FirstChildElement("IndoorWorld");
		UserAssert(wxml != nullptr, "XML Error: Cannot find a <World> or <IndoorWorld> tag in " + xmlPath);
		wxml = wxml->FirstChildElement();
		world.indoor = true;
		if (world.outdoor.empty()) {
			world.outdoor = currentXMLFile;
			world.outdoorCoords = vec2(0, 100);
		}
	}

	world.toLeft = world.toRight = "";
	currentXMLFile = file;

	//game::entities.reset();
	if (!savedEntities.empty()) {
		savedEntities.emplace_back(game::engine.getSystem<PlayerSystem>()->getId());
		game::entities.each<Position>(
			[&](entityx::Entity entity, Position& p) {
			(void)p;
			if (std::find(savedEntities.begin(), savedEntities.end(), entity.id())
				== savedEntities.end())
				entity.destroy();
		}, true);
	} else {
		game::entities.reset();
		game::engine.getSystem<PlayerSystem>()->create();
	}

	// iterate through tags
	while (wxml != nullptr) {
		std::string tagName = wxml->Name();

		// style tag
		if (tagName == "style") {
			world.styleFolder = wxml->StrAttribute("folder");

			unsigned int styleNo;
			if (wxml->QueryUnsignedAttribute("background", &styleNo) != XML_NO_ERROR)
				UserError("XML Error: No background given in <style> in " + xmlPath);

			world.style = static_cast<WorldBGType>(styleNo);
			world.bgm = wxml->StrAttribute("bgm");

			bgFiles.clear();

			for (const auto& f : bgPaths[styleNo])
				bgFiles.push_back(world.styleFolder + "bg/" + f);

			bgTex = TextureIterator(bgFiles);
		}

        // world generation
        else if (tagName == "generation") {
			generate(wxml->IntAttribute("width"));
		}

		// indoor stuff
		else if (tagName == "house") {
			if (!world.indoor)
				UserError("<house> can only be used inside <IndoorWorld>");

			//world.indoorWidth = wxml->FloatAttribute("width");
			world.indoorTex = render.loadTexture(wxml->StrAttribute("texture")); // TODO winbloze lol
			auto str = wxml->StrAttribute("texture");
			auto tex = render.loadTexture(str);
			world.indoorTex = tex;
		}

		// weather tag
		else if (tagName == "weather") {
			game::engine.getSystem<WeatherSystem>()->setWeather(wxml->GetText());
			//setWeather(wxml->GetText());
		}

		// link tags
		else if (tagName == "link") {
			auto linkTo = wxml->Attribute("left");
			if (linkTo != nullptr) {
				world.toLeft = linkTo;
			} else {
				linkTo = wxml->Attribute("right");
				if (linkTo != nullptr)
					world.toRight = linkTo;
				else
					UserError("<link> doesn't handle left or right... huh");
			}
		}

		// time setting
		else if (tagName == "time") {
            game::time::setTickCount(std::stoi(wxml->GetText()));
        }

		// custom entity tags
		else {
			auto cxml = xmlDoc.FirstChildElement(tagName.c_str());
			if (cxml != nullptr) {
				entity = game::entities.create();
				auto abcd = cxml->FirstChildElement();

				while (abcd) {
					std::string tname = abcd->Name();

					if (tname == "Position")
						entity.assign<Position>(wxml, abcd);
					else if (tname == "Visible")
						entity.assign<Visible>(wxml, abcd);
					else if (tname == "Sprite")
						entity.assign<Sprite>(wxml, abcd);
					else if (tname == "Portal")
						entity.assign<Portal>(wxml, abcd);
					else if (tname == "Solid") {
						auto solid = entity.assign<Solid>(wxml, abcd);
						if (solid->width == 0 && solid->height == 0) {
							auto dim = entity.component<Sprite>()->getSpriteSize();
							entity.remove<Solid>();
							entity.assign<Solid>(dim.x, dim.y);
						}
					} else if (tname == "Direction")
						entity.assign<Direction>(wxml, abcd);
					else if (tname == "Physics")
						entity.assign<Physics>(wxml, abcd);
					else if (tname == "Name")
						entity.assign<Name>(wxml, abcd);
					else if (tname == "Dialog")
						entity.assign<Dialog>(wxml, abcd);
					else if (tname == "Grounded")
						entity.assign<Grounded>(); // no need to pass xmls...
					else if (tname == "Wander")
						entity.assign<Wander>();
					else if (tname == "Hop")
						entity.assign<Hop>();
					else if (tname == "Health")
						entity.assign<Health>(wxml, abcd);
					else if (tname == "Aggro")
						entity.assign<Aggro>(wxml, abcd);
					else if (tname == "Animation")
						entity.assign<Animate>(wxml, abcd);
					else if (tname == "Trigger")
						entity.assign<Trigger>(wxml, abcd);

					abcd = abcd->NextSiblingElement();
				}

			} else {
				UserError("Unknown tag <" + tagName + "> in file " + currentXMLFile);
			}
		}

		wxml = wxml->NextSiblingElement();
	}

	// attempt to load data
	std::ifstream save (xmlFolder + currentXMLFile + ".dat");
	if (save.good()) {
		// check signature
		int signature;
		save >> signature;
		if (signature != 831998)
			UserError("Save file signature is invalid... (delete it)");

		char id;
		save >> id;

		entityx::ComponentHandle<Position> pos;
		for (entityx::Entity entity : game::entities.entities_with_components(pos)) {
			save >> pos->x >> pos->y;
			save >> id;

			while (!save.eof() && id != 'p') {
				switch (id) {
				case 'd':
					save >> entity.component<Dialog>()->index;
					break;
				case 'h':
					save >> entity.component<Health>()->health;
					break;
				}

				save >> id;
			}
		}

		save.close();
	}

	game::events.emit<BGMToggleEvent>();
}

/*
World *
loadWorldFromXMLNoSave(std::string path) {
	XMLDocument *_currentXMLDoc;
	static std::string _currentXML,
	                   _currentXMLRaw;

	XMLElement *wxml;
	XMLElement *vil;

	World *tmp;
	Entity *newEntity;
	bool Indoor;

	const char *ptr;
	std::string name, sptr;

	Village *vptr;
	Structures *s;

	if (vil) {
		vptr = tmp->addVillage(vil->StrAttribute("name"), tmp);
		vil = vil->FirstChildElement();
	}

	while(vil) {
		name = vil->Name();

		 //READS DATA ABOUT STRUCTURE CONTAINED IN VILLAGE

		if (name == "structure") {
			s = new Structures();
			s->createFromXML(vil, tmp);
			tmp->addStructure(s);
		} else if (name == "stall") {
            sptr = vil->StrAttribute("type");

            // handle markets
            if (sptr == "market") {

                // create a structure and a merchant, and pair them
				s = new Structures();
				s->createFromXML(vil, tmp);
				tmp->addStructure(s);
				tmp->addMerchant(0, 100, true);
            }

            // handle traders
            else if (sptr == "trader") {
				s = new Structures();
				s->createFromXML(vil, tmp);
				tmp->addStructure(s);
			}

            // loop through buy/sell/trade tags
            XMLElement *sxml = vil->FirstChildElement();
            std::string tag;

			Merchant *merch = dynamic_cast<Merchant *>(*std::find_if(tmp->entity.rbegin(), tmp->entity.rend(), [&](Entity *e) {
				return (e->type == MERCHT);
			}));

            while (sxml) {
                tag = sxml->Name();

                if (tag == "buy") { //converts price to the currencies determined in items.xml
                    // TODO
                } else if (tag == "sell") { //converts price so the player can sell
                    // TODO
                } else if (tag == "trade") { //doesn't have to convert anything, we just trade multiple items
                	merch->trade.push_back(Trade(sxml->IntAttribute("quantity"),
										   sxml->StrAttribute("item"),
										   sxml->IntAttribute("quantity1"),
										   sxml->StrAttribute("item1")));
				} else if (tag == "text") { //this is what the merchant says
                    XMLElement *txml = sxml->FirstChildElement();
                    std::string textOption;

                    while (txml) {
                        textOption = txml->Name();
                        const char* buf = txml->GetText();

                        if (textOption == "greet") { //when you talk to him
                            merch->text[0] = std::string(buf, strlen(buf));
                            merch->toSay = &merch->text[0];
                        } else if (textOption == "accept") { //when he accepts the trade
                            merch->text[1] = std::string(buf, strlen(buf));
                        } else if (textOption == "deny") { //when you don't have enough money
                            merch->text[2] = std::string(buf, strlen(buf));
                        } else if (textOption == "leave") { //when you leave the merchant
                            merch->text[3] = std::string(buf, strlen(buf));
                        }
                        txml = txml->NextSiblingElement();
                    }
                }

                sxml = sxml->NextSiblingElement();
			}
		}

        float buildx = tmp->getStructurePos(-1).x;

		if (buildx < vptr->start.x)
			vptr->start.x = buildx;

		if (buildx > vptr->end.x)
			vptr->end.x = buildx;

		//go to the next element in the village block
		vil = vil->NextSiblingElement();
	}

	return tmp;
}*/

WorldSystem::WorldSystem(void)
{
	bgmObj = nullptr;
}

WorldSystem::~WorldSystem(void)
{
    // SDL2_mixer's object
	if (bgmObj != nullptr)
		Mix_FreeMusic(bgmObj);
}

void WorldSystem::render(void)
{
	const auto SCREEN_WIDTH = game::SCREEN_WIDTH;
	const auto SCREEN_HEIGHT = game::SCREEN_HEIGHT;

	const vector2<int> backgroundOffset
		(static_cast<int>(SCREEN_WIDTH) / 2, static_cast<int>(SCREEN_HEIGHT) / 2);

	int iStart, iEnd, pOffset;

    // world width in pixels
	int width = HLINES(world.data.size());

	static bool ambientUpdaterStarted = false;
	if (!ambientUpdaterStarted) {
		ambientUpdaterStarted = true;
		thAmbient = std::thread([&](void) {
			const bool &run = game::engine.shouldRun;
			while (run) {
				float v = 75 * sin((game::time::getTickCount() + (DAY_CYCLE / 2)) / (DAY_CYCLE / PI));
				float rg = std::clamp(.5f + (-v / 100.0f), 0.01f, .9f);
				float b  = std::clamp(.5f + (-v / 80.0f), 0.03f, .9f);

				ambient = Color(rg, rg, b, 1.0f);

				std::this_thread::sleep_for(1ms);
			}
		});
		thAmbient.detach();
	}

	// shade value for GLSL
	float shadeAmbient = std::max(0.0f, static_cast<float>(-worldShade) / 50 + 0.5f); // 0 to 1.5f

	if (shadeAmbient > 0.9f)
		shadeAmbient = 1;

	// TODO scroll backdrop
	//GLfloat bgOff = game::time::getTickCount() / static_cast<float>(DAY_CYCLE * 2);
	GLfloat bgOff = -0.5f * cos(PI / DAY_CYCLE * game::time::getTickCount()) + 0.5f;

	GLfloat topS = .125f + bgOff;
	GLfloat bottomS = bgOff;

	if (topS < 0.0f)
		topS += 1.0f;
	if (bottomS < 0.0f)
		bottomS += 1.0f;

	GLfloat scrolling_tex_coord[] = {
		0.0f,  bottomS,
		1.0f,  bottomS,
		1.0f,  bottomS,

		1.0f,  bottomS,
		0.0f,  bottomS,
		0.0f,  bottomS
	};

   	static const vec2 bg_tex_coord[] = {
		vec2(0.0f, 0.0f),
        vec2(1.0f, 0.0f),
        vec2(1.0f, 1.0f),

        vec2(1.0f, 1.0f),
        vec2(0.0f, 1.0f),
        vec2(0.0f, 0.0f)
	};

    GLfloat back_tex_coord[] = {
		offset.x - backgroundOffset.x - 5, offset.y - backgroundOffset.y, 9.9f,
        offset.x + backgroundOffset.x + 5, offset.y - backgroundOffset.y, 9.9f,
        offset.x + backgroundOffset.x + 5, offset.y + backgroundOffset.y, 9.9f,

        offset.x + backgroundOffset.x + 5, offset.y + backgroundOffset.y, 9.9f,
        offset.x - backgroundOffset.x - 5, offset.y + backgroundOffset.y, 9.9f,
        offset.x - backgroundOffset.x - 5, offset.y - backgroundOffset.y, 9.9f
	};

	// rendering!!

    glActiveTexture(GL_TEXTURE0);
	Render::worldShader.use();
    glUniform1i(Render::worldShader.uniform[WU_texture], 0);
	glUniform1f(Render::worldShader.uniform[WU_light_impact], 0.0f);
	glUniform4f(Render::worldShader.uniform[WU_ambient], 1.0, 1.0, 1.0, 1.0);
    Render::worldShader.enable();

	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
    bgTex(0);
	glUniform4f(Render::worldShader.uniform[WU_tex_color], 1.0, 1.0, 1.0, 1.0);

	glVertexAttribPointer(Render::worldShader.coord, 3, GL_FLOAT, GL_FALSE, 0, back_tex_coord);
	glVertexAttribPointer(Render::worldShader.tex  , 2, GL_FLOAT, GL_FALSE, 0, scrolling_tex_coord);
	glDrawArrays(GL_TRIANGLES, 0, 6);

	// no more night bg
	//bgTex++;
	//glUniform4f(Render::worldShader.uniform[WU_tex_color], 1.0, 1.0, 1.0, 1.3 - static_cast<float>(alpha) / 255.0f);
	//makeWorldDrawingSimplerEvenThoughAndyDoesntThinkWeCanMakeItIntoFunctions(0, fron_tex_coord, tex_coord, 6);

	// TODO make stars dynamic (make them particles??)
	static const Texture starTex ("assets/style/classic/bg/star.png"); // TODO why in theme, not just std.?
	const static float stardim = 24;
	GLfloat* star_coord = new GLfloat[stars.size() * 5 * 6 + 1];
    GLfloat* si = &star_coord[0];

	if (worldShade > 0) {

		auto xcoord = offset.x * 0.9f;

		for (auto &s : stars) {
			float data[30] = {
				s.x + xcoord, s.y, 9.7, 0, 0,
				s.x + xcoord + stardim, s.y, 9.7, 1, 0,
				s.x + xcoord + stardim, s.y + stardim, 9.7, 1, 1,
				s.x + xcoord + stardim, s.y + stardim, 9.7, 1, 1,
				s.x + xcoord, s.y + stardim, 9.7, 0, 1,
				s.x + xcoord, s.y, 9.7, 0, 0
			};

			std::memcpy(si, data, sizeof(float) * 30);
			si += 30;
		}
		starTex.use();
		glUniform4f(Render::worldShader.uniform[WU_tex_color], 1.0, 1.0, 1.0, 1.3);

		glVertexAttribPointer(Render::worldShader.coord, 3, GL_FLOAT, GL_FALSE, 5 * sizeof(GLfloat), &star_coord[0]);
		glVertexAttribPointer(Render::worldShader.tex, 2, GL_FLOAT, GL_FALSE, 5 * sizeof(GLfloat), &star_coord[3]);
		glDrawArrays(GL_TRIANGLES, 0, stars.size() * 6);
	}

	Render::worldShader.disable();

	glUniform4f(Render::worldShader.uniform[WU_tex_color], 1.0, 1.0, 1.0, 1.0);
	glUniform4f(Render::worldShader.uniform[WU_ambient], ambient.red, ambient.green, ambient.blue, 1.0);
	Render::worldShader.unuse();

    std::vector<vec3> bg_items;
	std::vector<vec2> bg_tex;

	bgTex++;
	auto mountainDim = bgTex.getTextureDim();
	mountainDim.x = HLINES(mountainDim.x);
	mountainDim.y = HLINES(mountainDim.y);
    auto xcoord = width / 2 * -1 + offset.x * 0.85f;
	for (int i = 0; i <= width / mountainDim.x; i++) {
        bg_items.emplace_back(mountainDim.x * i       + xcoord, GROUND_HEIGHT_MINIMUM, 				 8.0f);
        bg_items.emplace_back(mountainDim.x * (i + 1) + xcoord, GROUND_HEIGHT_MINIMUM, 				 8.0f);
        bg_items.emplace_back(mountainDim.x * (i + 1) + xcoord, GROUND_HEIGHT_MINIMUM + mountainDim.y, 8.0f);

        bg_items.emplace_back(mountainDim.x * (i + 1) + xcoord, GROUND_HEIGHT_MINIMUM + mountainDim.y, 8.0f);
        bg_items.emplace_back(mountainDim.x * i       + xcoord, GROUND_HEIGHT_MINIMUM + mountainDim.y, 8.0f);
        bg_items.emplace_back(mountainDim.x * i       + xcoord, GROUND_HEIGHT_MINIMUM, 				 8.0f);
	}

    for (uint i = 0; i < bg_items.size()/6; i++) {
        for (auto &v : bg_tex_coord)
            bg_tex.push_back(v);
    }

    Render::worldShader.use();
	glUniform1f(Render::worldShader.uniform[WU_light_impact], 0.01);

	Render::worldShader.enable();

	glVertexAttribPointer(Render::worldShader.coord, 3, GL_FLOAT, GL_FALSE, 0, &bg_items[0]);
	glVertexAttribPointer(Render::worldShader.tex  , 2, GL_FLOAT, GL_FALSE, 0, &bg_tex[0]);
	glDrawArrays(GL_TRIANGLES, 0, bg_items.size());

	Render::worldShader.disable();

    Render::worldShader.unuse();
	// draw the remaining layers
	static const float alphas[4] = {
		0.6, 0.4, 0.25, 0.1
	};
	for (int i = 0; i < 4; i++) {
		bgTex++;
		auto xcoord = offset.x * alphas[i];

		bg_items.clear();
		bg_tex.clear();

		vec2 dim = bgTex.getTextureDim() * game::HLINE;

		if (world.indoor && i == 3) {
			world.indoorTex.use();

			const auto& startx = world.startX;
			dim = world.indoorTex.getDim() * game::HLINE;

			bg_items.emplace_back(startx,         GROUND_HEIGHT_MINIMUM,         7 - (i * 0.1f));
	        bg_items.emplace_back(startx + dim.x, GROUND_HEIGHT_MINIMUM,	     7 - (i * 0.1f));
	        bg_items.emplace_back(startx + dim.x, GROUND_HEIGHT_MINIMUM + dim.y, 7 - (i * 0.1f));

	        bg_items.emplace_back(startx + dim.x, GROUND_HEIGHT_MINIMUM + dim.y, 7 - (i * 0.1f));
	        bg_items.emplace_back(startx,         GROUND_HEIGHT_MINIMUM + dim.y, 7 - (i * 0.1f));
	        bg_items.emplace_back(startx,         GROUND_HEIGHT_MINIMUM,         7 - (i * 0.1f));
		} else {
			for (int j = world.startX; j <= -world.startX; j += dim.x) {
	            bg_items.emplace_back(j         + xcoord, GROUND_HEIGHT_MINIMUM,         7 - (i * 0.1f));
	            bg_items.emplace_back(j + dim.x + xcoord, GROUND_HEIGHT_MINIMUM,         7 - (i * 0.1f));
	            bg_items.emplace_back(j + dim.x + xcoord, GROUND_HEIGHT_MINIMUM + dim.y, 7 - (i * 0.1f));

	            bg_items.emplace_back(j + dim.x + xcoord, GROUND_HEIGHT_MINIMUM + dim.y, 7 - (i * 0.1f));
	            bg_items.emplace_back(j         + xcoord, GROUND_HEIGHT_MINIMUM + dim.y, 7 - (i * 0.1f));
	            bg_items.emplace_back(j         + xcoord, GROUND_HEIGHT_MINIMUM,         7 - (i * 0.1f));
			}
		}

   	    for (uint i = 0; i < bg_items.size() / 6; i++) {
			for (auto &v : bg_tex_coord)
				bg_tex.push_back(v);
		}

        Render::worldShader.use();
		glUniform1f(Render::worldShader.uniform[WU_light_impact], 0.075f + (0.2f * i));

	    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
	    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);

		Render::worldShader.enable();

		glVertexAttribPointer(Render::worldShader.coord, 3, GL_FLOAT, GL_FALSE, 0, bg_items.data());
		glVertexAttribPointer(Render::worldShader.tex  , 2, GL_FLOAT, GL_FALSE, 0, &bg_tex[0]);
		glDrawArrays(GL_TRIANGLES, 0, bg_items.size());

		Render::worldShader.disable();
        Render::worldShader.unuse();
	}

    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);

    // get the line that the player is currently standing on
    pOffset = (offset.x - world.startX) / game::HLINE;

    // only draw world within player vision
    iStart = std::clamp(static_cast<int>(pOffset - (SCREEN_WIDTH / 2 / game::HLINE) - GROUND_HILLINESS),
	                    0, static_cast<int>(world.data.size()));
	iEnd = std::clamp(static_cast<int>(pOffset + (SCREEN_WIDTH / 2 / game::HLINE) + 2),
                      0, static_cast<int>(world.data.size() - GROUND_HILLINESS));

    // draw the dirt
	waitToSwap = true;

    bgTex++;

	static std::vector<GLfloat> dirt;
	if (dirt.size() != world.data.size() * 30) {
		dirt.clear();
		dirt.resize(world.data.size() * 30);
	}

	auto push5 = [](GLfloat *&vec, GLfloat *five) {
		for (int i = 0; i < 5; i++)
			*vec++ = *five++;
	};

	GLfloat *dirtp = &dirt[0];
    for (int i = iStart; i < iEnd; i++) {
        if (world.data[i].groundHeight <= 0) { // TODO holes (andy) TODO TODO TODO
            world.data[i].groundHeight = GROUND_HEIGHT_MINIMUM - 1;
            //glColor4ub(0, 0, 0, 255);
        } else {
            //safeSetColorA(150, 150, 150, 255);
        }

        int ty = world.data[i].groundHeight / 64 + world.data[i].groundColor;

		GLfloat five[5] = {
			0, 0, world.startX + HLINES(i), world.data[i].groundHeight - GRASS_HEIGHT, -4
		};

		push5(dirtp, five);
		five[0]++, five[2] += game::HLINE;
		push5(dirtp, five);
		five[1] += ty, five[3] = 0;
		push5(dirtp, five);
		push5(dirtp, five);
		five[0]--, five[2] -= game::HLINE;
		push5(dirtp, five);
		five[1] = 0, five[3] = world.data[i].groundHeight - GRASS_HEIGHT;
		push5(dirtp, five);

        if (world.data[i].groundHeight == GROUND_HEIGHT_MINIMUM - 1)
            world.data[i].groundHeight = 0;
    }

    Render::worldShader.use();
	glUniform1f(Render::worldShader.uniform[WU_light_impact], 0.45f);

    Render::worldShader.enable();

    glVertexAttribPointer(Render::worldShader.coord, 3, GL_FLOAT, GL_FALSE, 5 * sizeof(GLfloat), &dirt[2]);
    glVertexAttribPointer(Render::worldShader.tex, 2, GL_FLOAT, GL_FALSE, 5 * sizeof(GLfloat), &dirt[0]);
    glDrawArrays(GL_TRIANGLES, 0 , dirt.size() / 5);

    Render::worldShader.disable();
	Render::worldShader.unuse();

	if (!world.indoor) {
		bgTex++;
	    //safeSetColorA(255, 255, 255, 255); TODO TODO TODO 

		static std::vector<GLfloat> grass;
		if (grass.size() != world.data.size() * 60) {
			grass.clear();
			grass.resize(world.data.size() * 60);
		}

		GLfloat *grassp = &grass[0];
		for (int i = iStart; i < iEnd; i++) {
        	auto wd = world.data[i];
	        auto gh = wd.grassHeight;

			// flatten the grass if the player is standing on it.
			if (!wd.grassUnpressed) {
				gh[0] /= 4;
				gh[1] /= 4;
			}

			// actually draw the grass.
	        if (wd.groundHeight) {
				float five[5] = {
					0, 1, world.startX + HLINES(i), wd.groundHeight + gh[0], -3
				};

				push5(grassp, five);
				five[0]++, five[1]--, five[2] += HLINES(0.5f);
				push5(grassp, five);
				five[1]++, five[3] = wd.groundHeight - GRASS_HEIGHT;
				push5(grassp, five);
				push5(grassp, five);
				five[0]--, five[2] -= HLINES(0.5f);
				push5(grassp, five);
				five[1]--, five[3] = wd.groundHeight + gh[0];
				push5(grassp, five);
				five[1]++;

				five[2] = world.startX + HLINES(i + 0.5), five[3] = wd.groundHeight + gh[1];

				push5(grassp, five);
				five[0]++, five[1]--, five[2] += HLINES(0.5f) + 1;
				push5(grassp, five);
				five[1]++, five[3] = wd.groundHeight - GRASS_HEIGHT;
				push5(grassp, five);
				push5(grassp, five);
				five[0]--, five[2] -= HLINES(0.5f) + 1;
				push5(grassp, five);
				five[1]--, five[3] = wd.groundHeight + gh[1];
				push5(grassp, five);
	        }
		}

	    Render::worldShader.use();
		glUniform1f(Render::worldShader.uniform[WU_light_impact], 1.0f);

	    Render::worldShader.enable();

	    glVertexAttribPointer(Render::worldShader.coord, 3, GL_FLOAT, GL_FALSE, 5 * sizeof(GLfloat), &grass[2]);
	    glVertexAttribPointer(Render::worldShader.tex, 2, GL_FLOAT, GL_FALSE, 5 * sizeof(GLfloat), &grass[0]);
	    glDrawArrays(GL_TRIANGLES, 0 , grass.size() / 5);

		// the starting pixel of the world
		static const float s = -(static_cast<float>(SCREEN_WIDTH) / 2.0f);
		// the ending pixel of the world
		static const float e = static_cast<float>(SCREEN_WIDTH) / 2.0f;

		static const float sheight = static_cast<float>(SCREEN_HEIGHT);
			

		if (offset.x + world.startX > s) {
			Colors::black.use();
			glUniform1f(Render::worldShader.uniform[WU_light_impact], 0.0f);

			auto off = offset.y - static_cast<float>(SCREEN_HEIGHT) / 2.0f;

			GLfloat blackBarLeft[] = {
				s,            0.0f    + off,    -3.5f, 0.0f, 0.0f,
				world.startX, 0.0f    + off,    -3.5f, 1.0f, 0.0f,
				world.startX, sheight + off, -3.5f, 1.0f, 1.0f,

				world.startX, sheight + off, -3.5f, 1.0f, 1.0f,
    			s,            sheight + off, -3.5f, 0.0f, 1.0f,
				s,            0.0f 	  + off,    -3.5f, 0.0f, 0.0f
			};

	    	glVertexAttribPointer(Render::worldShader.coord, 3, GL_FLOAT, GL_FALSE, sizeof(GLfloat) * 5, &blackBarLeft[0]);
	    	glVertexAttribPointer(Render::worldShader.tex, 2, GL_FLOAT, GL_FALSE, sizeof(GLfloat) * 5, &blackBarLeft[3]);
	    	glDrawArrays(GL_TRIANGLES, 0, 6);
		}

		if (offset.x - world.startX < e) {
			Colors::black.use();
			glUniform1f(Render::worldShader.uniform[WU_light_impact], 0.0f);
		
			auto off = offset.y - static_cast<float>(SCREEN_HEIGHT) / 2.0f;

			GLfloat blackBarRight[] = {
				-(world.startX), 0.0f    + off,    -3.5f, 0.0f, 0.0f,
				e,               0.0f    + off,    -3.5f, 1.0f, 0.0f,
				e,               sheight + off, -3.5f, 1.0f, 1.0f,

				e,               sheight + off, -3.5f, 1.0f, 1.0f,
    			-(world.startX), sheight + off, -3.5f, 0.0f, 1.0f,
				-(world.startX), 0.0f    + off,    -3.5f, 0.0f, 0.0f
			};

	    	glVertexAttribPointer(Render::worldShader.coord, 3, GL_FLOAT, GL_FALSE, sizeof(GLfloat) * 5, &blackBarRight[0]);
	    	glVertexAttribPointer(Render::worldShader.tex, 2, GL_FLOAT, GL_FALSE, sizeof(GLfloat) * 5, &blackBarRight[3]);
	    	glDrawArrays(GL_TRIANGLES, 0, 6);
		}

		Render::worldShader.disable();
		Render::worldShader.unuse();

	} else {
		Render::useShader(&Render::worldShader);
		Render::worldShader.use();
		Colors::red.use();
		vec2 ll = vec2 {world.startX, GROUND_HEIGHT_MINIMUM};
		Render::drawRect(ll, ll + vec2(world.indoorTex.getDim().x, 4), -3);
		Render::worldShader.unuse();
	}

	waitToSwap = false;
}

void WorldSystem::receive(const BGMToggleEvent &bte)
{
	if (bte.world == nullptr || world.bgm != bte.file) {
		if (bgmCurrent == world.bgm)
			return;

		Mix_FadeOutMusic(800);

		if (bgmObj != nullptr)
			Mix_FreeMusic(bgmObj);

		bgmCurrent = world.bgm;
		bgmObj = Mix_LoadMUS(world.bgm.c_str());
		Mix_PlayMusic(bgmObj, -1);
	}
}

void WorldSystem::update(entityx::EntityManager &en, entityx::EventManager &ev, entityx::TimeDelta dt)
{
	(void)en;
	(void)ev;

    // fade in music if not playing
	if (!Mix_PlayingMusic() && bgmObj != nullptr)
		Mix_FadeInMusic(bgmObj, -1, 2000);

	// run detect stuff
	detect(dt);
}

void WorldSystem::detect(entityx::TimeDelta dt)
{
	game::entities.each<Health>(
		[](entityx::Entity e, Health& h) {
		if (h.health <= 0) {
			e.kill();
			//e.destroy();
		}
	});

	game::entities.each<Grounded, Position, Solid>(
		[&](entityx::Entity e, Grounded &g, Position &loc, Solid &dim) {
		(void)e;
		if (!g.grounded) {
			// get the line the entity is on
			int line = std::clamp(static_cast<int>((loc.x + dim.width / 2 - world.startX) / game::HLINE),
				0, static_cast<int>(world.data.size()));
				// make sure entity is above ground
			const auto& data = world.data;
			if (loc.y != data[line].groundHeight) {
				loc.y = data[line].groundHeight;
				e.remove<Grounded>();
			}
		}
	});

	game::entities.each<Direction, Physics>(
		[&](entityx::Entity e, Direction &vel, Physics &phys) {
		(void)e;
		// handle gravity
        if (vel.y > -2.0f)
			vel.y -= (GRAVITY_CONSTANT * phys.g) * dt;
	});

	game::entities.each<Position, Direction, Solid>(
	    [&](entityx::Entity e, Position &loc, Direction &vel, Solid &dim) {
		(void)e;
		//if (health.health <= 0)
		//	UserError("die mofo");

		// get the line the entity is on
		int line = std::clamp(static_cast<int>((loc.x + dim.width / 2 - world.startX) / game::HLINE),
			0, static_cast<int>(world.data.size()));

		// make sure entity is above ground
		const auto& data = world.data;
		if (loc.y < data[line].groundHeight) {
			int dir = vel.x < 0 ? -1 : 1;
			auto thing = line + dir * 2;
			if (thing > 0 && thing < static_cast<int>(data.size()) &&
			    data[line + dir * 2].groundHeight - 30 > data[line + dir].groundHeight) {
				loc.x -= (PLAYER_SPEED_CONSTANT + 2.7f) * dir * 2;
				vel.x = 0;
			} else {
				loc.y = data[line].groundHeight - 0.001f * dt;
				vel.y = 0;
				if (!vel.grounded) {
					vel.grounded = true;
					game::engine.getSystem<ParticleSystem>()->addMultiple(20, ParticleType::SmallPoof,
						[&](){ return vec2(loc.x + randGet() % static_cast<int>(dim.width), loc.y);}, 500, 30);
				}
			}
		}


		// insure that the entity doesn't fall off either edge of the world.
        if (loc.x < world.startX) {
			std::cout << "Left!\n";
			vel.x = 0;
			loc.x = world.startX + HLINES(0.5f);
		} else if (loc.x + dim.width + game::HLINE > -(static_cast<int>(world.startX))) {
			std::cout << "Right\n";
			vel.x = 0;
			loc.x = -(static_cast<int>(world.startX)) - dim.width - game::HLINE;
		}
	});
}

void WorldSystem::goWorldRight(Position& p, Solid &d)
{
	if (!(world.toRight.empty()) && (p.x + d.width > world.startX * -1 - HLINES(5))) {
		UISystem::fadeToggle();
		UISystem::waitForCover();
		while (waitToSwap)
			std::this_thread::sleep_for(1ms);
		load(world.toRight);
		game::engine.getSystem<PlayerSystem>()->setX(world.startX + HLINES(10));
		UISystem::fadeToggle();
	}
}

void WorldSystem::goWorldLeft(Position& p)
{
	if (!(world.toLeft.empty()) && (p.x < world.startX + HLINES(10))) {
		UISystem::fadeToggle();
		UISystem::waitForCover();
		while (waitToSwap)
			std::this_thread::sleep_for(1ms);
		load(world.toLeft);
		game::engine.getSystem<PlayerSystem>()->setX(world.startX * -1 - HLINES(15));
		UISystem::fadeToggle();
	}
}

void WorldSystem::goWorldPortal(Position& p)
{
	std::string file;

	if (world.indoor) {
		file = world.outdoor;
		p.x = world.outdoorCoords.x; // ineffective, player is regen'd
		p.y = world.outdoorCoords.y;
	} else {
		game::entities.each<Position, Solid, Portal>(
			[&](entityx::Entity entity, Position& loc, Solid &dim, Portal &portal) {
			(void)entity;
			if (!(portal.toFile.empty()) && p.x > loc.x && p.x < loc.x + dim.width)  {
				file = portal.toFile;
				world.outdoor = currentXMLFile;
				world.outdoorCoords = vec2(loc.x + dim.width / 2, 100);
				return;
			}
		});
	}

	if (!file.empty()) {
		UISystem::fadeToggle();
		UISystem::waitForCover();
		while (waitToSwap)
			std::this_thread::sleep_for(1ms);
		load(file);
		UISystem::fadeToggle();
	}
}