/* (c) Magnus Auvinen. See licence.txt in the root of the distribution for more information. */
/* If you are missing that file, acquire a complete release at teeworlds.com.                */
#include "layer_tiles.h"

#include "image.h"

#include <engine/keys.h>
#include <engine/shared/config.h>
#include <engine/shared/map.h>

#include <game/editor/editor.h>
#include <game/editor/editor_actions.h>
#include <game/editor/enums.h>

#include <iterator>
#include <numeric>

CLayerTiles::CLayerTiles(CEditorMap *pMap, int w, int h) :
	CLayer(pMap, LAYERTYPE_TILES)
{
	m_aName[0] = '\0';
	m_Width = w;
	m_Height = h;
	m_Image = -1;
	m_HasGame = false;
	m_Color.r = 255;
	m_Color.g = 255;
	m_Color.b = 255;
	m_Color.a = 255;
	m_ColorEnv = -1;
	m_ColorEnvOffset = 0;

	m_HasTele = false;
	m_HasSpeedup = false;
	m_HasFront = false;
	m_HasSwitch = false;
	m_HasTune = false;
	m_AutoMapperConfig = -1;
	m_AutoMapperReference = -1;
	m_Seed = 0;
	m_AutoAutoMap = false;

	m_pTiles = new CTile[m_Width * m_Height];
	mem_zero(m_pTiles, (size_t)m_Width * m_Height * sizeof(CTile));
}

CLayerTiles::CLayerTiles(const CLayerTiles &Other) :
	CLayer(Other)
{
	m_Width = Other.m_Width;
	m_Height = Other.m_Height;
	m_pTiles = new CTile[m_Width * m_Height];
	mem_copy(m_pTiles, Other.m_pTiles, (size_t)m_Width * m_Height * sizeof(CTile));

	m_Image = Other.m_Image;
	m_HasGame = Other.m_HasGame;
	m_Color = Other.m_Color;
	m_ColorEnv = Other.m_ColorEnv;
	m_ColorEnvOffset = Other.m_ColorEnvOffset;

	m_AutoMapperConfig = Other.m_AutoMapperConfig;
	m_AutoMapperReference = Other.m_AutoMapperReference;
	m_Seed = Other.m_Seed;
	m_AutoAutoMap = Other.m_AutoAutoMap;
	m_HasTele = Other.m_HasTele;
	m_HasSpeedup = Other.m_HasSpeedup;
	m_HasFront = Other.m_HasFront;
	m_HasSwitch = Other.m_HasSwitch;
	m_HasTune = Other.m_HasTune;

	str_copy(m_aFilename, Other.m_aFilename);
}

CLayerTiles::~CLayerTiles()
{
	delete[] m_pTiles;
}

CTile CLayerTiles::GetTile(int x, int y) const
{
	return m_pTiles[y * m_Width + x];
}

void CLayerTiles::SetTile(int x, int y, CTile Tile)
{
	auto CurrentTile = m_pTiles[y * m_Width + x];
	SetTileIgnoreHistory(x, y, Tile);
	RecordStateChange(x, y, CurrentTile, Tile);

	if(m_FillGameTile != -1 && m_LiveGameTiles)
	{
		std::shared_ptr<CLayerTiles> pLayer = Map()->m_pGameLayer;
		if(m_FillGameTile == TILE_TELECHECKIN || m_FillGameTile == TILE_TELECHECKINEVIL)
		{
			if(!Map()->m_pTeleLayer)
			{
				std::shared_ptr<CLayerTele> pLayerTele = std::make_shared<CLayerTele>(Map(), m_Width, m_Height);
				Map()->MakeTeleLayer(pLayerTele);
				Map()->m_pGameGroup->AddLayer(pLayerTele);
				int GameGroupIndex = std::find(Map()->m_vpGroups.begin(), Map()->m_vpGroups.end(), Map()->m_pGameGroup) - Map()->m_vpGroups.begin();
				int LayerIndex = Map()->m_vpGroups[GameGroupIndex]->m_vpLayers.size() - 1;
				Editor()->m_EditorHistory.RecordAction(std::make_shared<CEditorActionAddLayer>(Editor(), GameGroupIndex, LayerIndex));
			}

			pLayer = Map()->m_pTeleLayer;
		}

		bool HasTile = Tile.m_Index != 0;
		pLayer->SetTile(x, y, CTile{(unsigned char)(HasTile ? m_FillGameTile : TILE_AIR)});
	}
}

void CLayerTiles::SetTileIgnoreHistory(int x, int y, CTile Tile) const
{
	m_pTiles[y * m_Width + x] = Tile;
}

void CLayerTiles::RecordStateChange(int x, int y, CTile Previous, CTile Tile)
{
	if(!m_TilesHistory[y][x].m_Changed)
		m_TilesHistory[y][x] = STileStateChange{true, Previous, Tile};
	else
		m_TilesHistory[y][x].m_Current = Tile;
}

void CLayerTiles::PrepareForSave()
{
	for(int y = 0; y < m_Height; y++)
		for(int x = 0; x < m_Width; x++)
			m_pTiles[y * m_Width + x].m_Flags &= TILEFLAG_XFLIP | TILEFLAG_YFLIP | TILEFLAG_ROTATE;

	if(m_Image != -1 && m_Color.a == 255)
	{
		for(int y = 0; y < m_Height; y++)
			for(int x = 0; x < m_Width; x++)
				m_pTiles[y * m_Width + x].m_Flags |= Map()->m_vpImages[m_Image]->m_aTileFlags[m_pTiles[y * m_Width + x].m_Index];
	}
}

void CLayerTiles::ExtractTiles(int TilemapItemVersion, const CTile *pSavedTiles, size_t SavedTilesSize) const
{
	const size_t DestSize = (size_t)m_Width * m_Height;
	if(TilemapItemVersion >= CMapItemLayerTilemap::VERSION_TEEWORLDS_TILESKIP)
		CMap::ExtractTiles(m_pTiles, DestSize, pSavedTiles, SavedTilesSize);
	else if(SavedTilesSize >= DestSize)
		mem_copy(m_pTiles, pSavedTiles, DestSize * sizeof(CTile));
}

void CLayerTiles::MakePalette() const
{
	for(int y = 0; y < m_Height; y++)
		for(int x = 0; x < m_Width; x++)
			m_pTiles[y * m_Width + x].m_Index = y * 16 + x;
}

void CLayerTiles::Render(bool Tileset)
{
	IGraphics::CTextureHandle Texture;
	if(m_Image >= 0 && (size_t)m_Image < Map()->m_vpImages.size())
		Texture = Map()->m_vpImages[m_Image]->m_Texture;
	else if(m_HasGame)
		Texture = Editor()->GetEntitiesTexture();
	else if(m_HasFront)
		Texture = Editor()->GetFrontTexture();
	else if(m_HasTele)
		Texture = Editor()->GetTeleTexture();
	else if(m_HasSpeedup)
		Texture = Editor()->GetSpeedupTexture();
	else if(m_HasSwitch)
		Texture = Editor()->GetSwitchTexture();
	else if(m_HasTune)
		Texture = Editor()->GetTuneTexture();
	Graphics()->TextureSet(Texture);

	ColorRGBA ColorEnv = ColorRGBA(1.0f, 1.0f, 1.0f, 1.0f);
	Editor()->EnvelopeEval(m_ColorEnvOffset, m_ColorEnv, ColorEnv, 4);
	const ColorRGBA Color = ColorRGBA(m_Color.r / 255.0f, m_Color.g / 255.0f, m_Color.b / 255.0f, m_Color.a / 255.0f).Multiply(ColorEnv);

	Graphics()->BlendNone();
	Editor()->RenderMap()->RenderTilemap(m_pTiles, m_Width, m_Height, 32.0f, Color, LAYERRENDERFLAG_OPAQUE);
	Graphics()->BlendNormal();
	Editor()->RenderMap()->RenderTilemap(m_pTiles, m_Width, m_Height, 32.0f, Color, LAYERRENDERFLAG_TRANSPARENT);

	// Render DDRace Layers
	if(!Tileset)
	{
		int OverlayRenderFlags = (g_Config.m_ClTextEntitiesEditor ? OVERLAYRENDERFLAG_TEXT : 0) | OVERLAYRENDERFLAG_EDITOR;
		if(m_HasTele)
			Editor()->RenderMap()->RenderTeleOverlay(static_cast<CLayerTele *>(this)->m_pTeleTile, m_Width, m_Height, 32.0f, OverlayRenderFlags);
		if(m_HasSpeedup)
			Editor()->RenderMap()->RenderSpeedupOverlay(static_cast<CLayerSpeedup *>(this)->m_pSpeedupTile, m_Width, m_Height, 32.0f, OverlayRenderFlags);
		if(m_HasSwitch)
			Editor()->RenderMap()->RenderSwitchOverlay(static_cast<CLayerSwitch *>(this)->m_pSwitchTile, m_Width, m_Height, 32.0f, OverlayRenderFlags);
		if(m_HasTune)
			Editor()->RenderMap()->RenderTuneOverlay(static_cast<CLayerTune *>(this)->m_pTuneTile, m_Width, m_Height, 32.0f, OverlayRenderFlags);
	}
}

