/*
 * OpenClonk, http://www.openclonk.org
 *
 * Copyright (c) 2001-2009, RedWolf Design GmbH, http://www.clonk.de/
 * Copyright (c) 2009-2016, The OpenClonk Team and contributors
 *
 * Distributed under the terms of the ISC license; see accompanying file
 * "COPYING" for details.
 *
 * "Clonk" is a registered trademark of Matthes Bender, used with permission.
 * See accompanying file "TRADEMARK" for details.
 *
 * To redistribute this file separately, substitute the full license texts
 * for the above references.
 */

#ifndef INC_StdMeshMaterial
#define INC_StdMeshMaterial

#include "graphics/C4Shader.h"
#include "graphics/C4Surface.h"

#include <tuple>

// TODO: Support more features of OGRE material scripts
// Refer to http://www.ogre3d.org/docs/manual/manual_14.html

class StdMeshMaterialParserCtx;

class StdMeshMaterialError: public std::exception
{
public:
	StdMeshMaterialError(const StdStrBuf& message, const char* file, unsigned int line);
	~StdMeshMaterialError() throw() override = default;

	const char* what() const throw() override { return Buf.getData(); }

protected:
	StdCopyStrBuf Buf;
};

class StdMeshMaterialShaderParameter
{
public:
	enum Type {
		AUTO,
		AUTO_TEXTURE_MATRIX, // Texture matrix for the i-th texture
		INT,
		FLOAT,
		FLOAT2,
		FLOAT3,
		FLOAT4,
		MATRIX_4X4
	};

	enum Auto {
		// TODO: OGRE auto values
		AUTO_DUMMY
	};

	StdMeshMaterialShaderParameter(); // type=FLOAT, value uninitialized
	StdMeshMaterialShaderParameter(Type type); // value uninitialized
	StdMeshMaterialShaderParameter(const StdMeshMaterialShaderParameter& other);
	StdMeshMaterialShaderParameter(StdMeshMaterialShaderParameter &&other);
	~StdMeshMaterialShaderParameter();

	StdMeshMaterialShaderParameter& operator=(const StdMeshMaterialShaderParameter& other);
	StdMeshMaterialShaderParameter& operator=(StdMeshMaterialShaderParameter &&other);

	Type GetType() const { return type; }
	void SetType(Type type); // changes type, new value is uninitialized

	// Getters
	Auto GetAuto() const { assert(type == AUTO); return a; }
	int GetInt() const { assert(type == INT || type == AUTO_TEXTURE_MATRIX); return i; }
	float GetFloat() const { assert(type == FLOAT); return f[0]; }
	const float* GetFloatv() const { assert(type == FLOAT2 || type == FLOAT3 || type == FLOAT4); return f; }
	const float* GetMatrix() const { assert(type == MATRIX_4X4); return matrix; }

	// Setters
	Auto& GetAuto() { assert(type == AUTO); return a; }
	int& GetInt() { assert(type == INT || type == AUTO_TEXTURE_MATRIX); return i; }
	float& GetFloat() { assert(type == FLOAT); return f[0]; }
	float* GetFloatv() { assert(type == FLOAT2 || type == FLOAT3 || type == FLOAT4); return f; }
	float* GetMatrix() { assert(type == MATRIX_4X4); return matrix; }
private:
	void CopyShallow(const StdMeshMaterialShaderParameter& other);
	void CopyDeep(const StdMeshMaterialShaderParameter& other);
	void Move(StdMeshMaterialShaderParameter &&other);

	Type type{FLOAT4};

	union {
		Auto a;
		int i;
		float f[4];
		float* matrix; // 16 floats, row-major order
	};
};

class StdMeshMaterialShaderParameters
{
public:
	StdMeshMaterialShaderParameters();

	void Load(StdMeshMaterialParserCtx& ctx);

	StdMeshMaterialShaderParameter& AddParameter(const char* name, StdMeshMaterialShaderParameter::Type type);

