#pragma once

#include "volumeshop.h"

#include "Exception.h"

#include <iostream>
#include <fstream>
#include <string>
#include <list>
#include <map>

class Program;

class Shader
{
	friend class Program;

public:

	~Shader()
	{
		uninitialize();
	};

protected:

	Shader(const unsigned int uType) : m_uType(uType), m_uObject(0)
	{
		initialize();
	};

	void initialize()
	{
		if (!m_uObject)
		{
			m_uObject = glCreateShaderObjectARB(m_uType);
			const GLenum glError = glGetError();

			if (glError != GL_NO_ERROR)
			{
				std::cerr << "Error generating shader (" << gluErrorString(glError) << ")." << std::endl << std::endl;
				throw Exception("Error generating shader (%s).",gluErrorString(glError));
			}

			s_mapAttachment[m_uObject] = 0;
		}
	};

	void uninitialize()
	{
		if (m_uObject)
		{
			if (s_mapAttachment[m_uObject] == 0)
			{
				glDeleteObjectARB(m_uObject);
				const GLenum glError = glGetError();

				if (glError != GL_NO_ERROR)
				{
					std::cerr << "Error deleting shader (" << gluErrorString(glError) << ")." << std::endl << std::endl;
					throw Exception("Error deleting shader (%s).",gluErrorString(glError));
				}
			}
		}
	};

	void load(const std::string & strFilename)
	{
		if (m_uObject)
		{
			std::ifstream fin(strFilename.c_str());
			
			if(!fin) 
			{
				std::cerr << "Unable to load " << strFilename << std::endl;
				throw Exception("Unable to load '%s'.",strFilename.c_str);
			}

			std::string strLine;
			std::string strCode;

			// Probably not efficient, but who cares...
			while(std::getline(fin, strLine))
				strCode += strLine + "\n";

			const char *vpStrings[] = { strCode.c_str() };
		
			glShaderSourceARB(m_uObject,1,vpStrings,NULL);
			GLenum glError = glGetError();

			if (glError != GL_NO_ERROR)
			{
				std::cerr << "Error loading shader " << strFilename << " (" << gluErrorString(glError) << ")." << std::endl << std::endl;
				throw Exception("Error loading shader '%s' (%s).",strFilename.c_str(),gluErrorString(glError));
			}

			glCompileShaderARB(m_uObject);
			glError = glGetError();

			if (glError != GL_NO_ERROR)
			{
				std::cerr << "Error compiling shader " << strFilename << " (" << gluErrorString(glError) << ")." << std::endl << std::endl;
				throw Exception("Error compiling shader '%s' (%s).",strFilename.c_str(),gluErrorString(glError));
			}

			int iReturn = 1;
			glGetObjectParameterivARB(m_uObject,GL_OBJECT_COMPILE_STATUS_ARB,&iReturn);

			if (!iReturn)
			{
				int iLength = 0;

				glGetObjectParameterivARB(m_uObject, GL_OBJECT_INFO_LOG_LENGTH_ARB,&iLength);

				char *pInfo = new char[iLength];
				int iWritten = 0;

				glGetInfoLogARB(m_uObject, iLength, &iWritten, pInfo);
				std::string strInfo(pInfo);
				delete[] pInfo;

				std::cerr << "Error compiling shader " << strFilename <<  std::endl << "(" << std::endl << strInfo << ")." << std::endl << std::endl;
				throw Exception("Error compiling shader '%s' (%s).",strFilename.c_str(),strInfo.c_str());
			}
		}
	};

