#include <algorithm>
#include <cmath>
#include <cstdlib>
#include <iostream>
#include <numeric>

#include "Box.hpp"

namespace nbody {
	//extend box to form cube
	void Box::extendToCube() {
		std::size_t longestSide = std::numeric_limits<std::size_t>::max();
		double sidelength = 0.0;

		for (std::size_t i = 0; i < 3; i++) {
			if (max[i] - min[i] >= sidelength) {
				longestSide = i;
				sidelength = max[i] - min[i];
			}
		}
		if (longestSide == std::numeric_limits<std::size_t>::max()) {
			return;
		}
		for (std::size_t i = 0; i < 3; i++) {
			if (i != longestSide) {
				double extend = (sidelength - (max[i] - min[i])) / 2.0;

				min[i] -= extend;
				max[i] += extend;
			}
		}
	}

	//extend for bodies
	void Box::extendForBodies(const std::vector<Body>& bodies) {
		min = std::accumulate(std::begin(bodies), std::end(bodies), min, [](Vec3 min_, const Body& b) { return MinComponents(min_, b.position); });
		max = std::accumulate(std::begin(bodies), std::end(bodies), max, [](Vec3 max_, const Body& b) { return MaxComponents(max_, b.position); });
	}

	//extract bodies within box
	std::vector<Body> Box::extractBodies(std::vector<Body>& bodies) const {
		const auto i = std::stable_partition(std::begin(bodies), std::end(bodies), [&](const Body& b) {return !(min <= b.position && b.position <= max); });
		std::vector<Body> bodiesWithin{ i, std::end(bodies) };
		bodies.erase(i, std::end(bodies));
		return bodiesWithin;
	}

	//copy bodies within box
	std::vector<Body> Box::copyBodies(const std::vector<Body>& bodies) const {
		std::vector<Body> bodiesWithin;
		std::copy_if(std::begin(bodies), std::end(bodies), std::back_inserter(bodiesWithin), [&](const Body& body) {
			return min <= body.position && body.position <= max;
		});
		return bodiesWithin;
	}

	//check for body inside box
	bool isContained(const Body& body, Box box) {
		return !(box.min > body.position || body.position > box.max);
	}

	//check for box inside box
	bool isContained(Box inner, Box outer) {
		return !(outer.min > inner.min || inner.max > outer.max);
	}

	//box volume
	double Box::volume() const {
		if (!isValid()) {
			return -1.0;
		}
		const auto diagonal = max - min;
		return diagonal.X * diagonal.Y * diagonal.Z;
	}

	double Box::maxSidelength() const {
		if (!isValid()) {
			return -1.0;
		}
		const auto diagonal = max - min;
		return std::max({ diagonal.X, diagonal.Y, diagonal.Z });
	}

	bool Box::isCorrectBox() const {
		if (!isValid()) {
			std::cout << "inverted bb\n";
			return false;
		}
		return true;
	}

	bool Box::isValid() const {
		return max > min;
	}

	void Box::printBB(std::size_t parallelId) const {
		std::cout << parallelId << ": min :" << min;
		std::cout << parallelId << ": max :" << max;
		std::cout << '\n';
	}


	//distance from nearest box order to position
	double Box::distanceToPosition(Vec3 position) const {
		if (!isValid()) {
			return std::numeric_limits<double>::max();
		}

		bool isInside{ min <= position && position <= max };

		if (isInside) {
			return 0.0;
		} else {
			auto nextpos = ClampComponents(position, min, max);
			return Distance(nextpos, position);
		}
	}

	//box - box distance
	double Box::distanceToBox(Box box2) const {
		double length = 0.0;

		if (!isValid() || !box2.isValid()) {
			return std::numeric_limits<double>::max();
		}

		for (std::size_t i = 0; i < 3; i++) {
			double elem;

			if (box2.min[i] < min[i] && box2.max[i] < min[i]) {
				elem = min[i] - box2.max[i];
			} else if (box2.min[i] > max[i] && box2.max[i] > max[i]) {
				elem = box2.min[i] - max[i];
			} else {
				elem = 0.0;
			}
			length += elem * elem;
		}
		return std::sqrt(length);
	}

	//determine octree subboxes
	std::array<Box, 8> Box::octreeSplit() const {
		if (!isValid()) {
			std::cerr << "cannot split invalid box\n";
			std::abort();
		}

		const auto diag = max - min;
		const auto mid = min + (diag / 2);

		return std::array<Box, 8> {{
			{ { min.X, min.Y, min.Z }, { mid.X, mid.Y, mid.Z } },
			{ { mid.X, min.Y, min.Z }, { max.X, mid.Y, mid.Z } },
			{ { min.X, mid.Y, min.Z }, { mid.X, max.Y, mid.Z } },
			{ { mid.X, mid.Y, min.Z }, { max.X, max.Y, mid.Z } },
			{ { min.X, min.Y, mid.Z }, { mid.X, mid.Y, max.Z } },
			{ { mid.X, min.Y, mid.Z }, { max.X, mid.Y, max.Z } },
			{ { min.X, mid.Y, mid.Z }, { mid.X, max.Y, max.Z } },
			{ { mid.X, mid.Y, mid.Z }, { max.X, max.Y, max.Z } }}};
	}

	//split box into two across longest side
	std::array<Box, 2> Box::splitLongestSide() const {
		std::size_t longestIndex = 0;
		double longestSide = -1.0;

		if (!isValid()) {
			std::cerr << "cannot split longest side of invalid box\n";
			std::abort();
		}
		for (std::size_t i = 0; i < 3; i++) {
			if (max[i] - min[i] > longestSide) {
				longestSide = max[i] - min[i];
				longestIndex = i;
			}
		}
		const double middle = min[longestIndex] + (max[longestIndex] - min[longestIndex]) / 2.0;
		auto first = *this; first.max[longestIndex] = middle;
		auto second = *this; second.min[longestIndex] = middle;
		return{ {first, second} };
	}

	//check for position in box
	bool Box::contained(Vec3 position) const {
		return min <= position && position <= max;
	}

	//create box out of two boxes
	Box extend(Box first, Box second){
		if (!first.isValid() || !second.isValid()) {
			std::cerr << "cannot extend invalid box.\n";
			std::abort();
		}
		return {
			MinComponents(first.min, second.min),
			MaxComponents(first.max, second.max)
		};
	}

	//extend box by body
	void Box::extend(const Body& extender) {
		min = MinComponents(min, extender.position);
		max = MaxComponents(max, extender.position);
	}

} // namespace nbody
