/*
    QGL Primitives
    Ivan Pelipenko peri4ko@yandex.ru

    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU Lesser General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU Lesser General Public License for more details.

    You should have received a copy of the GNU Lesser General Public License
    along with this program.  If not, see <http://www.gnu.org/licenses/>.
*/

#include "glprimitives.h"

#include "glmesh.h"


Mesh * Primitive::plane(float width, float length, int width_segments, int length_segments) {
	Mesh * ret = new Mesh();
	QVector<QVector3D> & v(ret->vertices());
	QVector<QVector3D> & n(ret->normals());
	QVector<QVector2D> & t(ret->texcoords());
	QVector<Vector3i> & i(ret->indicesTriangles());
	float hw = width / 2.f, hl = length / 2.f;
	float w1 = 1.f / width_segments, l1 = 1.f / length_segments;
	int sind = 0;
	for (int wi = 0; wi < width_segments; ++wi) {
		for (int li = 0; li < length_segments; ++li) {
			for (int j = 0; j < 4; ++j)
				n << QVector3D(0., 0., 1.);
			float ws = (float)wi / width_segments;
			float ls = (float)li / length_segments;
			t << QVector2D(ws, ls) << QVector2D(ws, ls + l1) << QVector2D(ws + w1, ls + l1) << QVector2D(ws + w1, ls);
			v << QVector3D(ws * width - hw, ls * length - hl, 0.) << QVector3D(ws * width - hw, (ls + l1) * length - hl, 0.)
			  << QVector3D((ws + w1) * width - hw, (ls + l1) * length - hl, 0.) << QVector3D((ws + w1) * width - hw, ls * length - hl, 0.);
			i << Vector3i(sind + 0, sind + 2, sind + 1) << Vector3i(sind + 0, sind + 3, sind + 2);
			sind += 4;
		}
	}
	return ret;
}


Mesh * Primitive::cube(float width, float length, float height, int width_segments, int length_segments, int height_segments) {
	Mesh * ret = new Mesh();
	QVector3D scale(width, length, height);
	QVector<QVector3D> & v(ret->vertices());
	QVector<QVector3D> & n(ret->normals());
	QVector<QVector2D> & t(ret->texcoords());
	QVector<Vector3i> & i(ret->indicesTriangles());
	float hs = 0.5f;
	int si   = 0;
	QMatrix4x4 mat;

	si = v.size();
	for (int j = 0; j < 4; ++j)
		n << QVector3D(0., -1., 0.);
	t << QVector2D(0., 0.) << QVector2D(1., 0.) << QVector2D(1., 1.) << QVector2D(0., 1.);
	v << QVector3D(-hs, -hs, -hs) << QVector3D(hs, -hs, -hs) << QVector3D(hs, -hs, hs) << QVector3D(-hs, -hs, hs);
	i << Vector3i(si + 0, si + 1, si + 2) << Vector3i(si + 0, si + 2, si + 3);

	for (int r = 0; r < 3; ++r) {
		si = v.size();
		mat.rotate(90., 0., 0., 1.);
		QVector3D cn = mat.map(n[0]);
		for (int j = 0; j < 4; ++j) {
			n << cn;
			v << mat.map(QVector4D(v[j])).toVector3D();
		}
		t << QVector2D(0., 0.) << QVector2D(1., 0.) << QVector2D(1., 1.) << QVector2D(0., 1.);
		i << Vector3i(si + 0, si + 1, si + 2) << Vector3i(si + 0, si + 2, si + 3);
	}

	mat.setToIdentity();
	mat.rotate(90., 1., 0., 0.);
	for (int r = 0; r < 2; ++r) {
		si = v.size();
		mat.rotate(180., 1., 0., 0.);
		QVector3D cn = mat.map(n[0]);
		for (int j = 0; j < 4; ++j) {
			n << cn;
			v << mat.map(QVector4D(v[j])).toVector3D();
		}
		t << QVector2D(0., 0.) << QVector2D(1., 0.) << QVector2D(1., 1.) << QVector2D(0., 1.);
		i << Vector3i(si + 0, si + 1, si + 2) << Vector3i(si + 0, si + 2, si + 3);
	}

	for (int i = 0; i < v.size(); ++i)
		v[i] *= scale;

	return ret;
}


