#include "image.h"
#include "sound.h"

#include <base/dbg.h>
#include <base/fs.h>
#include <base/log.h>
#include <base/mem.h>
#include <base/str.h>
#include <base/time.h>

#include <engine/client.h>
#include <engine/engine.h>
#include <engine/gfx/image_manipulation.h>
#include <engine/graphics.h>
#include <engine/map.h>
#include <engine/shared/config.h>
#include <engine/shared/datafile.h>
#include <engine/shared/filecollection.h>
#include <engine/sound.h>
#include <engine/storage.h>

#include <game/editor/editor.h>
#include <game/editor/editor_actions.h>
#include <game/gamecore.h>
#include <game/mapitems_ex.h>

// compatibility with old sound layers
class CSoundSourceDeprecated
{
public:
	CPoint m_Position;
	int m_Loop;
	int m_TimeDelay; // in s
	int m_FalloffDistance;
	int m_PosEnv;
	int m_PosEnvOffset;
	int m_SoundEnv;
	int m_SoundEnvOffset;
};

void CDataFileWriterFinishJob::Run()
{
	m_Writer.Finish();

	if(!m_pStorage->RenameFile(m_aTempFilename, m_aRealFilename, IStorage::TYPE_SAVE))
	{
		str_format(m_aErrorMessage, sizeof(m_aErrorMessage), "Saving failed: Could not move temporary map file '%s' to '%s'.", m_aTempFilename, m_aRealFilename);
		log_error("editor/save", "%s", m_aErrorMessage);
		return;
	}

	log_trace("editor/save", "Saved map to '%s'.", m_aRealFilename);
}

CDataFileWriterFinishJob::CDataFileWriterFinishJob(IStorage *pStorage, const char *pRealFilename, const char *pTempFilename, CDataFileWriter &&Writer) :
	m_pStorage(pStorage),
	m_Writer(std::move(Writer))
{
	str_copy(m_aRealFilename, pRealFilename);
	str_copy(m_aTempFilename, pTempFilename);
	m_aErrorMessage[0] = '\0';
}

