/*
    QGL ObjectBase & 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 "glmesh.h"
#include "glscene.h"

#include <chunkstream.h>
// static int _count = 0;

ObjectBase::ObjectBase(Mesh * geom, Material * mat) {
	type_     = glMesh;
	prev_pass = rpSolid;
	parent_   = nullptr;
	color_    = Qt::white;
	is_root = is_init = selected_ = false;
	visible_ = accept_fog = accept_light = cast_shadow = rec_shadow = select_ = true;
	line_width                                                                = -1.;
	id_                                                                       = 0;
	blend_src                                                                 = GL_SRC_ALPHA;
	blend_dest                                                                = GL_ONE_MINUS_SRC_ALPHA;
	type_                                                                     = glMesh;
	raw_matrix = selected_aim = false;
	mat_.setToIdentity();
	scene_    = nullptr;
	mesh_     = geom;
	material_ = mat;
	// qDebug() << "ObjectBase, now" << ++_count;
}


ObjectBase::~ObjectBase() {
	// qDebug() << "~ObjectBase, now" << --_count;
	if (parent_) parent_->children_.removeAll(this);
	if (scene_) {
		scene_->__objectDeleted(this);
		scene_->setTreeChanged();
		scene_->setTreeStructChanged();
	}
	foreach(ObjectBase * c, children_) {
		c->parent_ = nullptr;
		delete c;
	}
}


ObjectBase * ObjectBase::clone(bool withChildren) {
	ObjectBase * o   = new ObjectBase();
	o->prev_pass     = prev_pass;
	o->is_init       = false;
	o->accept_light  = accept_light;
	o->accept_fog    = accept_fog;
	o->visible_      = visible_;
	o->color_        = color_;
	o->type_         = type_;
	o->raw_matrix    = raw_matrix;
	o->mat_          = mat_;
	o->trans         = trans;
	o->trans_texture = trans_texture;
	o->itransform_   = itransform_;
	o->bound         = bound;
	o->name_         = name_; // + "_copy";
	o->blend_src     = blend_src;
	o->blend_dest    = blend_dest;
	o->pos_h         = pos_h;
	o->material_     = material_;
	o->mesh_         = mesh_;
	o->meta          = meta;
	o->scene_        = nullptr;
	if (withChildren) {
		for (int i = 0; i < children_.size(); ++i)
			o->addChild(children_[i]->clone(withChildren));
	}
	return o;
}


void ObjectBase::destroy() {
	if (mesh_) delete mesh_;
}


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


RenderPass ObjectBase::pass() const {
	RenderPass ret = rpSolid;
	if (material_)
		if (material_->hasTransparency()) ret = rpTransparent;
	return ret;
}


void ObjectBase::setScene(Scene * v) {
	scene_ = v;
	foreach(ObjectBase * c, children_)
		c->setScene(v);
}


void ObjectBase::addChild(ObjectBase * o) {
	if (o == this) return;
	if (o->parent_) o->parent_->children_.removeAll(o);
	children_ << o;
	o->parent_ = this;
	o->setScene(scene_);
	o->buildTransform();
	/*if (scene_) {
	    ObjectBaseList cl = o->children(true);
	    cl << o;
	    //foreach (ObjectBase * i, cl) {
	    //	emit view_->objectAdded(i);
	    //}
	}*/
	setSceneTreeChanged();
}


void ObjectBase::removeChild(ObjectBase * o) {
	if (o == this) return;
	children_.removeAll(o);
	o->parent_ = nullptr;
	o->buildTransform();
	setSceneTreeChanged();
}


void ObjectBase::removeChild(int index) {
	children_[index]->parent_ = nullptr;
	children_[index]->buildTransform();
	children_.removeAt(index);
	setSceneTreeChanged();
}


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


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


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


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


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


ObjectBaseList ObjectBase::children(bool all_) {
	if (!all_) return children_;
	ObjectBaseList cl;
	addChildren(cl, this);
	return cl;
}


bool ObjectBase::isVisible(bool check_parents) const {
	if (!check_parents) return visible_;
	if (!visible_) return false;
	ObjectBase * p = parent_;
	while (p) {
		if (!p->visible_) return false;
		p = p->parent_;
	}
	return true;
}


void ObjectBase::setVisible(bool v) {
	visible_ = v;
	setSceneTreeChanged();
}


