/*
    GLObjectBase & Light
    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 "globject.h"

#include "glcamera.h"
#include "qglview.h"


GLObjectBase::GLObjectBase() {
	type_       = glMesh;
	render_mode = View;
	pass_       = Solid;
	geom_prim   = Triangles;
	scale_      = QVector3D(1., 1., 1.);
	parent_     = nullptr;
	is_root = is_init = is_tex_loaded = selected_ = false;
	visible_ = accept_fog = accept_light = cast_shadow = rec_shadow = select_ = true;
	line_width                                                                = -1.;
	blend_src                                                                 = GL_SRC_ALPHA;
	blend_dest                                                                = GL_ONE_MINUS_SRC_ALPHA;
	type_                                                                     = glMesh;
	raw_matrix                                                                = false;
	mat_.setToIdentity();
	view_ = nullptr;
}


GLObjectBase::~GLObjectBase() {
	// qDebug() << "del" << name() << view_;
	if (parent_) parent_->children_.removeAll(this);
	if (view_) ((QGLView *)view_)->objectDeleted(this);
	foreach(GLObjectBase * c, children_) {
		c->parent_ = nullptr;
		delete c;
	}
}


GLObjectBase * GLObjectBase::clone(bool withChildren) {
	GLObjectBase * o  = new GLObjectBase();
	o->pass_          = pass_;
	o->is_init        = false;
	o->accept_light   = accept_light;
	o->accept_fog     = accept_fog;
	o->visible_       = visible_;
	o->type_          = type_;
	o->raw_matrix     = raw_matrix;
	o->mat_           = mat_;
	o->pos_           = pos_;
	o->angles_        = angles_;
	o->scale_         = scale_;
	o->itransform_    = itransform_;
	o->bound          = bound;
	o->name_          = name_ + "_copy";
	o->blend_src      = blend_src;
	o->blend_dest     = blend_dest;
	o->material_      = material_;
	o->pos_h          = pos_h;
	o->points         = points;
	o->puvws          = puvws;
	o->faces          = faces;
	o->uvws           = uvws;
	o->norms          = norms;
	o->normals        = normals;
	o->vbo.vertices_  = vbo.vertices_;
	o->vbo.normals_   = vbo.normals_;
	o->vbo.texcoords_ = vbo.texcoords_;
	o->vbo.colors_    = vbo.colors_;
	o->meta           = meta;
	o->view_          = nullptr;
	o->children_.clear();
	if (withChildren) {
		for (int i = 0; i < children_.size(); ++i)
			o->addChild(children_[i]->clone(withChildren));
	}
	return o;
}


void GLObjectBase::init() {
	calculateBoundingBox();
	vbo.init();
	vbo.rebuffer();
	// material_.reflection.create();
	// qDebug() << "init" << vbo.buffer_;
	is_init = true;
}


void GLObjectBase::draw(QOpenGLShaderProgram * prog, bool simplest) {
	vbo.draw(geom_prim, prog, simplest);
	/*if (!d_vertices.isEmpty()) {
	    glBindBuffer(GL_ARRAY_BUFFER, 0);
	    glVertexPointer(3, GL_FLOAT, 0, d_vertices.constData());
	    glTexCoordPointer(2, GL_FLOAT, 0, d_uvs.constData());
	    //glColorPointer(4, GL_FLOAT, 0, d_colors.constData());
	    glNormalPointer(GL_FLOAT, 0, d_normals.constData());
	    glDrawArrays(geom_prim, 0, d_vertices.size() / 3);*/
	/*if (pass_ == Reflection) {
	    glActiveTexture(GL_TEXTURE1);
	    glTexGeni(GL_S, GL_TEXTURE_GEN_MODE, GL_REFLECTION_MAP);
	    glTexGeni(GL_T, GL_TEXTURE_GEN_MODE, GL_REFLECTION_MAP);
	}*/
	//}
}


void GLObjectBase::setView(QGLView * v) {
	view_ = v;
	foreach(GLObjectBase * c, children_)
		c->setView(v);
}


void GLObjectBase::addChild(GLObjectBase * o) {
	if (o == this) return;
	if (o->parent_) o->parent_->children_.removeAll(o);
	children_ << o;
	o->parent_ = this;
	o->setView((QGLView *)view_);
	o->buildTransform();
	if (view_) {
		view_->collectLights();
		QList<GLObjectBase *> cl = o->children(true);
		cl << o;
		foreach(GLObjectBase * i, cl) {
			emit((QGLView *)view_)->objectAdded(i);
		}
	}
}