bool CEditorMap::Save(const char *pFilename, const FErrorHandler &ErrorHandler)
{
	char aFilenameTmp[IO_MAX_PATH_LENGTH];
	IStorage::FormatTmpPath(aFilenameTmp, sizeof(aFilenameTmp), pFilename);

	log_info("editor/save", "Saving map to '%s'...", aFilenameTmp);

	if(!PerformPreSaveSanityChecks(ErrorHandler))
	{
		return false;
	}

	CDataFileWriter Writer;
	if(!Writer.Open(m_pEditor->Storage(), aFilenameTmp))
	{
		char aBuf[IO_MAX_PATH_LENGTH + 64];
		str_format(aBuf, sizeof(aBuf), "Error: Failed to open file '%s' for writing.", aFilenameTmp);
		ErrorHandler(aBuf);
		return false;
	}

	// save version
	{
		CMapItemVersion Item;
		Item.m_Version = 1;
		Writer.AddItem(MAPITEMTYPE_VERSION, 0, sizeof(Item), &Item);
	}

	// save map info
	{
		CMapItemInfoSettings Item;
		Item.m_Version = 1;
		Item.m_Author = Writer.AddDataString(m_MapInfo.m_aAuthor);
		Item.m_MapVersion = Writer.AddDataString(m_MapInfo.m_aVersion);
		Item.m_Credits = Writer.AddDataString(m_MapInfo.m_aCredits);
		Item.m_License = Writer.AddDataString(m_MapInfo.m_aLicense);

		Item.m_Settings = -1;
		if(!m_vSettings.empty())
		{
			int Size = 0;
			for(const auto &Setting : m_vSettings)
			{
				Size += str_length(Setting.m_aCommand) + 1;
			}

			char *pSettings = (char *)malloc(std::max(Size, 1));
			char *pNext = pSettings;
			for(const auto &Setting : m_vSettings)
			{
				int Length = str_length(Setting.m_aCommand) + 1;
				mem_copy(pNext, Setting.m_aCommand, Length);
				pNext += Length;
			}
			Item.m_Settings = Writer.AddData(Size, pSettings);
			free(pSettings);
		}

		Writer.AddItem(MAPITEMTYPE_INFO, 0, sizeof(Item), &Item);
	}

	// save images
	for(size_t i = 0; i < m_vpImages.size(); i++)
	{
		std::shared_ptr<CEditorImage> pImg = m_vpImages[i];

		// analyse the image for when saving (should be done when we load the image)
		// TODO!
		pImg->AnalyseTileFlags();

		CMapItemImage Item;
		Item.m_Version = 1;

		Item.m_Width = pImg->m_Width;
		Item.m_Height = pImg->m_Height;
		Item.m_External = pImg->m_External;
		Item.m_ImageName = Writer.AddDataString(pImg->m_aName);
		if(pImg->m_External)
		{
			Item.m_ImageData = -1;
		}
		else
		{
			dbg_assert(pImg->m_Format == CImageInfo::FORMAT_RGBA, "Embedded images must be in RGBA format");
			Item.m_ImageData = Writer.AddData(pImg->DataSize(), pImg->m_pData);
		}
		Writer.AddItem(MAPITEMTYPE_IMAGE, i, sizeof(Item), &Item);
	}

	// save sounds
	for(size_t i = 0; i < m_vpSounds.size(); i++)
	{
		std::shared_ptr<CEditorSound> pSound = m_vpSounds[i];

		CMapItemSound Item;
		Item.m_Version = 1;

		Item.m_External = 0;
		Item.m_SoundName = Writer.AddDataString(pSound->m_aName);
		Item.m_SoundData = Writer.AddData(pSound->m_DataSize, pSound->m_pData);
		// Value is not read in new versions, but we still need to write it for compatibility with old versions.
		Item.m_SoundDataSize = pSound->m_DataSize;

		Writer.AddItem(MAPITEMTYPE_SOUND, i, sizeof(Item), &Item);
	}

	// save layers
	int LayerCount = 0, GroupCount = 0;
	int AutomapperCount = 0;
	for(const auto &pGroup : m_vpGroups)
	{
		log_trace("editor/save", "Saving group");

		CMapItemGroup GItem;
		GItem.m_Version = 3;

		GItem.m_ParallaxX = pGroup->m_ParallaxX;
		GItem.m_ParallaxY = pGroup->m_ParallaxY;
		GItem.m_OffsetX = pGroup->m_OffsetX;
		GItem.m_OffsetY = pGroup->m_OffsetY;
		GItem.m_UseClipping = pGroup->m_UseClipping;
		GItem.m_ClipX = pGroup->m_ClipX;
		GItem.m_ClipY = pGroup->m_ClipY;
		GItem.m_ClipW = pGroup->m_ClipW;
		GItem.m_ClipH = pGroup->m_ClipH;
		GItem.m_StartLayer = LayerCount;
		GItem.m_NumLayers = 0;

		// save group name
		StrToInts(GItem.m_aName, std::size(GItem.m_aName), pGroup->m_aName);

		for(const std::shared_ptr<CLayer> &pLayer : pGroup->m_vpLayers)
		{
			if(pLayer->m_Type == LAYERTYPE_TILES)
			{
				log_trace("editor/save", "Saving tiles layer");
				std::shared_ptr<CLayerTiles> pLayerTiles = std::static_pointer_cast<CLayerTiles>(pLayer);
				pLayerTiles->PrepareForSave();

				CMapItemLayerTilemap Item;
				Item.m_Version = 3;

				Item.m_Layer.m_Version = 0; // was previously uninitialized, do not rely on it being 0
				Item.m_Layer.m_Flags = pLayerTiles->m_Flags;
				Item.m_Layer.m_Type = pLayerTiles->m_Type;

				Item.m_Color = pLayerTiles->m_Color;
				Item.m_ColorEnv = pLayerTiles->m_ColorEnv;
				Item.m_ColorEnvOffset = pLayerTiles->m_ColorEnvOffset;

				Item.m_Width = pLayerTiles->m_Width;
				Item.m_Height = pLayerTiles->m_Height;
				// Item.m_Flags = pLayerTiles->m_Game ? TILESLAYERFLAG_GAME : 0;

				if(pLayerTiles->m_HasTele)
					Item.m_Flags = TILESLAYERFLAG_TELE;
				else if(pLayerTiles->m_HasSpeedup)
					Item.m_Flags = TILESLAYERFLAG_SPEEDUP;
				else if(pLayerTiles->m_HasFront)
					Item.m_Flags = TILESLAYERFLAG_FRONT;
				else if(pLayerTiles->m_HasSwitch)
					Item.m_Flags = TILESLAYERFLAG_SWITCH;
				else if(pLayerTiles->m_HasTune)
					Item.m_Flags = TILESLAYERFLAG_TUNE;
				else
					Item.m_Flags = pLayerTiles->m_HasGame ? TILESLAYERFLAG_GAME : 0;

				Item.m_Image = pLayerTiles->m_Image;

				// the following values were previously uninitialized, do not rely on them being -1 when unused
				Item.m_Tele = -1;
				Item.m_Speedup = -1;
				Item.m_Front = -1;
				Item.m_Switch = -1;
				Item.m_Tune = -1;

				if(Item.m_Flags && !(pLayerTiles->m_HasGame))
				{
					CTile *pEmptyTiles = (CTile *)calloc((size_t)pLayerTiles->m_Width * pLayerTiles->m_Height, sizeof(CTile));
					mem_zero(pEmptyTiles, (size_t)pLayerTiles->m_Width * pLayerTiles->m_Height * sizeof(CTile));
					Item.m_Data = Writer.AddData((size_t)pLayerTiles->m_Width * pLayerTiles->m_Height * sizeof(CTile), pEmptyTiles);
					free(pEmptyTiles);

					if(pLayerTiles->m_HasTele)
						Item.m_Tele = Writer.AddData((size_t)pLayerTiles->m_Width * pLayerTiles->m_Height * sizeof(CTeleTile), std::static_pointer_cast<CLayerTele>(pLayerTiles)->m_pTeleTile);
					else if(pLayerTiles->m_HasSpeedup)
						Item.m_Speedup = Writer.AddData((size_t)pLayerTiles->m_Width * pLayerTiles->m_Height * sizeof(CSpeedupTile), std::static_pointer_cast<CLayerSpeedup>(pLayerTiles)->m_pSpeedupTile);
					else if(pLayerTiles->m_HasFront)
						Item.m_Front = Writer.AddData((size_t)pLayerTiles->m_Width * pLayerTiles->m_Height * sizeof(CTile), pLayerTiles->m_pTiles);
					else if(pLayerTiles->m_HasSwitch)
						Item.m_Switch = Writer.AddData((size_t)pLayerTiles->m_Width * pLayerTiles->m_Height * sizeof(CSwitchTile), std::static_pointer_cast<CLayerSwitch>(pLayerTiles)->m_pSwitchTile);
					else if(pLayerTiles->m_HasTune)
						Item.m_Tune = Writer.AddData((size_t)pLayerTiles->m_Width * pLayerTiles->m_Height * sizeof(CTuneTile), std::static_pointer_cast<CLayerTune>(pLayerTiles)->m_pTuneTile);
				}
				else
					Item.m_Data = Writer.AddData((size_t)pLayerTiles->m_Width * pLayerTiles->m_Height * sizeof(CTile), pLayerTiles->m_pTiles);

				// save layer name
				StrToInts(Item.m_aName, std::size(Item.m_aName), pLayerTiles->m_aName);

				// save item
				Writer.AddItem(MAPITEMTYPE_LAYER, LayerCount, sizeof(Item), &Item);

				// save auto mapper of each tile layer (not physics layer)
				if(!Item.m_Flags)
				{
					CMapItemAutomapperConfig ItemAutomapper;
					ItemAutomapper.m_Version = 1;
					ItemAutomapper.m_GroupId = GroupCount;
					ItemAutomapper.m_LayerId = GItem.m_NumLayers;
					ItemAutomapper.m_AutomapperConfig = pLayerTiles->m_AutomapperConfig;
					ItemAutomapper.m_AutomapperSeed = pLayerTiles->m_Seed;
					ItemAutomapper.m_Flags = 0;
					if(pLayerTiles->m_AutoAutomapper)
						ItemAutomapper.m_Flags |= CMapItemAutomapperConfig::FLAG_AUTOMATIC;

					Writer.AddItem(MAPITEMTYPE_AUTOMAPPER_CONFIG, AutomapperCount, sizeof(ItemAutomapper), &ItemAutomapper);
					AutomapperCount++;
				}
			}
			else if(pLayer->m_Type == LAYERTYPE_QUADS)
			{
				log_trace("editor/save", "Saving quads layer");
				std::shared_ptr<CLayerQuads> pLayerQuads = std::static_pointer_cast<CLayerQuads>(pLayer);
				CMapItemLayerQuads Item;
				Item.m_Version = 2;
				Item.m_Layer.m_Version = 0; // was previously uninitialized, do not rely on it being 0
				Item.m_Layer.m_Flags = pLayerQuads->m_Flags;
				Item.m_Layer.m_Type = pLayerQuads->m_Type;
				Item.m_Image = pLayerQuads->m_Image;

				Item.m_NumQuads = 0;
				Item.m_Data = -1;
				if(!pLayerQuads->m_vQuads.empty())
				{
					// add the data
					Item.m_NumQuads = pLayerQuads->m_vQuads.size();
					Item.m_Data = Writer.AddDataSwapped(pLayerQuads->m_vQuads.size() * sizeof(CQuad), pLayerQuads->m_vQuads.data());
				}
				else
				{
					// add dummy data for backwards compatibility
					// this allows the layer to be loaded with an empty array since m_NumQuads is 0 while saving
					CQuad Dummy{};
					Item.m_Data = Writer.AddDataSwapped(sizeof(CQuad), &Dummy);
				}

				// save layer name
				StrToInts(Item.m_aName, std::size(Item.m_aName), pLayerQuads->m_aName);

				// save item
				Writer.AddItem(MAPITEMTYPE_LAYER, LayerCount, sizeof(Item), &Item);
			}
			else if(pLayer->m_Type == LAYERTYPE_SOUNDS)
			{
				log_trace("editor/save", "Saving sounds layer");
				std::shared_ptr<CLayerSounds> pLayerSounds = std::static_pointer_cast<CLayerSounds>(pLayer);
				CMapItemLayerSounds Item;
				Item.m_Version = 2;
				Item.m_Layer.m_Version = 0; // was previously uninitialized, do not rely on it being 0
				Item.m_Layer.m_Flags = pLayerSounds->m_Flags;
				Item.m_Layer.m_Type = pLayerSounds->m_Type;
				Item.m_Sound = pLayerSounds->m_Sound;

				Item.m_NumSources = 0;
				if(!pLayerSounds->m_vSources.empty())
				{
					// add the data
					Item.m_NumSources = pLayerSounds->m_vSources.size();
					Item.m_Data = Writer.AddDataSwapped(pLayerSounds->m_vSources.size() * sizeof(CSoundSource), pLayerSounds->m_vSources.data());
				}
				else
				{
					// add dummy data for backwards compatibility
					// this allows the layer to be loaded with an empty array since m_NumSources is 0 while saving
					CSoundSource Dummy{};
					Item.m_Data = Writer.AddDataSwapped(sizeof(CSoundSource), &Dummy);
				}

				// save layer name
				StrToInts(Item.m_aName, std::size(Item.m_aName), pLayerSounds->m_aName);

				// save item
				Writer.AddItem(MAPITEMTYPE_LAYER, LayerCount, sizeof(Item), &Item);
			}

			GItem.m_NumLayers++;
			LayerCount++;
		}

		Writer.AddItem(MAPITEMTYPE_GROUP, GroupCount, sizeof(GItem), &GItem);
		GroupCount++;
	}

	// save envelopes
	log_trace("editor/save", "Saving envelopes");
	int PointCount = 0;
	for(size_t e = 0; e < m_vpEnvelopes.size(); e++)
	{
		CMapItemEnvelope Item;
		Item.m_Version = 2;
		Item.m_Channels = m_vpEnvelopes[e]->GetChannels();
		Item.m_StartPoint = PointCount;
		Item.m_NumPoints = m_vpEnvelopes[e]->m_vPoints.size();
		Item.m_Synchronized = m_vpEnvelopes[e]->m_Synchronized;
		StrToInts(Item.m_aName, std::size(Item.m_aName), m_vpEnvelopes[e]->m_aName);

		Writer.AddItem(MAPITEMTYPE_ENVELOPE, e, sizeof(Item), &Item);
		PointCount += Item.m_NumPoints;
	}

	// save points
	log_trace("editor/save", "Saving envelope points");
	bool BezierUsed = false;
	for(const auto &pEnvelope : m_vpEnvelopes)
	{
		for(const auto &Point : pEnvelope->m_vPoints)
		{
			if(Point.m_Curvetype == CURVETYPE_BEZIER)
			{
				BezierUsed = true;
				break;
			}
		}
		if(BezierUsed)
			break;
	}

	CEnvPoint *pPoints = (CEnvPoint *)calloc(std::max(PointCount, 1), sizeof(CEnvPoint));
	CEnvPointBezier *pPointsBezier = nullptr;
	if(BezierUsed)
		pPointsBezier = (CEnvPointBezier *)calloc(std::max(PointCount, 1), sizeof(CEnvPointBezier));
	PointCount = 0;

	for(const auto &pEnvelope : m_vpEnvelopes)
	{
		const CEnvPoint_runtime *pPrevPoint = nullptr;
		for(const auto &Point : pEnvelope->m_vPoints)
		{
			mem_copy(&pPoints[PointCount], &Point, sizeof(CEnvPoint));
			if(pPointsBezier != nullptr)
			{
				if(Point.m_Curvetype == CURVETYPE_BEZIER)
				{
					mem_copy(&pPointsBezier[PointCount].m_aOutTangentDeltaX, &Point.m_Bezier.m_aOutTangentDeltaX, sizeof(Point.m_Bezier.m_aOutTangentDeltaX));
					mem_copy(&pPointsBezier[PointCount].m_aOutTangentDeltaY, &Point.m_Bezier.m_aOutTangentDeltaY, sizeof(Point.m_Bezier.m_aOutTangentDeltaY));
				}
				if(pPrevPoint != nullptr && pPrevPoint->m_Curvetype == CURVETYPE_BEZIER)
				{
					mem_copy(&pPointsBezier[PointCount].m_aInTangentDeltaX, &Point.m_Bezier.m_aInTangentDeltaX, sizeof(Point.m_Bezier.m_aInTangentDeltaX));
					mem_copy(&pPointsBezier[PointCount].m_aInTangentDeltaY, &Point.m_Bezier.m_aInTangentDeltaY, sizeof(Point.m_Bezier.m_aInTangentDeltaY));
				}
			}
			PointCount++;
			pPrevPoint = &Point;
		}
	}

	Writer.AddItem(MAPITEMTYPE_ENVPOINTS, 0, sizeof(CEnvPoint) * PointCount, pPoints);
	free(pPoints);

	if(pPointsBezier != nullptr)
	{
		Writer.AddItem(MAPITEMTYPE_ENVPOINTS_BEZIER, 0, sizeof(CEnvPointBezier) * PointCount, pPointsBezier);
		free(pPointsBezier);
	}

	// finish the data file
	std::shared_ptr<CDataFileWriterFinishJob> pWriterFinishJob = std::make_shared<CDataFileWriterFinishJob>(m_pEditor->Storage(), pFilename, aFilenameTmp, std::move(Writer));
	m_pEditor->Engine()->AddJob(pWriterFinishJob);
	m_pEditor->m_WriterFinishJobs.push_back(pWriterFinishJob);

	return true;
}