Mesh * Primitive::ellipsoid(int segments_wl, int segments_h, float radius, float end_angle) {
	Mesh * ret = new Mesh();
	QVector<QVector3D> & v(ret->vertices());
	QVector<QVector3D> & n(ret->normals());
	QVector<QVector2D> & t(ret->texcoords());
	QVector<Vector3i> & ind(ret->indicesTriangles());
	int hseg = segments_h + 1, wlseg = segments_wl + 1;
	double crw, crl, a, ch, twl;
	double eang = deg2rad * end_angle;

	QVector3D cp;
	for (int i = 0; i <= hseg; i++) {
		ch = -cos((double)i / hseg * M_PI);
		cp.setZ(ch * radius);
		twl       = sqrt(1. - ch * ch);
		crw       = twl * radius;
		crl       = twl * radius;
		int cvcnt = wlseg * 2;
		for (int j = 0; j < cvcnt; j++) {
			a = (double)j / (cvcnt - 1) * eang;
			cp.setX(crl * cos(a));
			cp.setY(crw * sin(a));
			v << cp;
			t << QVector2D((double)j / (cvcnt - 1), ch / 2.f + 0.5f);
			n << cp.normalized();
			int si = v.size() - 1;
			if (j > 0 && i > 0) {
				ind << Vector3i(si - cvcnt - 1, si, si - 1);
				ind << Vector3i(si - cvcnt, si, si - cvcnt - 1);
			}
		}
	}
	if (end_angle < 360.) {
		Mesh * cap = Primitive::disc(segments_h + 1, radius, 180);
		cap->rotatePoints(90, 0, 1, 0);
		cap->rotatePoints(-90, 0, 0, 1);
		ret->append(cap);
		cap->flipNormals();
		cap->rotatePoints(end_angle, 0, 0, 1);
		ret->append(cap);
		delete cap;
	}
	return ret;
}


Mesh * Primitive::disc(int segments, float radius, float end_angle) {
	Mesh * ret = new Mesh();
	QVector<QVector3D> & v(ret->vertices());
	QVector<QVector3D> & n(ret->normals());
	QVector<QVector2D> & t(ret->texcoords());
	QVector<Vector3i> & ind(ret->indicesTriangles());

	segments = qMax(segments + 1, 4);
	QVector3D cp;
	v << QVector3D();
	n << QVector3D(0, 0, 1);
	t << QVector2D(0.5f, 0.5f);
	end_angle *= deg2rad;
	for (int i = 0; i < segments; i++) {
		double a = (double)i / (segments - 1) * end_angle;
		cp.setX(radius * cos(a));
		cp.setY(radius * sin(a));
		v << cp;
		n << QVector3D(0, 0, 1);
		t << QVector2D(cp.x() / radius + 1, cp.y() / radius + 1);
		int si = v.size() - 1;
		if (i > 0) ind << Vector3i(si - 1, si, 0);
	}
	return ret;
}


QVector3D coneNormal(double r, double height, double ang) {
	QVector3D norm;
	norm.setX(r * cos(ang));
	norm.setY(r * sin(ang));
	norm.setZ(0.);
	double rl = norm.length();
	double ca = atan2(rl, height);
	norm *= cos(ca);
	norm.setZ(norm.length() * tan(ca));
	return norm.normalized();
}


Mesh * Primitive::cone(int segments, float radius, float height) {
	Mesh * ret = new Mesh();
	QVector<QVector3D> & v(ret->vertices());
	QVector<QVector3D> & n(ret->normals());
	QVector<QVector2D> & t(ret->texcoords());
	QVector<Vector3i> & ind(ret->indicesTriangles());

	int seg = qMax(segments + 1, 4);
	QVector3D cp;
	for (int i = 0; i < seg; i++) {
		double a = (double)i / (seg - 1) * M_2PI;
		cp.setX(radius * cos(a));
		cp.setY(radius * sin(a));
		if (i > 0) {
			v << QVector3D(0, 0, height);
			t << QVector2D((double)(i - 1) / (seg - 1), 1.f);
			double ta = ((double)i - 0.5) / (seg - 1) * M_2PI;
			n << coneNormal(radius, height, ta);
		}
		v << cp;
		t << QVector2D((double)i / (seg - 1), 0.f);
		n << coneNormal(radius, height, a);
		int si = v.size() - 1;
		if (i > 0) ind << Vector3i(si - 1, si - 2, si);
	}

	Mesh * cap = Primitive::disc(segments, radius);
	cap->flipNormals();
	ret->append(cap);
	delete cap;

	return ret;
}