	std::vector<std::pair<StdCopyStrBuf, StdMeshMaterialShaderParameter> > NamedParameters;
private:
	StdMeshMaterialShaderParameter LoadConstParameter(StdMeshMaterialParserCtx& ctx);
	StdMeshMaterialShaderParameter LoadAutoParameter(StdMeshMaterialParserCtx& ctx);
};

enum StdMeshMaterialShaderType {
	SMMS_FRAGMENT,
	SMMS_VERTEX,
	SMMS_GEOMETRY
};

// Interface to load additional resources.
// Given a texture filename occuring in the
// material script, this should load the texture from wherever the material
// script is actually loaded, for example from a C4Group.
// Given a shader filename, this should load the shader text.
class StdMeshMaterialLoader
{
public:
	virtual C4Surface* LoadTexture(const char* filename) = 0;
	virtual StdStrBuf LoadShaderCode(const char* filename) = 0;
	virtual void AddShaderSlices(C4Shader& shader, int ssc) = 0; // add default shader slices
	virtual ~StdMeshMaterialLoader() = default;
};

// This is just a container class to hold the shader code; the C4Shader
// objects are later created from that code by mixing them with the default
// slices.
class StdMeshMaterialShader
{
public:
	StdMeshMaterialShader(const char* filename, const char* name, const char* language, StdMeshMaterialShaderType /* type */, const char* code):
		Filename(filename), Name(name), Language(language), Code(code)
	{}

	const char* GetFilename() const { return Filename.getData(); }
	const char* GetCode() const { return Code.getData(); }

private:
	StdCopyStrBuf Filename;
	StdCopyStrBuf Name;
	StdCopyStrBuf Language;
	StdCopyStrBuf Code;
};

class StdMeshMaterialProgram
{
public:
	StdMeshMaterialProgram(const char* name, const StdMeshMaterialShader* fragment_shader, const StdMeshMaterialShader* vertex_shader, const StdMeshMaterialShader* geometry_shader);
	bool AddParameterNames(const StdMeshMaterialShaderParameters& parameters); // returns true if some parameter names were not yet registered.

	bool IsCompiled() const { return Shader.Initialised(); }
	bool Compile(StdMeshMaterialLoader& loader);

	const C4Shader* GetShader(int ssc) const;
	int GetParameterIndex(const char* name) const;

	const StdMeshMaterialShader* GetFragmentShader() const { return FragmentShader; }
	const StdMeshMaterialShader* GetVertexShader() const { return VertexShader; }
	const StdMeshMaterialShader* GetGeometryShader() const { return GeometryShader; }
private:
	bool CompileShader(StdMeshMaterialLoader& loader, C4Shader& shader, int ssc);

	// Human-readable program name
	const StdCopyStrBuf Name;

	// Program components
	const StdMeshMaterialShader* FragmentShader;
	const StdMeshMaterialShader* VertexShader;
	const StdMeshMaterialShader* GeometryShader;

	// Compiled shaders
	C4Shader Shader;
	C4Shader ShaderMod2;
	C4Shader ShaderLight;
	C4Shader ShaderLightMod2;

	// Filled as program references are encountered;
	std::vector<StdCopyStrBuf> ParameterNames;
};

class StdMeshMaterialTextureUnit
{
public:
	enum TexAddressModeType
	{
		AM_Wrap,
		AM_Clamp,
		AM_Mirror,
		AM_Border
	};

	enum FilteringType
	{
		F_None,
		F_Point,
		F_Linear,
		F_Anisotropic
	};

	enum BlendOpType
	{
		BO_Replace,
		BO_Add,
		BO_Modulate,
		BO_AlphaBlend
	};

	enum BlendOpExType
	{
		BOX_Source1,
		BOX_Source2,
		BOX_Modulate,
		BOX_ModulateX2,
		BOX_ModulateX4,
		BOX_Add,
		BOX_AddSigned,
		BOX_AddSmooth,
		BOX_Subtract,
		BOX_BlendDiffuseAlpha,
		BOX_BlendTextureAlpha,
		BOX_BlendCurrentAlpha,
		BOX_BlendManual,
		BOX_Dotproduct,
		BOX_BlendDiffuseColor
	};

