/* Copyright (C) 2011 Wildfire Games.
 * This file is part of 0 A.D.
 *
 * 0 A.D. is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 2 of the License, or
 * (at your option) any later version.
 *
 * 0 A.D. 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 General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with 0 A.D.  If not, see <http://www.gnu.org/licenses/>.
 */

#include "precompiled.h"

#include "MessageHandler.h"

#include "../CommandProc.h"

#include "graphics/Patch.h"
#include "graphics/TerrainTextureManager.h"
#include "graphics/TerrainTextureEntry.h"
#include "graphics/Terrain.h"
#include "ps/Game.h"
#include "ps/World.h"
#include "lib/ogl.h"
#include "lib/res/graphics/ogl_tex.h"
#include "simulation2/Simulation2.h"
#include "simulation2/components/ICmpPathfinder.h"
#include "simulation2/components/ICmpTerrain.h"

#include "../Brushes.h"
#include "../DeltaArray.h"
#include "../View.h"

namespace AtlasMessage {

QUERYHANDLER(GetTerrainGroups)
{
	const CTerrainTextureManager::TerrainGroupMap &groups = g_TexMan.GetGroups();
	std::vector<std::wstring> groupNames;
	for (CTerrainTextureManager::TerrainGroupMap::const_iterator it = groups.begin(); it != groups.end(); ++it)
		groupNames.push_back(it->first.FromUTF8());
	msg->groupNames = groupNames;
}

static bool CompareTerrain(const sTerrainGroupPreview& a, const sTerrainGroupPreview& b) 
{
	return (wcscmp(a.name.c_str(), b.name.c_str()) < 0);
}

QUERYHANDLER(GetTerrainGroupPreviews)
{
	std::vector<sTerrainGroupPreview> previews;

	CTerrainGroup* group = g_TexMan.FindGroup(CStrW(*msg->groupName).ToUTF8());
	for (std::vector<CTerrainTextureEntry*>::const_iterator it = group->GetTerrains().begin(); it != group->GetTerrains().end(); ++it)
	{
		previews.push_back(sTerrainGroupPreview());
		previews.back().name = (*it)->GetTag().FromUTF8();

		std::vector<unsigned char> buf (msg->imageWidth*msg->imageHeight*3);

		// It's not good to shrink the entire texture to fit the small preview
		// window, since it's the fine details in the texture that are
		// interesting; so just go down one mipmap level, then crop a chunk
		// out of the middle.

		// Read the size of the texture. (Usually loads the texture from
		// disk, which is slow.)
		GLint w, h;
		ssize_t level = 1; // level 0 is the original size
		(*it)->GetTexture()->Bind();
		glGetTexLevelParameteriv(GL_TEXTURE_2D, level, GL_TEXTURE_WIDTH,  &w);
		glGetTexLevelParameteriv(GL_TEXTURE_2D, level, GL_TEXTURE_HEIGHT, &h);

		if (w < msg->imageWidth || h < msg->imageHeight)
		{
			// Oops, too small to preview - just use a flat colour
			u32 c = (*it)->GetBaseColor();
			for (ssize_t i = 0; i < msg->imageWidth*msg->imageHeight; ++i)
			{
				buf[i*3+0] = (c>>16) & 0xff;
				buf[i*3+1] = (c>>8) & 0xff;
				buf[i*3+2] = (c>>0) & 0xff;
			}
		}
		else
		{
			// Read the whole texture into a new buffer
			unsigned char* texdata = new unsigned char[w*h*3];
			glGetTexImage(GL_TEXTURE_2D, level, GL_RGB, GL_UNSIGNED_BYTE, texdata);

			// Extract the middle section (as a representative preview),
			// and copy into buf
			unsigned char* texdata_ptr = texdata + (w*(h - msg->imageHeight)/2 + (w - msg->imageWidth)/2) * 3;
			unsigned char* buf_ptr = &buf[0];
			for (ssize_t y = 0; y < msg->imageHeight; ++y)
			{
				memcpy(buf_ptr, texdata_ptr, msg->imageWidth*3);
				buf_ptr += msg->imageWidth*3;
				texdata_ptr += w*3;
			}

			delete[] texdata;
		}

		previews.back().loaded = (*it)->GetTexture()->IsLoaded();
		previews.back().imageWidth = msg->imageWidth;
		previews.back().imageHeight = msg->imageHeight;
		previews.back().imageData = buf;
	}

	// Sort the list alphabetically by name
	std::sort(previews.begin(), previews.end(), CompareTerrain);
	msg->previews = previews;
}

QUERYHANDLER(GetTerrainPassabilityClasses)
{
	CmpPtr<ICmpPathfinder> cmpPathfinder(*View::GetView_Game()->GetSimulation2(), SYSTEM_ENTITY);
	if (!cmpPathfinder.null())
	{
		std::map<std::string, ICmpPathfinder::pass_class_t> classes = cmpPathfinder->GetPassabilityClasses();

		std::vector<std::wstring> classNames;
		for (std::map<std::string, ICmpPathfinder::pass_class_t>::iterator it = classes.begin(); it != classes.end(); ++it)
			classNames.push_back(CStr(it->first).FromUTF8());
		msg->classNames = classNames;
	}
}

//////////////////////////////////////////////////////////////////////////

namespace {

struct TerrainTile
{
	TerrainTile(CTerrainTextureEntry* t, ssize_t p) : tex(t), priority(p) {}
	CTerrainTextureEntry* tex;
	ssize_t priority;
};

class TerrainArray : public DeltaArray2D<TerrainTile>
{
public:
	void Init()
	{
		m_Terrain = g_Game->GetWorld()->GetTerrain();
		m_VertsPerSide = g_Game->GetWorld()->GetTerrain()->GetVerticesPerSide();
	}