bool CEditorMap::PerformPreSaveSanityChecks(const FErrorHandler &ErrorHandler)
{
	bool Success = true;
	char aErrorMessage[256];

	for(const std::shared_ptr<CEditorImage> &pImage : m_vpImages)
	{
		if(!pImage->m_External && pImage->m_pData == nullptr)
		{
			str_format(aErrorMessage, sizeof(aErrorMessage), "Error: Saving is not possible because the image '%s' could not be loaded. Remove or replace this image.", pImage->m_aName);
			ErrorHandler(aErrorMessage);
			Success = false;
		}
	}

	for(const std::shared_ptr<CEditorSound> &pSound : m_vpSounds)
	{
		if(pSound->m_pData == nullptr)
		{
			str_format(aErrorMessage, sizeof(aErrorMessage), "Error: Saving is not possible because the sound '%s' could not be loaded. Remove or replace this sound.", pSound->m_aName);
			ErrorHandler(aErrorMessage);
			Success = false;
		}
	}

	return Success;
}

bool CEditorMap::Load(const char *pFilename, int StorageType, const FErrorHandler &ErrorHandler)
{
	std::unique_ptr<IMap> pMap = CreateMap();
	if(!pMap->Load(Editor()->Storage(), pFilename, StorageType))
	{
		ErrorHandler("Error: Failed to open map file. See local console for details.");
		return false;
	}

	// load map info
	{
		int Start, Num;
		pMap->GetType(MAPITEMTYPE_INFO, &Start, &Num);
		for(int i = Start; i < Start + Num; i++)
		{
			int ItemSize = pMap->GetItemSize(i);
			int ItemId;
			CMapItemInfoSettings *pItem = (CMapItemInfoSettings *)pMap->GetItem(i, nullptr, &ItemId);
			if(!pItem || ItemId != 0)
				continue;

			const auto &&ReadStringInfo = [&](int Index, char *pBuffer, size_t BufferSize, const char *pErrorContext) {
				const char *pStr = pMap->GetDataString(Index);
				if(pStr == nullptr)
				{
					char aBuf[128];
					str_format(aBuf, sizeof(aBuf), "Error: Failed to read %s from map info.", pErrorContext);
					ErrorHandler(aBuf);
					pBuffer[0] = '\0';
				}
				else
				{
					str_copy(pBuffer, pStr, BufferSize);
				}
			};

			ReadStringInfo(pItem->m_Author, m_MapInfo.m_aAuthor, sizeof(m_MapInfo.m_aAuthor), "author");
			ReadStringInfo(pItem->m_MapVersion, m_MapInfo.m_aVersion, sizeof(m_MapInfo.m_aVersion), "version");
			ReadStringInfo(pItem->m_Credits, m_MapInfo.m_aCredits, sizeof(m_MapInfo.m_aCredits), "credits");
			ReadStringInfo(pItem->m_License, m_MapInfo.m_aLicense, sizeof(m_MapInfo.m_aLicense), "license");

			if(pItem->m_Version != 1 || ItemSize < (int)sizeof(CMapItemInfoSettings))
				break;

			if(!(pItem->m_Settings > -1))
				break;

			const unsigned Size = pMap->GetDataSize(pItem->m_Settings);
			char *pSettings = (char *)pMap->GetData(pItem->m_Settings);
			char *pNext = pSettings;
			while(pNext < pSettings + Size)
			{
				int StrSize = str_length(pNext) + 1;
				m_vSettings.emplace_back(pNext);
				pNext += StrSize;
			}
		}
	}

	// load images
	{
		int Start, Num;
		pMap->GetType(MAPITEMTYPE_IMAGE, &Start, &Num);
		for(int i = 0; i < Num; i++)
		{
			CMapItemImage_v2 *pItem = (CMapItemImage_v2 *)pMap->GetItem(Start + i);

			// copy base info
			std::shared_ptr<CEditorImage> pImg = std::make_shared<CEditorImage>(this);
			pImg->m_External = pItem->m_External;

			const char *pName = pMap->GetDataString(pItem->m_ImageName);
			if(pName == nullptr || pName[0] == '\0')
			{
				char aBuf[128];
				str_format(aBuf, sizeof(aBuf), "Error: Failed to read name of image %d.", i);
				ErrorHandler(aBuf);
			}
			else
				str_copy(pImg->m_aName, pName);

			if(pItem->m_Version > 1 && pItem->m_MustBe1 != 1)
			{
				char aBuf[128];
				str_format(aBuf, sizeof(aBuf), "Error: Unsupported image type of image %d '%s'.", i, pImg->m_aName);
				ErrorHandler(aBuf);
			}

			if(pImg->m_External || (pItem->m_Version > 1 && pItem->m_MustBe1 != 1))
			{
				char aBuf[IO_MAX_PATH_LENGTH];
				str_format(aBuf, sizeof(aBuf), "mapres/%s.png", pImg->m_aName);

				// load external
				if(m_pEditor->Graphics()->LoadPng(*pImg, aBuf, IStorage::TYPE_ALL))
				{
					ConvertToRgba(*pImg);

					int TextureLoadFlag = m_pEditor->Graphics()->TextureLoadFlags();
					if(pImg->m_Width % 16 != 0 || pImg->m_Height % 16 != 0)
						TextureLoadFlag = 0;
					pImg->m_External = 1;
					pImg->m_Texture = m_pEditor->Graphics()->LoadTextureRaw(*pImg, TextureLoadFlag, aBuf);
				}
				else
				{
					str_format(aBuf, sizeof(aBuf), "Error: Failed to load external image '%s'.", pImg->m_aName);
					ErrorHandler(aBuf);
				}
			}
			else
			{
				pImg->m_Width = pItem->m_Width;
				pImg->m_Height = pItem->m_Height;
				pImg->m_Format = CImageInfo::FORMAT_RGBA;

				const void *pData = pMap->GetData(pItem->m_ImageData);
				if(pItem->m_Width <= 0 || pItem->m_Height <= 0 || pData == nullptr || (size_t)pMap->GetDataSize(pItem->m_ImageData) < pImg->DataSize())
				{
					pImg->m_Width = 0;
					pImg->m_Height = 0;
					char aBuf[128];
					str_format(aBuf, sizeof(aBuf), "Error: Failed to load data of image %d '%s'.", i, pImg->m_aName);
					ErrorHandler(aBuf);
				}
				else
				{
					pImg->Allocate();

					// copy image data
					mem_copy(pImg->m_pData, pData, pImg->DataSize());
					int TextureLoadFlag = m_pEditor->Graphics()->TextureLoadFlags();
					if(pImg->m_Width % 16 != 0 || pImg->m_Height % 16 != 0)
						TextureLoadFlag = 0;
					pImg->m_Texture = m_pEditor->Graphics()->LoadTextureRaw(*pImg, TextureLoadFlag, pImg->m_aName);
				}
			}

			// load auto mapper file
			pImg->m_Automapper.Load(pImg->m_aName);

			m_vpImages.push_back(pImg);

			// unload image
			pMap->UnloadData(pItem->m_ImageData);
			pMap->UnloadData(pItem->m_ImageName);
		}
	}

	// load sounds
	{
		int Start, Num;
		pMap->GetType(MAPITEMTYPE_SOUND, &Start, &Num);
		for(int i = 0; i < Num; i++)
		{
			CMapItemSound *pItem = (CMapItemSound *)pMap->GetItem(Start + i);

			// copy base info
			std::shared_ptr<CEditorSound> pSound = std::make_shared<CEditorSound>(this);

			const char *pName = pMap->GetDataString(pItem->m_SoundName);
			if(pName == nullptr || pName[0] == '\0')
			{
				char aBuf[128];
				str_format(aBuf, sizeof(aBuf), "Error: Failed to read name of sound %d.", i);
				ErrorHandler(aBuf);
			}
			else
				str_copy(pSound->m_aName, pName);

			if(pItem->m_External)
			{
				char aBuf[IO_MAX_PATH_LENGTH];
				str_format(aBuf, sizeof(aBuf), "mapres/%s.opus", pSound->m_aName);

				// load external
				if(m_pEditor->Storage()->ReadFile(aBuf, IStorage::TYPE_ALL, &pSound->m_pData, &pSound->m_DataSize))
				{
					pSound->m_SoundId = m_pEditor->Sound()->LoadOpusFromMem(pSound->m_pData, pSound->m_DataSize, true, pSound->m_aName);
				}
				else
				{
					str_format(aBuf, sizeof(aBuf), "Error: Failed to load external sound '%s'.", pSound->m_aName);
					ErrorHandler(aBuf);
				}
			}
			else
			{
				pSound->m_DataSize = pMap->GetDataSize(pItem->m_SoundData);
				void *pData = pMap->GetData(pItem->m_SoundData);
				pSound->m_pData = malloc(pSound->m_DataSize);
				mem_copy(pSound->m_pData, pData, pSound->m_DataSize);
				pSound->m_SoundId = m_pEditor->Sound()->LoadOpusFromMem(pSound->m_pData, pSound->m_DataSize, true, pSound->m_aName);
			}

			m_vpSounds.push_back(pSound);

			// unload sound
			pMap->UnloadData(pItem->m_SoundData);
			pMap->UnloadData(pItem->m_SoundName);
		}
	}

	// load groups
	{
		int LayersStart, LayersNum;
		pMap->GetType(MAPITEMTYPE_LAYER, &LayersStart, &LayersNum);

		int Start, Num;
		pMap->GetType(MAPITEMTYPE_GROUP, &Start, &Num);

		for(int g = 0; g < Num; g++)
		{
			CMapItemGroup *pGItem = (CMapItemGroup *)pMap->GetItem(Start + g);

			if(pGItem->m_Version < 1 || pGItem->m_Version > 3)
				continue;

			std::shared_ptr<CLayerGroup> pGroup = NewGroup();
			pGroup->m_ParallaxX = pGItem->m_ParallaxX;
			pGroup->m_ParallaxY = pGItem->m_ParallaxY;
			pGroup->m_OffsetX = pGItem->m_OffsetX;
			pGroup->m_OffsetY = pGItem->m_OffsetY;

			if(pGItem->m_Version >= 2)
			{
				pGroup->m_UseClipping = pGItem->m_UseClipping;
				pGroup->m_ClipX = pGItem->m_ClipX;
				pGroup->m_ClipY = pGItem->m_ClipY;
				pGroup->m_ClipW = pGItem->m_ClipW;
				pGroup->m_ClipH = pGItem->m_ClipH;
			}

			// load group name
			if(pGItem->m_Version >= 3)
				IntsToStr(pGItem->m_aName, std::size(pGItem->m_aName), pGroup->m_aName, std::size(pGroup->m_aName));

			for(int l = 0; l < pGItem->m_NumLayers; l++)
			{
				CMapItemLayer *pLayerItem = (CMapItemLayer *)pMap->GetItem(LayersStart + pGItem->m_StartLayer + l);
				if(!pLayerItem)
					continue;

				if(pLayerItem->m_Type == LAYERTYPE_TILES)
				{
					CMapItemLayerTilemap *pTilemapItem = (CMapItemLayerTilemap *)pLayerItem;

					std::shared_ptr<CLayerTiles> pTiles;
					if(pTilemapItem->m_Flags & TILESLAYERFLAG_GAME)
					{
						pTiles = std::make_shared<CLayerGame>(this, pTilemapItem->m_Width, pTilemapItem->m_Height);
						MakeGameLayer(pTiles);
						MakeGameGroup(pGroup);
					}
					else if(pTilemapItem->m_Flags & TILESLAYERFLAG_TELE)
					{
						if(pTilemapItem->m_Version <= 2)
							pTilemapItem->m_Tele = *((const int *)(pTilemapItem) + 15);

						pTiles = std::make_shared<CLayerTele>(this, pTilemapItem->m_Width, pTilemapItem->m_Height);
						MakeTeleLayer(pTiles);
					}
					else if(pTilemapItem->m_Flags & TILESLAYERFLAG_SPEEDUP)
					{
						if(pTilemapItem->m_Version <= 2)
							pTilemapItem->m_Speedup = *((const int *)(pTilemapItem) + 16);

						pTiles = std::make_shared<CLayerSpeedup>(this, pTilemapItem->m_Width, pTilemapItem->m_Height);
						MakeSpeedupLayer(pTiles);
					}
					else if(pTilemapItem->m_Flags & TILESLAYERFLAG_FRONT)
					{
						if(pTilemapItem->m_Version <= 2)
							pTilemapItem->m_Front = *((const int *)(pTilemapItem) + 17);

						pTiles = std::make_shared<CLayerFront>(this, pTilemapItem->m_Width, pTilemapItem->m_Height);
						MakeFrontLayer(pTiles);
					}
					else if(pTilemapItem->m_Flags & TILESLAYERFLAG_SWITCH)
					{
						if(pTilemapItem->m_Version <= 2)
							pTilemapItem->m_Switch = *((const int *)(pTilemapItem) + 18);

						pTiles = std::make_shared<CLayerSwitch>(this, pTilemapItem->m_Width, pTilemapItem->m_Height);
						MakeSwitchLayer(pTiles);
					}
					else if(pTilemapItem->m_Flags & TILESLAYERFLAG_TUNE)
					{
						if(pTilemapItem->m_Version <= 2)
							pTilemapItem->m_Tune = *((const int *)(pTilemapItem) + 19);

						pTiles = std::make_shared<CLayerTune>(this, pTilemapItem->m_Width, pTilemapItem->m_Height);
						MakeTuneLayer(pTiles);
					}
					else
					{
						pTiles = std::make_shared<CLayerTiles>(this, pTilemapItem->m_Width, pTilemapItem->m_Height);
						pTiles->m_Color = pTilemapItem->m_Color;
						pTiles->m_ColorEnv = pTilemapItem->m_ColorEnv;
						pTiles->m_ColorEnvOffset = pTilemapItem->m_ColorEnvOffset;
					}

					pTiles->m_Flags = pLayerItem->m_Flags;

					pGroup->AddLayer(pTiles);
					pTiles->m_Image = pTilemapItem->m_Image;

					// validate image index
					if(pTiles->m_Image < -1 || pTiles->m_Image >= (int)m_vpImages.size())
					{
						pTiles->m_Image = -1;
					}

					// load layer name
					if(pTilemapItem->m_Version >= 3)
						IntsToStr(pTilemapItem->m_aName, std::size(pTilemapItem->m_aName), pTiles->m_aName, std::size(pTiles->m_aName));

					if(pTiles->m_HasTele)
					{
						void *pTeleData = pMap->GetData(pTilemapItem->m_Tele);
						unsigned int Size = pMap->GetDataSize(pTilemapItem->m_Tele);
						if(Size >= (size_t)pTiles->m_Width * pTiles->m_Height * sizeof(CTeleTile))
						{
							CTeleTile *pLayerTeleTiles = std::static_pointer_cast<CLayerTele>(pTiles)->m_pTeleTile;
							mem_copy(pLayerTeleTiles, pTeleData, (size_t)pTiles->m_Width * pTiles->m_Height * sizeof(CTeleTile));

							for(int i = 0; i < pTiles->m_Width * pTiles->m_Height; i++)
							{
								if(IsValidTeleTile(pLayerTeleTiles[i].m_Type))
									pTiles->m_pTiles[i].m_Index = pLayerTeleTiles[i].m_Type;
								else
									pTiles->m_pTiles[i].m_Index = 0;
							}
						}
						pMap->UnloadData(pTilemapItem->m_Tele);
					}
					else if(pTiles->m_HasSpeedup)
					{
						void *pSpeedupData = pMap->GetData(pTilemapItem->m_Speedup);
						unsigned int Size = pMap->GetDataSize(pTilemapItem->m_Speedup);

						if(Size >= (size_t)pTiles->m_Width * pTiles->m_Height * sizeof(CSpeedupTile))
						{
							CSpeedupTile *pLayerSpeedupTiles = std::static_pointer_cast<CLayerSpeedup>(pTiles)->m_pSpeedupTile;
							mem_copy(pLayerSpeedupTiles, pSpeedupData, (size_t)pTiles->m_Width * pTiles->m_Height * sizeof(CSpeedupTile));

							for(int i = 0; i < pTiles->m_Width * pTiles->m_Height; i++)
							{
								if(IsValidSpeedupTile(pLayerSpeedupTiles[i].m_Type) && pLayerSpeedupTiles[i].m_Force > 0)
									pTiles->m_pTiles[i].m_Index = pLayerSpeedupTiles[i].m_Type;
								else
									pTiles->m_pTiles[i].m_Index = 0;
							}
						}

						pMap->UnloadData(pTilemapItem->m_Speedup);
					}
					else if(pTiles->m_HasFront)
					{
						void *pFrontData = pMap->GetData(pTilemapItem->m_Front);
						unsigned int Size = pMap->GetDataSize(pTilemapItem->m_Front);
						pTiles->ExtractTiles((CTile *)pFrontData, Size);
						pMap->UnloadData(pTilemapItem->m_Front);
					}
					else if(pTiles->m_HasSwitch)
					{
						void *pSwitchData = pMap->GetData(pTilemapItem->m_Switch);
						unsigned int Size = pMap->GetDataSize(pTilemapItem->m_Switch);
						if(Size >= (size_t)pTiles->m_Width * pTiles->m_Height * sizeof(CSwitchTile))
						{
							CSwitchTile *pLayerSwitchTiles = std::static_pointer_cast<CLayerSwitch>(pTiles)->m_pSwitchTile;
							mem_copy(pLayerSwitchTiles, pSwitchData, (size_t)pTiles->m_Width * pTiles->m_Height * sizeof(CSwitchTile));

							for(int i = 0; i < pTiles->m_Width * pTiles->m_Height; i++)
							{
								if(((pLayerSwitchTiles[i].m_Type > (ENTITY_CRAZY_SHOTGUN + ENTITY_OFFSET) && pLayerSwitchTiles[i].m_Type < (ENTITY_DRAGGER_WEAK + ENTITY_OFFSET)) || pLayerSwitchTiles[i].m_Type == (ENTITY_LASER_O_FAST + 1 + ENTITY_OFFSET)))
									continue;
								else if(pLayerSwitchTiles[i].m_Type >= (ENTITY_ARMOR_1 + ENTITY_OFFSET) && pLayerSwitchTiles[i].m_Type <= (ENTITY_DOOR + ENTITY_OFFSET))
								{
									pTiles->m_pTiles[i].m_Index = pLayerSwitchTiles[i].m_Type;
									pTiles->m_pTiles[i].m_Flags = pLayerSwitchTiles[i].m_Flags;
									continue;
								}

								if(IsValidSwitchTile(pLayerSwitchTiles[i].m_Type))
								{
									pTiles->m_pTiles[i].m_Index = pLayerSwitchTiles[i].m_Type;
									pTiles->m_pTiles[i].m_Flags = pLayerSwitchTiles[i].m_Flags;
								}
							}
						}
						pMap->UnloadData(pTilemapItem->m_Switch);
					}
					else if(pTiles->m_HasTune)
					{
						void *pTuneData = pMap->GetData(pTilemapItem->m_Tune);
						unsigned int Size = pMap->GetDataSize(pTilemapItem->m_Tune);
						if(Size >= (size_t)pTiles->m_Width * pTiles->m_Height * sizeof(CTuneTile))
						{
							CTuneTile *pLayerTuneTiles = std::static_pointer_cast<CLayerTune>(pTiles)->m_pTuneTile;
							mem_copy(pLayerTuneTiles, pTuneData, (size_t)pTiles->m_Width * pTiles->m_Height * sizeof(CTuneTile));

							for(int i = 0; i < pTiles->m_Width * pTiles->m_Height; i++)
							{
								if(IsValidTuneTile(pLayerTuneTiles[i].m_Type))
									pTiles->m_pTiles[i].m_Index = pLayerTuneTiles[i].m_Type;
								else
									pTiles->m_pTiles[i].m_Index = 0;
							}
						}
						pMap->UnloadData(pTilemapItem->m_Tune);
					}
					else // regular tile layer or game layer
					{
						void *pData = pMap->GetData(pTilemapItem->m_Data);
						unsigned int Size = pMap->GetDataSize(pTilemapItem->m_Data);
						pTiles->ExtractTiles((CTile *)pData, Size);
						pMap->UnloadData(pTilemapItem->m_Data);
					}
				}
				else if(pLayerItem->m_Type == LAYERTYPE_QUADS)
				{
					const CMapItemLayerQuads *pQuadsItem = (CMapItemLayerQuads *)pLayerItem;

					std::shared_ptr<CLayerQuads> pQuads = std::make_shared<CLayerQuads>(this);
					pQuads->m_Flags = pLayerItem->m_Flags;
					pQuads->m_Image = pQuadsItem->m_Image;

					// validate image index
					if(pQuads->m_Image < -1 || pQuads->m_Image >= (int)m_vpImages.size())
					{
						pQuads->m_Image = -1;
					}

					// load layer name
					if(pQuadsItem->m_Version >= 2)
						IntsToStr(pQuadsItem->m_aName, std::size(pQuadsItem->m_aName), pQuads->m_aName, std::size(pQuads->m_aName));

					if(pQuadsItem->m_NumQuads > 0)
					{
						const void *pData = pMap->GetDataSwapped(pQuadsItem->m_Data);
						if(pData != nullptr && (size_t)pMap->GetDataSize(pQuadsItem->m_Data) >= sizeof(CQuad) * (size_t)pQuadsItem->m_NumQuads)
						{
							pQuads->m_vQuads.resize(pQuadsItem->m_NumQuads);
							mem_copy(pQuads->m_vQuads.data(), pData, sizeof(CQuad) * pQuadsItem->m_NumQuads);
						}
						else
						{
							char aBuf[128];
							str_format(aBuf, sizeof(aBuf), "Error: Failed to read quads of layer %d.", l);
							ErrorHandler(aBuf);
						}
						pMap->UnloadData(pQuadsItem->m_Data);
					}

					pGroup->AddLayer(pQuads);
				}
				else if(pLayerItem->m_Type == LAYERTYPE_SOUNDS)
				{
					const CMapItemLayerSounds *pSoundsItem = (CMapItemLayerSounds *)pLayerItem;
					if(pSoundsItem->m_Version < 1 || pSoundsItem->m_Version > 2)
						continue;

					std::shared_ptr<CLayerSounds> pSounds = std::make_shared<CLayerSounds>(this);
					pSounds->m_Flags = pLayerItem->m_Flags;
					pSounds->m_Sound = pSoundsItem->m_Sound;

					// validate sound index
					if(pSounds->m_Sound < -1 || pSounds->m_Sound >= (int)m_vpSounds.size())
					{
						pSounds->m_Sound = -1;
					}

					// load layer name
					IntsToStr(pSoundsItem->m_aName, std::size(pSoundsItem->m_aName), pSounds->m_aName, std::size(pSounds->m_aName));

					// load data
					if(pSoundsItem->m_NumSources > 0)
					{
						const void *pData = pMap->GetDataSwapped(pSoundsItem->m_Data);
						if(pData != nullptr && (size_t)pMap->GetDataSize(pSoundsItem->m_Data) >= sizeof(CSoundSource) * (size_t)pSoundsItem->m_NumSources)
						{
							pSounds->m_vSources.resize(pSoundsItem->m_NumSources);
							mem_copy(pSounds->m_vSources.data(), pData, sizeof(CSoundSource) * pSoundsItem->m_NumSources);
						}
						else
						{
							char aBuf[128];
							str_format(aBuf, sizeof(aBuf), "Error: Failed to read sound sources of layer %d.", l);
							ErrorHandler(aBuf);
						}
						pMap->UnloadData(pSoundsItem->m_Data);
					}

					pGroup->AddLayer(pSounds);
				}
				else if(pLayerItem->m_Type == LAYERTYPE_SOUNDS_DEPRECATED)
				{
					// compatibility with old sound layers
					const CMapItemLayerSounds *pSoundsItem = (CMapItemLayerSounds *)pLayerItem;
					if(pSoundsItem->m_Version < 1 || pSoundsItem->m_Version > 2)
						continue;

					std::shared_ptr<CLayerSounds> pSounds = std::make_shared<CLayerSounds>(this);
					pSounds->m_Flags = pLayerItem->m_Flags;
					pSounds->m_Sound = pSoundsItem->m_Sound;

					// validate sound index
					if(pSounds->m_Sound < -1 || pSounds->m_Sound >= (int)m_vpSounds.size())
					{
						pSounds->m_Sound = -1;
					}

					// load layer name
					IntsToStr(pSoundsItem->m_aName, std::size(pSoundsItem->m_aName), pSounds->m_aName, std::size(pSounds->m_aName));

					pGroup->AddLayer(pSounds);

					// load data
					if(pSoundsItem->m_NumSources > 0)
					{
						const CSoundSourceDeprecated *pData = (const CSoundSourceDeprecated *)pMap->GetDataSwapped(pSoundsItem->m_Data);
						if(pData == nullptr || (size_t)pMap->GetDataSize(pSoundsItem->m_Data) < sizeof(CSoundSourceDeprecated) * (size_t)pSoundsItem->m_NumSources)
						{
							char aBuf[128];
							str_format(aBuf, sizeof(aBuf), "Error: Failed to read sound sources of layer %d.", l);
							ErrorHandler(aBuf);
						}
						else
						{
							pSounds->m_vSources.resize(pSoundsItem->m_NumSources);

							for(int i = 0; i < pSoundsItem->m_NumSources; i++)
							{
								const CSoundSourceDeprecated *pOldSource = &pData[i];

								CSoundSource &Source = pSounds->m_vSources[i];
								Source.m_Position = pOldSource->m_Position;
								Source.m_Loop = pOldSource->m_Loop;
								Source.m_Pan = true;
								Source.m_TimeDelay = pOldSource->m_TimeDelay;
								Source.m_Falloff = 0;

								Source.m_PosEnv = pOldSource->m_PosEnv;
								Source.m_PosEnvOffset = pOldSource->m_PosEnvOffset;
								Source.m_SoundEnv = pOldSource->m_SoundEnv;
								Source.m_SoundEnvOffset = pOldSource->m_SoundEnvOffset;

								Source.m_Shape.m_Type = CSoundShape::SHAPE_CIRCLE;
								Source.m_Shape.m_Circle.m_Radius = pOldSource->m_FalloffDistance;
							}
						}

						pMap->UnloadData(pSoundsItem->m_Data);
					}
				}
			}
		}
	}

	// load envelopes
	{
		const CMapBasedEnvelopePointAccess EnvelopePoints(pMap.get());

		int EnvelopeStart, EnvelopeNum;
		pMap->GetType(MAPITEMTYPE_ENVELOPE, &EnvelopeStart, &EnvelopeNum);
		for(int EnvelopeIndex = 0; EnvelopeIndex < EnvelopeNum; EnvelopeIndex++)
		{
			CMapItemEnvelope *pItem = (CMapItemEnvelope *)pMap->GetItem(EnvelopeStart + EnvelopeIndex);
			int Channels = pItem->m_Channels;
			if(Channels <= 0 || Channels == 2 || Channels > CEnvPoint::MAX_CHANNELS)
			{
				// Fall back to showing all channels if the number of channels is unsupported
				Channels = CEnvPoint::MAX_CHANNELS;
			}
			if(Channels != pItem->m_Channels)
			{
				char aBuf[128];
				str_format(aBuf, sizeof(aBuf), "Error: Envelope %d had an invalid number of channels, %d, which was changed to %d.", EnvelopeIndex, pItem->m_Channels, Channels);
				ErrorHandler(aBuf);
			}

			std::shared_ptr<CEnvelope> pEnvelope = std::make_shared<CEnvelope>(Channels);
			pEnvelope->m_vPoints.resize(pItem->m_NumPoints);
			for(int PointIndex = 0; PointIndex < pItem->m_NumPoints; PointIndex++)
			{
				const CEnvPoint *pPoint = EnvelopePoints.GetPoint(pItem->m_StartPoint + PointIndex);
				if(pPoint != nullptr)
					mem_copy(&pEnvelope->m_vPoints[PointIndex], pPoint, sizeof(CEnvPoint));
				const CEnvPointBezier *pPointBezier = EnvelopePoints.GetBezier(pItem->m_StartPoint + PointIndex);
				if(pPointBezier != nullptr)
					mem_copy(&pEnvelope->m_vPoints[PointIndex].m_Bezier, pPointBezier, sizeof(CEnvPointBezier));
			}
			if(pItem->m_aName[0] != -1) // compatibility with old maps
				IntsToStr(pItem->m_aName, std::size(pItem->m_aName), pEnvelope->m_aName, std::size(pEnvelope->m_aName));
			m_vpEnvelopes.push_back(pEnvelope);
			if(pItem->m_Version >= 2)
				pEnvelope->m_Synchronized = pItem->m_Synchronized;
		}
	}

	// load automapper configurations
	{
		int AutomapperConfigStart, AutomapperConfigNum;
		pMap->GetType(MAPITEMTYPE_AUTOMAPPER_CONFIG, &AutomapperConfigStart, &AutomapperConfigNum);
		for(int i = 0; i < AutomapperConfigNum; i++)
		{
			CMapItemAutomapperConfig *pItem = (CMapItemAutomapperConfig *)pMap->GetItem(AutomapperConfigStart + i);
			if(pItem->m_Version == 1)
			{
				if(pItem->m_GroupId >= 0 && (size_t)pItem->m_GroupId < m_vpGroups.size() &&
					pItem->m_LayerId >= 0 && (size_t)pItem->m_LayerId < m_vpGroups[pItem->m_GroupId]->m_vpLayers.size())
				{
					std::shared_ptr<CLayer> pLayer = m_vpGroups[pItem->m_GroupId]->m_vpLayers[pItem->m_LayerId];
					if(pLayer->m_Type == LAYERTYPE_TILES)
					{
						std::shared_ptr<CLayerTiles> pTiles = std::static_pointer_cast<CLayerTiles>(m_vpGroups[pItem->m_GroupId]->m_vpLayers[pItem->m_LayerId]);
						// only load auto mappers for tile layers (not physics layers)
						if(!(pTiles->m_HasGame || pTiles->m_HasTele || pTiles->m_HasSpeedup ||
							   pTiles->m_HasFront || pTiles->m_HasSwitch || pTiles->m_HasTune))
						{
							pTiles->m_AutomapperConfig = pItem->m_AutomapperConfig;
							pTiles->m_Seed = pItem->m_AutomapperSeed;
							pTiles->m_AutoAutomapper = !!(pItem->m_Flags & CMapItemAutomapperConfig::FLAG_AUTOMATIC);
						}
					}
				}
			}
		}
	}

	str_copy(m_aFilename, pFilename);

	CheckIntegrity();
	PerformSanityChecks(ErrorHandler);

	SortImages();
	SelectGameLayer();

	ResetModifiedState();
	return true;
}