void GLObjectBase::removeChild(GLObjectBase * o) {
	if (o == this) return;
	children_.removeAll(o);
	o->parent_ = nullptr;
	o->buildTransform();
	if (view_ != nullptr) view_->collectLights();
}


void GLObjectBase::removeChild(int index) {
	children_[index]->parent_ = nullptr;
	children_[index]->buildTransform();
	children_.removeAt(index);
	if (view_ != nullptr) view_->collectLights();
}


void GLObjectBase::clearChildren(bool deleteAll) {
	foreach(GLObjectBase * i, children_) {
		i->view_   = nullptr;
		i->parent_ = nullptr;
		i->clearChildren(deleteAll);
		if (deleteAll) {
			delete i;
		} else {
			i->buildTransform();
		}
	}
	children_.clear();
	if (view_) view_->collectLights();
}


GLObjectBase * GLObjectBase::child(int index) {
	if (index < 0 || index >= children_.size()) return nullptr;
	return children_[index];
}


GLObjectBase * GLObjectBase::child(const QString & name) {
	foreach(GLObjectBase * i, children_)
		if (i->name_ == name) return i;
	return nullptr;
}


const GLObjectBase * GLObjectBase::child(int index) const {
	if (index < 0 || index >= children_.size()) return nullptr;
	return children_[index];
}


const GLObjectBase * GLObjectBase::child(const QString & name) const {
	foreach(GLObjectBase * i, children_)
		if (i->name_ == name) return i;
	return nullptr;
}


QList<GLObjectBase *> GLObjectBase::children(bool all_) {
	if (!all_) return children_;
	QList<GLObjectBase *> cl;
	addChildren(cl, this);
	return cl;
}


void GLObjectBase::rotateX(GLfloat a) {
	raw_matrix = false;
	angles_.setX(angles_.x() + a);
	buildTransform();
}


void GLObjectBase::rotateY(GLfloat a) {
	raw_matrix = false;
	angles_.setY(angles_.y() + a);
	buildTransform();
}


void GLObjectBase::rotateZ(GLfloat a) {
	raw_matrix = false;
	angles_.setZ(angles_.z() + a);
	while (angles_.z() < -360.f)
		angles_.setZ(angles_.z() + 360.f);
	while (angles_.z() > 360.f)
		angles_.setZ(angles_.z() - 360.f);
	buildTransform();
}


void GLObjectBase::setRotationX(GLfloat a) {
	raw_matrix = false;
	angles_.setX(a);
	buildTransform();
}


void GLObjectBase::setRotationY(GLfloat a) {
	raw_matrix = false;
	angles_.setY(a);
	buildTransform();
}


void GLObjectBase::setRotationZ(GLfloat a) {
	raw_matrix = false;
	angles_.setZ(a);
	while (angles_.z() < -360.f)
		angles_.setZ(angles_.z() + 360.f);
	while (angles_.z() > 360.f)
		angles_.setZ(angles_.z() - 360.f);
	buildTransform();
}


void GLObjectBase::setRotation(const QVector3D & a) {
	raw_matrix = false;
	angles_    = a;
	buildTransform();
}


void GLObjectBase::resetRotation() {
	raw_matrix = false;
	angles_    = QVector3D(0., 0., 0.);
	buildTransform();
}


void GLObjectBase::addChildren(QList<GLObjectBase *> & list, GLObjectBase * where) {
	foreach(GLObjectBase * i, where->children_) {
		list << i;
		addChildren(list, i);
	}
}


void GLObjectBase::loadTextures(bool with_children) {
	material_.loadTextures(view_->textureManager());
	if (with_children)
		foreach(GLObjectBase * i, children_)
			i->loadTextures();
	is_tex_loaded = true;
	checkPass();
}


void GLObjectBase::calculateBoundingBox() {
	bound                = vbo.boundingBox();
	QVector<QVector3D> c = bound.corners(), tc;
	// QMatrix4x4 mat = itransform_.inverted();
	// qDebug() << itransform_ << mat_ << mat;
	foreach(QVector3D p, c)
		tc << (itransform_ * QVector4D(p, 1)).toVector3D();
	bound = Box3D(tc);
	foreach(GLObjectBase * i, children_) {
		i->calculateBoundingBox();
		bound |= i->boundingBox();
	}
}


