/*
    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/>.
*/

#ifndef GLOBJECT_H
#define GLOBJECT_H

#include "glframebuffer.h"
#include "glmaterial.h"
#include "gltransform.h"
#include "gltypes.h"


class QGLENGINE_CORE_EXPORT ObjectBase {
	friend class Scene;
	friend class RendererSelection;
	friend QGLENGINE_CORE_EXPORT QDataStream & operator<<(QDataStream & s, const ObjectBase * p);
	friend QGLENGINE_CORE_EXPORT QDataStream & operator>>(QDataStream & s, ObjectBase *& p);
	friend QGLENGINE_CORE_EXPORT QDataStream & operator>>(QDataStream & s, Scene *& p);

public:
	enum Type {
		glMesh,
		glLight,
		glCamera,
		glParticlesSystem
	};

	explicit ObjectBase(Mesh * geom = 0, Material * mat = 0);
	virtual ~ObjectBase();

	virtual ObjectBase * clone(bool withChildren = true);
	void destroy();

	QString name() const { return name_; }
	void setName(const QString & name) { name_ = name; }
	virtual void init();
	virtual void update() {}
	bool isInit() const { return is_init; }
	Type type() const { return type_; }
	RenderPass pass() const;
	uint id() const { return id_; }

	float lineWidth() const { return line_width; }
	void setLineWidth(const float & width) { line_width = width; }

	ObjectBase * parent() { return parent_; }
	void setParent(ObjectBase * o) { parent_ = o; }
	bool hasParent() const { return parent_ != nullptr; }
	bool hasChildren() const { return !children_.isEmpty(); }
	void setScene(Scene * v);

	void addChild(ObjectBase * o);
	void removeChild(ObjectBase * o);
	void removeChild(int index);
	void clearChildren(bool deleteAll = false);
	int childCount() const { return children_.size(); }
	ObjectBase * child(int index);
	ObjectBase * child(const QString & name);
	const ObjectBase * child(int index) const;
	const ObjectBase * child(const QString & name) const;
	ObjectBaseList children(bool all_ = false);

	bool isVisible(bool check_parents = false) const;
	bool isHidden(bool check_parents = false) const { return !isVisible(check_parents); }
	void setVisible(bool v);
	void setHidden(bool v) { setVisible(!v); }
	void show() { setVisible(true); }
	void hide() { setVisible(false); }

	bool isReceiveShadows() const { return rec_shadow; }
	bool isCastShadows() const { return cast_shadow; }
	void setReceiveShadows(bool on) {
		rec_shadow = on;
		setObjectsChanged();
	}
	void setCastShadows(bool on);

	void move(const QVector3D & dv) {
		trans.setTranslation(pos() + dv);
		buildTransform();
	}
	void moveTo(const QVector3D & dv) {
		trans.setTranslation(dv);
		buildTransform();
	}
	void move(GLfloat dx, GLfloat dy, GLfloat dz = 0.) {
		move(QVector3D(dx, dy, dz));
		buildTransform();
	}
	void moveTo(GLfloat dx, GLfloat dy, GLfloat dz = 0.) {
		moveTo(QVector3D(dx, dy, dz));
		buildTransform();
	}
	void moveX(GLfloat o) {
		trans.setTranslationX(posX() + o);
		buildTransform();
	}
	void moveY(GLfloat o) {
		trans.setTranslationY(posY() + o);
		buildTransform();
	}
	void moveZ(GLfloat o) {
		trans.setTranslationZ(posZ() + o);
		buildTransform();
	}
	void setPosX(GLfloat o) {
		trans.setTranslationX(o);
		buildTransform();
	}
	void setPosY(GLfloat o) {
		trans.setTranslationY(o);
		buildTransform();
	}
	void setPosZ(GLfloat o) {
		trans.setTranslationZ(o);
		buildTransform();
	}
	void setPos(GLfloat x, GLfloat y, GLfloat z) {
		trans.setTranslation(QVector3D(x, y, z));
		buildTransform();
	}
	void setPos(const QVector3D & p) {
		trans.setTranslation(p);
		buildTransform();
	}
	void resetPos() {
		trans.setTranslation(QVector3D());
		buildTransform();
	}

	QVector3D pos() const { return trans.translation(); }
	float posX() const { return trans.translation().x(); }
	float posY() const { return trans.translation().y(); }
	float posZ() const { return trans.translation().z(); }
	QVector3D worldPos() const { return (itransform_ * QVector4D(0, 0, 0, 1.)).toVector3D(); }
	QMatrix4x4 worldTransform() const { return itransform_; }