bool CEditorMap::Append(const char *pFilename, int StorageType, bool IgnoreHistory, const FErrorHandler &ErrorHandler)
{
	CEditorMap NewMap(Editor());
	if(!NewMap.Load(pFilename, StorageType, ErrorHandler))
		return false;

	CEditorActionAppendMap::SPrevInfo Info{
		(int)m_vpGroups.size(),
		(int)m_vpImages.size(),
		(int)m_vpSounds.size(),
		(int)m_vpEnvelopes.size()};

	// Keep a map to check if specific indices have already been replaced to prevent
	// replacing those indices again when transferring images
	std::map<int *, bool> ReplacedIndicesMap;
	const auto &&ReplaceIndex = [&ReplacedIndicesMap](int ToReplace, int ReplaceWith) {
		return [&ReplacedIndicesMap, ToReplace, ReplaceWith](int *pIndex) {
			if(*pIndex == ToReplace && !ReplacedIndicesMap[pIndex])
			{
				*pIndex = ReplaceWith;
				ReplacedIndicesMap[pIndex] = true;
			}
		};
	};

	const auto &&Rename = [&](const std::shared_ptr<CEditorImage> &pImage) {
		char aRenamed[IO_MAX_PATH_LENGTH];
		int DuplicateCount = 1;
		str_copy(aRenamed, pImage->m_aName);
		while(std::find_if(m_vpImages.begin(), m_vpImages.end(), [aRenamed](const std::shared_ptr<CEditorImage> &OtherImage) { return str_comp(OtherImage->m_aName, aRenamed) == 0; }) != m_vpImages.end())
			str_format(aRenamed, sizeof(aRenamed), "%s (%d)", pImage->m_aName, DuplicateCount++); // Rename to "image_name (%d)"
		str_copy(pImage->m_aName, aRenamed);
	};

	// Transfer non-duplicate images
	for(auto NewMapIt = NewMap.m_vpImages.begin(); NewMapIt != NewMap.m_vpImages.end(); ++NewMapIt)
	{
		const auto &pNewImage = *NewMapIt;
		auto NameIsTaken = [pNewImage](const std::shared_ptr<CEditorImage> &OtherImage) { return str_comp(pNewImage->m_aName, OtherImage->m_aName) == 0; };
		auto MatchInCurrentMap = std::find_if(m_vpImages.begin(), m_vpImages.end(), NameIsTaken);

		const bool IsDuplicate = MatchInCurrentMap != m_vpImages.end();
		const int IndexToReplace = NewMapIt - NewMap.m_vpImages.begin();

		if(IsDuplicate)
		{
			// Check for image data
			const bool ImageDataEquals = (*MatchInCurrentMap)->DataEquals(*pNewImage);

			if(ImageDataEquals)
			{
				const int IndexToReplaceWith = MatchInCurrentMap - m_vpImages.begin();

				dbg_msg("editor", "map already contains image %s with the same data, removing duplicate", pNewImage->m_aName);

				// In the new map, replace the index of the duplicate image to the index of the same in the current map.
				NewMap.ModifyImageIndex(ReplaceIndex(IndexToReplace, IndexToReplaceWith));
			}
			else
			{
				// Rename image and add it
				Rename(pNewImage);

				dbg_msg("editor", "map already contains image %s but contents of appended image is different. Renaming to %s", (*MatchInCurrentMap)->m_aName, pNewImage->m_aName);

				NewMap.ModifyImageIndex(ReplaceIndex(IndexToReplace, m_vpImages.size()));
				pNewImage->OnAttach(this);
				m_vpImages.push_back(pNewImage);
			}
		}
		else
		{
			NewMap.ModifyImageIndex(ReplaceIndex(IndexToReplace, m_vpImages.size()));
			pNewImage->OnAttach(this);
			m_vpImages.push_back(pNewImage);
		}
	}
	NewMap.m_vpImages.clear();

	// modify indices
	const auto &&ModifyAddIndex = [](int AddAmount) {
		return [AddAmount](int *pIndex) {
			if(*pIndex >= 0)
				*pIndex += AddAmount;
		};
	};
	NewMap.ModifySoundIndex(ModifyAddIndex(m_vpSounds.size()));
	NewMap.ModifyEnvelopeIndex(ModifyAddIndex(m_vpEnvelopes.size()));

	// transfer sounds
	for(const auto &pSound : NewMap.m_vpSounds)
	{
		pSound->OnAttach(this);
		m_vpSounds.push_back(pSound);
	}
	NewMap.m_vpSounds.clear();

	// transfer envelopes
	for(const auto &pEnvelope : NewMap.m_vpEnvelopes)
		m_vpEnvelopes.push_back(pEnvelope);
	NewMap.m_vpEnvelopes.clear();

	// transfer groups
	for(const auto &pGroup : NewMap.m_vpGroups)
	{
		if(pGroup != NewMap.m_pGameGroup)
		{
			pGroup->OnAttach(this);
			m_vpGroups.push_back(pGroup);
		}
	}
	NewMap.m_vpGroups.clear();

	// transfer server settings
	for(const auto &pSetting : NewMap.m_vSettings)
	{
		// Check if setting already exists
		bool AlreadyExists = false;
		for(const auto &pExistingSetting : m_vSettings)
		{
			if(!str_comp(pExistingSetting.m_aCommand, pSetting.m_aCommand))
				AlreadyExists = true;
		}
		if(!AlreadyExists)
			m_vSettings.push_back(pSetting);
	}
	NewMap.m_vSettings.clear();

	auto IndexMap = SortImages();

	if(!IgnoreHistory)
		m_EditorHistory.RecordAction(std::make_shared<CEditorActionAppendMap>(this, pFilename, Info, IndexMap));

	CheckIntegrity();
	OnModify();

	// all done \o/
	return true;
}