Mesh * Primitive::cylinder(int segments, float radius, float height, float end_angle) {
	Mesh * ret = new Mesh();
	QVector<QVector3D> & v(ret->vertices());
	QVector<QVector3D> & n(ret->normals());
	QVector<QVector2D> & t(ret->texcoords());
	QVector<Vector3i> & ind(ret->indicesTriangles());

	int seg = qMax(segments + 1, 4);
	QVector3D cp, norm;
	double eang = deg2rad * end_angle;

	for (int i = 0; i < seg; i++) {
		double a = (double)i / (seg - 1) * eang;
		cp.setX(radius * cos(a));
		cp.setY(radius * sin(a));
		cp.setZ(0.);
		norm = cp.normalized();
		v << cp;
		cp.setZ(height);
		v << cp;
		t << QVector2D((double)i / (seg - 1), 0.f);
		t << QVector2D((double)i / (seg - 1), 1.f);
		n << norm;
		n << norm;
		int si = v.size() - 1;
		if (i > 0) {
			ind << Vector3i(si - 2, si - 1, si);
			ind << Vector3i(si - 1, si - 2, si - 3);
		}
	}

	Mesh * cap = Primitive::disc(segments, radius, end_angle);
	cap->flipNormals();
	ret->append(cap);
	cap->translatePoints(QVector3D(0., 0., height));
	cap->flipNormals();
	ret->append(cap);
	delete cap;

	if (end_angle < 360.) {
		Mesh * cap = Primitive::plane(radius, height);
		cap->rotatePoints(90, 1, 0, 0);
		cap->translatePoints(radius / 2, 0, height / 2);
		ret->append(cap);
		cap->flipNormals();
		cap->rotatePoints(end_angle, 0, 0, 1);
		ret->append(cap);
		delete cap;
	}

	return ret;
}


Mesh * Primitive::arrow(int segments, float thick, float angle) {
	double cone_r = 1.5 * thick;
	double cone_h = 2. * cone_r / tan(angle * deg2rad);
	Mesh * ret    = new Mesh();
	Mesh * m      = Primitive::cylinder(segments, thick / 2., 1. - cone_h);
	ret->append(m);
	delete m;
	m = Primitive::cone(segments, cone_r, cone_h);
	m->translatePoints(QVector3D(0., 0., 1. - cone_h));
	ret->append(m);
	delete m;
	return ret;
}


Mesh * Primitive::torus(int segments_main, int segments_second, float radius_main, float radius_second, float end_angle) {
	Mesh * ret = new Mesh();
	QVector<QVector3D> & v(ret->vertices());
	QVector<QVector3D> & n(ret->normals());
	QVector<QVector2D> & t(ret->texcoords());
	QVector<Vector3i> & ind(ret->indicesTriangles());

	QVector<QVector3D> cv, cn;
	QVector<QVector2D> ct;
	segments_second = qMax(segments_second + 1, 4);
	for (int i = 0; i < segments_second; i++) {
		double x = (double)i / (segments_second - 1);
		double a = x * M_2PI;
		cv << QVector3D(radius_second * cos(a), 0., radius_second * sin(a));
		cn << cv.back().normalized();
		ct << QVector2D(0., x);
		cv.back() += QVector3D(radius_main, 0., 0.);
	}

	segments_main = qMax(segments_main + 1, 4);
	int ccnt = cv.size(), pcnt = 0;
	for (int i = 0; i < segments_main; i++) {
		double x = (double)i / (segments_main - 1);
		QMatrix4x4 rm;
		rm.rotate(x * end_angle, 0., 0., 1.);
		for (int j = 0; j < ccnt; j++) {
			ct[j].setX(x);
			v << rm.map(cv[j]);
			n << rm.map(cn[j]);
		}
		t.append(ct);
		if (i > 0) {
			for (int j = 0; j < ccnt - 1; j++) {
				ind << Vector3i(pcnt + j, pcnt + j + 1, pcnt + j - ccnt);
				ind << Vector3i(pcnt + j + 1, pcnt + j + 1 - ccnt, pcnt + j - ccnt);
			}
		}
		pcnt = v.size();
	}
	if (end_angle < 360.) {
		Mesh * cap = Primitive::disc(segments_second - 1, radius_second);
		cap->rotatePoints(90, 1, 0, 0);
		cap->translatePoints(radius_main, 0, 0);
		ret->append(cap);
		cap->flipNormals();
		cap->rotatePoints(end_angle, 0, 0, 1);
		ret->append(cap);
		delete cap;
	}
	return ret;
}