void ObjectBase::setCastShadows(bool on) {
	cast_shadow = on;
	if (type_ == glLight)
		((Light *)this)->apply();
	else
		setObjectsChanged();
}


void ObjectBase::rotateZ(GLfloat a) {
	raw_matrix = false;
	trans.setRotationZ(trans.rotationZ() + a);
	// 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 ObjectBase::setRotationZ(GLfloat a) {
	raw_matrix = false;
	trans.setRotationZ(a);
	// 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 ObjectBase::setTransform(const Transform & t) {
	trans = t;
	buildTransform();
}


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


void ObjectBase::calculateBoundingBox() {
	bound = Box3D();
	if (mesh_) {
		bound                = mesh_->boundingBox();
		QVector<QVector3D> c = bound.corners(), tc;
		foreach(QVector3D p, c)
			tc << (itransform_ * QVector4D(p, 1)).toVector3D();
		bound = Box3D(tc);
	}
	foreach(ObjectBase * i, children_) {
		i->calculateBoundingBox();
		bound |= i->boundingBox();
	}
}


void ObjectBase::updateTransform() {
	buildTransform(true);
}


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


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


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


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


void ObjectBase::setMatrix(const QMatrix4x4 & t) {
	// raw_matrix = true;
	// mat_ = t;
	// pos_ = mat_.column(3).toVector3D();
	// mat_.setColumn(3, QVector4D(0., 0., 0., 1.));
	raw_matrix = false;
	trans.setMatrix(t);
	buildTransform();
}


QMatrix4x4 ObjectBase::matrix() const {
	return trans.matrix();
}


QVector3D ObjectBase::inParentSpace(const QVector3D & v) const {
	if (!parent_) return v;
	return (parent_->matrix() * QVector4D(v, 1)).toVector3D();
}


void ObjectBase::transferTransformToChildren(bool only_scale) {
	QMatrix4x4 m = trans.matrix();
	if (only_scale) m = trans.matrixScale();
	foreach(ObjectBase * i, children_)
		i->trans.setMatrix(m * i->trans.matrix());
	if (only_scale)
		resetScale();
	else
		setMatrix(QMatrix4x4());
}


void ObjectBase::cleanTree() {
	for (int i = 0; i < children_.size(); ++i) {
		ObjectBase * o = children_[i];
		if (!o->hasChildren() && !o->mesh() && (o->type() == glMesh)) {
			delete o;
			--i;
		}
		o->cleanTree();
	}
}


void ObjectBase::setTextureTransform(const Transform & t) {
	trans_texture = t;
	setObjectsChanged();
}


void ObjectBase::setTextureMatrix(const QMatrix4x4 & t) {
	trans_texture.setMatrix(t);
	setObjectsChanged();
}


QMatrix4x4 ObjectBase::textureMatrix() const {
	return trans_texture.matrix();
}


QGenericMatrix<3, 2, float> ObjectBase::textureGLMatrix() const {
	QGenericMatrix<3, 2, float> ret = textureMatrix().toGenericMatrix<3, 2>();
	ret(0, 2)                       = -trans_texture.translation().x();
	ret(1, 2)                       = -trans_texture.translation().y();
	return ret;
}


bool ObjectBase::isSelected(bool check_parents) const {
	if (!check_parents) return selected_;
	if (selected_) return true;
	ObjectBase * p = parent_;
	while (p) {
		if (p->selected_) return true;
		p = p->parent_;
	}
	return false;
}


void ObjectBase::setSelected(bool yes) {
	// qDebug() << "select" << name() << view_;
	if (select_) selected_ = yes;
	if (!selected_) selected_aim = false;
}


ObjectBase * ObjectBase::selectedParent() const {
	ObjectBase * p = parent_;
	while (p) {
		if (p->selected_) return p;
		p = p->parent_;
	}
	return 0;
}


void ObjectBase::setMaterial(Material * m, bool with_children) {
	material_ = m;
	if (with_children)
		foreach(ObjectBase * i, children_)
			i->setMaterial(m, true);
	setObjectsChanged();
	if (scene_) scene_->mat_changed = scene_->tree_changed = true;
}


void ObjectBase::setColor(QColor c, bool with_children) {
	color_ = c;
	if (with_children)
		foreach(ObjectBase * i, children_)
			i->setColor(c, true);
	setObjectsChanged();
}