void CEditorMap::PerformSanityChecks(const FErrorHandler &ErrorHandler)
{
	// Check if there are any images with a width or height that is not divisible by 16 which are
	// used in tile layers. Reset the image for these layers, to prevent crashes with some drivers.
	size_t ImageIndex = 0;
	for(const std::shared_ptr<CEditorImage> &pImage : m_vpImages)
	{
		if(pImage->m_Width % 16 != 0 || pImage->m_Height % 16 != 0)
		{
			size_t GroupIndex = 0;
			for(const std::shared_ptr<CLayerGroup> &pGroup : m_vpGroups)
			{
				size_t LayerIndex = 0;
				for(const std::shared_ptr<CLayer> &pLayer : pGroup->m_vpLayers)
				{
					if(pLayer->m_Type == LAYERTYPE_TILES)
					{
						std::shared_ptr<CLayerTiles> pLayerTiles = std::static_pointer_cast<CLayerTiles>(pLayer);
						if(pLayerTiles->m_Image >= 0 && (size_t)pLayerTiles->m_Image == ImageIndex)
						{
							pLayerTiles->m_Image = -1;
							char aBuf[IO_MAX_PATH_LENGTH + 128];
							str_format(aBuf, sizeof(aBuf), "Error: The image '%s' (size %" PRIzu "x%" PRIzu ") has a width or height that is not divisible by 16 and therefore cannot be used for tile layers. The image of layer #%" PRIzu " '%s' in group #%" PRIzu " '%s' has been unset.", pImage->m_aName, pImage->m_Width, pImage->m_Height, LayerIndex, pLayer->m_aName, GroupIndex, pGroup->m_aName);
							ErrorHandler(aBuf);
						}
					}
					++LayerIndex;
				}
				++GroupIndex;
			}
		}
		++ImageIndex;
	}
}

bool CEditorMap::PerformAutosave(const std::function<void(const char *pErrorMessage)> &ErrorHandler)
{
	char aDate[20];
	char aAutosavePath[IO_MAX_PATH_LENGTH];
	str_timestamp(aDate, sizeof(aDate));
	str_format(aAutosavePath, sizeof(aAutosavePath), "maps/auto/%s_%s.map", m_aAutosaveName, aDate);

	m_LastSaveTime = Editor()->Client()->GlobalTime();
	if(Save(aAutosavePath, ErrorHandler))
	{
		m_ModifiedAuto = false;
		// Clean up autosaves
		if(g_Config.m_EdAutosaveMax)
		{
			CFileCollection AutosavedMaps;
			AutosavedMaps.Init(Editor()->Storage(), "maps/auto", m_aAutosaveName, ".map", g_Config.m_EdAutosaveMax);
		}
		return true;
	}
	else
	{
		char aErrorMessage[IO_MAX_PATH_LENGTH + 128];
		str_format(aErrorMessage, sizeof(aErrorMessage), "Failed to automatically save map to file '%s'.", aAutosavePath);
		ErrorHandler(aErrorMessage);
		return false;
	}
}
