#pragma once

// system
#include <vector>

// external
#include <glm/vec3.hpp>
#include <glm/gtc/random.hpp>

// vendor
#include <RTree.h>
#include <PerlinNoise.hpp>
#include <stb_image_write.h>

struct Impact {
	glm::vec3 pos;
	float r;
	bool contains(const glm::vec3 &p) const {
		return (glm::length(pos - p) < r);
	}
};

class Density {
	std::vector<Impact> impacts;
	RTree<int, float, 3, float> impactTree;
	siv::PerlinNoise noise;
	glm::vec3 scale;
	float scaleFactor;
	float frequency, octave;
	size_t version;
public:
	Density() :
			scale(1.0f), scaleFactor(4), frequency(2), octave(2), version(0) {

	}

	const std::vector<Impact> &getImpacts() {
		return impacts;
	}

	size_t getVersion() {
		return version;
	}

	void addImpact(Impact &impact) {
		size_t idx = impacts.size();
		impacts.push_back(impact);
		glm::vec3 l = impact.pos - glm::vec3(impact.r);
		glm::vec3 u = impact.pos + glm::vec3(impact.r);
		impactTree.Insert(&l.x, &u.x, idx);
		version++;
	}

	void setSeed(uint32_t seed) {
		noise.reseed(seed);
		version++;
		impacts.clear();
	}

	void setScale(const glm::vec3 &scale) {
		this->scale = scale;
		scaleFactor = 4.f / std::min(scale.x, std::min(scale.y, scale.z));
		impacts.clear();
		version++;

	}

	void setFrequency(float f) {
		frequency = f;
		impacts.clear();
		version++;
	}

	void setOctave(float o) {
		octave = o;
		impacts.clear();
		version++;
	}

	float operator()(const glm::vec3 &p, bool includeImpacts = true) const {
		if (includeImpacts && impacts.size() > 0) {
			bool insideImpact = false;
			impactTree.Search(&p.x, &p.x, [&](int idx) -> bool {
				if (impacts[idx].contains(p)) {
					insideImpact = true;
					return false;
				} else {
					return true;
				}
			});

			if (insideImpact)
				return 0.f;
		}
		float v = (float) noise.octaveNoise0_1(p.x * frequency, p.y * frequency,
				p.z * frequency, octave);
		glm::vec3 r = p - glm::vec3(0.5);
		v *= std::max(1.f - scaleFactor * dot(scale * r, r), 0.f);
		return v;
	}

	bool intersectIsolevel(const glm::vec3 &start, const glm::vec3 &end,
			glm::vec3 &pos, float isolevel, float resolution) const {
		glm::vec3 step = glm::normalize(end - start) * glm::vec3(resolution);
		size_t nSteps = ceil(glm::length(end - start) / resolution);
		pos = start;
		for (size_t iStep = 0; iStep < nSteps; iStep++) {
			if ((*this)(pos) > isolevel)
				return true;
			pos += step;
		}
		return false;
	}

	void addRandomImpacts(size_t count, float isolevel, float resolution,
			float minRadius, float maxRadius, int exponent) {
		for (size_t i = 0; i < count; i++) {
			glm::vec3 start = glm::vec3(0.5f) + glm::sphericalRand(0.6f);
			glm::vec3 end = glm::vec3(0.5f) + glm::sphericalRand(0.6f);
			Impact impact;
			if (intersectIsolevel(start, end, impact.pos, isolevel,
					resolution)) {
				impact.pos -= (impact.r / 1.5f) * normalize(end-start);
				impact.r = minRadius
						+ pow(glm::linearRand(0.f, 1.f), exponent) * maxRadius;
				addImpact(impact);
			}
		}
	}

	void saveCrossSection(const char *filename, size_t pixels) {
		float step = 1.f / pixels;
		glm::vec3 pos(step / 2, step / 2, 0.5f);
		std::vector<uint8_t> img(pixels * pixels);
		for (size_t ix = 0; ix < pixels; ix++) {
			for (size_t iy = 0; iy < pixels; iy++) {
				glm::vec3 pos(step * (0.5f + ix), step * (0.5f + iy), 0.5f);
				img[iy * pixels + ix] = (*this)(pos) * 255;
			}
		}

		stbi_write_png(filename, pixels, pixels, 1, img.data(), 0);
	}
};