int CLayerTiles::ConvertX(float x) const { return (int)(x / 32.0f); }
int CLayerTiles::ConvertY(float y) const { return (int)(y / 32.0f); }

void CLayerTiles::Convert(CUIRect Rect, CIntRect *pOut) const
{
	pOut->x = ConvertX(Rect.x);
	pOut->y = ConvertY(Rect.y);
	pOut->w = ConvertX(Rect.x + Rect.w + 31) - pOut->x;
	pOut->h = ConvertY(Rect.y + Rect.h + 31) - pOut->y;
}

void CLayerTiles::Snap(CUIRect *pRect) const
{
	CIntRect Out;
	Convert(*pRect, &Out);
	pRect->x = Out.x * 32.0f;
	pRect->y = Out.y * 32.0f;
	pRect->w = Out.w * 32.0f;
	pRect->h = Out.h * 32.0f;
}

void CLayerTiles::Clamp(CIntRect *pRect) const
{
	if(pRect->x < 0)
	{
		pRect->w += pRect->x;
		pRect->x = 0;
	}

	if(pRect->y < 0)
	{
		pRect->h += pRect->y;
		pRect->y = 0;
	}

	if(pRect->x + pRect->w > m_Width)
		pRect->w = m_Width - pRect->x;

	if(pRect->y + pRect->h > m_Height)
		pRect->h = m_Height - pRect->y;

	if(pRect->h < 0)
		pRect->h = 0;
	if(pRect->w < 0)
		pRect->w = 0;
}

bool CLayerTiles::IsEntitiesLayer() const
{
	return Map()->m_pGameLayer.get() == this || Map()->m_pTeleLayer.get() == this || Map()->m_pSpeedupLayer.get() == this || Map()->m_pFrontLayer.get() == this || Map()->m_pSwitchLayer.get() == this || Map()->m_pTuneLayer.get() == this;
}

bool CLayerTiles::IsEmpty() const
{
	for(int y = 0; y < m_Height; y++)
	{
		for(int x = 0; x < m_Width; x++)
		{
			if(GetTile(x, y).m_Index != 0)
			{
				return false;
			}
		}
	}
	return true;
}

void CLayerTiles::BrushSelecting(CUIRect Rect)
{
	Graphics()->TextureClear();
	Graphics()->QuadsBegin();
	Graphics()->SetColor(1.0f, 1.0f, 1.0f, 0.4f);
	Snap(&Rect);
	IGraphics::CQuadItem QuadItem(Rect.x, Rect.y, Rect.w, Rect.h);
	Graphics()->QuadsDrawTL(&QuadItem, 1);
	Graphics()->QuadsEnd();
	char aBuf[16];
	str_format(aBuf, sizeof(aBuf), "%d⨯%d", ConvertX(Rect.w), ConvertY(Rect.h));
	TextRender()->Text(Rect.x + 3.0f, Rect.y + 3.0f, Editor()->m_ShowPicker ? 15.0f : Editor()->MapView()->ScaleLength(15.0f), aBuf, -1.0f);
}

template<typename T>
static void InitGrabbedLayer(std::shared_ptr<T> &pLayer, CLayerTiles *pThisLayer)
{
	pLayer->m_Image = pThisLayer->m_Image;
	pLayer->m_HasGame = pThisLayer->m_HasGame;
	pLayer->m_HasFront = pThisLayer->m_HasFront;
	pLayer->m_HasTele = pThisLayer->m_HasTele;
	pLayer->m_HasSpeedup = pThisLayer->m_HasSpeedup;
	pLayer->m_HasSwitch = pThisLayer->m_HasSwitch;
	pLayer->m_HasTune = pThisLayer->m_HasTune;
	if(pThisLayer->Editor()->m_BrushColorEnabled)
	{
		pLayer->m_Color = pThisLayer->m_Color;
		pLayer->m_Color.a = 255;
	}
}