	enum BlendOpSourceType
	{
		BOS_Current,
		BOS_Texture,
		BOS_Diffuse,
		BOS_Specular,
		BOS_PlayerColor, // not specified in ogre, added in OpenClonk
		BOS_Manual
	};

	struct Transformation
	{
		enum Type
		{
			T_SCROLL,
			T_SCROLL_ANIM,
			T_ROTATE,
			T_ROTATE_ANIM,
			T_SCALE,
			T_TRANSFORM,
			T_WAVE_XFORM
		};

		enum XFormType
		{
			XF_SCROLL_X,
			XF_SCROLL_Y,
			XF_ROTATE,
			XF_SCALE_X,
			XF_SCALE_Y
		};

		enum WaveType
		{
			W_SINE,
			W_TRIANGLE,
			W_SQUARE,
			W_SAWTOOTH,
			W_INVERSE_SAWTOOTH
		};

		Type TransformType;

		union
		{
			struct { float X; float Y; } Scroll;
			struct { float XSpeed; float YSpeed; } ScrollAnim;
			struct { float Angle; } Rotate;
			struct { float RevsPerSec; } RotateAnim;
			struct { float X; float Y; } Scale;
			struct { float M[16]; } Transform;
			struct { XFormType XForm; WaveType Wave; float Base; float Frequency; float Phase; float Amplitude; } WaveXForm;
		};

		double GetScrollX(double t) const { assert(TransformType == T_SCROLL_ANIM); return ScrollAnim.XSpeed * t; }
		double GetScrollY(double t) const { assert(TransformType == T_SCROLL_ANIM); return ScrollAnim.YSpeed * t; }
		double GetRotate(double t) const { assert(TransformType == T_ROTATE_ANIM); return fmod(RotateAnim.RevsPerSec * t, 1.0) * 360.0; }
		double GetWaveXForm(double t) const;
	};

	// Ref-counted texture. When a meterial inherits from one which contains
	// a TextureUnit, then they will share the same C4TexRef.
	class Tex
	{
	public:
		Tex(C4Surface* Surface); // Takes ownership
		~Tex();

		unsigned int RefCount;

		// TODO: Note this cannot be C4Surface here, because C4Surface
		// does not have a virtual destructor, so we couldn't delete it
		// properly in that case. I am a bit annoyed that this
		// currently requires a cross-ref to lib/texture. I think
		// C4Surface should go away and the file loading/saving
		// should be free functions instead. I also think the file
		// loading/saving should be decoupled from the surfaces, so we
		// can skip the surface here and simply use a C4TexRef. armin.
		C4Surface* Surf;
		C4TexRef& Texture;
	};

	// Simple wrapper which handles refcounting of Tex
	class TexPtr
	{
	public:
		TexPtr(C4Surface* Surface);
		TexPtr(const TexPtr& other);
		~TexPtr();
		
		TexPtr& operator=(const TexPtr& other);
		
		Tex* pTex;
	};

	StdMeshMaterialTextureUnit();

	void LoadTexture(StdMeshMaterialParserCtx& ctx, const char* texname);
	void Load(StdMeshMaterialParserCtx& ctx);

	bool HasTexture() const { return !Textures.empty(); }
	size_t GetNumTextures() const { return Textures.size(); }
	const C4TexRef& GetTexture(unsigned int i) const { return Textures[i].pTex->Texture; }
	bool HasFrameAnimation() const { return Duration > 0; }
	bool HasTexCoordAnimation() const { return !Transformations.empty(); }

	StdCopyStrBuf Name;
	float Duration{0.0f}; // Duration of texture animation, if any.

	TexAddressModeType TexAddressMode{AM_Wrap};
	float TexBorderColor[4];
	FilteringType Filtering[3]; // min, max, mipmap

	BlendOpExType ColorOpEx{BOX_Modulate};
	BlendOpSourceType ColorOpSources[2];
	float ColorOpManualFactor{0.0f};
	float ColorOpManualColor1[3];
	float ColorOpManualColor2[3];

