/*
    QGL Scene
    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 "glscene.h"

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

#include <chunkstream.h>


Scene::Scene() {
	root_ = new ObjectBase();
	root_->setScene(this);
	tree_changed = mat_changed = lights_changed = true;
	destroying                                  = false;
	need_reload_materials = tree_struct_changed = true;
	sel_mode_                                   = smSingleSelection;
}


Scene::~Scene() {
	destroying = true;
	clear();
	delete root_;
}


Scene * Scene::clone() {
	Scene * ret    = new Scene();
	ObjectBase * o = root_->clone();
	foreach(ObjectBase * co, o->children())
		ret->addObject(co);
	o->clearChildren();
	delete o;
	return ret;
}


void Scene::addObject(ObjectBase * o, ObjectBase * parent) {
	if (!parent) parent = root_;
	ObjectBaseList aol = o->children(true);
	attachObject(o);
	foreach(ObjectBase * c, aol)
		attachObject(c);
	parent->addChild(o);
	tree_changed = tree_struct_changed = true;
}


void Scene::addScene(const Scene * s) {
	if (!s) return;
	// qDebug() << "addScene clone ...";
	ObjectBase * o = s->root_->clone();
	o->setName(s->name());
	// qDebug() << "addScene clone ok" << o << o->children(true).size();
	addObject(o);
	makeMaterialsUniqueNames();
	// qDebug() << "addScene add ok" << o;
}


void Scene::assignFrom(const Scene * s) {
	clear();
	if (!s) return;
	setName(s->name());
	foreach(Material * m, s->materials) {
		Material * nm = new Material();
		*nm           = *m;
		nm->_changed  = true;
		nm->setMapsChanged();
		materials << nm;
	}
	for (int i = 0; i < s->root_->childCount(); ++i) {
		addObject(s->root_->child(i)->clone());
		// qDebug() << i << o->child(i)->pos();
	}
	tree_changed = mat_changed = lights_changed = need_reload_materials = tree_struct_changed = true;
}


void Scene::clear() {
	selected_.clear();
	selected_top.clear();
	emitSelectionChanged();
	root_->clearChildren(true);
	td_geometries << geometries;
	qDeleteAll(materials);
	geometries.clear();
	materials.clear();
	emit __destroyed();
	if (destroying) return;
	emit treeChanged();
}


void Scene::reinitAll() {
	for (auto * i: geometries)
		i->reinit();
	for (auto * i: td_geometries)
		i->reinit();
	auto aol = root_->children(true);
	for (auto * i: aol) {
		if (i->type() == ObjectBase::glLight) ((Light *)i)->shadow_map.reinit();
	}
	setTreeChanged();
	mat_changed = lights_changed = true;
	need_reload_materials        = true;
	tree_struct_changed          = true;
}


void Scene::objectsCountInternal(int * cnt, ObjectBase * where) {
	++(*cnt);
	foreach(ObjectBase * i, where->children())
		objectsCountInternal(cnt, i);
}
int Scene::objectsCount(bool all) {
	if (!all) return root_->childCount();
	int cnt = 0;
	objectsCountInternal(&cnt, root_);
	return cnt;
}


void Scene::removeObjectInternal(ObjectBase * o, ObjectBase * where) {
	if (destroying) return;
	foreach(ObjectBase * i, where->children()) {
		if (o == i) {
			where->removeChild(i);
			setObjectMeshChanged(i);
		} else
			removeObjectInternal(o, i);
	}
}


void Scene::emitSelectionChanged() {
	if (destroying) return;
	selected_top.clear();
	foreach(ObjectBase * o, selected_) {
		ObjectBase * po = o->selectedParent();
		if (!po) po = o;
		if (!selected_top.contains(po)) selected_top << po;
	}
	foreach(Mesh * m, geometries)
		m->setAllSelectionChanged(true);
	selectionChanged();
}


QString Scene::uniqueName(QString n, const QSet<QString> & names) {
	if (!names.contains(n)) return n;
	QString num;
	while (!n.isEmpty()) {
		if (n.right(1)[0].isDigit()) {
			num.push_front(n.right(1));
			n.chop(1);
		} else
			break;
	}
	if (!n.endsWith('_')) n += '_';
	int in     = num.toInt() + 1;
	QString nn = n + QString::number(in).rightJustified(3, '0');
	while (names.contains(nn))
		nn = n + QString::number(++in).rightJustified(3, '0');
	return nn;
}


void Scene::removeObject(ObjectBase * o, bool inChildren) {
	if (destroying) return;
	o->setScene(nullptr);
	setObjectMeshChanged(o);
	if (inChildren)
		removeObjectInternal(o, root_);
	else
		root_->removeChild(o);
	__objectDeleted(o);
	setTreeStructChanged();
}


void Scene::removeObject(ObjectBase & o, bool inChildren) {
	if (destroying) return;
	removeObject(&o, inChildren);
}


void Scene::clearObjects(bool deleteAll) {
	root_->clearChildren(deleteAll);
	cleanUnused();
	setTreeStructChanged();
	emitSelectionChanged();
}


void Scene::selectObject(ObjectBase * o, bool add_to_selection) {
	// qDebug() << "selectObject" << o << add_to_selection;
	if (!add_to_selection || (sel_mode_ == smSingleSelection)) clearSelection();
	if (o) {
		if (!add_to_selection)
			o->setSelected(true);
		else
			o->setSelected(!o->isSelected());
		gatherSelection();
	}
	emitSelectionChanged();
}


void Scene::selectObjects(ObjectBaseList ol, bool add_to_selection) {
	if (!add_to_selection || (sel_mode_ == smSingleSelection)) clearSelection();
	foreach(ObjectBase * o, ol) {
		if (!o) continue;
		o->setSelected(true);
	}
	gatherSelection();
	emitSelectionChanged();
}


void Scene::selectObjectsByMesh() {
	ObjectBaseList csl = selected_;
	QSet<Mesh *> sml;
	foreach(ObjectBase * o, csl)
		if (o->mesh()) sml << o->mesh();
	ObjectBaseList ol = root_->children(true);
	foreach(ObjectBase * o, ol) {
		if (sml.contains(o->mesh())) o->setSelected(true);
	}
	gatherSelection();
	emitSelectionChanged();
}


void Scene::selectObjectsByMaterial() {
	ObjectBaseList csl = selected_;
	QSet<Material *> sml;
	foreach(ObjectBase * o, csl)
		if (o->material()) sml << o->material();
	ObjectBaseList ol = root_->children(true);
	foreach(ObjectBase * o, ol) {
		if (sml.contains(o->material())) o->setSelected(true);
	}
	gatherSelection();
	emitSelectionChanged();
}


void Scene::clearSelection() {
	selected_.clear();
	selected_top.clear();
	ObjectBaseList ol = root_->children(true);
	foreach(ObjectBase * o, ol) {
		o->setSelected(false);
		o->setAimSelected(false);
	}
	emitSelectionChanged();
}


ObjectBaseList Scene::selectedObjects(bool top_only) const {
	return top_only ? selected_top : selected_;
}


ObjectBase * Scene::selectedObject() const {
	if (selected_.isEmpty()) return 0;
	return selected_[0];
}


void gatherMeshes(ObjectBase * o, QSet<Mesh *> & ums) {
	if (o->mesh()) ums << o->mesh();
	for (int i = 0; i < o->childCount(); ++i)
		gatherMeshes(o->child(i), ums);
}
void Scene::cleanUnused() {
	QSet<Mesh *> ums;
	gatherMeshes(root_, ums);
	for (int i = 0; i < geometries.size(); ++i) {
		if (ums.contains(geometries[i])) continue;
		td_geometries << geometries[i];
		geometries.removeAt(i);
		--i;
	}
}


const Box3D & Scene::boundingBox() const {
	root_->calculateBoundingBox();
	return root_->boundingBox();
}


Material * Scene::newMaterial(const QString & name) {
	materials << new Material(name);
	makeMaterialsUniqueNames();
	mat_changed = true;
	return materials.back();
}


void Scene::removeMaterial(Material * m) {
	if (!m || !materials.contains(m)) return;
	ObjectBaseList ol = root_->children(true);
	foreach(ObjectBase * o, ol)
		if (o->material_ == m) o->setMaterial(0);
	materials.removeAll(m);
	changed_materials.removeAll(m);
	mat_changed = true;
}


void Scene::makeMaterialsUniqueNames() {
	QSet<QString> names;
	foreach(Material * m, materials) {
		if (m->name.isEmpty()) m->name = "default_000";
		m->name = uniqueName(m->name, names);
		names << m->name;
	}
}


ObjectBaseList Scene::objects(bool all) {
	return root_->children(all);
}


void Scene::removeLight(Light * l) {
	removeObject(l);
}


void Scene::dump() {
	qDebug() << "Scene" << name();
	qDebug() << "Meshes:" << geometries.size();
	qDebug() << "Objects:" << root_->children(true).size();
}


void Scene::gatherSelection() {
	selected_.clear();
	ObjectBaseList ol = root_->children(true);
	foreach(ObjectBase * o, ol)
		if (o->isSelected()) selected_ << o;
}


void Scene::attachObject(ObjectBase * o) {
	if (!o) return;
	o->setScene(this);
	if (o->mesh()) { // search suitable mesh in this scene
		o->setMesh(attachMesh(o->mesh()));
		setObjectMeshChanged(o);
	}
	if (o->material()) { // search suitable material in this scene
		uint ohash    = o->material()->hash();
		bool need_new = true;
		foreach(Material * m, materials) {
			if (m == o->material()) { // already exists by ptr
				need_new = false;
				break;
			}
			if (m->hash() == ohash) { // already exists by hash
				need_new = false;
				o->setMaterial(m);
				break;
			}
		}
		if (need_new) { // need to clone material and add to scene
			Material * nmat = new Material();
			*nmat           = *(o->material());
			nmat->setMapsChanged();
			o->setMaterial(nmat);
			materials << nmat;
		}
	}
	setTreeStructChanged();
}


Mesh * Scene::attachMesh(Mesh * mesh) {
	if (!mesh) return 0;
	uint mhash = mesh->hash();
	foreach(Mesh * m, geometries) {
		if (m == mesh) { // already exists by ptr
			return m;
		}
		if (m->hash() == mhash) { // already exists by hash
			return m;
		}
	}
	// need to clone mesh and add to scene
	Mesh * nmesh = mesh->clone();
	geometries << nmesh;
	return nmesh;
}


void Scene::setTreeChanged() {
	if (destroying) return;
	tree_changed = true;
	foreach(Mesh * m, geometries) {
		m->setAllObjectsChanged(true);
		m->setAllSelectionChanged(true);
	}
	gatherSelection();
}


void Scene::setTreeStructChanged() {
	tree_struct_changed = true;
}


void Scene::setObjectMeshChanged(ObjectBase * o) {
	if (o) o->setObjectsChanged();
}


void Scene::prepareTree(ObjectBase * o) {
	foreach(ObjectBase * co, o->children_) {
		if (co->isHidden()) continue;
		switch (co->type_) {
		case ObjectBase::glLight: {
			Light * l = globject_cast<Light *>(co);
			lights_used[l->light_type] << l;
			if (l->light_map._changed) need_reload_materials = true;
		} break;
		case ObjectBase::glMesh:
			if (co->mesh()) {
				geometries_used[co->pass()][co->mesh()] << co;
				co->setObjectsChanged();
			}
			break;
		case ObjectBase::glCamera: cameras_used << globject_cast<Camera *>(co); break;
		default: break;
		}
		prepareTree(co);
	}
}


bool Scene::prepare() {
	changed_materials.clear();
	foreach(Material * m, materials) {
		if (m->_changed) {
			need_reload_materials = tree_changed = true;
			changed_materials << m;
		}
	}

	ObjectBaseList aol;
	if (!tree_changed && !mat_changed) return false;
	aol = root_->children(true);
	if (tree_changed) {
		geometries_used[rpSolid].clear();
		geometries_used[rpTransparent].clear();
		lights_used.clear();
		cameras_used.clear();
		prepareTree(root_);
		if (tree_struct_changed) {
			tree_struct_changed = false;
			cleanUnused();
			QMetaObject::invokeMethod(this, "treeChanged", Qt::QueuedConnection);
		}
		tree_changed   = false;
		lights_changed = true;
	}
	mat_changed = false;
	return true;
}


void Scene::destroy(QOpenGLExtraFunctions * f) {
	foreach(Mesh * g, geometries)
		g->destroy(f);
}


void Scene::destroyUnused(QOpenGLExtraFunctions * f) {
	foreach(Mesh * i, td_geometries)
		i->destroy(f);
	qDeleteAll(td_geometries);
	td_geometries.clear();
}


QDataStream & operator<<(QDataStream & s, const Scene * p) {
	ChunkStream cs;
	// qDebug() << "place" << p->name() << "...";
	QVector<short> geom_ind, mat_ind;
	ObjectBaseList cl = p->root_->children(true);
	geom_ind.reserve(cl.size());
	mat_ind.reserve(cl.size());
	foreach(ObjectBase * c, cl) {
		geom_ind << p->geometries.indexOf(c->mesh());
		mat_ind << p->materials.indexOf(c->material());
	}
	cs.add(1, p->name_).add(10, p->geometries).add(11, p->materials).add(20, p->root_).add(21, geom_ind).add(22, mat_ind);
	s << qCompress(cs.data());
	return s;
}
QDataStream & operator>>(QDataStream & s, Scene *& p) {
	p = new Scene();
	QByteArray ba;
	s >> ba;
	ba = qUncompress(ba);
	ChunkStream cs(ba);
	QVector<short> geom_ind, mat_ind;
	while (!cs.atEnd()) {
		switch (cs.read()) {
		case 1: cs.get(p->name_); break;
		case 10: cs.get(p->geometries); break;
		case 11: cs.get(p->materials); break;
		case 20:
			cs.get(p->root_);
			p->root_->setScene(p);
			break;
		case 21: cs.get(geom_ind); break;
		case 22: cs.get(mat_ind); break;
		}
	}
	p->makeMaterialsUniqueNames();
	ObjectBaseList cl = p->root_->children(true);
	int cnt           = qMin(qMin(cl.size(), geom_ind.size()), mat_ind.size());
	for (int i = 0; i < cnt; ++i) {
		ObjectBase * c(cl[i]);
		if (geom_ind[i] >= 0) c->mesh_ = p->geometries[geom_ind[i]];
		if (mat_ind[i] >= 0) c->material_ = p->materials[mat_ind[i]];
	}
	return s;
}