	void UpdatePriority(ssize_t x, ssize_t y, CTerrainTextureEntry* tex, ssize_t priorityScale, ssize_t& priority)
	{
		CMiniPatch* tile = m_Terrain->GetTile(x, y);
		if (!tile)
			return; // tile was out-of-bounds

		// If this tile matches the current texture, we just want to match its
		// priority; otherwise we want to exceed its priority
		if (tile->GetTextureEntry() == tex)
			priority = std::max(priority, tile->GetPriority()*priorityScale);
		else
			priority = std::max(priority, tile->GetPriority()*priorityScale + 1);
	}

	CTerrainTextureEntry* GetTexEntry(ssize_t x, ssize_t y)
	{
		if (size_t(x) >= size_t(m_VertsPerSide-1) || size_t(y) >= size_t(m_VertsPerSide-1))
			return NULL;

		return get(x, y).tex;
	}

	ssize_t GetPriority(ssize_t x, ssize_t y)
	{
		if (size_t(x) >= size_t(m_VertsPerSide-1) || size_t(y) >= size_t(m_VertsPerSide-1))
			return 0;

		return get(x, y).priority;
	}

	void PaintTile(ssize_t x, ssize_t y, CTerrainTextureEntry* tex, ssize_t priority)
	{
		// Ignore out-of-bounds tiles
		if (size_t(x) >= size_t(m_VertsPerSide-1) || size_t(y) >= size_t(m_VertsPerSide-1))
			return;

		set(x,y, TerrainTile(tex, priority));
	}

	ssize_t GetTilesPerSide()
	{
		return m_VertsPerSide-1;
	}

protected:
	TerrainTile getOld(ssize_t x, ssize_t y)
	{
		CMiniPatch* mp = m_Terrain->GetTile(x, y);
		ENSURE(mp);
		return TerrainTile(mp->Tex, mp->Priority);
	}
	void setNew(ssize_t x, ssize_t y, const TerrainTile& val)
	{
		CMiniPatch* mp = m_Terrain->GetTile(x, y);
		ENSURE(mp);
		mp->Tex = val.tex;
		mp->Priority = val.priority;
	}