void ObjectBase::setMesh(Mesh * v) {
	if (scene_) v = scene_->attachMesh(v);
	mesh_ = v;
	setSceneTreeChanged();
	setObjectsChanged();
}


void ObjectBase::buildTransform(bool force) {
	if (force) trans.setDirty();
	itransform_.setToIdentity();
	ObjectBase * 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(ObjectBase * i, children_)
		i->buildTransform(force);
	setObjectsChanged();
}


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


void ObjectBase::localTransform(QMatrix4x4 & m) {
	m *= trans.matrix();
}


void ObjectBase::setSceneTreeChanged() {
	if (scene_) {
		scene_->setTreeChanged();
		scene_->setTreeStructChanged();
	}
	setObjectsChanged();
}


void ObjectBase::setObjectsChanged() {
	int p = pass();
	if (mesh_) {
		mesh_->setObjectsChanged(p, true);
		mesh_->setSelectionChanged(p, true);
		if (prev_pass != p) {
			mesh_->setObjectsChanged(prev_pass, true);
			mesh_->setSelectionChanged(prev_pass, true);
		}
	}
	prev_pass = p;
}


QMatrix4x4 ObjectBase::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_);
	// }
	mat = trans.matrix();
	return parent * mat;
}


AimedObject::AimedObject() {
	aim_dist = 1.;
}


QVector3D AimedObject::worldAim() const {
	QVector3D ret = worldPos() + worldDirection() * aim_dist;
	return ret;
}


void AimedObject::setAim(const QVector3D & p) {
	QVector3D dir = p - pos();
	trans.setRotation(Transform::fromDirection(dir, trans.rotationY()));
	aim_dist = dir.length();
	buildTransform();
	// if (!p.isNull())
	// qDebug() << "setAim" << p << aim() << worldAim();
}


QVector3D AimedObject::direction() const {
	return trans.direction();
}


void AimedObject::setDirection(const QVector3D & d) {
	// double len = qMax(aim_.length(), 0.001f);
	// aim_ = d.normalized() * len;
	buildTransform();
}


void AimedObject::flyCloser(double s) {
	double tl = 1. / (1. + s);
	move(direction() * aim_dist * (1. - tl));
	aim_dist *= tl;
}


void AimedObject::flyFarer(double s) {
	double tl = 1. * (1. + s);
	move(direction() * aim_dist * (1. - tl));
	aim_dist *= tl;
}


void AimedObject::flyToDistance(double d) {
	move(direction() * (aim_dist - d));
	aim_dist = d;
	// qDebug() << d << (aim() - pos()).length() << aim();
}


void AimedObject::moveForward(const float & x, bool withZ) {
	QVector3D dv = itransform_.mapVector(QVector3D(0, 0, -x));
	if (!withZ) dv[2] = 0.;
	move(dv);
}


void AimedObject::moveLeft(const float & x, bool withZ) {
	QVector3D dv = itransform_.mapVector(QVector3D(-x, 0, 0));
	if (!withZ) dv[2] = 0.;
	move(dv);
}


void AimedObject::moveUp(const float & x, bool onlyZ) {
	QVector3D dv = itransform_.mapVector(QVector3D(0, x, 0));
	if (onlyZ) dv[0] = dv[1] = 0.;
	move(dv);
}


void AimedObject::orbitZ(const float & a) {
	QVector3D pa = aim();
	rotateZ(-a);
	move(pa - aim());
}


void AimedObject::orbitXY(const float & a) {
	QVector3D pa = aim();
	rotateX(-a);
	move(pa - aim());
}


void AimedObject::transformChanged() {}


Light::Light(const QVector3D & p, const QColor & c, float i): AimedObject(), shadow_map(nullptr, 0, false) {
	type_       = glLight;
	light_type  = Omni;
	intensity   = i;
	color_      = c;
	angle_start = angle_end = 90.;
	decay_linear = decay_quadratic = decay_start = 0.;
	decay_const = decay_end = 1.;
	size                    = 0.1;
	setPos(p);
	setDirection(0, 0, -1.);
}