	void compile()
	{
		glCompileShaderARB(m_uObject);
		const GLenum glError = glGetError();

		if (glError != GL_NO_ERROR)
		{
			std::cerr << "Error compiling shader (" << gluErrorString(glError) << ")." << std::endl << std::endl;
			throw Exception("Error compiling shader (%s).",gluErrorString(glError));
		}

		int iReturn = 1;
		glGetObjectParameterivARB(m_uObject,GL_OBJECT_COMPILE_STATUS_ARB,&iReturn);

		if (!iReturn)
		{
			int iLength = 0;

			glGetObjectParameterivARB(m_uObject, GL_OBJECT_INFO_LOG_LENGTH_ARB,&iLength);

			char *pInfo = new char[iLength];
			int iWritten = 0;

			glGetInfoLogARB(m_uObject, iLength, &iWritten, pInfo);
			std::string strInfo(pInfo);
			delete[] pInfo;

			std::cerr << "Error compiling shader " <<  std::endl << "(" << std::endl << strInfo << ")." << std::endl << std::endl;
			throw Exception("Error compiling shader (%s).",strInfo.c_str());
		}
	};

	const unsigned int GetObject() const
	{
		return m_uObject;
	};

	const unsigned int GetType() const
	{
		return m_uType;
	};

	void attach() const
	{
		s_mapAttachment[m_uObject]++;
	};

	void detach() const
	{
		s_mapAttachment[m_uObject]--;
	};

private:

	const unsigned int m_uType;
	unsigned int m_uObject;

	static std::map<unsigned int, unsigned int> s_mapAttachment;
};

class FragmentShader : public Shader
{
public:
	FragmentShader(const std::string & strFilename) : Shader(GL_FRAGMENT_SHADER_ARB)
	{
		load(strFilename);
	};

	~FragmentShader()
	{
	};
};

class VertexShader : public Shader
{
public:
	VertexShader(const std::string & strFilename) : Shader(GL_VERTEX_SHADER_ARB)
	{
		load(strFilename);
	};

	~VertexShader()
	{
	};
};

class Program
{
public:

	Program() : m_uProgram(0)
	{
		initialize();
	};

	Program(const Shader & shaShader) : m_uProgram(0)
	{
		initialize();
		attach(shaShader);
	};

	Program(const Shader & shaShaderA, const Shader & shaShaderB) : m_uProgram(0)
	{
		initialize();
		attach(shaShaderA);
		attach(shaShaderB);
	};

	~Program()
	{
		uninitialize();
	};

	void attach(const Shader & shaShader)
	{
		bool bAttached = false;

		for (std::list<Shader>::const_iterator i = m_lisShaders.begin(); i != m_lisShaders.end(); i++)
		{
			if ((*i).GetObject() == shaShader.GetObject())
			{
				bAttached = true;
				break;
			}
		}

		if (!bAttached)
		{
			glAttachObjectARB(m_uProgram, shaShader.GetObject());
			GLenum glError = glGetError();

			if (glError != GL_NO_ERROR)
			{
				std::cerr << "Error attaching shader (" << gluErrorString(glError) << ")." << std::endl << std::endl;
				throw Exception("Error attaching shader (%s).",gluErrorString(glError));
			}

			m_lisShaders.push_back(shaShader);
			shaShader.attach();

			link();

		}
	};

	void detach(const Shader & shaShader)
	{
		std::list<Shader>::iterator iteShader = m_lisShaders.end();

		for (std::list<Shader>::iterator i = m_lisShaders.begin(); i != m_lisShaders.end(); i++)
		{
			if ((*i).GetObject() == shaShader.GetObject())
			{
				iteShader = i;
				break;
			}
		}

		if (iteShader != m_lisShaders.end())
		{
			glDetachObjectARB(m_uProgram, shaShader.GetObject());
			GLenum glError = glGetError();

			if (glError != GL_NO_ERROR)
			{
				std::cerr << "Error detaching shader (" << gluErrorString(glError) << ")." << std::endl << std::endl;
				throw Exception("Error detaching shader (%s).",gluErrorString(glError));
			}

			m_lisShaders.erase(iteShader);
			shaShader.detach();

			link();
		}
	};

	void bind()
	{
		glUseProgramObjectARB(m_uProgram);
		const GLenum glError = glGetError();

		if (glError != GL_NO_ERROR)
		{
			std::cerr << "Error binding program (" << gluErrorString(glError) << ")." << std::endl << std::endl;
			throw Exception("Error binding program (%s).",gluErrorString(glError));
		}

	};