	CTerrain* m_Terrain;
	ssize_t m_VertsPerSide;
};

}

BEGIN_COMMAND(PaintTerrain)
{
	TerrainArray m_TerrainDelta;
	ssize_t m_i0, m_j0, m_i1, m_j1;

	cPaintTerrain()
	{
		m_TerrainDelta.Init();
	}

	void MakeDirty()
	{
		g_Game->GetWorld()->GetTerrain()->MakeDirty(m_i0, m_j0, m_i1, m_j1, RENDERDATA_UPDATE_INDICES);
		CmpPtr<ICmpTerrain> cmpTerrain(*g_Game->GetSimulation2(), SYSTEM_ENTITY);
		if (!cmpTerrain.null())
			cmpTerrain->MakeDirty(m_i0, m_j0, m_i1, m_j1);
	}

	void Do()
	{
		g_CurrentBrush.m_Centre = msg->pos->GetWorldSpace();

		ssize_t x0, y0;
		g_CurrentBrush.GetBottomLeft(x0, y0);

		CTerrainTextureEntry* texentry = g_TexMan.FindTexture(CStrW(*msg->texture).ToUTF8());
		if (! texentry)
		{
			debug_warn(L"Can't find texentry"); // TODO: nicer error handling
			return;
		}

		// Priority system: If the new tile should have a high priority,
		// set it to one plus the maximum priority of all surrounding tiles
		// that aren't included in the brush (so that it's definitely the highest).
		// Similar for low priority.
		ssize_t priorityScale = (msg->priority == ePaintTerrainPriority::HIGH ? +1 : -1);
		ssize_t priority = 0;

		for (ssize_t dy = -1; dy < g_CurrentBrush.m_H+1; ++dy)
		{
			for (ssize_t dx = -1; dx < g_CurrentBrush.m_W+1; ++dx)
			{
				if (!(g_CurrentBrush.Get(dx, dy) > 0.5f)) // ignore tiles that will be painted over
					m_TerrainDelta.UpdatePriority(x0+dx, y0+dy, texentry, priorityScale, priority);
			}
		}

		for (ssize_t dy = 0; dy < g_CurrentBrush.m_H; ++dy)
		{
			for (ssize_t dx = 0; dx < g_CurrentBrush.m_W; ++dx)
			{
				if (g_CurrentBrush.Get(dx, dy) > 0.5f) // TODO: proper solid brushes
					m_TerrainDelta.PaintTile(x0+dx, y0+dy, texentry, priority*priorityScale);
			}
		}

		m_i0 = x0;
		m_j0 = y0;
		m_i1 = x0 + g_CurrentBrush.m_W;
		m_j1 = y0 + g_CurrentBrush.m_H;
		MakeDirty();
	}

	void Undo()
	{
		m_TerrainDelta.Undo();
		MakeDirty();
	}

	void Redo()
	{
		m_TerrainDelta.Redo();
		MakeDirty();
	}

	void MergeIntoPrevious(cPaintTerrain* prev)
	{
		prev->m_TerrainDelta.OverlayWith(m_TerrainDelta);
		prev->m_i0 = std::min(prev->m_i0, m_i0);
		prev->m_j0 = std::min(prev->m_j0, m_j0);
		prev->m_i1 = std::max(prev->m_i1, m_i1);
		prev->m_j1 = std::max(prev->m_j1, m_j1);
	}
};
END_COMMAND(PaintTerrain)

//////////////////////////////////////////////////////////////////////////

BEGIN_COMMAND(ReplaceTerrain)
{
	TerrainArray m_TerrainDelta;
	ssize_t m_i0, m_j0, m_i1, m_j1;

	cReplaceTerrain()
	{
		m_TerrainDelta.Init();
	}

	void MakeDirty()
	{
		g_Game->GetWorld()->GetTerrain()->MakeDirty(m_i0, m_j0, m_i1, m_j1, RENDERDATA_UPDATE_INDICES);
		CmpPtr<ICmpTerrain> cmpTerrain(*g_Game->GetSimulation2(), SYSTEM_ENTITY);
		if (!cmpTerrain.null())
			cmpTerrain->MakeDirty(m_i0, m_j0, m_i1, m_j1);
	}

	void Do()
	{
		g_CurrentBrush.m_Centre = msg->pos->GetWorldSpace();

		ssize_t x0, y0;
		g_CurrentBrush.GetBottomLeft(x0, y0);

		m_i0 = m_i1 = x0;
		m_j0 = m_j1 = y0;

		CTerrainTextureEntry* texentry = g_TexMan.FindTexture(CStrW(*msg->texture).ToUTF8());
		if (! texentry)
		{
			debug_warn(L"Can't find texentry"); // TODO: nicer error handling
			return;
		}

		CTerrainTextureEntry* replacedTex = m_TerrainDelta.GetTexEntry(x0, y0);

		ssize_t tiles = m_TerrainDelta.GetTilesPerSide();

		for (ssize_t j = 0; j < tiles; ++j)
		{
			for (ssize_t i = 0; i < tiles; ++i)
			{
				if (m_TerrainDelta.GetTexEntry(i, j) == replacedTex)
				{
					m_i0 = std::min(m_i0, i);
					m_j0 = std::min(m_j0, j);
					m_i1 = std::max(m_i1, i+1);
					m_j1 = std::max(m_j1, j+1);
					m_TerrainDelta.PaintTile(i, j, texentry, m_TerrainDelta.GetPriority(i, j));
				}
			}
		}

		MakeDirty();
	}

	void Undo()
	{
		m_TerrainDelta.Undo();
		MakeDirty();
	}

	void Redo()
	{
		m_TerrainDelta.Redo();
		MakeDirty();
	}
};
END_COMMAND(ReplaceTerrain)


}