void GLObjectBase::setProperty(const QString & pn, const QVariant & v) {
	meta[pn] = v;
}


QVariant GLObjectBase::property(const QString & pn, bool * exists) const {
	if (exists) *exists = meta.contains(pn);
	return meta.value(pn);
}


bool GLObjectBase::hasProperty(const QString & pn) const {
	return meta.contains(pn);
}


void GLObjectBase::removeProperty(const QString & pn) {
	meta.remove(pn);
}


void GLObjectBase::setTransform(const QMatrix4x4 & t) {
	raw_matrix = true;
	mat_       = t;
	pos_       = mat_.column(3).toVector3D();
	mat_.setColumn(3, QVector4D(0., 0., 0., 1.));
	buildTransform();
}


void GLObjectBase::select() {
	// qDebug() << "select" << name() << view_;
	selected_ = true;
	if (view_) ((QGLView *)view_)->selectObject(this);
}


void GLObjectBase::setMaterial(const Material & m, bool with_children) {
	material_ = m;
	if (with_children)
		foreach(GLObjectBase * i, children_)
			i->setMaterial(m, true);
	checkPass();
	is_tex_loaded = false;
}


void GLObjectBase::buildTransform() {
	itransform_.setToIdentity();
	GLObjectBase * p = parent_;
	if (p) itransform_ = p->itransform_;
	if (raw_matrix) {
		itransform_.translate(pos_);
		itransform_ *= mat_;
		// qDebug() << "raw_matrix" << itransform_;
	} else
		localTransform(itransform_);
	// qDebug() << name_ << itransform_;
	foreach(GLObjectBase * i, children_)
		i->buildTransform();
}


void GLObjectBase::initInternal() {
	init();
	loadTextures();
	foreach(GLObjectBase * i, children_)
		i->initInternal();
}


void GLObjectBase::localTransform(QMatrix4x4 & m) {
	m.translate(pos_);
	m.rotate(angles_.z(), 0., 0., 1.);
	m.rotate(angles_.y(), 0., 1., 0.);
	m.rotate(angles_.x(), 1., 0., 0.);
	m.scale(scale_);
}


void GLObjectBase::checkPass() {
	if (float(material_.color_diffuse.alphaF()) * (1.f - material_.transparency) < 1.f)
		pass_ = Transparent;
	else
		pass_ = Solid;
}


QMatrix4x4 GLObjectBase::worldMatrix(QMatrix4x4 parent) const {
	QMatrix4x4 mat;
	mat.translate(pos_);
	if (raw_matrix) {
		mat *= mat_;
	} else {
		if (angles_.z() != 0.f) mat.rotate(angles_.z(), 0., 0., 1.);
		if (angles_.y() != 0.f) mat.rotate(angles_.y(), 0., 1., 0.);
		if (angles_.x() != 0.f) mat.rotate(angles_.x(), 1., 0., 0.);
		mat.scale(scale_);
	}
	return parent * mat;
}


void GLObjectBase::render(int * id, QMap<int, GLObjectBase *> * ids, int sh_id_loc) {
	if (!visible_) return;
	// glPushMatrix();
	/// qglMultMatrix TODO
	material_.apply(nullptr);
	if (id != nullptr) {
		++(*id);
		ids->insert(*id, this);
		// glVertexAttrib1f(sh_id_loc, (*id) / 255.f);
		// qDebug() << "assign to" << sh_id_loc << (*id) / 255.f;
	}
	draw(nullptr);
	foreach(GLObjectBase * i, children_)
		i->render(id, ids, sh_id_loc);
	// glPopMatrix();
}


Light::Light(): GLObjectBase(), shadow_map(0, true, GL_R16F) {
	type_       = glLight;
	light_type  = Omni;
	intensity   = 1.;
	angle_start = angle_end = 180.;
	decay_linear = decay_quadratic = decay_start = 0.;
	decay_const = decay_end = 1.;
	direction.setZ(1.);
}


