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

#include "Box.hpp"

namespace nbody {
	//extend box to form cube
	void Box::extendToCube() {
		const auto longest = longestSide();

		for (std::size_t i = 0; i < 3; i++) {
			if (i != longest.Idx) {
				double extend = (longest.Length - (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 });
	}

	Box::LongestSide Box::longestSide() const {
		const auto diag = max - min;
		const auto components = diag.getComponents();
		const auto it = std::max_element(std::begin(components), std::end(components));
		const auto longestSide = static_cast<std::size_t>(std::distance(std::begin(components), it));
		const auto sidelength = *it;
		return{ longestSide, sidelength };
	}

	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';
	}

	//box - box distance
	double Box::distanceSquaredToBox(Box box2) const {

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

		double length2 = 0.0;
		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;
			}
			length2 += elem * elem;
		}
		return length2;
	}

	//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 {
		const auto longestIndex = longestSide().Idx;
		const auto 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