int CLayerTiles::BrushGrab(CLayerGroup *pBrush, CUIRect Rect)
{
	CIntRect r;
	Convert(Rect, &r);
	Clamp(&r);

	if(!r.w || !r.h)
		return 0;

	// create new layers
	if(m_HasTele)
	{
		std::shared_ptr<CLayerTele> pGrabbed = std::make_shared<CLayerTele>(pBrush->Map(), r.w, r.h);
		InitGrabbedLayer(pGrabbed, this);

		pBrush->AddLayer(pGrabbed);

		for(int y = 0; y < r.h; y++)
		{
			for(int x = 0; x < r.w; x++)
			{
				// copy the tiles
				pGrabbed->m_pTiles[y * pGrabbed->m_Width + x] = GetTile(r.x + x, r.y + y);

				// copy the tele data
				if(!Editor()->Input()->KeyIsPressed(KEY_SPACE))
				{
					pGrabbed->m_pTeleTile[y * pGrabbed->m_Width + x] = static_cast<CLayerTele *>(this)->m_pTeleTile[(r.y + y) * m_Width + (r.x + x)];
					unsigned char TgtIndex = pGrabbed->m_pTeleTile[y * pGrabbed->m_Width + x].m_Type;
					if(IsValidTeleTile(TgtIndex))
					{
						if(IsTeleTileNumberUsed(TgtIndex, false))
							Editor()->m_TeleNumber = pGrabbed->m_pTeleTile[y * pGrabbed->m_Width + x].m_Number;
						else if(IsTeleTileNumberUsed(TgtIndex, true))
							Editor()->m_TeleCheckpointNumber = pGrabbed->m_pTeleTile[y * pGrabbed->m_Width + x].m_Number;
					}
				}
				else
				{
					const CTile &Tile = pGrabbed->m_pTiles[y * pGrabbed->m_Width + x];
					if(IsValidTeleTile(Tile.m_Index) && IsTeleTileNumberUsedAny(Tile.m_Index))
					{
						pGrabbed->m_pTeleTile[y * pGrabbed->m_Width + x].m_Type = Tile.m_Index;
						pGrabbed->m_pTeleTile[y * pGrabbed->m_Width + x].m_Number = IsTeleTileCheckpoint(Tile.m_Index) ? Editor()->m_TeleCheckpointNumber : Editor()->m_TeleNumber;
					}
				}
			}
		}

		pGrabbed->m_TeleNumber = Editor()->m_TeleNumber;
		pGrabbed->m_TeleCheckpointNumber = Editor()->m_TeleCheckpointNumber;

		str_copy(pGrabbed->m_aFilename, Editor()->m_aFilename);
	}
	else if(m_HasSpeedup)
	{
		std::shared_ptr<CLayerSpeedup> pGrabbed = std::make_shared<CLayerSpeedup>(pBrush->Map(), r.w, r.h);
		InitGrabbedLayer(pGrabbed, this);

		pBrush->AddLayer(pGrabbed);

		for(int y = 0; y < r.h; y++)
		{
			for(int x = 0; x < r.w; x++)
			{
				// copy the tiles
				pGrabbed->m_pTiles[y * pGrabbed->m_Width + x] = GetTile(r.x + x, r.y + y);

				// copy the speedup data
				if(!Editor()->Input()->KeyIsPressed(KEY_SPACE))
				{
					pGrabbed->m_pSpeedupTile[y * pGrabbed->m_Width + x] = static_cast<CLayerSpeedup *>(this)->m_pSpeedupTile[(r.y + y) * m_Width + (r.x + x)];
					if(IsValidSpeedupTile(pGrabbed->m_pSpeedupTile[y * pGrabbed->m_Width + x].m_Type))
					{
						Editor()->m_SpeedupAngle = pGrabbed->m_pSpeedupTile[y * pGrabbed->m_Width + x].m_Angle;
						Editor()->m_SpeedupForce = pGrabbed->m_pSpeedupTile[y * pGrabbed->m_Width + x].m_Force;
						Editor()->m_SpeedupMaxSpeed = pGrabbed->m_pSpeedupTile[y * pGrabbed->m_Width + x].m_MaxSpeed;
					}
				}
				else
				{
					const CTile &Tile = pGrabbed->m_pTiles[y * pGrabbed->m_Width + x];
					if(IsValidSpeedupTile(Tile.m_Index))
					{
						pGrabbed->m_pSpeedupTile[y * pGrabbed->m_Width + x].m_Type = Tile.m_Index;
						pGrabbed->m_pSpeedupTile[y * pGrabbed->m_Width + x].m_Angle = Editor()->m_SpeedupAngle;
						pGrabbed->m_pSpeedupTile[y * pGrabbed->m_Width + x].m_Force = Editor()->m_SpeedupForce;
						pGrabbed->m_pSpeedupTile[y * pGrabbed->m_Width + x].m_MaxSpeed = Editor()->m_SpeedupMaxSpeed;
					}
				}
			}
		}

		pGrabbed->m_SpeedupForce = Editor()->m_SpeedupForce;
		pGrabbed->m_SpeedupMaxSpeed = Editor()->m_SpeedupMaxSpeed;
		pGrabbed->m_SpeedupAngle = Editor()->m_SpeedupAngle;
		str_copy(pGrabbed->m_aFilename, Editor()->m_aFilename);
	}
	else if(m_HasSwitch)
	{
		std::shared_ptr<CLayerSwitch> pGrabbed = std::make_shared<CLayerSwitch>(pBrush->Map(), r.w, r.h);
		InitGrabbedLayer(pGrabbed, this);

		pBrush->AddLayer(pGrabbed);

		for(int y = 0; y < r.h; y++)
		{
			for(int x = 0; x < r.w; x++)
			{
				// copy the tiles
				pGrabbed->m_pTiles[y * pGrabbed->m_Width + x] = GetTile(r.x + x, r.y + y);

				// copy the switch data
				if(!Editor()->Input()->KeyIsPressed(KEY_SPACE))
				{
					pGrabbed->m_pSwitchTile[y * pGrabbed->m_Width + x] = static_cast<CLayerSwitch *>(this)->m_pSwitchTile[(r.y + y) * m_Width + (r.x + x)];
					if(IsValidSwitchTile(pGrabbed->m_pSwitchTile[y * pGrabbed->m_Width + x].m_Type))
					{
						Editor()->m_SwitchNumber = pGrabbed->m_pSwitchTile[y * pGrabbed->m_Width + x].m_Number;
						Editor()->m_SwitchDelay = pGrabbed->m_pSwitchTile[y * pGrabbed->m_Width + x].m_Delay;
					}
				}
				else
				{
					const CTile &Tile = pGrabbed->m_pTiles[y * pGrabbed->m_Width + x];
					if(IsValidSwitchTile(Tile.m_Index))
					{
						pGrabbed->m_pSwitchTile[y * pGrabbed->m_Width + x].m_Type = Tile.m_Index;
						pGrabbed->m_pSwitchTile[y * pGrabbed->m_Width + x].m_Number = Editor()->m_SwitchNumber;
						pGrabbed->m_pSwitchTile[y * pGrabbed->m_Width + x].m_Delay = Editor()->m_SwitchDelay;
						pGrabbed->m_pSwitchTile[y * pGrabbed->m_Width + x].m_Flags = Tile.m_Flags;
					}
				}
			}
		}

		pGrabbed->m_SwitchNumber = Editor()->m_SwitchNumber;
		pGrabbed->m_SwitchDelay = Editor()->m_SwitchDelay;
		str_copy(pGrabbed->m_aFilename, Editor()->m_aFilename);
	}

	else if(m_HasTune)
	{
		std::shared_ptr<CLayerTune> pGrabbed = std::make_shared<CLayerTune>(pBrush->Map(), r.w, r.h);
		InitGrabbedLayer(pGrabbed, this);

		pBrush->AddLayer(pGrabbed);

		// copy the tiles
		for(int y = 0; y < r.h; y++)
		{
			for(int x = 0; x < r.w; x++)
			{
				pGrabbed->m_pTiles[y * pGrabbed->m_Width + x] = GetTile(r.x + x, r.y + y);

				if(!Editor()->Input()->KeyIsPressed(KEY_SPACE))
				{
					pGrabbed->m_pTuneTile[y * pGrabbed->m_Width + x] = static_cast<CLayerTune *>(this)->m_pTuneTile[(r.y + y) * m_Width + (r.x + x)];
					if(IsValidTuneTile(pGrabbed->m_pTuneTile[y * pGrabbed->m_Width + x].m_Type))
					{
						Editor()->m_TuningNumber = pGrabbed->m_pTuneTile[y * pGrabbed->m_Width + x].m_Number;
					}
				}
				else
				{
					const CTile &Tile = pGrabbed->m_pTiles[y * pGrabbed->m_Width + x];
					if(IsValidTuneTile(Tile.m_Index))
					{
						pGrabbed->m_pTuneTile[y * pGrabbed->m_Width + x].m_Type = Tile.m_Index;
						pGrabbed->m_pTuneTile[y * pGrabbed->m_Width + x].m_Number = Editor()->m_TuningNumber;
					}
				}
			}
		}

		pGrabbed->m_TuningNumber = Editor()->m_TuningNumber;
		str_copy(pGrabbed->m_aFilename, Editor()->m_aFilename);
	}
	else // game, front and tiles layers
	{
		std::shared_ptr<CLayerTiles> pGrabbed;
		if(m_HasGame)
		{
			pGrabbed = std::make_shared<CLayerGame>(pBrush->Map(), r.w, r.h);
		}
		else if(m_HasFront)
		{
			pGrabbed = std::make_shared<CLayerFront>(pBrush->Map(), r.w, r.h);
		}
		else
		{
			pGrabbed = std::make_shared<CLayerTiles>(pBrush->Map(), r.w, r.h);
		}
		InitGrabbedLayer(pGrabbed, this);

		pBrush->AddLayer(pGrabbed);

		// copy the tiles
		for(int y = 0; y < r.h; y++)
			for(int x = 0; x < r.w; x++)
				pGrabbed->m_pTiles[y * pGrabbed->m_Width + x] = GetTile(r.x + x, r.y + y);
		str_copy(pGrabbed->m_aFilename, Editor()->m_aFilename);
	}

	return 1;
}

void CLayerTiles::FillSelection(bool Empty, CLayer *pBrush, CUIRect Rect)
{
	if(m_Readonly || (!Empty && pBrush->m_Type != LAYERTYPE_TILES))
		return;

	Snap(&Rect);

	int sx = ConvertX(Rect.x);
	int sy = ConvertY(Rect.y);
	int w = ConvertX(Rect.w);
	int h = ConvertY(Rect.h);

	CLayerTiles *pLt = static_cast<CLayerTiles *>(pBrush);

	bool Destructive = Editor()->m_BrushDrawDestructive || Empty || pLt->IsEmpty();

	for(int y = 0; y < h; y++)
	{
		for(int x = 0; x < w; x++)
		{
			int fx = x + sx;
			int fy = y + sy;

			if(fx < 0 || fx >= m_Width || fy < 0 || fy >= m_Height)
				continue;

			bool HasTile = GetTile(fx, fy).m_Index;
			if(!Empty && pLt->GetTile(x % pLt->m_Width, y % pLt->m_Height).m_Index == TILE_THROUGH_CUT)
			{
				if(m_HasGame && Map()->m_pFrontLayer)
				{
					HasTile = HasTile || Map()->m_pFrontLayer->GetTile(fx, fy).m_Index;
				}
				else if(m_HasFront)
				{
					HasTile = HasTile || Map()->m_pGameLayer->GetTile(fx, fy).m_Index;
				}
			}

			if(!Destructive && HasTile)
				continue;

			SetTile(fx, fy, Empty ? CTile{TILE_AIR} : pLt->m_pTiles[(y * pLt->m_Width + x % pLt->m_Width) % (pLt->m_Width * pLt->m_Height)]);
		}
	}
	FlagModified(sx, sy, w, h);
}