	QVector3D rotation() const { return trans.rotation(); }
	float rotationX() const { return rotation().x(); }
	float rotationY() const { return rotation().y(); }
	float rotationZ() const { return rotation().z(); }
	void rotateX(GLfloat a) {
		raw_matrix = false;
		trans.setRotationX(trans.rotationX() + a);
		buildTransform();
	}
	void rotateY(GLfloat a) {
		raw_matrix = false;
		trans.setRotationY(trans.rotationY() + a);
		buildTransform();
	}
	void rotateZ(GLfloat a);
	void setRotationX(GLfloat a) {
		raw_matrix = false;
		trans.setRotationX(a);
		buildTransform();
	}
	void setRotationY(GLfloat a) {
		raw_matrix = false;
		trans.setRotationY(a);
		buildTransform();
	}
	void setRotationZ(GLfloat a);
	void setRotation(const QVector3D & a) {
		raw_matrix = false;
		trans.setRotation(a);
		buildTransform();
	}
	void resetRotation() {
		raw_matrix = false;
		trans.setRotation(QVector3D());
		buildTransform();
	}

	QVector3D scale() { return trans.scale3D(); }
	float scaleX() { return trans.scale3D().x(); }
	float scaleY() { return trans.scale3D().y(); }
	float scaleZ() { return trans.scale3D().z(); }
	void scale(const QVector3D & sv) {
		raw_matrix = false;
		trans.setScale(trans.scale3D() * sv);
		buildTransform();
	}
	void scale(GLfloat sx, GLfloat sy, GLfloat sz) {
		raw_matrix = false;
		scale(QVector3D(sx, sy, sz));
		buildTransform();
	}
	void scale(GLfloat sx, GLfloat sy) {
		raw_matrix = false;
		scale(QVector3D(sx, sy, sy));
		buildTransform();
	}
	void scale(GLfloat sx) {
		raw_matrix = false;
		scale(QVector3D(sx, sx, sx));
		buildTransform();
	}
	void scaleX(GLfloat a) {
		raw_matrix = false;
		trans.setScaleX(trans.scale3D().x() + a);
		buildTransform();
	}
	void scaleY(GLfloat a) {
		raw_matrix = false;
		trans.setScaleY(trans.scale3D().y() + a);
		buildTransform();
	}
	void scaleZ(GLfloat a) {
		raw_matrix = false;
		trans.setScaleZ(trans.scale3D().z() + a);
		buildTransform();
	}
	void setScale(const QVector3D & a) {
		raw_matrix = false;
		trans.setScale(a);
		buildTransform();
	}
	void setScale(GLfloat a) {
		raw_matrix = false;
		trans.setScale(a);
		buildTransform();
	}
	void setScaleX(GLfloat a) {
		raw_matrix = false;
		trans.setScaleX(a);
		buildTransform();
	}
	void setScaleY(GLfloat a) {
		raw_matrix = false;
		trans.setScaleY(a);
		buildTransform();
	}
	void setScaleZ(GLfloat a) {
		raw_matrix = false;
		trans.setScaleZ(a);
		buildTransform();
	}
	void resetScale() {
		raw_matrix = false;
		trans.setScale(1.f);
		buildTransform();
	}

	Transform transform() { return trans; }
	void setTransform(const Transform & t);
	void setMatrix(const QMatrix4x4 & t);
	QMatrix4x4 matrix() const;

	bool isRawMatrix() { return raw_matrix; }
	QVector3D inParentSpace(const QVector3D & v) const;
	void transferTransformToChildren(bool only_scale = false);
	void cleanTree();

	Transform textureTransform() { return trans_texture; }
	void setTextureTransform(const Transform & t);
	void setTextureMatrix(const QMatrix4x4 & t);
	QMatrix4x4 textureMatrix() const;
	QGenericMatrix<3, 2, float> textureGLMatrix() const;

	bool isAcceptLight() const { return accept_light; }
	void setAcceptLight(bool yes) {
		accept_light = yes;
		setObjectsChanged();
	}

	bool isAcceptFog() const { return accept_fog; }
	void setAcceptFog(bool yes) {
		accept_fog = yes;
		setObjectsChanged();
	}

	bool isSelected(bool check_parents = false) const;
	void setSelected(bool yes);
	void select() { setSelected(true); }
	void deselect() { setSelected(false); }
	ObjectBase * selectedParent() const;
	void setAimSelected(bool yes) { selected_aim = yes; }
	bool isAimSelected() const { return selected_aim; }

	bool isSelectable() const { return select_; }
	void setSelectable(bool yes) { select_ = yes; }

	GLenum srcAlpha() const { return blend_src; }
	GLenum destAlpha() const { return blend_dest; }
	void setSrcAlpha(GLenum mode) { blend_src = mode; }
	void setDestAlpha(GLenum mode) { blend_dest = mode; }

	void setMaterial(Material * m, bool with_children = false);
	Material * material() { return material_; }

	void setColor(QColor c, bool with_children = false);
	QColor color() { return color_; }