Light::Light(const QVector3D & p, const QColor & c, float i): GLObjectBase(), shadow_map(0, true, GL_R16F) {
	type_       = glLight;
	light_type  = Omni;
	pos_        = p;
	intensity   = i;
	/*color_ = c;*/
	angle_start = angle_end = 180.;
	decay_linear = decay_quadratic = decay_start = 0.;
	decay_const = decay_end = 1.;
	direction.setZ(1.);
}


GLObjectBase * Light::clone(bool withChildren) {
	Light * o  = new Light(*this);
	// GLObjectBase::clone(withChildren);
	o->is_init = false;
	o->name_   = name_ + "_copy";
	o->view_   = nullptr;
	o->children_.clear();
	if (withChildren) {
		for (int i = 0; i < children_.size(); ++i)
			o->addChild(children_[i]->clone(withChildren));
	}
	o->light_type      = light_type;
	o->direction       = direction;
	o->angle_start     = angle_start;
	o->angle_end       = angle_end;
	o->intensity       = intensity;
	o->decay_const     = decay_const;
	o->decay_linear    = decay_linear;
	o->decay_quadratic = decay_quadratic;
	o->meta            = meta;
	return o;
}


void Light::draw(QOpenGLShaderProgram * prog, bool simplest) {
	bool l = glIsEnabled(GL_LIGHTING);
	glDisable(GL_LIGHTING);
	glPointSize(8.);
	glColor3f(0., 0., 0.);
	glBegin(GL_POINTS);
	glVertex3f(0., 0., 0.);
	glEnd();
	float s = 10;
	if (light_type != Omni) {
		glBegin(GL_LINES);
		QVector4D dir = QVector4D(direction);
		if (raw_matrix) dir = transform().inverted() * dir;
		glVertex3f(0., 0., 0.);
		glVertex3f(dir.x() * s, dir.y() * s, dir.z() * s);
		glEnd();
	}
	if (l) glEnable(GL_LIGHTING);
}