void CLayerTiles::BrushDraw(CLayer *pBrush, vec2 WorldPos)
{
	if(m_Readonly)
		return;

	CLayerTiles *pTileLayer = static_cast<CLayerTiles *>(pBrush);
	int sx = ConvertX(WorldPos.x);
	int sy = ConvertY(WorldPos.y);

	bool Destructive = Editor()->m_BrushDrawDestructive || pTileLayer->IsEmpty();

	for(int y = 0; y < pTileLayer->m_Height; y++)
		for(int x = 0; x < pTileLayer->m_Width; x++)
		{
			int fx = x + sx;
			int fy = y + sy;

			if(fx < 0 || fx >= m_Width || fy < 0 || fy >= m_Height)
				continue;

			bool HasTile = GetTile(fx, fy).m_Index;
			if(pTileLayer->GetTile(x, y).m_Index == TILE_THROUGH_CUT)
			{
				if(m_HasGame && Map()->m_pFrontLayer)
				{
					HasTile = HasTile || Map()->m_pFrontLayer->GetTile(fx, fy).m_Index;
				}
				else if(m_HasFront)
				{
					HasTile = HasTile || Map()->m_pGameLayer->GetTile(fx, fy).m_Index;
				}
			}

			if(!Destructive && HasTile)
				continue;

			SetTile(fx, fy, pTileLayer->GetTile(x, y));
		}

	FlagModified(sx, sy, pTileLayer->m_Width, pTileLayer->m_Height);
}

void CLayerTiles::BrushFlipX()
{
	BrushFlipXImpl(m_pTiles);

	if(m_HasTele || m_HasSpeedup || m_HasTune)
		return;

	bool Rotate = !(m_HasGame || m_HasFront || m_HasSwitch) || Editor()->IsAllowPlaceUnusedTiles();
	for(int y = 0; y < m_Height; y++)
		for(int x = 0; x < m_Width; x++)
			if(!Rotate && !IsRotatableTile(m_pTiles[y * m_Width + x].m_Index))
				m_pTiles[y * m_Width + x].m_Flags = 0;
			else
				m_pTiles[y * m_Width + x].m_Flags ^= (m_pTiles[y * m_Width + x].m_Flags & TILEFLAG_ROTATE) ? TILEFLAG_YFLIP : TILEFLAG_XFLIP;
}

void CLayerTiles::BrushFlipY()
{
	BrushFlipYImpl(m_pTiles);

	if(m_HasTele || m_HasSpeedup || m_HasTune)
		return;

	bool Rotate = !(m_HasGame || m_HasFront || m_HasSwitch) || Editor()->IsAllowPlaceUnusedTiles();
	for(int y = 0; y < m_Height; y++)
		for(int x = 0; x < m_Width; x++)
			if(!Rotate && !IsRotatableTile(m_pTiles[y * m_Width + x].m_Index))
				m_pTiles[y * m_Width + x].m_Flags = 0;
			else
				m_pTiles[y * m_Width + x].m_Flags ^= (m_pTiles[y * m_Width + x].m_Flags & TILEFLAG_ROTATE) ? TILEFLAG_XFLIP : TILEFLAG_YFLIP;
}

void CLayerTiles::BrushRotate(float Amount)
{
	int Rotation = (round_to_int(360.0f * Amount / (pi * 2)) / 90) % 4; // 0=0°, 1=90°, 2=180°, 3=270°
	if(Rotation < 0)
		Rotation += 4;

	if(Rotation == 1 || Rotation == 3)
	{
		// 90° rotation
		CTile *pTempData = new CTile[m_Width * m_Height];
		mem_copy(pTempData, m_pTiles, (size_t)m_Width * m_Height * sizeof(CTile));
		CTile *pDst = m_pTiles;
		bool Rotate = !(m_HasGame || m_HasFront) || Editor()->IsAllowPlaceUnusedTiles();
		for(int x = 0; x < m_Width; ++x)
			for(int y = m_Height - 1; y >= 0; --y, ++pDst)
			{
				*pDst = pTempData[y * m_Width + x];
				if(!Rotate && !IsRotatableTile(pDst->m_Index))
					pDst->m_Flags = 0;
				else
				{
					if(pDst->m_Flags & TILEFLAG_ROTATE)
						pDst->m_Flags ^= (TILEFLAG_YFLIP | TILEFLAG_XFLIP);
					pDst->m_Flags ^= TILEFLAG_ROTATE;
				}
			}

		std::swap(m_Width, m_Height);
		delete[] pTempData;
	}

	if(Rotation == 2 || Rotation == 3)
	{
		BrushFlipX();
		BrushFlipY();
	}
}

std::shared_ptr<CLayer> CLayerTiles::Duplicate() const
{
	return std::make_shared<CLayerTiles>(*this);
}

const char *CLayerTiles::TypeName() const
{
	return "tiles";
}

void CLayerTiles::Resize(int NewW, int NewH)
{
	CTile *pNewData = new CTile[NewW * NewH];
	mem_zero(pNewData, (size_t)NewW * NewH * sizeof(CTile));

	// copy old data
	for(int y = 0; y < minimum(NewH, m_Height); y++)
		mem_copy(&pNewData[y * NewW], &m_pTiles[y * m_Width], minimum(m_Width, NewW) * sizeof(CTile));

	// replace old
	delete[] m_pTiles;
	m_pTiles = pNewData;
	m_Width = NewW;
	m_Height = NewH;

	// resize tele layer if available
	if(m_HasGame && Map()->m_pTeleLayer && (Map()->m_pTeleLayer->m_Width != NewW || Map()->m_pTeleLayer->m_Height != NewH))
		Map()->m_pTeleLayer->Resize(NewW, NewH);

	// resize speedup layer if available
	if(m_HasGame && Map()->m_pSpeedupLayer && (Map()->m_pSpeedupLayer->m_Width != NewW || Map()->m_pSpeedupLayer->m_Height != NewH))
		Map()->m_pSpeedupLayer->Resize(NewW, NewH);

	// resize front layer
	if(m_HasGame && Map()->m_pFrontLayer && (Map()->m_pFrontLayer->m_Width != NewW || Map()->m_pFrontLayer->m_Height != NewH))
		Map()->m_pFrontLayer->Resize(NewW, NewH);

	// resize switch layer if available
	if(m_HasGame && Map()->m_pSwitchLayer && (Map()->m_pSwitchLayer->m_Width != NewW || Map()->m_pSwitchLayer->m_Height != NewH))
		Map()->m_pSwitchLayer->Resize(NewW, NewH);

	// resize tune layer if available
	if(m_HasGame && Map()->m_pTuneLayer && (Map()->m_pTuneLayer->m_Width != NewW || Map()->m_pTuneLayer->m_Height != NewH))
		Map()->m_pTuneLayer->Resize(NewW, NewH);
}

void CLayerTiles::Shift(EShiftDirection Direction)
{
	ShiftImpl(m_pTiles, Direction, Editor()->m_ShiftBy);
}

void CLayerTiles::ShowInfo()
{
	float ScreenX0, ScreenY0, ScreenX1, ScreenY1;
	Graphics()->GetScreen(&ScreenX0, &ScreenY0, &ScreenX1, &ScreenY1);
	Graphics()->TextureSet(Editor()->Client()->GetDebugFont());
	Graphics()->QuadsBegin();

	int StartY = maximum(0, (int)(ScreenY0 / 32.0f) - 1);
	int StartX = maximum(0, (int)(ScreenX0 / 32.0f) - 1);
	int EndY = minimum((int)(ScreenY1 / 32.0f) + 1, m_Height);
	int EndX = minimum((int)(ScreenX1 / 32.0f) + 1, m_Width);

	for(int y = StartY; y < EndY; y++)
		for(int x = StartX; x < EndX; x++)
		{
			int c = x + y * m_Width;
			if(m_pTiles[c].m_Index)
			{
				char aBuf[4];
				if(Editor()->m_ShowTileInfo == CEditor::SHOW_TILE_HEXADECIMAL)
				{
					str_hex(aBuf, sizeof(aBuf), &m_pTiles[c].m_Index, 1);
					aBuf[2] = '\0'; // would otherwise be a space
				}
				else
				{
					str_format(aBuf, sizeof(aBuf), "%d", m_pTiles[c].m_Index);
				}
				Graphics()->QuadsText(x * 32, y * 32, 16.0f, aBuf);

				char aFlags[4] = {m_pTiles[c].m_Flags & TILEFLAG_XFLIP ? 'X' : ' ',
					m_pTiles[c].m_Flags & TILEFLAG_YFLIP ? 'Y' : ' ',
					m_pTiles[c].m_Flags & TILEFLAG_ROTATE ? 'R' : ' ',
					0};
				Graphics()->QuadsText(x * 32, y * 32 + 16, 16.0f, aFlags);
			}
			x += m_pTiles[c].m_Skip;
		}

	Graphics()->QuadsEnd();
	Graphics()->MapScreen(ScreenX0, ScreenY0, ScreenX1, ScreenY1);
}