ObjectBase * Light::clone(bool withChildren) {
	Light * o  = new Light(*this);
	// GLObjectBase::clone(withChildren);
	o->is_init = false;
	o->name_   = name_; // + "_copy";
	o->scene_  = nullptr;
	o->children_.clear();
	if (withChildren) {
		for (int i = 0; i < children_.size(); ++i)
			o->addChild(children_[i]->clone(withChildren));
	}
	o->color_          = color_;
	o->light_type      = light_type;
	o->trans           = trans;
	o->trans_texture   = trans_texture;
	o->aim_dist        = aim_dist;
	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;
	o->shadow_map.reinit();
	return o;
}


void Light::apply() {
	if (scene_) scene_->setLightsChanged();
}


QDataStream & operator<<(QDataStream & s, const ObjectBase * p) {
	ChunkStream cs;
	// qDebug() << "place" << p->name() << "...";
	cs.add(1, int(p->type_))
		.add(2, p->accept_light)
		.add(3, p->accept_fog)
		.add(4, p->visible_)
		.add(5, p->cast_shadow)
		.add(6, p->rec_shadow)
		.add(7, p->raw_matrix)
		.add(8, p->line_width)
		.add(14, p->mat_)
		.add(16, p->children_.size())
		.add(17, p->name_)
		.add(18, p->meta)
		.add(19, p->color_)
		.add(20, p->trans)
		.add(21, p->trans_texture);
	// qDebug() << "place self done";
	if (p->type_ == ObjectBase::glLight) {
		// qDebug() << "place light ...";
		const Light * l = (const Light *)p;
		cs.add(101, l->angle_start)
			.add(102, l->angle_end)
			.add(103, l->intensity)
			.add(104, l->decay_const)
			.add(105, l->decay_linear)
			.add(106, l->decay_quadratic)
			.add(107, l->decay_start)
			.add(108, l->decay_end)
			.add(109, int(l->light_type))
			.add(111, l->distance())
			.add(112, l->size)
			.add(113, l->light_map);
	}
	if (p->type_ == ObjectBase::glCamera) {
		// qDebug() << "place camera ...";
		const Camera * c = (const Camera *)p;
		cs.add(200, c->aim())
			.add(201, c->fov_)
			.add(202, c->depth_start)
			.add(206, c->mirror_x)
			.add(207, c->mirror_y)
			.add(208, c->distance())
			.add(209, c->roll_);
	}
	// qDebug() << "place" << p->name() << cs.data().size() << s.device()->size();
	s << cs.data();
	foreach(const ObjectBase * c, p->children_)
		s << c;
	return s;
}
QDataStream & operator>>(QDataStream & s, ObjectBase *& p) {
	ChunkStream cs(s);
	p          = nullptr;
	int ccnt   = 0;
	Light * l  = nullptr;
	Camera * c = nullptr;
	// qDebug() << "read obj ...";
	while (!cs.atEnd()) {
		switch (cs.read()) {
		case 1: {
			ObjectBase::Type type = (ObjectBase::Type)cs.getData<int>();
			switch (type) {
			case ObjectBase::glMesh: p = new ObjectBase(); break;
			case ObjectBase::glLight:
				p = new Light();
				l = (Light *)p;
				break;
			case ObjectBase::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 14:
			if (p) p->mat_ = cs.getData<QMatrix4x4>();
			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 19:
			if (p) p->color_ = cs.getData<QColor>();
			break;
		case 20:
			if (p) p->trans = cs.getData<Transform>();
			break;
		case 21:
			if (p) p->trans_texture = cs.getData<Transform>();
			break;
		case 100:
			if (l) l->setAim(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 111:
			if (l) l->setDistance(cs.getData<double>());
			break;
		case 112:
			if (l) l->size = cs.getData<double>();
			break;
		case 113:
			if (l) l->light_map = cs.getData<Map>();
			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 206:
			if (c) c->mirror_x = cs.getData<bool>();
			break;
		case 207:
			if (c) c->mirror_y = cs.getData<bool>();
			break;
		case 208:
			if (c) c->setDistance(cs.getData<double>());
			break;
		case 209:
			if (c) c->roll_ = cs.getData<GLfloat>();
			break;
		}
	}
	// qDebug() << p->name() << ccnt;
	for (int i = 0; i < ccnt; ++i) {
		ObjectBase * c = nullptr;
		s >> c;
		if (!c) continue;
		c->parent_ = p;
		p->children_ << c;
	}
	p->buildTransform();
	return s;
}