QDataStream & operator<<(QDataStream & s, const GLObjectBase * p) {
	ChunkStream cs;
	// qDebug() << "place" << p->name() << "...";
	cs << cs.chunk(1, int(p->type_)) << cs.chunk(2, p->accept_light) << cs.chunk(3, p->accept_fog) << cs.chunk(4, p->visible_)
	   << cs.chunk(5, p->cast_shadow) << cs.chunk(6, p->rec_shadow) << cs.chunk(7, p->raw_matrix) << cs.chunk(8, p->line_width)
	   << cs.chunk(9, int(p->render_mode)) << cs.chunk(10, p->material_) << cs.chunk(11, p->pos_) << cs.chunk(12, p->angles_)
	   << cs.chunk(13, p->scale_) << cs.chunk(14, p->mat_) << cs.chunk(15, p->vbo) << cs.chunk(16, p->children_.size())
	   << cs.chunk(17, p->name_) << cs.chunk(18, p->meta);
	// qDebug() << "place self done";
	if (p->type_ == GLObjectBase::glLight) {
		// qDebug() << "place light ...";
		const Light * l = (const Light *)p;
		cs << cs.chunk(100, l->direction) << cs.chunk(101, l->angle_start) << cs.chunk(102, l->angle_end) << cs.chunk(103, l->intensity)
		   << cs.chunk(104, l->decay_const) << cs.chunk(105, l->decay_linear) << cs.chunk(106, l->decay_quadratic)
		   << cs.chunk(107, l->decay_start) << cs.chunk(108, l->decay_end) << cs.chunk(109, int(l->light_type));
	}
	if (p->type_ == GLObjectBase::glCamera) {
		// qDebug() << "place camera ...";
		const Camera * c = (const Camera *)p;
		cs << cs.chunk(200, c->aim_) << cs.chunk(201, c->fov_) << cs.chunk(202, c->depth_start) << cs.chunk(203, c->depth_end)
		   << cs.chunk(204, c->angle_limit_lower_xy) << cs.chunk(205, c->angle_limit_upper_xy) << cs.chunk(206, c->mirror_x)
		   << cs.chunk(207, c->mirror_y);
	}
	// qDebug() << "place" << p->name() << cs.data().size() << s.device()->size();
	s << cs.data();
	foreach(const GLObjectBase * c, p->children_)
		s << c;
	return s;
}
QDataStream & operator>>(QDataStream & s, GLObjectBase *& p) {
	ChunkStream cs(s);
	p          = nullptr;
	int ccnt   = 0;
	Light * l  = nullptr;
	Camera * c = nullptr;
	QVector3D cam_angles;
	// qDebug() << "read obj ...";
	while (!cs.atEnd()) {
		switch (cs.read()) {
		case 1: {
			GLObjectBase::Type type = (GLObjectBase::Type)cs.getData<int>();
			switch (type) {
			case GLObjectBase::glMesh: p = new GLObjectBase(); break;
			case GLObjectBase::glLight:
				p = new Light();
				l = (Light *)p;
				break;
			case GLObjectBase::glCamera:
				p = new Camera();
				c = (Camera *)p;
				break;
			default: break;
			}
			if (p) p->type_ = type;
		} break;
		case 2:
			if (p) p->accept_light = cs.getData<bool>();
			break;
		case 3:
			if (p) p->accept_fog = cs.getData<bool>();
			break;
		case 4:
			if (p) p->visible_ = cs.getData<bool>();
			break;
		case 5:
			if (p) p->cast_shadow = cs.getData<bool>();
			break;
		case 6:
			if (p) p->rec_shadow = cs.getData<bool>();
			break;
		case 7:
			if (p) p->raw_matrix = cs.getData<bool>();
			break;
		case 8:
			if (p) p->line_width = cs.getData<float>();
			break;
		case 9:
			if (p) p->render_mode = (GLObjectBase::RenderMode)cs.getData<int>();
			break;
		case 10:
			if (p) p->material_ = cs.getData<Material>();
			break;
		case 11:
			if (p) p->pos_ = cs.getData<QVector3D>();
			break;
		case 12:
			if (p) p->angles_ = cs.getData<QVector3D>();
			if (c) {
				c->setAngles(cs.getData<QVector3D>());
				cam_angles = c->angles();
			}
			break;
		case 13:
			if (p) p->scale_ = cs.getData<QVector3D>();
			break;
		case 14:
			if (p) p->mat_ = cs.getData<QMatrix4x4>();
			break;
		case 15:
			if (p) p->vbo = cs.getData<GLVBO>();
			break;
		case 16:
			if (p) ccnt = cs.getData<int>();
			break;
		case 17:
			if (p) p->name_ = cs.getData<QString>();
			break;
		case 18:
			if (p) p->meta = cs.getData<QVariantMap>();
			break;
		case 100:
			if (l) l->direction = cs.getData<QVector3D>();
			break;
		case 101:
			if (l) l->angle_start = cs.getData<GLfloat>();
			break;
		case 102:
			if (l) l->angle_end = cs.getData<GLfloat>();
			break;
		case 103:
			if (l) l->intensity = cs.getData<GLfloat>();
			break;
		case 104:
			if (l) l->decay_const = cs.getData<GLfloat>();
			break;
		case 105:
			if (l) l->decay_linear = cs.getData<GLfloat>();
			break;
		case 106:
			if (l) l->decay_quadratic = cs.getData<GLfloat>();
			break;
		case 107:
			if (l) l->decay_start = cs.getData<GLfloat>();
			break;
		case 108:
			if (l) l->decay_end = cs.getData<GLfloat>();
			break;
		case 109:
			if (l) l->light_type = (Light::Type)cs.getData<int>();
			break;
		case 200:
			if (c) c->setAim(cs.getData<QVector3D>());
			break;
		case 201:
			if (c) c->setFOV(cs.getData<GLfloat>());
			break;
		case 202:
			if (c) c->setDepthStart(cs.getData<GLfloat>());
			break;
		case 203:
			if (c) c->setDepthEnd(cs.getData<GLfloat>());
			break;
		case 204:
			if (c) c->setAngleLowerLimitXY(cs.getData<GLfloat>());
			break;
		case 205:
			if (c) c->setAngleUpperLimitXY(cs.getData<GLfloat>());
			break;
		case 206:
			if (c) c->mirror_x = cs.getData<bool>();
			break;
		case 207:
			if (c) c->mirror_y = cs.getData<bool>();
			break;
		}
	}
	if (c) c->setAngles(cam_angles);
	// qDebug() << p->name() << ccnt;
	for (int i = 0; i < ccnt; ++i) {
		GLObjectBase * c = nullptr;
		s >> c;
		if (!c) continue;
		c->parent_ = p;
		p->children_ << c;
	}
	return s;
}