void CLayerTiles::FillGameTiles(EGameTileOp Fill)
{
	if(!CanFillGameTiles())
		return;

	auto GameTileOpToIndex = [](EGameTileOp Op) -> int {
		switch(Op)
		{
		case EGameTileOp::AIR: return TILE_AIR;
		case EGameTileOp::HOOKABLE: return TILE_SOLID;
		case EGameTileOp::DEATH: return TILE_DEATH;
		case EGameTileOp::UNHOOKABLE: return TILE_NOHOOK;
		case EGameTileOp::HOOKTHROUGH: return TILE_THROUGH_CUT;
		case EGameTileOp::FREEZE: return TILE_FREEZE;
		case EGameTileOp::UNFREEZE: return TILE_UNFREEZE;
		case EGameTileOp::DEEP_FREEZE: return TILE_DFREEZE;
		case EGameTileOp::DEEP_UNFREEZE: return TILE_DUNFREEZE;
		case EGameTileOp::BLUE_CHECK_TELE: return TILE_TELECHECKIN;
		case EGameTileOp::RED_CHECK_TELE: return TILE_TELECHECKINEVIL;
		case EGameTileOp::LIVE_FREEZE: return TILE_LFREEZE;
		case EGameTileOp::LIVE_UNFREEZE: return TILE_LUNFREEZE;
		default: return -1;
		}
	};

	int Result = GameTileOpToIndex(Fill);
	if(Result > -1)
	{
		std::shared_ptr<CLayerGroup> pGroup = Map()->m_vpGroups[Editor()->m_SelectedGroup];
		m_FillGameTile = Result;
		const int OffsetX = -pGroup->m_OffsetX / 32;
		const int OffsetY = -pGroup->m_OffsetY / 32;

		std::vector<std::shared_ptr<IEditorAction>> vpActions;
		std::shared_ptr<CLayerTiles> pGLayer = Map()->m_pGameLayer;
		int GameLayerIndex = std::find(Map()->m_pGameGroup->m_vpLayers.begin(), Map()->m_pGameGroup->m_vpLayers.end(), pGLayer) - Map()->m_pGameGroup->m_vpLayers.begin();
		int GameGroupIndex = std::find(Map()->m_vpGroups.begin(), Map()->m_vpGroups.end(), Map()->m_pGameGroup) - Map()->m_vpGroups.begin();

		if(Result != TILE_TELECHECKIN && Result != TILE_TELECHECKINEVIL)
		{
			if(pGLayer->m_Width < m_Width + OffsetX || pGLayer->m_Height < m_Height + OffsetY)
			{
				std::map<int, std::shared_ptr<CLayer>> SavedLayers;
				SavedLayers[LAYERTYPE_TILES] = pGLayer->Duplicate();
				SavedLayers[LAYERTYPE_GAME] = SavedLayers[LAYERTYPE_TILES];

				int PrevW = pGLayer->m_Width;
				int PrevH = pGLayer->m_Height;
				const int NewW = pGLayer->m_Width < m_Width + OffsetX ? m_Width + OffsetX : pGLayer->m_Width;
				const int NewH = pGLayer->m_Height < m_Height + OffsetY ? m_Height + OffsetY : pGLayer->m_Height;
				pGLayer->Resize(NewW, NewH);
				vpActions.push_back(std::make_shared<CEditorActionEditLayerTilesProp>(Editor(), GameGroupIndex, GameLayerIndex, ETilesProp::PROP_WIDTH, PrevW, NewW));
				const std::shared_ptr<CEditorActionEditLayerTilesProp> &Action1 = std::static_pointer_cast<CEditorActionEditLayerTilesProp>(vpActions[vpActions.size() - 1]);
				vpActions.push_back(std::make_shared<CEditorActionEditLayerTilesProp>(Editor(), GameGroupIndex, GameLayerIndex, ETilesProp::PROP_HEIGHT, PrevH, NewH));
				const std::shared_ptr<CEditorActionEditLayerTilesProp> &Action2 = std::static_pointer_cast<CEditorActionEditLayerTilesProp>(vpActions[vpActions.size() - 1]);

				Action1->SetSavedLayers(SavedLayers);
				Action2->SetSavedLayers(SavedLayers);
			}

			int Changes = 0;
			for(int y = OffsetY < 0 ? -OffsetY : 0; y < m_Height; y++)
			{
				for(int x = OffsetX < 0 ? -OffsetX : 0; x < m_Width; x++)
				{
					if(GetTile(x, y).m_Index)
					{
						pGLayer->SetTile(x + OffsetX, y + OffsetY, CTile{(unsigned char)Result});
						Changes++;
					}
				}
			}

			vpActions.push_back(std::make_shared<CEditorBrushDrawAction>(Editor(), GameGroupIndex));
			char aDisplay[256];
			str_format(aDisplay, sizeof(aDisplay), "Construct '%s' game tiles (x%d)", GAME_TILE_OP_NAMES[(int)Fill], Changes);
			Editor()->m_EditorHistory.RecordAction(std::make_shared<CEditorActionBulk>(Editor(), vpActions, aDisplay, true));
		}
		else
		{
			if(!Map()->m_pTeleLayer)
			{
				std::shared_ptr<CLayerTele> pLayer = std::make_shared<CLayerTele>(Map(), m_Width, m_Height);
				Map()->MakeTeleLayer(pLayer);
				Map()->m_pGameGroup->AddLayer(pLayer);

				vpActions.push_back(std::make_shared<CEditorActionAddLayer>(Editor(), GameGroupIndex, Map()->m_pGameGroup->m_vpLayers.size() - 1));

				if(m_Width != pGLayer->m_Width || m_Height > pGLayer->m_Height)
				{
					std::map<int, std::shared_ptr<CLayer>> SavedLayers;
					SavedLayers[LAYERTYPE_TILES] = pGLayer->Duplicate();
					SavedLayers[LAYERTYPE_GAME] = SavedLayers[LAYERTYPE_TILES];

					int NewW = pGLayer->m_Width;
					int NewH = pGLayer->m_Height;
					if(m_Width > pGLayer->m_Width)
					{
						NewW = m_Width;
					}
					if(m_Height > pGLayer->m_Height)
					{
						NewH = m_Height;
					}

					int PrevW = pGLayer->m_Width;
					int PrevH = pGLayer->m_Height;
					pLayer->Resize(NewW, NewH);
					vpActions.push_back(std::make_shared<CEditorActionEditLayerTilesProp>(Editor(), GameGroupIndex, GameLayerIndex, ETilesProp::PROP_WIDTH, PrevW, NewW));
					const std::shared_ptr<CEditorActionEditLayerTilesProp> &Action1 = std::static_pointer_cast<CEditorActionEditLayerTilesProp>(vpActions[vpActions.size() - 1]);
					vpActions.push_back(std::make_shared<CEditorActionEditLayerTilesProp>(Editor(), GameGroupIndex, GameLayerIndex, ETilesProp::PROP_HEIGHT, PrevH, NewH));
					const std::shared_ptr<CEditorActionEditLayerTilesProp> &Action2 = std::static_pointer_cast<CEditorActionEditLayerTilesProp>(vpActions[vpActions.size() - 1]);

					Action1->SetSavedLayers(SavedLayers);
					Action2->SetSavedLayers(SavedLayers);
				}
			}

			std::shared_ptr<CLayerTele> pTLayer = Map()->m_pTeleLayer;
			int TeleLayerIndex = std::find(Map()->m_pGameGroup->m_vpLayers.begin(), Map()->m_pGameGroup->m_vpLayers.end(), pTLayer) - Map()->m_pGameGroup->m_vpLayers.begin();

			if(pTLayer->m_Width < m_Width + OffsetX || pTLayer->m_Height < m_Height + OffsetY)
			{
				std::map<int, std::shared_ptr<CLayer>> SavedLayers;
				SavedLayers[LAYERTYPE_TILES] = pTLayer->Duplicate();
				SavedLayers[LAYERTYPE_TELE] = SavedLayers[LAYERTYPE_TILES];

				int PrevW = pTLayer->m_Width;
				int PrevH = pTLayer->m_Height;
				int NewW = pTLayer->m_Width < m_Width + OffsetX ? m_Width + OffsetX : pTLayer->m_Width;
				int NewH = pTLayer->m_Height < m_Height + OffsetY ? m_Height + OffsetY : pTLayer->m_Height;
				pTLayer->Resize(NewW, NewH);
				std::shared_ptr<CEditorActionEditLayerTilesProp> Action1, Action2;
				vpActions.push_back(Action1 = std::make_shared<CEditorActionEditLayerTilesProp>(Editor(), GameGroupIndex, TeleLayerIndex, ETilesProp::PROP_WIDTH, PrevW, NewW));
				vpActions.push_back(Action2 = std::make_shared<CEditorActionEditLayerTilesProp>(Editor(), GameGroupIndex, TeleLayerIndex, ETilesProp::PROP_HEIGHT, PrevH, NewH));

				Action1->SetSavedLayers(SavedLayers);
				Action2->SetSavedLayers(SavedLayers);
			}

			int Changes = 0;
			for(int y = OffsetY < 0 ? -OffsetY : 0; y < m_Height; y++)
			{
				for(int x = OffsetX < 0 ? -OffsetX : 0; x < m_Width; x++)
				{
					if(GetTile(x, y).m_Index)
					{
						auto TileIndex = (y + OffsetY) * pTLayer->m_Width + x + OffsetX;
						Changes++;

						STeleTileStateChange::SData Previous{
							pTLayer->m_pTeleTile[TileIndex].m_Number,
							pTLayer->m_pTeleTile[TileIndex].m_Type,
							pTLayer->m_pTiles[TileIndex].m_Index};

						pTLayer->m_pTiles[TileIndex].m_Index = TILE_AIR + Result;
						pTLayer->m_pTeleTile[TileIndex].m_Number = 1;
						pTLayer->m_pTeleTile[TileIndex].m_Type = TILE_AIR + Result;

						STeleTileStateChange::SData Current{
							pTLayer->m_pTeleTile[TileIndex].m_Number,
							pTLayer->m_pTeleTile[TileIndex].m_Type,
							pTLayer->m_pTiles[TileIndex].m_Index};

						pTLayer->RecordStateChange(x, y, Previous, Current);
					}
				}
			}

			vpActions.push_back(std::make_shared<CEditorBrushDrawAction>(Editor(), GameGroupIndex));
			char aDisplay[256];
			str_format(aDisplay, sizeof(aDisplay), "Construct 'tele' game tiles (x%d)", Changes);
			Editor()->m_EditorHistory.RecordAction(std::make_shared<CEditorActionBulk>(Editor(), vpActions, aDisplay, true));
		}
	}
}