	BlendOpExType AlphaOpEx{BOX_Modulate};
	BlendOpSourceType AlphaOpSources[2];
	float AlphaOpManualFactor{0.0f};
	float AlphaOpManualAlpha1;
	float AlphaOpManualAlpha2;

	// Transformations to be applied to texture coordinates in order
	std::vector<Transformation> Transformations;

private:
	std::vector<TexPtr> Textures;
};

class StdMeshMaterialPass
{
public:
	enum CullHardwareType
	{
		CH_Clockwise,
		CH_CounterClockwise,
		CH_None
	};

	enum SceneBlendType
	{
		SB_One,
		SB_Zero,
		SB_DestColor,
		SB_SrcColor,
		SB_OneMinusDestColor,
		SB_OneMinusSrcColor,
		SB_DestAlpha,
		SB_SrcAlpha,
		SB_OneMinusDestAlpha,
		SB_OneMinusSrcAlpha
	};

	enum DepthFunctionType
	{
		DF_AlwaysFail,
		DF_AlwaysPass,
		DF_Less,
		DF_LessEqual,
		DF_Equal,
		DF_NotEqual,
		DF_GreaterEqual,
		DF_Greater
	};

	StdMeshMaterialPass();
	void Load(StdMeshMaterialParserCtx& ctx);

	bool IsOpaque() const { return SceneBlendFactors[1] == SB_Zero; }

	StdCopyStrBuf Name;
	std::vector<StdMeshMaterialTextureUnit> TextureUnits;

	float Ambient[4];
	float Diffuse[4];
	float Specular[4];
	float Emissive[4];
	float Shininess;

	bool DepthCheck{true};
	bool DepthWrite{true};

	CullHardwareType CullHardware{CH_Clockwise};
	SceneBlendType SceneBlendFactors[2];
	DepthFunctionType AlphaRejectionFunction;
	float AlphaRejectionValue;
	bool AlphaToCoverage;

	struct ShaderInstance
	{
		// This points into the StdMeshMatManager maps
		const StdMeshMaterialShader* Shader;
		// Parameters for this instance
		StdMeshMaterialShaderParameters Parameters;
	};

	class ProgramInstance
	{
	public:
		ProgramInstance(const StdMeshMaterialProgram* program, const ShaderInstance* fragment_instance, const ShaderInstance* vertex_instance, const ShaderInstance* geometry_instance);

		// This points into the StdMeshMatManager map
		const StdMeshMaterialProgram* const Program;

		// Parameters for this instance
		struct ParameterRef {
			const StdMeshMaterialShaderParameter* Parameter;
			int UniformIndex; // Index into parameter table for this program
		};

		std::vector<ParameterRef> Parameters;

	private:
		void LoadParameterRefs(const ShaderInstance* instance);
	};

	ShaderInstance FragmentShader;
	ShaderInstance VertexShader;
	ShaderInstance GeometryShader;

	// This is a shared_ptr and not a unique_ptr so that this class is
	// copyable, so it can be inherited. However, when the inherited
	// material is prepared, the ProgramInstance will be overwritten
	// anyway, so in that sense a unique_ptr would be enough. We could
	// change it and make this class only movable (not copyable), and
	// provide inheritance by copying all other fields, and letting
	// PrepareMaterial fill the program instance.
	std::shared_ptr<ProgramInstance> Program;

private:
	void LoadShaderRef(StdMeshMaterialParserCtx& ctx, StdMeshMaterialShaderType type);
};

class StdMeshMaterialTechnique
{
public:
	StdMeshMaterialTechnique();

	void Load(StdMeshMaterialParserCtx& ctx);

	bool IsOpaque() const;

	StdCopyStrBuf Name;
	std::vector<StdMeshMaterialPass> Passes;

	// Filled in by gfx implementation: Whether this technique is available on
	// the hardware and gfx engine (DX/GL) we are running on
	bool Available{false};
};

class StdMeshMaterial
{
public:
	StdMeshMaterial();
	void Load(StdMeshMaterialParserCtx& ctx);