	const Box3D & boundingBox() const { return bound; }
	void setMesh(Mesh * v);
	Mesh * mesh() { return mesh_; }

	void calculateBoundingBox();
	void updateTransform();

	void setProperty(const QString & pn, const QVariant & v);
	QVariant property(const QString & pn, bool * exists = 0) const;
	bool hasProperty(const QString & pn) const;
	void removeProperty(const QString & pn);

	QVector3D pos_h;

protected:
	virtual void transformChanged() {}
	void addChildren(ObjectBaseList & list, ObjectBase * where);
	void buildTransform(bool force = false);
	void initInternal();
	void setSceneTreeChanged();
	void setObjectsChanged();
	void localTransform(QMatrix4x4 & m);
	QMatrix4x4 worldMatrix(QMatrix4x4 parent) const;

	int prev_pass; // Pass
	bool is_init, accept_light, accept_fog, visible_, cast_shadow, rec_shadow, select_, selected_, raw_matrix;
	bool is_root, selected_aim;
	float line_width;
	QColor color_;
	uint id_;
	Type type_;
	Box3D bound;
	Transform trans, trans_texture;
	ObjectBaseList children_;
	QMatrix4x4 itransform_, mat_;
	QString name_;
	GLenum blend_src, blend_dest;
	ObjectBase * parent_;
	Scene * scene_;
	Material * material_;
	Mesh * mesh_;
	QVariantMap meta;
};

inline bool operator<(const ObjectBase & f, const ObjectBase & s) {
	return f.pos_h.z() < s.pos_h.z();
}


class QGLENGINE_CORE_EXPORT AimedObject: public ObjectBase {
	friend class QGLView;
	friend class GLRendererBase;
	friend class Light;
	friend class Camera;

public:
	AimedObject();
	~AimedObject() {}
	QVector3D aim() const { return pos() + (direction() * aim_dist); }
	QVector3D worldAim() const;
	void setAim(const QVector3D & p);
	QVector3D direction() const;
	QVector3D worldDirection() const { return (itransform_ * QVector4D(QVector3D(0, 0, -1), 0.)).toVector3D().normalized(); }
	void setDirection(const QVector3D & d);
	void setDirection(double x, double y, double z) { setDirection(QVector3D(x, y, z)); }

	double distance() const { return aim_dist; }
	void setDistance(double d) { aim_dist = d; }
	void flyCloser(double s);
	void flyFarer(double s);
	void flyToDistance(double d);

	void moveForward(const float & x, bool withZ = true);
	void moveBackward(const float & x, bool withZ = true) { moveForward(-x, withZ); }
	void moveLeft(const float & x, bool withZ = true);
	void moveRight(const float & x, bool withZ = true) { moveLeft(-x, withZ); }
	void moveUp(const float & x, bool onlyZ = false);
	void moveDown(const float & x, bool onlyZ = false) { moveUp(-x, onlyZ); }

	void rotateRoll(const float & a) { rotateY(a); }
	void orbitZ(const float & a);
	void orbitXY(const float & a);

protected:
	void transformChanged() override;
	double aim_dist;
};


class QGLENGINE_CORE_EXPORT Light: public AimedObject {
	friend class QGLView;
	friend class RendererBase;

public:
	enum Type {
		Omni,
		Cone,
		Directional
	};

	Light(const QVector3D & p = QVector3D(), const QColor & c = Qt::white, float i = 1.);
	virtual ObjectBase * clone(bool withChildren = true);
	virtual void init() {
		shadow_map.resize(512, 512);
		is_init = true;
	}
	void apply();

	float angle_start;
	float angle_end;
	float intensity;
	float decay_const;
	float decay_linear;
	float decay_quadratic;
	float decay_start;
	float decay_end;
	float size;
	Type light_type;
	Framebuffer shadow_map;
	QMatrix4x4 shadow_matrix;
	Map light_map;
};

template<class T>
inline T globject_cast(ObjectBase * object) {
	return reinterpret_cast<T>(object);
}

template<class T>
inline T globject_cast(const ObjectBase * object) {
	return reinterpret_cast<T>(object);
}


QGLENGINE_CORE_EXPORT QDataStream & operator<<(QDataStream & s, const ObjectBase * p);
QGLENGINE_CORE_EXPORT QDataStream & operator>>(QDataStream & s, ObjectBase *& p);

inline ObjectBaseList lights2objectList(const QList<Light *> & v) {
	ObjectBaseList ret;
	foreach(Light * i, v)
		ret << (ObjectBase *)i;
	return ret;
}
inline ObjectBaseList cameras2objectList(const QList<Camera *> & v) {
	ObjectBaseList ret;
	foreach(Camera * i, v)
		ret << (ObjectBase *)i;
	return ret;
}

#endif // GLOBJECT_H