bool CLayerTiles::CanFillGameTiles() const
{
	const bool EntitiesLayer = IsEntitiesLayer();
	if(EntitiesLayer)
		return false;

	std::shared_ptr<CLayerGroup> pGroup = Map()->m_vpGroups[Editor()->m_SelectedGroup];

	// Game tiles can only be constructed if the layer is relative to the game layer
	return !(pGroup->m_OffsetX % 32) && !(pGroup->m_OffsetY % 32) && pGroup->m_ParallaxX == 100 && pGroup->m_ParallaxY == 100;
}

CUi::EPopupMenuFunctionResult CLayerTiles::RenderProperties(CUIRect *pToolBox)
{
	CUIRect Button;

	const bool EntitiesLayer = IsEntitiesLayer();

	if(CanFillGameTiles())
	{
		pToolBox->HSplitBottom(12.0f, pToolBox, &Button);
		static int s_GameTilesButton = 0;

		auto GameTileToOp = [](int TileIndex) -> EGameTileOp {
			switch(TileIndex)
			{
			case TILE_AIR: return EGameTileOp::AIR;
			case TILE_SOLID: return EGameTileOp::HOOKABLE;
			case TILE_DEATH: return EGameTileOp::DEATH;
			case TILE_NOHOOK: return EGameTileOp::UNHOOKABLE;
			case TILE_THROUGH_CUT: return EGameTileOp::HOOKTHROUGH;
			case TILE_FREEZE: return EGameTileOp::FREEZE;
			case TILE_UNFREEZE: return EGameTileOp::UNFREEZE;
			case TILE_DFREEZE: return EGameTileOp::DEEP_FREEZE;
			case TILE_DUNFREEZE: return EGameTileOp::DEEP_UNFREEZE;
			case TILE_TELECHECKIN: return EGameTileOp::BLUE_CHECK_TELE;
			case TILE_TELECHECKINEVIL: return EGameTileOp::RED_CHECK_TELE;
			case TILE_LFREEZE: return EGameTileOp::LIVE_FREEZE;
			case TILE_LUNFREEZE: return EGameTileOp::LIVE_UNFREEZE;
			default: return EGameTileOp::AIR;
			}
		};

		char aBuf[128] = "Game tiles";
		if(m_LiveGameTiles)
		{
			auto TileOp = GameTileToOp(m_FillGameTile);
			if(TileOp != EGameTileOp::AIR)
				str_format(aBuf, sizeof(aBuf), "Game tiles: %s", GAME_TILE_OP_NAMES[(size_t)TileOp]);
		}
		if(Editor()->DoButton_Editor(&s_GameTilesButton, aBuf, 0, &Button, BUTTONFLAG_LEFT, "Construct game tiles from this layer."))
			Editor()->PopupSelectGametileOpInvoke(Editor()->Ui()->MouseX(), Editor()->Ui()->MouseY());
		const int Selected = Editor()->PopupSelectGameTileOpResult();
		FillGameTiles((EGameTileOp)Selected);
	}

	if(Map()->m_pGameLayer.get() != this)
	{
		if(m_Image >= 0 && (size_t)m_Image < Map()->m_vpImages.size() && Map()->m_vpImages[m_Image]->m_AutoMapper.IsLoaded() && m_AutoMapperConfig != -1)
		{
			pToolBox->HSplitBottom(2.0f, pToolBox, nullptr);
			pToolBox->HSplitBottom(12.0f, pToolBox, &Button);
			if(m_Seed != 0)
			{
				CUIRect ButtonAuto;
				Button.VSplitRight(16.0f, &Button, &ButtonAuto);
				Button.VSplitRight(2.0f, &Button, nullptr);
				static int s_AutoMapperButtonAuto = 0;
				if(Editor()->DoButton_Editor(&s_AutoMapperButtonAuto, "A", m_AutoAutoMap, &ButtonAuto, BUTTONFLAG_LEFT, "Automatically run the automapper after modifications."))
				{
					m_AutoAutoMap = !m_AutoAutoMap;
					FlagModified(0, 0, m_Width, m_Height);
					if(!m_TilesHistory.empty()) // Sometimes pressing that button causes the automap to run so we should be able to undo that
					{
						// record undo
						Editor()->m_EditorHistory.RecordAction(std::make_shared<CEditorActionTileChanges>(Editor(), Editor()->m_SelectedGroup, Editor()->m_vSelectedLayers[0], "Auto map", m_TilesHistory));
						ClearHistory();
					}
				}
			}

			static int s_AutoMapperButton = 0;
			if(Editor()->DoButton_Editor(&s_AutoMapperButton, "Automap", 0, &Button, BUTTONFLAG_LEFT, "Run the automapper."))
			{
				Map()->m_vpImages[m_Image]->m_AutoMapper.Proceed(this, Map()->m_pGameLayer.get(), m_AutoMapperReference, m_AutoMapperConfig, m_Seed);
				// record undo
				Editor()->m_EditorHistory.RecordAction(std::make_shared<CEditorActionTileChanges>(Editor(), Editor()->m_SelectedGroup, Editor()->m_vSelectedLayers[0], "Auto map", m_TilesHistory));
				ClearHistory();
				return CUi::POPUP_CLOSE_CURRENT;
			}
		}
	}

	CProperty aProps[] = {
		{"Width", m_Width, PROPTYPE_INT, 1, 100000},
		{"Height", m_Height, PROPTYPE_INT, 1, 100000},
		{"Shift", 0, PROPTYPE_SHIFT, 0, 0},
		{"Shift by", Editor()->m_ShiftBy, PROPTYPE_INT, 1, 100000},
		{"Image", m_Image, PROPTYPE_IMAGE, 0, 0},
		{"Color", PackColor(m_Color), PROPTYPE_COLOR, 0, 0},
		{"Color Env", m_ColorEnv + 1, PROPTYPE_ENVELOPE, 0, 0},
		{"Color TO", m_ColorEnvOffset, PROPTYPE_INT, -1000000, 1000000},
		{"Auto Rule", m_AutoMapperConfig, PROPTYPE_AUTOMAPPER, m_Image, 0},
		{"Reference", m_AutoMapperReference, PROPTYPE_AUTOMAPPER_REFERENCE, 0, 0},
		{"Live Gametiles", m_LiveGameTiles, PROPTYPE_BOOL, 0, 1},
		{"Seed", m_Seed, PROPTYPE_INT, 0, 1000000000},
		{nullptr},
	};

	if(EntitiesLayer) // remove the image and color properties if this is a game layer
	{
		aProps[(int)ETilesProp::PROP_IMAGE].m_pName = nullptr;
		aProps[(int)ETilesProp::PROP_COLOR].m_pName = nullptr;
		aProps[(int)ETilesProp::PROP_AUTOMAPPER].m_pName = nullptr;
		aProps[(int)ETilesProp::PROP_AUTOMAPPER_REFERENCE].m_pName = nullptr;
	}
	if(m_Image == -1)
	{
		aProps[(int)ETilesProp::PROP_AUTOMAPPER].m_pName = nullptr;
		aProps[(int)ETilesProp::PROP_AUTOMAPPER_REFERENCE].m_pName = nullptr;
		aProps[(int)ETilesProp::PROP_SEED].m_pName = nullptr;
	}

	static int s_aIds[(int)ETilesProp::NUM_PROPS] = {0};
	int NewVal = 0;
	auto [State, Prop] = Editor()->DoPropertiesWithState<ETilesProp>(pToolBox, aProps, s_aIds, &NewVal);

	static CLayerTilesPropTracker s_Tracker(Editor());
	s_Tracker.Begin(this, Prop, State);
	Editor()->m_EditorHistory.BeginBulk();

	if(Prop == ETilesProp::PROP_WIDTH && NewVal > 1)
	{
		if(NewVal > 1000 && !Editor()->m_LargeLayerWasWarned)
		{
			Editor()->m_PopupEventType = CEditor::POPEVENT_LARGELAYER;
			Editor()->m_PopupEventActivated = true;
			Editor()->m_LargeLayerWasWarned = true;
		}
		Resize(NewVal, m_Height);
	}
	else if(Prop == ETilesProp::PROP_HEIGHT && NewVal > 1)
	{
		if(NewVal > 1000 && !Editor()->m_LargeLayerWasWarned)
		{
			Editor()->m_PopupEventType = CEditor::POPEVENT_LARGELAYER;
			Editor()->m_PopupEventActivated = true;
			Editor()->m_LargeLayerWasWarned = true;
		}
		Resize(m_Width, NewVal);
	}
	else if(Prop == ETilesProp::PROP_SHIFT)
	{
		Shift((EShiftDirection)NewVal);
	}
	else if(Prop == ETilesProp::PROP_SHIFT_BY)
	{
		Editor()->m_ShiftBy = NewVal;
	}
	else if(Prop == ETilesProp::PROP_IMAGE)
	{
		m_Image = NewVal;
		if(NewVal == -1)
		{
			m_Image = -1;
		}
		else
		{
			m_Image = NewVal % Map()->m_vpImages.size();
			m_AutoMapperConfig = -1;

			if(Map()->m_vpImages[m_Image]->m_Width % 16 != 0 || Map()->m_vpImages[m_Image]->m_Height % 16 != 0)
			{
				Editor()->m_PopupEventType = CEditor::POPEVENT_IMAGEDIV16;
				Editor()->m_PopupEventActivated = true;
				m_Image = -1;
			}
		}
	}
	else if(Prop == ETilesProp::PROP_COLOR)
	{
		m_Color = UnpackColor(NewVal);
	}
	else if(Prop == ETilesProp::PROP_COLOR_ENV)
	{
		int Index = std::clamp(NewVal - 1, -1, (int)Map()->m_vpEnvelopes.size() - 1);
		const int Step = (Index - m_ColorEnv) % 2;
		if(Step != 0)
		{
			for(; Index >= -1 && Index < (int)Map()->m_vpEnvelopes.size(); Index += Step)
			{
				if(Index == -1 || Map()->m_vpEnvelopes[Index]->GetChannels() == 4)
				{
					m_ColorEnv = Index;
					break;
				}
			}
		}
	}
	else if(Prop == ETilesProp::PROP_COLOR_ENV_OFFSET)
	{
		m_ColorEnvOffset = NewVal;
	}
	else if(Prop == ETilesProp::PROP_SEED)
	{
		m_Seed = NewVal;
	}
	else if(Prop == ETilesProp::PROP_AUTOMAPPER)
	{
		if(m_Image >= 0 && Map()->m_vpImages[m_Image]->m_AutoMapper.ConfigNamesNum() > 0 && NewVal >= 0)
			m_AutoMapperConfig = NewVal % Map()->m_vpImages[m_Image]->m_AutoMapper.ConfigNamesNum();
		else
			m_AutoMapperConfig = -1;
	}
	else if(Prop == ETilesProp::PROP_AUTOMAPPER_REFERENCE)
	{
		m_AutoMapperReference = NewVal;
	}
	else if(Prop == ETilesProp::PROP_LIVE_GAMETILES)
	{
		m_LiveGameTiles = NewVal != 0;
	}

	s_Tracker.End(Prop, State);

	// Check if modified property could have an effect on automapper
	if((State == EEditState::END || State == EEditState::ONE_GO) && HasAutomapEffect(Prop))
	{
		FlagModified(0, 0, m_Width, m_Height);

		// Record undo if automapper was ran
		if(m_AutoAutoMap && !m_TilesHistory.empty())
		{
			Editor()->m_EditorHistory.RecordAction(std::make_shared<CEditorActionTileChanges>(Editor(), Editor()->m_SelectedGroup, Editor()->m_vSelectedLayers[0], "Auto map", m_TilesHistory));
			ClearHistory();
		}
	}

	// End undo bulk, taking the first action display as the displayed text in the history
	// This is usually the resulting text of the edit layer tiles prop action
	// Since we may also squeeze a tile changes action, we want both to appear as one, thus using a bulk
	Editor()->m_EditorHistory.EndBulk(0);

	return CUi::POPUP_KEEP_OPEN;
}