Mesh * Primitive::cubeFrame(float width, float length, float height) {
	Mesh * ret = new Mesh(GL_LINES);
	QVector3D scale(width, length, height);
	QVector<QVector3D> & v(ret->vertices());
	QVector<QVector3D> & n(ret->normals());
	QVector<QVector2D> & t(ret->texcoords());
	QVector<Vector2i> & i(ret->indicesLines());
	float hs = 0.5f;
	v << QVector3D(-hs, -hs, -hs) << QVector3D(-hs, hs, -hs) << QVector3D(hs, hs, -hs) << QVector3D(hs, -hs, -hs);
	v << QVector3D(-hs, -hs, hs) << QVector3D(-hs, hs, hs) << QVector3D(hs, hs, hs) << QVector3D(hs, -hs, hs);
	for (int j = 0; j < 8; ++j) {
		v[j] *= scale;
		t << QVector2D(0, 0);
		n << QVector3D(0, 0, 1);
	}
	for (int j = 0; j < 4; ++j) {
		i << Vector2i(j, (j + 1) % 4);
		i << Vector2i(j, j + 4);
		i << Vector2i(j + 4, (j + 1) % 4 + 4);
	}
	return ret;
}


Mesh * Primitive::ellipsoidFrame(int segments_wl, int segments_h, float radius) {
	Mesh * ret = new Mesh(GL_LINES);
	QVector<QVector3D> & v(ret->vertices());
	QVector<QVector3D> & n(ret->normals());
	QVector<QVector2D> & t(ret->texcoords());
	QVector<Vector2i> & ind(ret->indicesLines());
	int hseg = segments_h + 1, wlseg = segments_wl + 1;
	double crw, crl, a, ch, twl;

	QVector3D cp;
	for (int i = 0; i <= hseg; i++) {
		ch = -cos((double)i / hseg * M_PI);
		cp.setZ(ch * radius);
		twl       = sqrt(1. - ch * ch);
		crw       = twl * radius;
		crl       = twl * radius;
		int cvcnt = wlseg * 2;
		for (int j = 0; j < cvcnt; j++) {
			a = (double)j / (cvcnt - 1) * M_2PI;
			cp.setX(crl * cos(a));
			cp.setY(crw * sin(a));
			v << cp;
			t << QVector2D((double)j / (cvcnt - 1), ch / 2.f + 0.5f);
			n << cp.normalized();
			int si = v.size() - 1;
			if (j > 0 && i > 0) {
				ind << Vector2i(si, si - 1);
				ind << Vector2i(si - cvcnt, si);
			}
		}
	}
	return ret;
}


Mesh * Primitive::coneFrame(int segments, float radius, float height) {
	Mesh * ret = new Mesh(GL_LINES);
	QVector<QVector3D> & v(ret->vertices());
	QVector<QVector3D> & n(ret->normals());
	QVector<QVector2D> & t(ret->texcoords());
	QVector<Vector2i> & ind(ret->indicesLines());

	int seg = qMax(segments + 1, 4);
	QVector3D cp;
	for (int i = 0; i < seg; i++) {
		double a = (double)i / (seg - 1) * M_2PI;
		cp.setX(radius * cos(a));
		cp.setY(radius * sin(a));
		if (i > 0) {
			v << QVector3D(0, 0, height);
			t << QVector2D((double)(i - 1) / (seg - 1), 1.f);
			double ta = ((double)i - 0.5) / (seg - 1) * M_2PI;
			n << coneNormal(radius, height, ta);
		}
		v << cp;
		t << QVector2D((double)i / (seg - 1), 0.f);
		n << coneNormal(radius, height, a);
		int si = v.size() - 1;
		if (i > 0) {
			ind << Vector2i(si - 1, si);
			ind << Vector2i(si - 2, si);
		}
	}

	return ret;
}


Mesh * Primitive::lineFrame(QVector3D p0, QVector3D p1) {
	Mesh * ret = new Mesh(GL_LINES);
	QVector<QVector3D> & v(ret->vertices());
	QVector<QVector3D> & n(ret->normals());
	QVector<QVector2D> & t(ret->texcoords());
	QVector<Vector2i> & ind(ret->indicesLines());
	v << p0 << p1;
	n << QVector3D(0, 0, 1) << QVector3D(0, 0, 1);
	t << QVector2D(0, 0) << QVector2D(1, 0);
	ind << Vector2i(0, 1);
	return ret;
}