	void release()
	{
		glUseProgramObjectARB(0);
		const GLenum glError = glGetError();

		if (glError != GL_NO_ERROR)
		{
			std::cerr << "Error releasing program (" << gluErrorString(glError) << ")." << std::endl << std::endl;
			throw Exception("Error releasing program (%s).",gluErrorString(glError));
		}
	};

	const int GetUniformLocation(const std::string & strName) const
	{
		int iLocation = -1;

		iLocation = glGetUniformLocationARB(m_uProgram,strName.c_str()); 
		const GLenum glError = glGetError();

		if (glError != GL_NO_ERROR)
		{
			std::cerr << "Error retrieving location (" << gluErrorString(glError) << ")." << std::endl << std::endl;
			throw Exception("Error retrieving location (%s).",gluErrorString(glError));
		}

		return iLocation;
	};

	const int GetAttributeLocation(const std::string & strName) const
	{
		int iLocation = -1;

		iLocation = glGetUniformLocationARB(m_uProgram,strName.c_str()); 
		const GLenum glError = glGetError();

		if (glError != GL_NO_ERROR)
		{
			std::cerr << "Error retrieving location (" << gluErrorString(glError) << ")." << std::endl << std::endl;
			throw Exception("Error retrieving location (%s).",gluErrorString(glError));
		}

		return iLocation;
	};

protected:

	void link()
	{
		glLinkProgramARB(m_uProgram);
		const GLenum glError = glGetError();

		if (glError != GL_NO_ERROR)
		{
			std::cerr << "Error linking program (" << gluErrorString(glError) << ")." << std::endl << std::endl;
			throw Exception("Error linking program (%s).",gluErrorString(glError));
		}

		int iReturn = 1;
		glGetObjectParameterivARB(m_uProgram,GL_OBJECT_LINK_STATUS_ARB,&iReturn);

		if (!iReturn)
		{
			int iLength = 0;

			glGetObjectParameterivARB(m_uProgram, GL_OBJECT_INFO_LOG_LENGTH_ARB,&iLength);

			char *pInfo = new char[iLength];
			int iWritten = 0;

			glGetInfoLogARB(m_uProgram, iLength, &iWritten, pInfo);
			std::string strInfo(pInfo);
			delete[] pInfo;

			std::cerr << "Error linking program " <<  std::endl << "(" << std::endl << strInfo << ")." << std::endl << std::endl;
			throw Exception("Error linking program (%s).",strInfo.c_str());
		}
	};

	void initialize()
	{
		if (!m_uProgram)
		{
			m_uProgram = glCreateProgramObjectARB();
			const GLenum glError = glGetError();

			if (glError != GL_NO_ERROR)
			{
				std::cerr << "Error creating program (" << gluErrorString(glError) << ")." << std::endl << std::endl;
				throw Exception("Error creating program (%s).",gluErrorString(glError));
			}
		}
	};

	void uninitialize()
	{
		if (m_uProgram)
		{
			for (std::list<Shader>::const_iterator i = m_lisShaders.begin(); i != m_lisShaders.end(); i++)
			{
				glDetachObjectARB(m_uProgram, (*i).GetObject());
				const GLenum glError = glGetError();

				if (glError != GL_NO_ERROR)
				{
					std::cerr << "Error detaching shader (" << gluErrorString(glError) << ")." << std::endl << std::endl;
					throw Exception("Error detaching shader (%s).",gluErrorString(glError));

				}

				(*i).detach();
			}

			m_lisShaders.clear();

			glDeleteObjectARB(m_uProgram);
			const GLenum glError = glGetError();

			if (glError != GL_NO_ERROR)
			{
				std::cerr << "Error deleting program (" << gluErrorString(glError) << ")." << std::endl << std::endl;
				throw Exception("Error deleting program (%s).",gluErrorString(glError));
			}
		}
	};

private:

	unsigned int m_uProgram;
	std::list<Shader> m_lisShaders;

};