CUi::EPopupMenuFunctionResult CLayerTiles::RenderCommonProperties(SCommonPropState &State, CEditor *pEditor, CUIRect *pToolbox, std::vector<std::shared_ptr<CLayerTiles>> &vpLayers, std::vector<int> &vLayerIndices)
{
	if(State.m_Modified)
	{
		CUIRect Commit;
		pToolbox->HSplitBottom(20.0f, pToolbox, &Commit);
		static int s_CommitButton = 0;
		if(pEditor->DoButton_Editor(&s_CommitButton, "Commit", 0, &Commit, BUTTONFLAG_LEFT, "Apply the changes."))
		{
			bool HasModifiedSize = (State.m_Modified & SCommonPropState::MODIFIED_SIZE) != 0;
			bool HasModifiedColor = (State.m_Modified & SCommonPropState::MODIFIED_COLOR) != 0;

			std::vector<std::shared_ptr<IEditorAction>> vpActions;
			int j = 0;
			int GroupIndex = pEditor->m_SelectedGroup;
			for(auto &pLayer : vpLayers)
			{
				int LayerIndex = vLayerIndices[j++];
				if(HasModifiedSize)
				{
					std::map<int, std::shared_ptr<CLayer>> SavedLayers;
					SavedLayers[LAYERTYPE_TILES] = pLayer->Duplicate();
					if(pLayer->m_HasGame || pLayer->m_HasFront || pLayer->m_HasSwitch || pLayer->m_HasSpeedup || pLayer->m_HasTune || pLayer->m_HasTele)
					{ // Need to save all entities layers when any entity layer
						if(pEditor->m_Map.m_pFrontLayer && !pLayer->m_HasFront)
							SavedLayers[LAYERTYPE_FRONT] = pEditor->m_Map.m_pFrontLayer->Duplicate();
						if(pEditor->m_Map.m_pTeleLayer && !pLayer->m_HasTele)
							SavedLayers[LAYERTYPE_TELE] = pEditor->m_Map.m_pTeleLayer->Duplicate();
						if(pEditor->m_Map.m_pSwitchLayer && !pLayer->m_HasSwitch)
							SavedLayers[LAYERTYPE_SWITCH] = pEditor->m_Map.m_pSwitchLayer->Duplicate();
						if(pEditor->m_Map.m_pSpeedupLayer && !pLayer->m_HasSpeedup)
							SavedLayers[LAYERTYPE_SPEEDUP] = pEditor->m_Map.m_pSpeedupLayer->Duplicate();
						if(pEditor->m_Map.m_pTuneLayer && !pLayer->m_HasTune)
							SavedLayers[LAYERTYPE_TUNE] = pEditor->m_Map.m_pTuneLayer->Duplicate();
						if(!pLayer->m_HasGame)
							SavedLayers[LAYERTYPE_GAME] = pEditor->m_Map.m_pGameLayer->Duplicate();
					}

					int PrevW = pLayer->m_Width;
					int PrevH = pLayer->m_Height;
					pLayer->Resize(State.m_Width, State.m_Height);

					if(PrevW != State.m_Width)
					{
						std::shared_ptr<CEditorActionEditLayerTilesProp> pAction;
						vpActions.push_back(pAction = std::make_shared<CEditorActionEditLayerTilesProp>(pEditor, GroupIndex, LayerIndex, ETilesProp::PROP_WIDTH, PrevW, State.m_Width));
						pAction->SetSavedLayers(SavedLayers);
					}

					if(PrevH != State.m_Height)
					{
						std::shared_ptr<CEditorActionEditLayerTilesProp> pAction;
						vpActions.push_back(pAction = std::make_shared<CEditorActionEditLayerTilesProp>(pEditor, GroupIndex, LayerIndex, ETilesProp::PROP_HEIGHT, PrevH, State.m_Height));
						pAction->SetSavedLayers(SavedLayers);
					}
				}

				if(HasModifiedColor && !pLayer->IsEntitiesLayer())
				{
					const int PackedColor = PackColor(pLayer->m_Color);
					pLayer->m_Color = UnpackColor(State.m_Color);
					vpActions.push_back(std::make_shared<CEditorActionEditLayerTilesProp>(pEditor, GroupIndex, LayerIndex, ETilesProp::PROP_COLOR, PackedColor, State.m_Color));
				}

				pLayer->FlagModified(0, 0, pLayer->m_Width, pLayer->m_Height);
			}
			State.m_Modified = 0;

			char aDisplay[256];
			str_format(aDisplay, sizeof(aDisplay), "Edit %d layers common properties: %s", (int)vpLayers.size(), HasModifiedColor && HasModifiedSize ? "color, size" : (HasModifiedColor ? "color" : "size"));
			pEditor->m_EditorHistory.RecordAction(std::make_shared<CEditorActionBulk>(pEditor, vpActions, aDisplay));
		}
	}
	else
	{
		for(auto &pLayer : vpLayers)
		{
			if(pLayer->m_Width > State.m_Width)
				State.m_Width = pLayer->m_Width;
			if(pLayer->m_Height > State.m_Height)
				State.m_Height = pLayer->m_Height;
		}

		State.m_Color = PackColor(vpLayers[0]->m_Color);
	}

	{
		CUIRect Warning;
		pToolbox->HSplitTop(13.0f, &Warning, pToolbox);
		Warning.HMargin(0.5f, &Warning);

		pEditor->TextRender()->TextColor(ColorRGBA(1.0f, 0.0f, 0.0f, 1.0f));
		SLabelProperties Props;
		Props.m_MaxWidth = Warning.w;
		pEditor->Ui()->DoLabel(&Warning, "Editing multiple layers", 9.0f, TEXTALIGN_ML, Props);
		pEditor->TextRender()->TextColor(ColorRGBA(1.0f, 1.0f, 1.0f, 1.0f));
		pToolbox->HSplitTop(2.0f, nullptr, pToolbox);
	}

	CProperty aProps[] = {
		{"Width", State.m_Width, PROPTYPE_INT, 1, 100000},
		{"Height", State.m_Height, PROPTYPE_INT, 1, 100000},
		{"Shift", 0, PROPTYPE_SHIFT, 0, 0},
		{"Shift by", pEditor->m_ShiftBy, PROPTYPE_INT, 1, 100000},
		{"Color", State.m_Color, PROPTYPE_COLOR, 0, 0},
		{nullptr},
	};

	static int s_aIds[(int)ETilesCommonProp::NUM_PROPS] = {0};
	int NewVal = 0;
	auto [PropState, Prop] = pEditor->DoPropertiesWithState<ETilesCommonProp>(pToolbox, aProps, s_aIds, &NewVal);

	static CLayerTilesCommonPropTracker s_Tracker(pEditor);
	s_Tracker.m_vpLayers = vpLayers;
	s_Tracker.m_vLayerIndices = vLayerIndices;

	s_Tracker.Begin(nullptr, Prop, PropState);

	if(Prop == ETilesCommonProp::PROP_WIDTH && NewVal > 1)
	{
		if(NewVal > 1000 && !pEditor->m_LargeLayerWasWarned)
		{
			pEditor->m_PopupEventType = CEditor::POPEVENT_LARGELAYER;
			pEditor->m_PopupEventActivated = true;
			pEditor->m_LargeLayerWasWarned = true;
		}
		State.m_Width = NewVal;
	}
	else if(Prop == ETilesCommonProp::PROP_HEIGHT && NewVal > 1)
	{
		if(NewVal > 1000 && !pEditor->m_LargeLayerWasWarned)
		{
			pEditor->m_PopupEventType = CEditor::POPEVENT_LARGELAYER;
			pEditor->m_PopupEventActivated = true;
			pEditor->m_LargeLayerWasWarned = true;
		}
		State.m_Height = NewVal;
	}
	else if(Prop == ETilesCommonProp::PROP_SHIFT)
	{
		for(auto &pLayer : vpLayers)
			pLayer->Shift((EShiftDirection)NewVal);
	}
	else if(Prop == ETilesCommonProp::PROP_SHIFT_BY)
	{
		pEditor->m_ShiftBy = NewVal;
	}
	else if(Prop == ETilesCommonProp::PROP_COLOR)
	{
		State.m_Color = NewVal;
	}

	s_Tracker.End(Prop, PropState);

	if(PropState == EEditState::END || PropState == EEditState::ONE_GO)
	{
		if(Prop == ETilesCommonProp::PROP_WIDTH || Prop == ETilesCommonProp::PROP_HEIGHT)
		{
			State.m_Modified |= SCommonPropState::MODIFIED_SIZE;
		}
		else if(Prop == ETilesCommonProp::PROP_COLOR)
		{
			State.m_Modified |= SCommonPropState::MODIFIED_COLOR;
		}
	}

	return CUi::POPUP_KEEP_OPEN;
}