	bool IsOpaque() const { assert(BestTechniqueIndex >= 0); return Techniques[BestTechniqueIndex].IsOpaque(); }

	// Location the Material was loaded from
	StdCopyStrBuf FileName;
	unsigned int Line{0};

	// Material name
	StdCopyStrBuf Name;

	// Not currently used in Clonk, but don't fail when we see this in a
	// Material script:
	bool ReceiveShadows{true};

	// Available techniques
	std::vector<StdMeshMaterialTechnique> Techniques;

	// Filled in by gfx implementation: Best technique to use
	int BestTechniqueIndex{-1}; // Don't use a pointer into the Technique vector to save us from implementing a copyctor
};

class StdMeshMatManager
{
	friend class StdMeshMaterialUpdate;
private:
	typedef std::map<StdCopyStrBuf, StdMeshMaterial> MaterialMap;

public:
	enum ShaderLoadFlag {
		SMM_AcceptExisting = 1,
		SMM_ForceReload = 2
	};

	class Iterator
	{
		friend class StdMeshMatManager;
	public:
		Iterator(const MaterialMap::iterator& iter): iter_(iter) {}
		Iterator(const Iterator& iter) = default;

		Iterator operator=(const Iterator& iter) { iter_ = iter.iter_; return *this; }
		Iterator& operator++() { ++iter_; return *this; }
		bool operator==(const Iterator& other) const { return iter_ == other.iter_; }
		bool operator!=(const Iterator& other) const { return iter_ != other.iter_; }

		const StdMeshMaterial& operator*() const { return iter_->second; }
		const StdMeshMaterial* operator->() const { return &iter_->second; }
	private:
		MaterialMap::iterator iter_;
	};

	// Remove all materials from manager. Make sure there is no StdMesh
	// referencing any out there before calling this.
	void Clear();

	// Parse a material script file, and add the materials to the manager.
	// filename may be nullptr if the source is not a file. It will only be used
	// for error messages.
	// Throws StdMeshMaterialError.
	// Returns a set of all loaded materials.
	std::set<StdCopyStrBuf> Parse(const char* mat_script, const char* filename, StdMeshMaterialLoader& loader);

	// Get material by name. nullptr if there is no such material with this name.
	const StdMeshMaterial* GetMaterial(const char* material_name) const;

	Iterator Begin() { return Iterator(Materials.begin()); }
	Iterator End() { return Iterator(Materials.end()); }
	void Remove(const StdStrBuf& name, class StdMeshMaterialUpdate* update);
	Iterator Remove(const Iterator& iter, class StdMeshMaterialUpdate* update);

	const StdMeshMaterialShader* AddShader(const char* filename, const char* name, const char* language, StdMeshMaterialShaderType type, const char* text, uint32_t load_flags); // if load_flags & SMM_AcceptExisting, the function returns the existing shader, otherwise returns nullptr.
	const StdMeshMaterialProgram* AddProgram(const char* name, StdMeshMaterialLoader& loader, const StdMeshMaterialPass::ShaderInstance& fragment_shader, const StdMeshMaterialPass::ShaderInstance& vertex_shader, const StdMeshMaterialPass::ShaderInstance& geometry_shader); // returns nullptr if shader code cannot be compiled

	const StdMeshMaterialShader* GetFragmentShader(const char* name) const;
	const StdMeshMaterialShader* GetVertexShader(const char* name) const;
	const StdMeshMaterialShader* GetGeometryShader(const char* name) const;
private:
	MaterialMap Materials;

	// Shader code for custom shaders.
	typedef std::map<StdCopyStrBuf, std::unique_ptr<StdMeshMaterialShader>> ShaderMap;
	ShaderMap FragmentShaders;
	ShaderMap VertexShaders;
	ShaderMap GeometryShaders;

	// Linked programs
	typedef std::map<std::tuple<const StdMeshMaterialShader*, const StdMeshMaterialShader*, const StdMeshMaterialShader*>, std::unique_ptr<StdMeshMaterialProgram> > ProgramMap;
	ProgramMap Programs;
};

extern StdMeshMatManager MeshMaterialManager;

#endif
