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

#ifndef GLOBJECT_H
#define GLOBJECT_H

#include "glframebuffer.h"
#include "glmaterial.h"
#include "glvbo.h"

class Camera;
class QGLView;

class GLObjectBase {
	friend class QGLView;
	friend class GLRendererBase;
	friend QDataStream & operator<<(QDataStream & s, const GLObjectBase * p);
	friend QDataStream & operator>>(QDataStream & s, GLObjectBase *& p);
	friend GLObjectBase * loadFromQGLFile(const QString & filepath);

public:
	enum Type {
		glMesh,
		glLight,
		glCamera,
		glParticlesSystem
	};
	enum Pass {
		Solid,
		Transparent,
		Reflection,
		User
	};
	enum GeomPrimitives {
		Triangles = GL_TRIANGLES,
		Quads     = GL_QUADS
	};
	enum RenderMode {
		View  = 0,
		Point = GL_POINT,
		Line  = GL_LINE,
		Fill  = GL_FILL
	};

	explicit GLObjectBase();
	virtual ~GLObjectBase();

	virtual GLObjectBase * clone(bool withChildren = true);

	QString name() const { return name_; }
	void setName(const QString & name) { name_ = name; }
	// virtual GLuint hList() {return list;}
	virtual void init();
	virtual void draw(QOpenGLShaderProgram * prog, bool simplest = false);
	virtual void update() {}
	bool isInit() const { return is_init; }
	bool isTexturesLoaded() const { return is_tex_loaded; }
	Type type() const { return type_; }

	RenderMode renderMode() const { return render_mode; }
	void setRenderMode(RenderMode mode) { render_mode = mode; }

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

	GLObjectBase * parent() { return parent_; }
	void setParent(GLObjectBase * o) { parent_ = o; }
	bool hasParent() const { return parent_ != nullptr; }
	bool hasChildren() const { return children_.size() != 0; }
	void setView(QGLView * v);

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

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

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

	void move(const QVector3D & dv) {
		pos_ += dv;
		buildTransform();
	}
	void moveTo(const QVector3D & dv) {
		pos_ = 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) {
		pos_.setX(pos_.x() + o);
		buildTransform();
	}
	void moveY(GLfloat o) {
		pos_.setY(pos_.y() + o);
		buildTransform();
	}
	void moveZ(GLfloat o) {
		pos_.setZ(pos_.z() + o);
		buildTransform();
	}
	void setPosX(GLfloat o) {
		pos_.setX(o);
		buildTransform();
	}
	void setPosY(GLfloat o) {
		pos_.setY(o);
		buildTransform();
	}
	void setPosZ(GLfloat o) {
		pos_.setZ(o);
		buildTransform();
	}
	void setPos(GLfloat x, GLfloat y, GLfloat z) {
		pos_ = QVector3D(x, y, z);
		buildTransform();
	}
	void setPos(const QVector3D & p) {
		pos_ = p;
		buildTransform();
	}
	void resetPos() {
		pos_ = QVector3D(0., 0., 0.);
		buildTransform();
	}

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

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

	QVector3D scale() { return scale_; }
	float scaleX() { return scale_.x(); }
	float scaleY() { return scale_.y(); }
	float scaleZ() { return scale_.z(); }
	void scale(const QVector3D & sv) {
		raw_matrix = false;
		scale_ *= 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;
		scale_.setX(scale_.x() + a);
		buildTransform();
	}
	void scaleY(GLfloat a) {
		raw_matrix = false;
		scale_.setY(scale_.y() + a);
		buildTransform();
	}
	void scaleZ(GLfloat a) {
		raw_matrix = false;
		scale_.setZ(scale_.z() + a);
		buildTransform();
	}
	void setScale(const QVector3D & a) {
		raw_matrix = false;
		scale_     = a;
		buildTransform();
	}
	void setScale(GLfloat a) {
		raw_matrix = false;
		scale_     = QVector3D(a, a, a);
		buildTransform();
	}
	void setScaleX(GLfloat a) {
		raw_matrix = false;
		scale_.setX(a);
		buildTransform();
	}
	void setScaleY(GLfloat a) {
		raw_matrix = false;
		scale_.setY(a);
		buildTransform();
	}
	void setScaleZ(GLfloat a) {
		raw_matrix = false;
		scale_.setZ(a);
		buildTransform();
	}
	void resetScale() {
		raw_matrix = false;
		scale_     = QVector3D(1., 1., 1.);
		buildTransform();
	}

	QMatrix4x4 transform() { return mat_; }
	void setTransform(const QMatrix4x4 & t);
	bool isRawMatrix() { return raw_matrix; }

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

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

	bool isSelected() const { return selected_; }
	void setSelected(bool yes) { selected_ = yes; }
	void select();
	void deselect() { selected_ = false; }

	bool isSelectable() const { return select_; }
	void setSelectable(bool yes) { select_ = yes; }
	/*
	bool isWriteDepth() const {return write_depth_;}
	void setWriteDepth(bool yes) {write_depth_ = yes;}*/

	QColor color() const { return material_.color_diffuse; }
	void setColor(const QColor & c) {
		material_.color_diffuse = c;
		checkPass();
	}

	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(const Material & m, bool with_children = false);
	Material & material() { return material_; }

	const Box3D & boundingBox(bool withChildren = true) const { return bound; }
	GLVBO & VBO() { return vbo; }

	void calculateBoundingBox();

	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;
	QVector<Vector3d> points, puvws;
	QVector<Vector3i> faces, uvws, norms;
	QVector<Vector3d> normals;

	// QVector<GLfloat> d_vertices, d_normals, d_uvs;

protected:
	void addChildren(QList<GLObjectBase *> & list, GLObjectBase * where);
	void loadTextures(bool with_children = false);
	// void deleteTextures() {foreach (GLuint i, textures) currentQGLView->deleteTexture(i); textures.clear();}
	void buildTransform();
	void initInternal();
	void render(int * id = nullptr, QMap<int, GLObjectBase *> * ids = nullptr, int sh_id_loc = 0);
	void checkPass();
	virtual void localTransform(QMatrix4x4 & m);
	QMatrix4x4 worldMatrix(QMatrix4x4 parent) const;

	int pass_; // Pass
	bool is_init, is_tex_loaded, accept_light, accept_fog, /*write_depth_,*/ visible_, cast_shadow, rec_shadow, select_, selected_,
		raw_matrix;
	bool is_root;
	float line_width;
	Type type_;
	GeomPrimitives geom_prim;
	RenderMode render_mode;
	Material material_;
	Box3D bound;
	QVector3D pos_, angles_, scale_;
	QList<GLObjectBase *> children_;
	QList<GLuint> textures;
	QMatrix4x4 itransform_, mat_;
	// QColor color_;
	QString name_;
	GLenum blend_src, blend_dest;
	GLObjectBase * parent_;
	QGLViewBase * view_;
	GLVBO vbo;
	QVariantMap meta;
};

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

class Light: public GLObjectBase {
	friend class QGLView;
	friend class GLRendererBase;

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

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

	QVector3D direction, dir0, dir1;
	float angle_start;
	float angle_end;
	float intensity;
	float decay_const;
	float decay_linear;
	float decay_quadratic;
	float decay_start;
	float decay_end;
	Type light_type;
	GLFramebuffer shadow_map;
	QMatrix4x4 shadow_matrix;

protected:
};

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

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


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

#endif // GLOBJECT_H