void CLayerTiles::FlagModified(int x, int y, int w, int h)
{
	Map()->OnModify();
	if(m_Seed != 0 && m_AutoMapperConfig != -1 && m_AutoAutoMap && m_Image >= 0)
	{
		Map()->m_vpImages[m_Image]->m_AutoMapper.ProceedLocalized(this, Map()->m_pGameLayer.get(), m_AutoMapperReference, m_AutoMapperConfig, m_Seed, x, y, w, h);
	}
}

void CLayerTiles::ModifyImageIndex(const FIndexModifyFunction &IndexModifyFunction)
{
	IndexModifyFunction(&m_Image);
}

void CLayerTiles::ModifyEnvelopeIndex(const FIndexModifyFunction &IndexModifyFunction)
{
	IndexModifyFunction(&m_ColorEnv);
}

void CLayerTiles::ShowPreventUnusedTilesWarning()
{
	if(!Editor()->m_PreventUnusedTilesWasWarned)
	{
		Editor()->m_PopupEventType = CEditor::POPEVENT_PREVENTUNUSEDTILES;
		Editor()->m_PopupEventActivated = true;
		Editor()->m_PreventUnusedTilesWasWarned = true;
	}
}

bool CLayerTiles::HasAutomapEffect(ETilesProp Prop)
{
	switch(Prop)
	{
	case ETilesProp::PROP_WIDTH:
	case ETilesProp::PROP_HEIGHT:
	case ETilesProp::PROP_SHIFT:
	case ETilesProp::PROP_IMAGE:
	case ETilesProp::PROP_AUTOMAPPER:
	case ETilesProp::PROP_SEED:
		return true;
	default:
		return false;
	}
	return false;
}
