#include "editor_actions.h"

#include <base/log.h>

#include <game/editor/editor.h>
#include <game/editor/mapitems.h>
#include <game/editor/mapitems/image.h>
#include <game/editor/mapitems/layer.h>
#include <game/editor/mapitems/layer_front.h>
#include <game/editor/mapitems/layer_group.h>
#include <game/editor/mapitems/layer_quads.h>
#include <game/editor/mapitems/layer_sounds.h>
#include <game/editor/mapitems/map.h>

CEditorBrushDrawAction::CEditorBrushDrawAction(CEditorMap *pMap, int Group) :
	IEditorAction(pMap), m_Group(Group)
{
	for(size_t k = 0; k < Map()->m_vpGroups[Group]->m_vpLayers.size(); k++)
	{
		auto pLayer = Map()->m_vpGroups[Group]->m_vpLayers[k];

		if(pLayer->m_Type == LAYERTYPE_TILES)
		{
			auto pLayerTiles = std::static_pointer_cast<CLayerTiles>(pLayer);

			if(pLayer == Map()->m_pTeleLayer)
			{
				if(!Map()->m_pTeleLayer->m_History.empty())
				{
					m_TeleTileChanges = std::map(Map()->m_pTeleLayer->m_History);
					Map()->m_pTeleLayer->ClearHistory();
				}
			}
			else if(pLayer == Map()->m_pTuneLayer)
			{
				if(!Map()->m_pTuneLayer->m_History.empty())
				{
					m_TuneTileChanges = std::map(Map()->m_pTuneLayer->m_History);
					Map()->m_pTuneLayer->ClearHistory();
				}
			}
			else if(pLayer == Map()->m_pSwitchLayer)
			{
				if(!Map()->m_pSwitchLayer->m_History.empty())
				{
					m_SwitchTileChanges = std::map(Map()->m_pSwitchLayer->m_History);
					Map()->m_pSwitchLayer->ClearHistory();
				}
			}
			else if(pLayer == Map()->m_pSpeedupLayer)
			{
				if(!Map()->m_pSpeedupLayer->m_History.empty())
				{
					m_SpeedupTileChanges = std::map(Map()->m_pSpeedupLayer->m_History);
					Map()->m_pSpeedupLayer->ClearHistory();
				}
			}

			if(!pLayerTiles->m_TilesHistory.empty())
			{
				m_vTileChanges.emplace_back(k, std::map(pLayerTiles->m_TilesHistory));
				pLayerTiles->ClearHistory();
			}
		}
	}

	SetInfos();
	str_format(m_aDisplayText, sizeof(m_aDisplayText), "Brush draw (x%d) on %d layers", m_TotalTilesDrawn, m_TotalLayers);
}

void CEditorBrushDrawAction::SetInfos()
{
	m_TotalTilesDrawn = 0;
	m_TotalLayers = 0;

	// Process normal tiles
	for(auto const &Pair : m_vTileChanges)
	{
		int Layer = Pair.first;
		std::shared_ptr<CLayer> pLayer = Map()->m_vpGroups[m_Group]->m_vpLayers[Layer];
		m_TotalLayers++;

		if(pLayer->m_Type == LAYERTYPE_TILES)
		{
			auto Changes = Pair.second;
			for(auto &Change : Changes)
			{
				m_TotalTilesDrawn += Change.second.size();
			}
		}
	}

	// Process speedup tiles
	for(auto const &SpeedupChange : m_SpeedupTileChanges)
	{
		m_TotalTilesDrawn += SpeedupChange.second.size();
	}

	// Process tele tiles
	for(auto const &TeleChange : m_TeleTileChanges)
	{
		m_TotalTilesDrawn += TeleChange.second.size();
	}

	// Process switch tiles
	for(auto const &SwitchChange : m_SwitchTileChanges)
	{
		m_TotalTilesDrawn += SwitchChange.second.size();
	}

	// Process tune tiles
	for(auto const &TuneChange : m_TuneTileChanges)
	{
		m_TotalTilesDrawn += TuneChange.second.size();
	}

	m_TotalLayers += !m_SpeedupTileChanges.empty();
	m_TotalLayers += !m_SwitchTileChanges.empty();
	m_TotalLayers += !m_TeleTileChanges.empty();
	m_TotalLayers += !m_TuneTileChanges.empty();
}

bool CEditorBrushDrawAction::IsEmpty()
{
	return m_vTileChanges.empty() && m_SpeedupTileChanges.empty() && m_SwitchTileChanges.empty() && m_TeleTileChanges.empty() && m_TuneTileChanges.empty();
}

void CEditorBrushDrawAction::Undo()
{
	Apply(true);
}

void CEditorBrushDrawAction::Redo()
{
	Apply(false);
}

void CEditorBrushDrawAction::Apply(bool Undo)
{
	// Process normal tiles
	for(auto const &Pair : m_vTileChanges)
	{
		int Layer = Pair.first;
		std::shared_ptr<CLayer> pLayer = Map()->m_vpGroups[m_Group]->m_vpLayers[Layer];

		if(pLayer->m_Type == LAYERTYPE_TILES)
		{
			std::shared_ptr<CLayerTiles> pLayerTiles = std::static_pointer_cast<CLayerTiles>(pLayer);
			auto Changes = Pair.second;
			for(auto &Change : Changes)
			{
				int y = Change.first;
				auto Line = Change.second;
				for(auto &Tile : Line)
				{
					int x = Tile.first;
					STileStateChange State = Tile.second;
					pLayerTiles->SetTileIgnoreHistory(x, y, Undo ? State.m_Previous : State.m_Current);
				}
			}
		}
	}

	// Process speedup tiles
	for(auto const &SpeedupChange : m_SpeedupTileChanges)
	{
		int y = SpeedupChange.first;
		auto Line = SpeedupChange.second;
		for(auto &Tile : Line)
		{
			int x = Tile.first;
			int Index = y * Map()->m_pSpeedupLayer->m_Width + x;
			SSpeedupTileStateChange State = Tile.second;
			SSpeedupTileStateChange::SData Data = Undo ? State.m_Previous : State.m_Current;

			Map()->m_pSpeedupLayer->m_pSpeedupTile[Index].m_Force = Data.m_Force;
			Map()->m_pSpeedupLayer->m_pSpeedupTile[Index].m_MaxSpeed = Data.m_MaxSpeed;
			Map()->m_pSpeedupLayer->m_pSpeedupTile[Index].m_Angle = Data.m_Angle;
			Map()->m_pSpeedupLayer->m_pSpeedupTile[Index].m_Type = Data.m_Type;
			Map()->m_pSpeedupLayer->m_pTiles[Index].m_Index = Data.m_Index;
		}
	}

	// Process tele tiles
	for(auto const &TeleChange : m_TeleTileChanges)
	{
		int y = TeleChange.first;
		auto Line = TeleChange.second;
		for(auto &Tile : Line)
		{
			int x = Tile.first;
			int Index = y * Map()->m_pTeleLayer->m_Width + x;
			STeleTileStateChange State = Tile.second;
			STeleTileStateChange::SData Data = Undo ? State.m_Previous : State.m_Current;

			Map()->m_pTeleLayer->m_pTeleTile[Index].m_Number = Data.m_Number;
			Map()->m_pTeleLayer->m_pTeleTile[Index].m_Type = Data.m_Type;
			Map()->m_pTeleLayer->m_pTiles[Index].m_Index = Data.m_Index;
		}
	}

	// Process switch tiles
	for(auto const &SwitchChange : m_SwitchTileChanges)
	{
		int y = SwitchChange.first;
		auto Line = SwitchChange.second;
		for(auto &Tile : Line)
		{
			int x = Tile.first;
			int Index = y * Map()->m_pSwitchLayer->m_Width + x;
			SSwitchTileStateChange State = Tile.second;
			SSwitchTileStateChange::SData Data = Undo ? State.m_Previous : State.m_Current;

			Map()->m_pSwitchLayer->m_pSwitchTile[Index].m_Number = Data.m_Number;
			Map()->m_pSwitchLayer->m_pSwitchTile[Index].m_Type = Data.m_Type;
			Map()->m_pSwitchLayer->m_pSwitchTile[Index].m_Flags = Data.m_Flags;
			Map()->m_pSwitchLayer->m_pSwitchTile[Index].m_Delay = Data.m_Delay;
			Map()->m_pSwitchLayer->m_pTiles[Index].m_Index = Data.m_Index;
		}
	}

	// Process tune tiles
	for(auto const &TuneChange : m_TuneTileChanges)
	{
		int y = TuneChange.first;
		auto Line = TuneChange.second;
		for(auto &Tile : Line)
		{
			int x = Tile.first;
			int Index = y * Map()->m_pTuneLayer->m_Width + x;
			STuneTileStateChange State = Tile.second;
			STuneTileStateChange::SData Data = Undo ? State.m_Previous : State.m_Current;

			Map()->m_pTuneLayer->m_pTuneTile[Index].m_Number = Data.m_Number;
			Map()->m_pTuneLayer->m_pTuneTile[Index].m_Type = Data.m_Type;
			Map()->m_pTuneLayer->m_pTiles[Index].m_Index = Data.m_Index;
		}
	}
}

// -------------------------------------------

CEditorActionQuadPlace::CEditorActionQuadPlace(CEditorMap *pMap, int GroupIndex, int LayerIndex, std::vector<CQuad> &vBrush) :
	CEditorActionLayerBase(pMap, GroupIndex, LayerIndex), m_vBrush(vBrush)
{
	str_format(m_aDisplayText, sizeof(m_aDisplayText), "Quad place (x%d)", (int)m_vBrush.size());
}

void CEditorActionQuadPlace::Undo()
{
	std::shared_ptr<CLayerQuads> pLayerQuads = std::static_pointer_cast<CLayerQuads>(m_pLayer);
	for(size_t k = 0; k < m_vBrush.size(); k++)
		pLayerQuads->m_vQuads.pop_back();

	Map()->OnModify();
}
void CEditorActionQuadPlace::Redo()
{
	std::shared_ptr<CLayerQuads> pLayerQuads = std::static_pointer_cast<CLayerQuads>(m_pLayer);
	for(auto &Brush : m_vBrush)
		pLayerQuads->m_vQuads.push_back(Brush);

	Map()->OnModify();
}

CEditorActionSoundPlace::CEditorActionSoundPlace(CEditorMap *pMap, int GroupIndex, int LayerIndex, std::vector<CSoundSource> &vBrush) :
	CEditorActionLayerBase(pMap, GroupIndex, LayerIndex), m_vBrush(vBrush)
{
	str_format(m_aDisplayText, sizeof(m_aDisplayText), "Sound place (x%d)", (int)m_vBrush.size());
}

void CEditorActionSoundPlace::Undo()
{
	std::shared_ptr<CLayerSounds> pLayerSounds = std::static_pointer_cast<CLayerSounds>(m_pLayer);
	for(size_t k = 0; k < m_vBrush.size(); k++)
		pLayerSounds->m_vSources.pop_back();

	Map()->OnModify();
}

void CEditorActionSoundPlace::Redo()
{
	std::shared_ptr<CLayerSounds> pLayerSounds = std::static_pointer_cast<CLayerSounds>(m_pLayer);
	for(auto &Brush : m_vBrush)
		pLayerSounds->m_vSources.push_back(Brush);

	Map()->OnModify();
}

// ---------------------------------------------------------------------------------------

CEditorActionDeleteQuad::CEditorActionDeleteQuad(CEditorMap *pMap, int GroupIndex, int LayerIndex) :
	CEditorActionLayerBase(pMap, GroupIndex, LayerIndex)
{
	std::shared_ptr<CLayerQuads> pLayerQuads = std::static_pointer_cast<CLayerQuads>(m_pLayer);
	m_vQuadsIndices = Map()->m_vSelectedQuads;

	// make sure the indices are descending
	std::sort(m_vQuadsIndices.begin(), m_vQuadsIndices.end(), std::greater<>());

	dbg_assert(m_vQuadsIndices[0] < (int)pLayerQuads->m_vQuads.size(), "Tried to delete quad with Id %d, while the layer only contains %d quads", m_vQuadsIndices[0], (int)pLayerQuads->m_vQuads.size());
	dbg_assert(m_vQuadsIndices.back() >= 0, "Tried to delete quad with negative Id %d", m_vQuadsIndices.back());

	m_vDeletedQuads.reserve(Map()->m_vSelectedQuads.size());
	for(int QuadId : m_vQuadsIndices)
	{
		m_vDeletedQuads.emplace_back(pLayerQuads->m_vQuads[QuadId]);
	}

	str_format(m_aDisplayText, sizeof(m_aDisplayText), "Delete quad (x%d)", (int)m_vDeletedQuads.size());
}

void CEditorActionDeleteQuad::Undo()
{
	std::shared_ptr<CLayerQuads> pLayerQuads = std::static_pointer_cast<CLayerQuads>(m_pLayer);

	// Quad indices are in descending order, so we add them back in ascending order
	for(int IndexId = (int)m_vQuadsIndices.size() - 1; IndexId >= 0; --IndexId)
	{
		pLayerQuads->m_vQuads.insert(pLayerQuads->m_vQuads.begin() + m_vQuadsIndices[IndexId], m_vDeletedQuads[IndexId]);
	}
	Map()->m_vSelectedQuads = m_vQuadsIndices;

	Map()->OnModify();
}

void CEditorActionDeleteQuad::Redo()
{
	std::shared_ptr<CLayerQuads> pLayerQuads = std::static_pointer_cast<CLayerQuads>(m_pLayer);

	// Quad indices are in descending order
	for(const int &QuadId : m_vQuadsIndices)
	{
		pLayerQuads->m_vQuads.erase(pLayerQuads->m_vQuads.begin() + QuadId);
	}
	Map()->m_vSelectedQuads.clear();

	Map()->OnModify();
}

// ---------------------------------------------------------------------------------------

CEditorActionEditQuadPoint::CEditorActionEditQuadPoint(CEditorMap *pMap, int GroupIndex, int LayerIndex, int QuadIndex, std::vector<CPoint> const &vPreviousPoints, std::vector<CPoint> const &vCurrentPoints) :
	CEditorActionLayerBase(pMap, GroupIndex, LayerIndex), m_QuadIndex(QuadIndex), m_vPreviousPoints(vPreviousPoints), m_vCurrentPoints(vCurrentPoints)
{
	str_copy(m_aDisplayText, "Edit quad points");
}

void CEditorActionEditQuadPoint::Undo()
{
	Apply(m_vPreviousPoints);
}

void CEditorActionEditQuadPoint::Redo()
{
	Apply(m_vCurrentPoints);
}

void CEditorActionEditQuadPoint::Apply(const std::vector<CPoint> &vValue)
{
	std::shared_ptr<CLayerQuads> pLayerQuads = std::static_pointer_cast<CLayerQuads>(m_pLayer);
	CQuad &Quad = pLayerQuads->m_vQuads[m_QuadIndex];
	dbg_assert(std::size(Quad.m_aPoints) == vValue.size(), "Expected %d values, got %d", (int)std::size(Quad.m_aPoints), (int)vValue.size());
	std::copy_n(vValue.begin(), std::size(Quad.m_aPoints), Quad.m_aPoints);
}

CEditorActionEditQuadColor::CEditorActionEditQuadColor(CEditorMap *pMap, int GroupIndex, int LayerIndex, int QuadIndex, std::vector<CColor> const &vPreviousColors, std::vector<CColor> const &vCurrentColors) :
	CEditorActionLayerBase(pMap, GroupIndex, LayerIndex), m_QuadIndex(QuadIndex), m_vPreviousColors(vPreviousColors), m_vCurrentColors(vCurrentColors)
{
	str_copy(m_aDisplayText, "Edit quad point colors");
}

void CEditorActionEditQuadColor::Undo()
{
	Apply(m_vPreviousColors);
}

void CEditorActionEditQuadColor::Redo()
{
	Apply(m_vCurrentColors);
}

void CEditorActionEditQuadColor::Apply(std::vector<CColor> &vValue)
{
	std::shared_ptr<CLayerQuads> pLayerQuads = std::static_pointer_cast<CLayerQuads>(m_pLayer);
	CQuad &Quad = pLayerQuads->m_vQuads[m_QuadIndex];
	dbg_assert(std::size(Quad.m_aColors) == vValue.size(), "Expected %d values, got %d", (int)std::size(Quad.m_aColors), (int)vValue.size());
	std::copy_n(vValue.begin(), std::size(Quad.m_aColors), Quad.m_aColors);
}

CEditorActionEditQuadProp::CEditorActionEditQuadProp(CEditorMap *pMap, int GroupIndex, int LayerIndex, int QuadIndex, EQuadProp Prop, int Previous, int Current) :
	CEditorActionLayerBase(pMap, GroupIndex, LayerIndex), m_QuadIndex(QuadIndex), m_Prop(Prop), m_Previous(Previous), m_Current(Current)
{
	static const char *s_apNames[] = {
		"order",
		"pos X",
		"pos Y",
		"pos env",
		"pos env offset",
		nullptr, // color
		"color env",
		"color env offset"};
	static_assert(std::size(s_apNames) == (size_t)EQuadProp::NUM_PROPS);
	dbg_assert(Prop != EQuadProp::COLOR, "Color prop implemented by CEditorActionEditQuadPointProp");
	str_format(m_aDisplayText, sizeof(m_aDisplayText), "Edit quad %s property in layer %d of group %d", s_apNames[(int)m_Prop], m_LayerIndex, m_GroupIndex);
}

void CEditorActionEditQuadProp::Undo()
{
	Apply(m_Previous);
}

void CEditorActionEditQuadProp::Redo()
{
	Apply(m_Current);
}

void CEditorActionEditQuadProp::Apply(int Value)
{
	std::shared_ptr<CLayerQuads> pLayerQuads = std::static_pointer_cast<CLayerQuads>(m_pLayer);
	CQuad &Quad = pLayerQuads->m_vQuads[m_QuadIndex];
	if(m_Prop == EQuadProp::POS_ENV)
		Quad.m_PosEnv = Value;
	else if(m_Prop == EQuadProp::POS_ENV_OFFSET)
		Quad.m_PosEnvOffset = Value;
	else if(m_Prop == EQuadProp::COLOR_ENV)
		Quad.m_ColorEnv = Value;
	else if(m_Prop == EQuadProp::COLOR_ENV_OFFSET)
		Quad.m_ColorEnvOffset = Value;
}

CEditorActionEditQuadPointProp::CEditorActionEditQuadPointProp(CEditorMap *pMap, int GroupIndex, int LayerIndex, int QuadIndex, int PointIndex, EQuadPointProp Prop, int Previous, int Current) :
	CEditorActionLayerBase(pMap, GroupIndex, LayerIndex), m_QuadIndex(QuadIndex), m_PointIndex(PointIndex), m_Prop(Prop), m_Previous(Previous), m_Current(Current)
{
	static const char *s_apNames[] = {
		"pos X",
		"pos Y",
		"color",
		"tex U",
		"tex V"};
	static_assert(std::size(s_apNames) == (size_t)EQuadPointProp::NUM_PROPS);
	str_format(m_aDisplayText, sizeof(m_aDisplayText), "Edit quad point %s property in layer %d of group %d", s_apNames[(int)m_Prop], m_LayerIndex, m_GroupIndex);
}

void CEditorActionEditQuadPointProp::Undo()
{
	Apply(m_Previous);
}

void CEditorActionEditQuadPointProp::Redo()
{
	Apply(m_Current);
}

void CEditorActionEditQuadPointProp::Apply(int Value)
{
	std::shared_ptr<CLayerQuads> pLayerQuads = std::static_pointer_cast<CLayerQuads>(m_pLayer);
	CQuad &Quad = pLayerQuads->m_vQuads[m_QuadIndex];

	if(m_Prop == EQuadPointProp::COLOR)
	{
		const ColorRGBA ColorPick = ColorRGBA::UnpackAlphaLast<ColorRGBA>(Value);

		Quad.m_aColors[m_PointIndex].r = ColorPick.r * 255.0f;
		Quad.m_aColors[m_PointIndex].g = ColorPick.g * 255.0f;
		Quad.m_aColors[m_PointIndex].b = ColorPick.b * 255.0f;
		Quad.m_aColors[m_PointIndex].a = ColorPick.a * 255.0f;

		Editor()->m_ColorPickerPopupContext.m_RgbaColor = ColorPick;
		Editor()->m_ColorPickerPopupContext.m_HslaColor = color_cast<ColorHSLA>(ColorPick);
		Editor()->m_ColorPickerPopupContext.m_HsvaColor = color_cast<ColorHSVA>(Editor()->m_ColorPickerPopupContext.m_HslaColor);
	}
	else if(m_Prop == EQuadPointProp::TEX_U)
	{
		Quad.m_aTexcoords[m_PointIndex].x = Value;
	}
	else if(m_Prop == EQuadPointProp::TEX_V)
	{
		Quad.m_aTexcoords[m_PointIndex].y = Value;
	}
}

// ---------------------------------------------------------------------------------------

CEditorActionBulk::CEditorActionBulk(CEditorMap *pMap, const std::vector<std::shared_ptr<IEditorAction>> &vpActions, const char *pDisplay, bool Reverse) :
	IEditorAction(pMap), m_vpActions(vpActions), m_Reverse(Reverse)
{
	// Assuming we only use bulk for actions of same type, if no display was provided
	if(!pDisplay)
	{
		const char *pBaseDisplay = m_vpActions[0]->DisplayText();
		if(m_vpActions.size() == 1)
			str_copy(m_aDisplayText, pBaseDisplay);
		else
			str_format(m_aDisplayText, sizeof(m_aDisplayText), "%s (x%d)", pBaseDisplay, (int)m_vpActions.size());
	}
	else
	{
		str_copy(m_aDisplayText, pDisplay);
	}
}

void CEditorActionBulk::Undo()
{
	if(m_Reverse)
	{
		// reverse_view is not supported in gcc 10
		for(auto pIt = m_vpActions.rbegin(); pIt != m_vpActions.rend(); pIt++) // NOLINT: modernize-loop-convert
		{
			auto &pAction = *pIt;
			pAction->Undo();
		}
	}
	else
	{
		for(auto &pAction : m_vpActions)
		{
			pAction->Undo();
		}
	}
}

void CEditorActionBulk::Redo()
{
	for(auto &pAction : m_vpActions)
	{
		pAction->Redo();
	}
}

// ---------

CEditorActionTileChanges::CEditorActionTileChanges(CEditorMap *pMap, int GroupIndex, int LayerIndex, const char *pAction, const EditorTileStateChangeHistory<STileStateChange> &Changes) :
	CEditorActionLayerBase(pMap, GroupIndex, LayerIndex), m_Changes(Changes)
{
	ComputeInfos();
	str_format(m_aDisplayText, sizeof(m_aDisplayText), "%s (x%d)", pAction, m_TotalChanges);
}

void CEditorActionTileChanges::Undo()
{
	Apply(true);
}

void CEditorActionTileChanges::Redo()
{
	Apply(false);
}

void CEditorActionTileChanges::Apply(bool Undo)
{
	std::shared_ptr<CLayerTiles> pLayerTiles = std::static_pointer_cast<CLayerTiles>(m_pLayer);
	for(auto &Change : m_Changes)
	{
		int y = Change.first;
		auto Line = Change.second;
		for(auto &Tile : Line)
		{
			int x = Tile.first;
			STileStateChange State = Tile.second;
			pLayerTiles->SetTileIgnoreHistory(x, y, Undo ? State.m_Previous : State.m_Current);
		}
	}

	Map()->OnModify();
}

void CEditorActionTileChanges::ComputeInfos()
{
	m_TotalChanges = 0;
	for(auto &Line : m_Changes)
		m_TotalChanges += Line.second.size();
}

// ---------

CEditorActionLayerBase::CEditorActionLayerBase(CEditorMap *pMap, int GroupIndex, int LayerIndex) :
	IEditorAction(pMap), m_GroupIndex(GroupIndex), m_LayerIndex(LayerIndex)
{
	m_pLayer = Map()->m_vpGroups[GroupIndex]->m_vpLayers[LayerIndex];
}

// ----------

CEditorActionAddLayer::CEditorActionAddLayer(CEditorMap *pMap, int GroupIndex, int LayerIndex, bool Duplicate) :
	CEditorActionLayerBase(pMap, GroupIndex, LayerIndex), m_Duplicate(Duplicate)
{
	str_format(m_aDisplayText, sizeof(m_aDisplayText), "%s %s layer in group %d", m_Duplicate ? "Duplicate" : "New", m_pLayer->TypeName(), m_GroupIndex);
}

void CEditorActionAddLayer::Undo()
{
	// Undo: remove layer from vector but keep it in case we want to add it back
	auto &vLayers = Map()->m_vpGroups[m_GroupIndex]->m_vpLayers;

	if(m_pLayer->m_Type == LAYERTYPE_TILES)
	{
		std::shared_ptr<CLayerTiles> pLayerTiles = std::static_pointer_cast<CLayerTiles>(m_pLayer);
		if(pLayerTiles->m_HasFront)
			Map()->m_pFrontLayer = nullptr;
		else if(pLayerTiles->m_HasTele)
			Map()->m_pTeleLayer = nullptr;
		else if(pLayerTiles->m_HasSpeedup)
			Map()->m_pSpeedupLayer = nullptr;
		else if(pLayerTiles->m_HasSwitch)
			Map()->m_pSwitchLayer = nullptr;
		else if(pLayerTiles->m_HasTune)
			Map()->m_pTuneLayer = nullptr;
	}

	vLayers.erase(vLayers.begin() + m_LayerIndex);

	Map()->m_vpGroups[m_GroupIndex]->m_Collapse = false;
	if(m_LayerIndex >= (int)vLayers.size())
		Map()->SelectLayer(vLayers.size() - 1, m_GroupIndex);

	Map()->OnModify();
}

void CEditorActionAddLayer::Redo()
{
	// Redo: add back the removed layer contained in this class
	auto &vLayers = Map()->m_vpGroups[m_GroupIndex]->m_vpLayers;

	if(m_pLayer->m_Type == LAYERTYPE_TILES)
	{
		std::shared_ptr<CLayerTiles> pLayerTiles = std::static_pointer_cast<CLayerTiles>(m_pLayer);
		if(pLayerTiles->m_HasFront)
			Map()->m_pFrontLayer = std::static_pointer_cast<CLayerFront>(m_pLayer);
		else if(pLayerTiles->m_HasTele)
			Map()->m_pTeleLayer = std::static_pointer_cast<CLayerTele>(m_pLayer);
		else if(pLayerTiles->m_HasSpeedup)
			Map()->m_pSpeedupLayer = std::static_pointer_cast<CLayerSpeedup>(m_pLayer);
		else if(pLayerTiles->m_HasSwitch)
			Map()->m_pSwitchLayer = std::static_pointer_cast<CLayerSwitch>(m_pLayer);
		else if(pLayerTiles->m_HasTune)
			Map()->m_pTuneLayer = std::static_pointer_cast<CLayerTune>(m_pLayer);
	}

	vLayers.insert(vLayers.begin() + m_LayerIndex, m_pLayer);

	Map()->m_vpGroups[m_GroupIndex]->m_Collapse = false;
	Map()->SelectLayer(m_LayerIndex, m_GroupIndex);
	Map()->OnModify();
}

CEditorActionDeleteLayer::CEditorActionDeleteLayer(CEditorMap *pMap, int GroupIndex, int LayerIndex) :
	CEditorActionLayerBase(pMap, GroupIndex, LayerIndex)
{
	str_format(m_aDisplayText, sizeof(m_aDisplayText), "Delete %s layer of group %d", m_pLayer->TypeName(), m_GroupIndex);
}

void CEditorActionDeleteLayer::Redo()
{
	// Redo: remove layer from vector but keep it in case we want to add it back
	auto &vLayers = Map()->m_vpGroups[m_GroupIndex]->m_vpLayers;

	if(m_pLayer->m_Type == LAYERTYPE_TILES)
	{
		std::shared_ptr<CLayerTiles> pLayerTiles = std::static_pointer_cast<CLayerTiles>(m_pLayer);
		if(pLayerTiles->m_HasFront)
			Map()->m_pFrontLayer = nullptr;
		else if(pLayerTiles->m_HasTele)
			Map()->m_pTeleLayer = nullptr;
		else if(pLayerTiles->m_HasSpeedup)
			Map()->m_pSpeedupLayer = nullptr;
		else if(pLayerTiles->m_HasSwitch)
			Map()->m_pSwitchLayer = nullptr;
		else if(pLayerTiles->m_HasTune)
			Map()->m_pTuneLayer = nullptr;
	}

	Map()->m_vpGroups[m_GroupIndex]->DeleteLayer(m_LayerIndex);

	Map()->m_vpGroups[m_GroupIndex]->m_Collapse = false;
	if(m_LayerIndex >= (int)vLayers.size())
		Map()->SelectLayer(vLayers.size() - 1, m_GroupIndex);

	Map()->OnModify();
}

void CEditorActionDeleteLayer::Undo()
{
	// Undo: add back the removed layer contained in this class
	auto &vLayers = Map()->m_vpGroups[m_GroupIndex]->m_vpLayers;

	if(m_pLayer->m_Type == LAYERTYPE_TILES)
	{
		std::shared_ptr<CLayerTiles> pLayerTiles = std::static_pointer_cast<CLayerTiles>(m_pLayer);
		if(pLayerTiles->m_HasFront)
			Map()->m_pFrontLayer = std::static_pointer_cast<CLayerFront>(m_pLayer);
		else if(pLayerTiles->m_HasTele)
			Map()->m_pTeleLayer = std::static_pointer_cast<CLayerTele>(m_pLayer);
		else if(pLayerTiles->m_HasSpeedup)
			Map()->m_pSpeedupLayer = std::static_pointer_cast<CLayerSpeedup>(m_pLayer);
		else if(pLayerTiles->m_HasSwitch)
			Map()->m_pSwitchLayer = std::static_pointer_cast<CLayerSwitch>(m_pLayer);
		else if(pLayerTiles->m_HasTune)
			Map()->m_pTuneLayer = std::static_pointer_cast<CLayerTune>(m_pLayer);
	}

	vLayers.insert(vLayers.begin() + m_LayerIndex, m_pLayer);

	Map()->m_vpGroups[m_GroupIndex]->m_Collapse = false;
	Map()->SelectLayer(m_LayerIndex, m_GroupIndex);
	Map()->OnModify();
}

CEditorActionGroup::CEditorActionGroup(CEditorMap *pMap, int GroupIndex, bool Delete) :
	IEditorAction(pMap), m_GroupIndex(GroupIndex), m_Delete(Delete)
{
	m_pGroup = Map()->m_vpGroups[GroupIndex];
	if(m_Delete)
		str_format(m_aDisplayText, sizeof(m_aDisplayText), "Delete group %d", m_GroupIndex);
	else
		str_copy(m_aDisplayText, "New group");
}

void CEditorActionGroup::Undo()
{
	if(m_Delete)
	{
		// Undo: add back the group
		Map()->m_vpGroups.insert(Map()->m_vpGroups.begin() + m_GroupIndex, m_pGroup);
		Map()->m_SelectedGroup = m_GroupIndex;
		Map()->OnModify();
	}
	else
	{
		// Undo: delete the group
		Map()->DeleteGroup(m_GroupIndex);
		Map()->m_SelectedGroup = std::max(0, m_GroupIndex - 1);
	}

	Map()->OnModify();
}

void CEditorActionGroup::Redo()
{
	if(!m_Delete)
	{
		// Redo: add back the group
		Map()->m_vpGroups.insert(Map()->m_vpGroups.begin() + m_GroupIndex, m_pGroup);
		Map()->m_SelectedGroup = m_GroupIndex;
	}
	else
	{
		// Redo: delete the group
		Map()->DeleteGroup(m_GroupIndex);
		Map()->m_SelectedGroup = std::max(0, m_GroupIndex - 1);
	}

	Map()->OnModify();
}

CEditorActionEditGroupProp::CEditorActionEditGroupProp(CEditorMap *pMap, int GroupIndex, EGroupProp Prop, int Previous, int Current) :
	IEditorAction(pMap), m_GroupIndex(GroupIndex), m_Prop(Prop), m_Previous(Previous), m_Current(Current)
{
	static const char *s_apNames[] = {
		"order",
		"pos X",
		"pos Y",
		"para X",
		"para Y",
		"use clipping",
		"clip X",
		"clip Y",
		"clip W",
		"clip H"};
	static_assert(std::size(s_apNames) == (size_t)EGroupProp::NUM_PROPS);

	str_format(m_aDisplayText, sizeof(m_aDisplayText), "Edit group %d %s property", m_GroupIndex, s_apNames[(int)Prop]);
}

void CEditorActionEditGroupProp::Undo()
{
	if(m_Prop == EGroupProp::ORDER)
	{
		Map()->m_SelectedGroup = Map()->MoveGroup(m_Current, m_Previous);
	}
	else
		Apply(m_Previous);
}

void CEditorActionEditGroupProp::Redo()
{
	if(m_Prop == EGroupProp::ORDER)
	{
		Map()->m_SelectedGroup = Map()->MoveGroup(m_Previous, m_Current);
	}
	else
		Apply(m_Current);
}

void CEditorActionEditGroupProp::Apply(int Value)
{
	auto pGroup = Map()->m_vpGroups[m_GroupIndex];

	if(m_Prop == EGroupProp::POS_X)
		pGroup->m_OffsetX = Value;
	if(m_Prop == EGroupProp::POS_Y)
		pGroup->m_OffsetY = Value;
	if(m_Prop == EGroupProp::PARA_X)
		pGroup->m_ParallaxX = Value;
	if(m_Prop == EGroupProp::PARA_Y)
		pGroup->m_ParallaxY = Value;
	if(m_Prop == EGroupProp::USE_CLIPPING)
		pGroup->m_UseClipping = Value;
	if(m_Prop == EGroupProp::CLIP_X)
		pGroup->m_ClipX = Value;
	if(m_Prop == EGroupProp::CLIP_Y)
		pGroup->m_ClipY = Value;
	if(m_Prop == EGroupProp::CLIP_W)
		pGroup->m_ClipW = Value;
	if(m_Prop == EGroupProp::CLIP_H)
		pGroup->m_ClipH = Value;

	Map()->OnModify();
}

template<typename E>
CEditorActionEditLayerPropBase<E>::CEditorActionEditLayerPropBase(CEditorMap *pMap, int GroupIndex, int LayerIndex, E Prop, int Previous, int Current) :
	CEditorActionLayerBase(pMap, GroupIndex, LayerIndex), m_Prop(Prop), m_Previous(Previous), m_Current(Current)
{
}

CEditorActionEditLayerProp::CEditorActionEditLayerProp(CEditorMap *pMap, int GroupIndex, int LayerIndex, ELayerProp Prop, int Previous, int Current) :
	CEditorActionEditLayerPropBase(pMap, GroupIndex, LayerIndex, Prop, Previous, Current)
{
	static const char *s_apNames[] = {
		"group",
		"order",
		"HQ"};
	static_assert(std::size(s_apNames) == (size_t)ELayerProp::NUM_PROPS);

	str_format(m_aDisplayText, sizeof(m_aDisplayText), "Edit layer %d in group %d %s property", m_LayerIndex, m_GroupIndex, s_apNames[(int)m_Prop]);
}

void CEditorActionEditLayerProp::Undo()
{
	auto pCurrentGroup = Map()->m_vpGroups[m_GroupIndex];

	if(m_Prop == ELayerProp::ORDER)
	{
		Map()->SelectLayer(pCurrentGroup->MoveLayer(m_Current, m_Previous));
	}
	else
		Apply(m_Previous);
}

void CEditorActionEditLayerProp::Redo()
{
	auto pCurrentGroup = Map()->m_vpGroups[m_GroupIndex];

	if(m_Prop == ELayerProp::ORDER)
	{
		Map()->SelectLayer(pCurrentGroup->MoveLayer(m_Previous, m_Current));
	}
	else
		Apply(m_Current);
}

void CEditorActionEditLayerProp::Apply(int Value)
{
	if(m_Prop == ELayerProp::GROUP)
	{
		auto pCurrentGroup = Map()->m_vpGroups[Value == m_Previous ? m_Current : m_Previous];
		auto pPreviousGroup = Map()->m_vpGroups[Value];
		pCurrentGroup->m_vpLayers.erase(pCurrentGroup->m_vpLayers.begin() + pCurrentGroup->m_vpLayers.size() - 1);
		if(Value == m_Previous)
			pPreviousGroup->m_vpLayers.insert(pPreviousGroup->m_vpLayers.begin() + m_LayerIndex, m_pLayer);
		else
			pPreviousGroup->m_vpLayers.push_back(m_pLayer);
		Map()->m_SelectedGroup = Value;
		Map()->SelectLayer(m_LayerIndex);
	}
	else if(m_Prop == ELayerProp::HQ)
	{
		m_pLayer->m_Flags = Value;
	}

	Map()->OnModify();
}

CEditorActionEditLayerTilesProp::CEditorActionEditLayerTilesProp(CEditorMap *pMap, int GroupIndex, int LayerIndex, ETilesProp Prop, int Previous, int Current) :
	CEditorActionEditLayerPropBase(pMap, GroupIndex, LayerIndex, Prop, Previous, Current)
{
	static const char *s_apNames[] = {
		"width",
		"height",
		"shift",
		"shift by",
		"image",
		"color",
		"color env",
		"color env offset",
		"automapper",
		"automapper reference",
		"live gametiles",
		"seed"};
	static_assert(std::size(s_apNames) == (size_t)ETilesProp::NUM_PROPS);

	str_format(m_aDisplayText, sizeof(m_aDisplayText), "Edit tiles layer %d in group %d %s property", m_LayerIndex, m_GroupIndex, s_apNames[(int)Prop]);
}

void CEditorActionEditLayerTilesProp::SetSavedLayers(const std::map<int, std::shared_ptr<CLayer>> &SavedLayers)
{
	m_SavedLayers = std::map(SavedLayers);
}

void CEditorActionEditLayerTilesProp::Undo()
{
	std::shared_ptr<CLayerTiles> pLayerTiles = std::static_pointer_cast<CLayerTiles>(m_pLayer);
	std::shared_ptr<CLayerTiles> pSavedLayerTiles = nullptr;

	if(m_Prop == ETilesProp::WIDTH || m_Prop == ETilesProp::HEIGHT)
	{
		if(m_Prop == ETilesProp::HEIGHT)
			pLayerTiles->Resize(pLayerTiles->m_Width, m_Previous);
		else if(m_Prop == ETilesProp::WIDTH)
			pLayerTiles->Resize(m_Previous, pLayerTiles->m_Height);

		RestoreLayer(LAYERTYPE_TILES, pLayerTiles);
		if(pLayerTiles->m_HasGame || pLayerTiles->m_HasFront || pLayerTiles->m_HasSwitch || pLayerTiles->m_HasSpeedup || pLayerTiles->m_HasTune)
		{
			if(Map()->m_pFrontLayer && !pLayerTiles->m_HasFront)
				RestoreLayer(LAYERTYPE_FRONT, Map()->m_pFrontLayer);
			if(Map()->m_pTeleLayer && !pLayerTiles->m_HasTele)
				RestoreLayer(LAYERTYPE_TELE, Map()->m_pTeleLayer);
			if(Map()->m_pSwitchLayer && !pLayerTiles->m_HasSwitch)
				RestoreLayer(LAYERTYPE_SWITCH, Map()->m_pSwitchLayer);
			if(Map()->m_pSpeedupLayer && !pLayerTiles->m_HasSpeedup)
				RestoreLayer(LAYERTYPE_SPEEDUP, Map()->m_pSpeedupLayer);
			if(Map()->m_pTuneLayer && !pLayerTiles->m_HasTune)
				RestoreLayer(LAYERTYPE_TUNE, Map()->m_pTuneLayer);
			if(!pLayerTiles->m_HasGame)
				RestoreLayer(LAYERTYPE_GAME, Map()->m_pGameLayer);
		}
	}
	else if(m_Prop == ETilesProp::SHIFT)
	{
		RestoreLayer(LAYERTYPE_TILES, pLayerTiles);
	}
	else if(m_Prop == ETilesProp::SHIFT_BY)
	{
		Map()->m_ShiftBy = m_Previous;
	}
	else if(m_Prop == ETilesProp::IMAGE)
	{
		if(m_Previous == -1 || Map()->m_vpImages.empty())
		{
			pLayerTiles->m_Image = -1;
		}
		else
		{
			pLayerTiles->m_Image = m_Previous % Map()->m_vpImages.size();
			pLayerTiles->m_AutomapperConfig = -1;
		}
	}
	else if(m_Prop == ETilesProp::COLOR)
	{
		const ColorRGBA ColorPick = ColorRGBA::UnpackAlphaLast<ColorRGBA>(m_Previous);

		pLayerTiles->m_Color.r = ColorPick.r * 255.0f;
		pLayerTiles->m_Color.g = ColorPick.g * 255.0f;
		pLayerTiles->m_Color.b = ColorPick.b * 255.0f;
		pLayerTiles->m_Color.a = ColorPick.a * 255.0f;

		Editor()->m_ColorPickerPopupContext.m_RgbaColor = ColorPick;
		Editor()->m_ColorPickerPopupContext.m_HslaColor = color_cast<ColorHSLA>(ColorPick);
		Editor()->m_ColorPickerPopupContext.m_HsvaColor = color_cast<ColorHSVA>(Editor()->m_ColorPickerPopupContext.m_HslaColor);
	}
	else if(m_Prop == ETilesProp::COLOR_ENV)
	{
		pLayerTiles->m_ColorEnv = m_Previous;
	}
	else if(m_Prop == ETilesProp::COLOR_ENV_OFFSET)
	{
		pLayerTiles->m_ColorEnvOffset = m_Previous;
	}
	else if(m_Prop == ETilesProp::AUTOMAPPER)
	{
		pLayerTiles->m_AutomapperConfig = m_Previous;
	}
	else if(m_Prop == ETilesProp::LIVE_GAMETILES)
	{
		pLayerTiles->m_LiveGameTiles = m_Previous;
	}
	else if(m_Prop == ETilesProp::SEED)
	{
		pLayerTiles->m_Seed = m_Previous;
	}

	Map()->OnModify();
}

void CEditorActionEditLayerTilesProp::Redo()
{
	std::shared_ptr<CLayerTiles> pLayerTiles = std::static_pointer_cast<CLayerTiles>(m_pLayer);

	if(m_Prop == ETilesProp::WIDTH || m_Prop == ETilesProp::HEIGHT)
	{
		if(m_Prop == ETilesProp::HEIGHT)
			pLayerTiles->Resize(pLayerTiles->m_Width, m_Current);
		else if(m_Prop == ETilesProp::WIDTH)
			pLayerTiles->Resize(m_Current, pLayerTiles->m_Height);

		if(pLayerTiles->m_HasGame || pLayerTiles->m_HasFront || pLayerTiles->m_HasSwitch || pLayerTiles->m_HasSpeedup || pLayerTiles->m_HasTune)
		{
			if(Map()->m_pFrontLayer && !pLayerTiles->m_HasFront)
				Map()->m_pFrontLayer->Resize(pLayerTiles->m_Width, pLayerTiles->m_Height);
			if(Map()->m_pTeleLayer && !pLayerTiles->m_HasTele)
				Map()->m_pTeleLayer->Resize(pLayerTiles->m_Width, pLayerTiles->m_Height);
			if(Map()->m_pSwitchLayer && !pLayerTiles->m_HasSwitch)
				Map()->m_pSwitchLayer->Resize(pLayerTiles->m_Width, pLayerTiles->m_Height);
			if(Map()->m_pSpeedupLayer && !pLayerTiles->m_HasSpeedup)
				Map()->m_pSpeedupLayer->Resize(pLayerTiles->m_Width, pLayerTiles->m_Height);
			if(Map()->m_pTuneLayer && !pLayerTiles->m_HasTune)
				Map()->m_pTuneLayer->Resize(pLayerTiles->m_Width, pLayerTiles->m_Height);
			if(!pLayerTiles->m_HasGame)
				Map()->m_pGameLayer->Resize(pLayerTiles->m_Width, pLayerTiles->m_Height);
		}
	}
	else if(m_Prop == ETilesProp::SHIFT)
	{
		pLayerTiles->Shift((EShiftDirection)m_Current);
	}
	else if(m_Prop == ETilesProp::SHIFT_BY)
	{
		Map()->m_ShiftBy = m_Current;
	}
	else if(m_Prop == ETilesProp::IMAGE)
	{
		if(m_Current == -1 || Map()->m_vpImages.empty())
		{
			pLayerTiles->m_Image = -1;
		}
		else
		{
			pLayerTiles->m_Image = m_Current % Map()->m_vpImages.size();
			pLayerTiles->m_AutomapperConfig = -1;
		}
	}
	else if(m_Prop == ETilesProp::COLOR)
	{
		const ColorRGBA ColorPick = ColorRGBA::UnpackAlphaLast<ColorRGBA>(m_Current);

		pLayerTiles->m_Color.r = ColorPick.r * 255.0f;
		pLayerTiles->m_Color.g = ColorPick.g * 255.0f;
		pLayerTiles->m_Color.b = ColorPick.b * 255.0f;
		pLayerTiles->m_Color.a = ColorPick.a * 255.0f;

		Editor()->m_ColorPickerPopupContext.m_RgbaColor = ColorPick;
		Editor()->m_ColorPickerPopupContext.m_HslaColor = color_cast<ColorHSLA>(ColorPick);
		Editor()->m_ColorPickerPopupContext.m_HsvaColor = color_cast<ColorHSVA>(Editor()->m_ColorPickerPopupContext.m_HslaColor);
	}
	else if(m_Prop == ETilesProp::COLOR_ENV)
	{
		pLayerTiles->m_ColorEnv = m_Current;
	}
	else if(m_Prop == ETilesProp::COLOR_ENV_OFFSET)
	{
		pLayerTiles->m_ColorEnvOffset = m_Current;
	}
	else if(m_Prop == ETilesProp::AUTOMAPPER)
	{
		pLayerTiles->m_AutomapperConfig = m_Current;
	}
	else if(m_Prop == ETilesProp::LIVE_GAMETILES)
	{
		pLayerTiles->m_LiveGameTiles = m_Current;
	}
	else if(m_Prop == ETilesProp::SEED)
	{
		pLayerTiles->m_Seed = m_Current;
	}

	Map()->OnModify();
}

void CEditorActionEditLayerTilesProp::RestoreLayer(int Layer, const std::shared_ptr<CLayerTiles> &pLayerTiles)
{
	if(m_SavedLayers[Layer] != nullptr)
	{
		std::shared_ptr<CLayerTiles> pSavedLayerTiles = std::static_pointer_cast<CLayerTiles>(m_SavedLayers[Layer]);
		mem_copy(pLayerTiles->m_pTiles, pSavedLayerTiles->m_pTiles, (size_t)pLayerTiles->m_Width * pLayerTiles->m_Height * sizeof(CTile));

		if(pLayerTiles->m_HasTele)
		{
			std::shared_ptr<CLayerTele> pLayerTele = std::static_pointer_cast<CLayerTele>(pLayerTiles);
			std::shared_ptr<CLayerTele> pSavedLayerTele = std::static_pointer_cast<CLayerTele>(pSavedLayerTiles);
			mem_copy(pLayerTele->m_pTeleTile, pSavedLayerTele->m_pTeleTile, (size_t)pLayerTiles->m_Width * pLayerTiles->m_Height * sizeof(CTeleTile));
		}
		else if(pLayerTiles->m_HasSpeedup)
		{
			std::shared_ptr<CLayerSpeedup> pLayerSpeedup = std::static_pointer_cast<CLayerSpeedup>(pLayerTiles);
			std::shared_ptr<CLayerSpeedup> pSavedLayerSpeedup = std::static_pointer_cast<CLayerSpeedup>(pSavedLayerTiles);
			mem_copy(pLayerSpeedup->m_pSpeedupTile, pSavedLayerSpeedup->m_pSpeedupTile, (size_t)pLayerTiles->m_Width * pLayerTiles->m_Height * sizeof(CSpeedupTile));
		}
		else if(pLayerTiles->m_HasSwitch)
		{
			std::shared_ptr<CLayerSwitch> pLayerSwitch = std::static_pointer_cast<CLayerSwitch>(pLayerTiles);
			std::shared_ptr<CLayerSwitch> pSavedLayerSwitch = std::static_pointer_cast<CLayerSwitch>(pSavedLayerTiles);
			mem_copy(pLayerSwitch->m_pSwitchTile, pSavedLayerSwitch->m_pSwitchTile, (size_t)pLayerTiles->m_Width * pLayerTiles->m_Height * sizeof(CSwitchTile));
		}
		else if(pLayerTiles->m_HasTune)
		{
			std::shared_ptr<CLayerTune> pLayerTune = std::static_pointer_cast<CLayerTune>(pLayerTiles);
			std::shared_ptr<CLayerTune> pSavedLayerTune = std::static_pointer_cast<CLayerTune>(pSavedLayerTiles);
			mem_copy(pLayerTune->m_pTuneTile, pSavedLayerTune->m_pTuneTile, (size_t)pLayerTiles->m_Width * pLayerTiles->m_Height * sizeof(CTuneTile));
		}
	}
}

CEditorActionEditLayerQuadsProp::CEditorActionEditLayerQuadsProp(CEditorMap *pMap, int GroupIndex, int LayerIndex, ELayerQuadsProp Prop, int Previous, int Current) :
	CEditorActionEditLayerPropBase(pMap, GroupIndex, LayerIndex, Prop, Previous, Current)
{
	static const char *s_apNames[] = {
		"image"};
	static_assert(std::size(s_apNames) == (size_t)ELayerQuadsProp::NUM_PROPS);
	str_format(m_aDisplayText, sizeof(m_aDisplayText), "Edit quads layer %d in group %d %s property", m_LayerIndex, m_GroupIndex, s_apNames[(int)m_Prop]);
}

void CEditorActionEditLayerQuadsProp::Undo()
{
	Apply(m_Previous);
}

void CEditorActionEditLayerQuadsProp::Redo()
{
	Apply(m_Current);
}

void CEditorActionEditLayerQuadsProp::Apply(int Value)
{
	std::shared_ptr<CLayerQuads> pLayerQuads = std::static_pointer_cast<CLayerQuads>(m_pLayer);
	if(m_Prop == ELayerQuadsProp::IMAGE)
	{
		if(Value >= 0 && !Map()->m_vpImages.empty())
			pLayerQuads->m_Image = Value % Map()->m_vpImages.size();
		else
			pLayerQuads->m_Image = -1;
	}

	Map()->OnModify();
}

// --------------------------------------------------------------

CEditorActionEditLayersGroupAndOrder::CEditorActionEditLayersGroupAndOrder(CEditorMap *pMap, int GroupIndex, const std::vector<int> &LayerIndices, int NewGroupIndex, const std::vector<int> &NewLayerIndices) :
	IEditorAction(pMap), m_GroupIndex(GroupIndex), m_LayerIndices(LayerIndices), m_NewGroupIndex(NewGroupIndex), m_NewLayerIndices(NewLayerIndices)
{
	std::sort(m_LayerIndices.begin(), m_LayerIndices.end());
	std::sort(m_NewLayerIndices.begin(), m_NewLayerIndices.end());

	str_format(m_aDisplayText, sizeof(m_aDisplayText), "Edit layers group and order (x%d)", (int)m_LayerIndices.size());
}

void CEditorActionEditLayersGroupAndOrder::Undo()
{
	// Undo : restore group and order
	auto &pCurrentGroup = Map()->m_vpGroups[m_NewGroupIndex];
	auto &pPreviousGroup = Map()->m_vpGroups[m_GroupIndex];
	std::vector<std::shared_ptr<CLayer>> vpLayers;
	vpLayers.reserve(m_NewLayerIndices.size());
	for(auto &LayerIndex : m_NewLayerIndices)
		vpLayers.push_back(pCurrentGroup->m_vpLayers[LayerIndex]);

	int k = 0;
	for(auto &pLayer : vpLayers)
	{
		pCurrentGroup->m_vpLayers.erase(std::find(pCurrentGroup->m_vpLayers.begin(), pCurrentGroup->m_vpLayers.end(), pLayer));
		pPreviousGroup->m_vpLayers.insert(pPreviousGroup->m_vpLayers.begin() + m_LayerIndices[k++], pLayer);
	}

	Map()->m_vSelectedLayers = m_LayerIndices;
	Map()->m_SelectedGroup = m_GroupIndex;
}

void CEditorActionEditLayersGroupAndOrder::Redo()
{
	// Redo : move layers
	auto &pCurrentGroup = Map()->m_vpGroups[m_GroupIndex];
	auto &pPreviousGroup = Map()->m_vpGroups[m_NewGroupIndex];
	std::vector<std::shared_ptr<CLayer>> vpLayers;
	vpLayers.reserve(m_LayerIndices.size());
	for(auto &LayerIndex : m_LayerIndices)
		vpLayers.push_back(pCurrentGroup->m_vpLayers[LayerIndex]);

	int k = 0;
	for(auto &pLayer : vpLayers)
	{
		pCurrentGroup->m_vpLayers.erase(std::find(pCurrentGroup->m_vpLayers.begin(), pCurrentGroup->m_vpLayers.end(), pLayer));
		pPreviousGroup->m_vpLayers.insert(pPreviousGroup->m_vpLayers.begin() + m_NewLayerIndices[k++], pLayer);
	}

	Map()->m_vSelectedLayers = m_NewLayerIndices;
	Map()->m_SelectedGroup = m_NewGroupIndex;
}

// -----------------------------------

CEditorActionAppendMap::CEditorActionAppendMap(CEditorMap *pMap, const char *pMapName, const SPrevInfo &PrevInfo, std::vector<int> &vImageIndexMap) :
	IEditorAction(pMap), m_PrevInfo(PrevInfo), m_vImageIndexMap(vImageIndexMap)
{
	str_copy(m_aMapName, pMapName);
	str_format(m_aDisplayText, sizeof(m_aDisplayText), "Append %s", m_aMapName);
}

void CEditorActionAppendMap::Undo()
{
	// Undo append:
	// - delete added groups
	// - delete added envelopes
	// - delete added images
	// - delete added sounds

	// Delete added groups
	while((int)Map()->m_vpGroups.size() > m_PrevInfo.m_Groups)
	{
		Map()->m_vpGroups.pop_back();
	}

	// Delete added envelopes
	while((int)Map()->m_vpEnvelopes.size() > m_PrevInfo.m_Envelopes)
	{
		Map()->m_vpEnvelopes.pop_back();
	}

	// Delete added sounds
	while((int)Map()->m_vpSounds.size() > m_PrevInfo.m_Sounds)
	{
		Map()->m_vpSounds.pop_back();
	}

	// Delete added images
	// Images are sorted when appending, so we need to revert sorting before deleting the images
	if(!m_vImageIndexMap.empty())
	{
		std::vector<int> vReverseIndexMap;
		vReverseIndexMap.resize(m_vImageIndexMap.size());

		for(int k = 0; k < (int)m_vImageIndexMap.size(); k++)
			vReverseIndexMap[m_vImageIndexMap[k]] = k;

		std::vector<std::shared_ptr<CEditorImage>> vpRevertedImages;
		vpRevertedImages.resize(Map()->m_vpImages.size());

		for(int k = 0; k < (int)vReverseIndexMap.size(); k++)
		{
			vpRevertedImages[vReverseIndexMap[k]] = Map()->m_vpImages[k];
		}
		Map()->m_vpImages = vpRevertedImages;

		Map()->ModifyImageIndex([vReverseIndexMap](int *pIndex) {
			if(*pIndex >= 0)
			{
				*pIndex = vReverseIndexMap[*pIndex];
			}
		});
	}

	while((int)Map()->m_vpImages.size() > m_PrevInfo.m_Images)
	{
		Map()->m_vpImages.pop_back();
	}

	Map()->OnModify();
}

void CEditorActionAppendMap::Redo()
{
	const auto &&ErrorHandler = [this](const char *pErrorMessage) {
		Editor()->ShowFileDialogError("%s", pErrorMessage);
		log_error("editor/append", "%s", pErrorMessage);
	};
	// Redo is just re-appending the same map
	Map()->Append(m_aMapName, IStorage::TYPE_ALL, true, ErrorHandler);
}

// ---------------------------

CEditorActionTileArt::CEditorActionTileArt(CEditorMap *pMap, int PreviousImageCount, const char *pFilename, std::vector<int> &vImageIndexMap) :
	IEditorAction(pMap), m_PreviousImageCount(PreviousImageCount), m_vImageIndexMap(vImageIndexMap)
{
	str_copy(m_aFilename, pFilename);
	str_copy(m_aDisplayText, "Add tile art");
}

void CEditorActionTileArt::Undo()
{
	// Delete added group
	Map()->m_vpGroups.pop_back();

	// Delete added images
	// Images are sorted when appending, so we need to revert sorting before deleting the images
	if(!m_vImageIndexMap.empty())
	{
		std::vector<int> vReverseIndexMap;
		vReverseIndexMap.resize(m_vImageIndexMap.size());

		for(int k = 0; k < (int)m_vImageIndexMap.size(); k++)
			vReverseIndexMap[m_vImageIndexMap[k]] = k;

		std::vector<std::shared_ptr<CEditorImage>> vpRevertedImages;
		vpRevertedImages.resize(Map()->m_vpImages.size());

		for(int k = 0; k < (int)vReverseIndexMap.size(); k++)
		{
			vpRevertedImages[vReverseIndexMap[k]] = Map()->m_vpImages[k];
		}
		Map()->m_vpImages = vpRevertedImages;

		Map()->ModifyImageIndex([vReverseIndexMap](int *pIndex) {
			if(*pIndex >= 0)
			{
				*pIndex = vReverseIndexMap[*pIndex];
			}
		});
	}

	while((int)Map()->m_vpImages.size() > m_PreviousImageCount)
	{
		Map()->m_vpImages.pop_back();
	}
}

void CEditorActionTileArt::Redo()
{
	CImageInfo Image;
	if(!Graphics()->LoadPng(Image, m_aFilename, IStorage::TYPE_ALL))
	{
		Editor()->ShowFileDialogError("Failed to load image from file '%s'.", m_aFilename);
		return;
	}
	Map()->AddTileArt(std::move(Image), m_aFilename, true);
}

// ---------------------------

CEditorActionQuadArt::CEditorActionQuadArt(CEditorMap *pMap, const std::shared_ptr<CLayerGroup> &pGroup) :
	IEditorAction(pMap), m_pGroup(pGroup)
{
	str_copy(m_aDisplayText, "Add quad art");
}

void CEditorActionQuadArt::Undo()
{
	// Delete added group (keep pointer for redo)
	auto &vGroups = Map()->m_vpGroups;
	auto It = std::find(vGroups.begin(), vGroups.end(), m_pGroup);
	if(It != vGroups.end())
		vGroups.erase(It);
}

void CEditorActionQuadArt::Redo()
{
	auto &vGroups = Map()->m_vpGroups;
	if(std::find(vGroups.begin(), vGroups.end(), m_pGroup) == vGroups.end())
		vGroups.push_back(m_pGroup);
}

// ---------------------------------

CEditorCommandAction::CEditorCommandAction(CEditorMap *pMap, EType Type, int *pSelectedCommandIndex, int CommandIndex, const char *pPreviousCommand, const char *pCurrentCommand) :
	IEditorAction(pMap), m_Type(Type), m_pSelectedCommandIndex(pSelectedCommandIndex), m_CommandIndex(CommandIndex)
{
	if(pPreviousCommand != nullptr)
		m_PreviousCommand = std::string(pPreviousCommand);
	if(pCurrentCommand != nullptr)
		m_CurrentCommand = std::string(pCurrentCommand);

	switch(m_Type)
	{
	case EType::ADD:
		str_copy(m_aDisplayText, "Add command");
		break;
	case EType::EDIT:
		str_format(m_aDisplayText, sizeof(m_aDisplayText), "Edit command %d", m_CommandIndex);
		break;
	case EType::DELETE:
		str_format(m_aDisplayText, sizeof(m_aDisplayText), "Delete command %d", m_CommandIndex);
		break;
	case EType::MOVE_UP:
		str_format(m_aDisplayText, sizeof(m_aDisplayText), "Move command %d up", m_CommandIndex);
		break;
	case EType::MOVE_DOWN:
		str_format(m_aDisplayText, sizeof(m_aDisplayText), "Move command %d down", m_CommandIndex);
		break;
	default:
		str_format(m_aDisplayText, sizeof(m_aDisplayText), "Edit command %d", m_CommandIndex);
		break;
	}
}

void CEditorCommandAction::Undo()
{
	switch(m_Type)
	{
	case EType::DELETE:
	{
		Map()->m_vSettings.insert(Map()->m_vSettings.begin() + m_CommandIndex, m_PreviousCommand.c_str());
		*m_pSelectedCommandIndex = m_CommandIndex;
		break;
	}
	case EType::ADD:
	{
		Map()->m_vSettings.erase(Map()->m_vSettings.begin() + m_CommandIndex);
		*m_pSelectedCommandIndex = Map()->m_vSettings.size() - 1;
		break;
	}
	case EType::EDIT:
	{
		str_copy(Map()->m_vSettings[m_CommandIndex].m_aCommand, m_PreviousCommand.c_str());
		*m_pSelectedCommandIndex = m_CommandIndex;
		break;
	}
	case EType::MOVE_DOWN:
	{
		std::swap(Map()->m_vSettings[m_CommandIndex], Map()->m_vSettings[m_CommandIndex + 1]);
		*m_pSelectedCommandIndex = m_CommandIndex;
		break;
	}
	case EType::MOVE_UP:
	{
		std::swap(Map()->m_vSettings[m_CommandIndex], Map()->m_vSettings[m_CommandIndex - 1]);
		*m_pSelectedCommandIndex = m_CommandIndex;
		break;
	}
	}
}

void CEditorCommandAction::Redo()
{
	switch(m_Type)
	{
	case EType::DELETE:
	{
		Map()->m_vSettings.erase(Map()->m_vSettings.begin() + m_CommandIndex);
		*m_pSelectedCommandIndex = Map()->m_vSettings.size() - 1;
		break;
	}
	case EType::ADD:
	{
		Map()->m_vSettings.insert(Map()->m_vSettings.begin() + m_CommandIndex, m_PreviousCommand.c_str());
		*m_pSelectedCommandIndex = m_CommandIndex;
		break;
	}
	case EType::EDIT:
	{
		str_copy(Map()->m_vSettings[m_CommandIndex].m_aCommand, m_CurrentCommand.c_str());
		*m_pSelectedCommandIndex = m_CommandIndex;
		break;
	}
	case EType::MOVE_DOWN:
	{
		std::swap(Map()->m_vSettings[m_CommandIndex], Map()->m_vSettings[m_CommandIndex + 1]);
		*m_pSelectedCommandIndex = m_CommandIndex;
		break;
	}
	case EType::MOVE_UP:
	{
		std::swap(Map()->m_vSettings[m_CommandIndex], Map()->m_vSettings[m_CommandIndex - 1]);
		*m_pSelectedCommandIndex = m_CommandIndex;
		break;
	}
	}
}

// ------------------------------------------------

CEditorActionEnvelopeAdd::CEditorActionEnvelopeAdd(CEditorMap *pMap, CEnvelope::EType EnvelopeType) :
	IEditorAction(pMap),
	m_EnvelopeType(EnvelopeType)
{
	str_format(m_aDisplayText, sizeof(m_aDisplayText), "Add new %s envelope", EnvelopeType == CEnvelope::EType::COLOR ? "color" : (EnvelopeType == CEnvelope::EType::POSITION ? "position" : "sound"));
	m_PreviousSelectedEnvelope = Map()->m_SelectedEnvelope;
}

void CEditorActionEnvelopeAdd::Undo()
{
	// Undo is removing the envelope, which was added at the back of the list
	Map()->m_vpEnvelopes.pop_back();
	Map()->OnModify();
	Map()->m_SelectedEnvelope = m_PreviousSelectedEnvelope;
}

void CEditorActionEnvelopeAdd::Redo()
{
	// Redo is adding a new envelope at the back of the list
	Map()->NewEnvelope(m_EnvelopeType);
	Map()->m_SelectedEnvelope = Map()->m_vpEnvelopes.size() - 1;
}

CEditorActionEnvelopeDelete::CEditorActionEnvelopeDelete(CEditorMap *pMap, int EnvelopeIndex, std::vector<std::shared_ptr<IEditorEnvelopeReference>> &vpObjectReferences, std::shared_ptr<CEnvelope> &pEnvelope) :
	IEditorAction(pMap), m_EnvelopeIndex(EnvelopeIndex), m_pEnv(pEnvelope), m_vpObjectReferences(vpObjectReferences)
{
	str_format(m_aDisplayText, sizeof(m_aDisplayText), "Delete envelope %d", m_EnvelopeIndex);
}

void CEditorActionEnvelopeDelete::Undo()
{
	// Undo is adding back the envelope
	Map()->InsertEnvelope(m_EnvelopeIndex, m_pEnv);
	Map()->UpdateEnvelopeReferences(m_EnvelopeIndex, m_pEnv, m_vpObjectReferences);
}

void CEditorActionEnvelopeDelete::Redo()
{
	// Redo is erasing the same envelope index
	Map()->DeleteEnvelope(m_EnvelopeIndex);
}

CEditorActionEnvelopeEdit::CEditorActionEnvelopeEdit(CEditorMap *pMap, int EnvelopeIndex, EEditType EditType, int Previous, int Current) :
	IEditorAction(pMap), m_EnvelopeIndex(EnvelopeIndex), m_EditType(EditType), m_Previous(Previous), m_Current(Current)
{
	static const char *s_apNames[] = {
		"sync",
		"order"};
	str_format(m_aDisplayText, sizeof(m_aDisplayText), "Edit envelope %d %s", m_EnvelopeIndex, s_apNames[(int)m_EditType]);
}

void CEditorActionEnvelopeEdit::Undo()
{
	switch(m_EditType)
	{
	case EEditType::ORDER:
	{
		Map()->MoveEnvelope(m_Current, m_Previous);
		break;
	}
	case EEditType::SYNC:
	{
		Map()->m_vpEnvelopes[m_EnvelopeIndex]->m_Synchronized = m_Previous;
		break;
	}
	}
	Map()->OnModify();
	Map()->m_SelectedEnvelope = m_EnvelopeIndex;
}

void CEditorActionEnvelopeEdit::Redo()
{
	switch(m_EditType)
	{
	case EEditType::ORDER:
	{
		Map()->MoveEnvelope(m_Previous, m_Current);
		break;
	}
	case EEditType::SYNC:
	{
		Map()->m_vpEnvelopes[m_EnvelopeIndex]->m_Synchronized = m_Current;
		break;
	}
	}
	Map()->OnModify();
	Map()->m_SelectedEnvelope = m_EnvelopeIndex;
}

CEditorActionEnvelopeEditPointTime::CEditorActionEnvelopeEditPointTime(CEditorMap *pMap, int EnvelopeIndex, int PointIndex, CFixedTime Previous, CFixedTime Current) :
	IEditorAction(pMap), m_EnvelopeIndex(EnvelopeIndex), m_PointIndex(PointIndex), m_Previous(Previous), m_Current(Current)
{
	str_format(m_aDisplayText, sizeof(m_aDisplayText), "Edit time of point %d of env %d", m_PointIndex, m_EnvelopeIndex);
}

void CEditorActionEnvelopeEditPointTime::Undo()
{
	Apply(m_Previous);
}

void CEditorActionEnvelopeEditPointTime::Redo()
{
	Apply(m_Current);
}

void CEditorActionEnvelopeEditPointTime::Apply(CFixedTime Value)
{
	Map()->m_vpEnvelopes[m_EnvelopeIndex]->m_vPoints[m_PointIndex].m_Time = Value;
	Map()->OnModify();
}

CEditorActionEnvelopeEditPoint::CEditorActionEnvelopeEditPoint(CEditorMap *pMap, int EnvelopeIndex, int PointIndex, int Channel, EEditType EditType, int Previous, int Current) :
	IEditorAction(pMap), m_EnvelopeIndex(EnvelopeIndex), m_PointIndex(PointIndex), m_Channel(Channel), m_EditType(EditType), m_Previous(Previous), m_Current(Current)
{
	static const char *s_apNames[] = {
		"value",
		"curve type"};
	str_format(m_aDisplayText, sizeof(m_aDisplayText), "Edit %s of point %d (channel %d) of env %d", s_apNames[(int)m_EditType], m_PointIndex, m_Channel, m_EnvelopeIndex);
}

void CEditorActionEnvelopeEditPoint::Undo()
{
	Apply(m_Previous);
}

void CEditorActionEnvelopeEditPoint::Redo()
{
	Apply(m_Current);
}

void CEditorActionEnvelopeEditPoint::Apply(int Value)
{
	auto pEnvelope = Map()->m_vpEnvelopes[m_EnvelopeIndex];

	if(m_EditType == EEditType::VALUE)
	{
		pEnvelope->m_vPoints[m_PointIndex].m_aValues[m_Channel] = Value;

		if(pEnvelope->GetChannels() == 4)
		{
			Editor()->m_ColorPickerPopupContext.m_RgbaColor = pEnvelope->m_vPoints[m_PointIndex].ColorValue();
			Editor()->m_ColorPickerPopupContext.m_HslaColor = color_cast<ColorHSLA>(Editor()->m_ColorPickerPopupContext.m_RgbaColor);
			Editor()->m_ColorPickerPopupContext.m_HsvaColor = color_cast<ColorHSVA>(Editor()->m_ColorPickerPopupContext.m_HslaColor);
		}
	}
	else if(m_EditType == EEditType::CURVE_TYPE)
	{
		pEnvelope->m_vPoints[m_PointIndex].m_Curvetype = Value;
	}

	Map()->OnModify();
}

// ----

CEditorActionEditEnvelopePointValue::CEditorActionEditEnvelopePointValue(CEditorMap *pMap, int EnvelopeIndex, int PointIndex, int Channel, EType Type, CFixedTime OldTime, int OldValue, CFixedTime NewTime, int NewValue) :
	IEditorAction(pMap), m_EnvelopeIndex(EnvelopeIndex), m_PointIndex(PointIndex), m_Channel(Channel), m_Type(Type), m_OldTime(OldTime), m_OldValue(OldValue), m_NewTime(NewTime), m_NewValue(NewValue)
{
	str_format(m_aDisplayText, sizeof(m_aDisplayText), "Edit point %d%s value (envelope %d, channel %d)", PointIndex, m_Type == EType::TANGENT_IN ? "tangent in" : (m_Type == EType::TANGENT_OUT ? "tangent out" : ""), m_EnvelopeIndex, m_Channel);
}

void CEditorActionEditEnvelopePointValue::Undo()
{
	Apply(true);
}

void CEditorActionEditEnvelopePointValue::Redo()
{
	Apply(false);
}

void CEditorActionEditEnvelopePointValue::Apply(bool Undo)
{
	float CurrentValue = fx2f(Undo ? m_OldValue : m_NewValue);
	CFixedTime CurrentTime = (Undo ? m_OldTime : m_NewTime);

	std::shared_ptr<CEnvelope> pEnvelope = Map()->m_vpEnvelopes[m_EnvelopeIndex];
	if(m_Type == EType::TANGENT_IN)
	{
		pEnvelope->m_vPoints[m_PointIndex].m_Bezier.m_aInTangentDeltaX[m_Channel] = std::min(CurrentTime - pEnvelope->m_vPoints[m_PointIndex].m_Time, CFixedTime(0));
		pEnvelope->m_vPoints[m_PointIndex].m_Bezier.m_aInTangentDeltaY[m_Channel] = f2fx(CurrentValue) - pEnvelope->m_vPoints[m_PointIndex].m_aValues[m_Channel];
	}
	else if(m_Type == EType::TANGENT_OUT)
	{
		pEnvelope->m_vPoints[m_PointIndex].m_Bezier.m_aOutTangentDeltaX[m_Channel] = std::max(CurrentTime - pEnvelope->m_vPoints[m_PointIndex].m_Time, CFixedTime(0));
		pEnvelope->m_vPoints[m_PointIndex].m_Bezier.m_aOutTangentDeltaY[m_Channel] = f2fx(CurrentValue) - pEnvelope->m_vPoints[m_PointIndex].m_aValues[m_Channel];
	}
	else
	{
		if(pEnvelope->GetChannels() == 1 || pEnvelope->GetChannels() == 4)
			CurrentValue = std::clamp(CurrentValue, 0.0f, 1.0f);
		pEnvelope->m_vPoints[m_PointIndex].m_aValues[m_Channel] = f2fx(CurrentValue);

		if(m_PointIndex != 0)
		{
			pEnvelope->m_vPoints[m_PointIndex].m_Time = CurrentTime;

			if(pEnvelope->m_vPoints[m_PointIndex].m_Time < pEnvelope->m_vPoints[m_PointIndex - 1].m_Time)
				pEnvelope->m_vPoints[m_PointIndex].m_Time = pEnvelope->m_vPoints[m_PointIndex - 1].m_Time + CFixedTime(1);
			if(static_cast<size_t>(m_PointIndex) + 1 != pEnvelope->m_vPoints.size() && pEnvelope->m_vPoints[m_PointIndex].m_Time > pEnvelope->m_vPoints[m_PointIndex + 1].m_Time)
				pEnvelope->m_vPoints[m_PointIndex].m_Time = pEnvelope->m_vPoints[m_PointIndex + 1].m_Time - CFixedTime(1);
		}
		else
		{
			pEnvelope->m_vPoints[m_PointIndex].m_Time = CFixedTime(0);
		}
	}

	Map()->OnModify();
	Map()->m_UpdateEnvPointInfo = true;
}

// ---------------------

CEditorActionResetEnvelopePointTangent::CEditorActionResetEnvelopePointTangent(CEditorMap *pMap, int EnvelopeIndex, int PointIndex, int Channel, bool In, CFixedTime OldTime, int OldValue) :
	CEditorActionEditEnvelopePointValue(pMap, EnvelopeIndex, PointIndex, Channel, In ? EType::TANGENT_IN : EType::TANGENT_OUT, OldTime, OldValue, CFixedTime(0), 0)
{
	str_format(m_aDisplayText, sizeof(m_aDisplayText), "Reset point %d of env %d tangent %s", PointIndex, EnvelopeIndex, In ? "in" : "out");
}

// ------------------

CEditorActionAddEnvelopePoint::CEditorActionAddEnvelopePoint(CEditorMap *pMap, int EnvelopeIndex, CFixedTime Time, ColorRGBA Channels) :
	IEditorAction(pMap), m_EnvelopeIndex(EnvelopeIndex), m_Time(Time), m_Channels(Channels)
{
	str_format(m_aDisplayText, sizeof(m_aDisplayText), "Add new point in envelope %d at time %f", m_EnvelopeIndex, Time.AsSeconds());
}

void CEditorActionAddEnvelopePoint::Undo()
{
	// Delete added point
	auto pEnvelope = Map()->m_vpEnvelopes[m_EnvelopeIndex];
	auto pIt = std::find_if(pEnvelope->m_vPoints.begin(), pEnvelope->m_vPoints.end(), [this](const CEnvPoint_runtime &Point) {
		return Point.m_Time == m_Time;
	});
	if(pIt != pEnvelope->m_vPoints.end())
	{
		pEnvelope->m_vPoints.erase(pIt);
	}

	Map()->OnModify();
}

void CEditorActionAddEnvelopePoint::Redo()
{
	auto pEnvelope = Map()->m_vpEnvelopes[m_EnvelopeIndex];
	pEnvelope->AddPoint(m_Time, {f2fx(m_Channels.r), f2fx(m_Channels.g), f2fx(m_Channels.b), f2fx(m_Channels.a)});

	Map()->OnModify();
}

CEditorActionDeleteEnvelopePoint::CEditorActionDeleteEnvelopePoint(CEditorMap *pMap, int EnvelopeIndex, int PointIndex) :
	IEditorAction(pMap), m_EnvelopeIndex(EnvelopeIndex), m_PointIndex(PointIndex), m_Point(Map()->m_vpEnvelopes[EnvelopeIndex]->m_vPoints[PointIndex])
{
	str_format(m_aDisplayText, sizeof(m_aDisplayText), "Delete point %d of envelope %d", m_PointIndex, m_EnvelopeIndex);
}

void CEditorActionDeleteEnvelopePoint::Undo()
{
	std::shared_ptr<CEnvelope> pEnvelope = Map()->m_vpEnvelopes[m_EnvelopeIndex];
	pEnvelope->m_vPoints.insert(pEnvelope->m_vPoints.begin() + m_PointIndex, m_Point);

	Map()->OnModify();
}

void CEditorActionDeleteEnvelopePoint::Redo()
{
	std::shared_ptr<CEnvelope> pEnvelope = Map()->m_vpEnvelopes[m_EnvelopeIndex];
	pEnvelope->m_vPoints.erase(pEnvelope->m_vPoints.begin() + m_PointIndex);

	auto pSelectedPointIt = std::find_if(Map()->m_vSelectedEnvelopePoints.begin(), Map()->m_vSelectedEnvelopePoints.end(), [this](const std::pair<int, int> &Pair) {
		return Pair.first == m_PointIndex;
	});

	if(pSelectedPointIt != Map()->m_vSelectedEnvelopePoints.end())
		Map()->m_vSelectedEnvelopePoints.erase(pSelectedPointIt);

	Map()->OnModify();
}

// -------------------------------

CEditorActionEditLayerSoundsProp::CEditorActionEditLayerSoundsProp(CEditorMap *pMap, int GroupIndex, int LayerIndex, ELayerSoundsProp Prop, int Previous, int Current) :
	CEditorActionEditLayerPropBase(pMap, GroupIndex, LayerIndex, Prop, Previous, Current)
{
	static const char *s_apNames[] = {
		"sound"};
	static_assert(std::size(s_apNames) == (size_t)ELayerSoundsProp::NUM_PROPS);
	str_format(m_aDisplayText, sizeof(m_aDisplayText), "Edit sounds layer %d in group %d %s property", m_LayerIndex, m_GroupIndex, s_apNames[(int)m_Prop]);
}

void CEditorActionEditLayerSoundsProp::Undo()
{
	Apply(m_Previous);
}

void CEditorActionEditLayerSoundsProp::Redo()
{
	Apply(m_Current);
}

void CEditorActionEditLayerSoundsProp::Apply(int Value)
{
	std::shared_ptr<CLayerSounds> pLayerSounds = std::static_pointer_cast<CLayerSounds>(m_pLayer);
	if(m_Prop == ELayerSoundsProp::SOUND)
	{
		if(Value >= 0 && !Map()->m_vpSounds.empty())
			pLayerSounds->m_Sound = Value % Map()->m_vpSounds.size();
		else
			pLayerSounds->m_Sound = -1;
	}

	Map()->OnModify();
}

// ---

CEditorActionDeleteSoundSource::CEditorActionDeleteSoundSource(CEditorMap *pMap, int GroupIndex, int LayerIndex, int SourceIndex) :
	CEditorActionLayerBase(pMap, GroupIndex, LayerIndex), m_SourceIndex(SourceIndex)
{
	std::shared_ptr<CLayerSounds> pLayerSounds = std::static_pointer_cast<CLayerSounds>(m_pLayer);
	m_Source = pLayerSounds->m_vSources[SourceIndex];

	str_format(m_aDisplayText, sizeof(m_aDisplayText), "Delete sound source %d in layer %d of group %d", SourceIndex, LayerIndex, GroupIndex);
}

void CEditorActionDeleteSoundSource::Undo()
{
	std::shared_ptr<CLayerSounds> pLayerSounds = std::static_pointer_cast<CLayerSounds>(m_pLayer);
	pLayerSounds->m_vSources.insert(pLayerSounds->m_vSources.begin() + m_SourceIndex, m_Source);
	Map()->m_SelectedSoundSource = m_SourceIndex;
	Map()->OnModify();
}

void CEditorActionDeleteSoundSource::Redo()
{
	std::shared_ptr<CLayerSounds> pLayerSounds = std::static_pointer_cast<CLayerSounds>(m_pLayer);
	pLayerSounds->m_vSources.erase(pLayerSounds->m_vSources.begin() + m_SourceIndex);
	Map()->m_SelectedSoundSource--;
	Map()->OnModify();
}

// ---------------

CEditorActionEditSoundSourceShape::CEditorActionEditSoundSourceShape(CEditorMap *pMap, int GroupIndex, int LayerIndex, int SourceIndex, int Value) :
	CEditorActionLayerBase(pMap, GroupIndex, LayerIndex), m_SourceIndex(SourceIndex), m_CurrentValue(Value)
{
	Save();

	static const char *const SHAPE_NAMES[] = {
		"rectangle",
		"circle",
	};
	static_assert(std::size(SHAPE_NAMES) == (size_t)CSoundShape::NUM_SHAPES);
	str_format(m_aDisplayText, sizeof(m_aDisplayText),
		"Edit shape of sound source %d in layer %d of group %d to %s",
		SourceIndex, LayerIndex, GroupIndex, SHAPE_NAMES[Value]);
}

void CEditorActionEditSoundSourceShape::Undo()
{
	std::shared_ptr<CLayerSounds> pLayerSounds = std::static_pointer_cast<CLayerSounds>(m_pLayer);
	CSoundSource *pSource = &pLayerSounds->m_vSources[m_SourceIndex];

	pSource->m_Shape = m_SavedShape;

	Map()->OnModify();
}

void CEditorActionEditSoundSourceShape::Redo()
{
	std::shared_ptr<CLayerSounds> pLayerSounds = std::static_pointer_cast<CLayerSounds>(m_pLayer);
	CSoundSource *pSource = &pLayerSounds->m_vSources[m_SourceIndex];

	pSource->m_Shape.m_Type = m_CurrentValue;

	// set default values
	switch(pSource->m_Shape.m_Type)
	{
	case CSoundShape::SHAPE_CIRCLE:
	{
		pSource->m_Shape.m_Circle.m_Radius = 1000;
		break;
	}
	case CSoundShape::SHAPE_RECTANGLE:
	{
		pSource->m_Shape.m_Rectangle.m_Width = f2fx(1000.0f);
		pSource->m_Shape.m_Rectangle.m_Height = f2fx(800.0f);
		break;
	}
	}

	Map()->OnModify();
}

void CEditorActionEditSoundSourceShape::Save()
{
	std::shared_ptr<CLayerSounds> pLayerSounds = std::static_pointer_cast<CLayerSounds>(m_pLayer);
	m_SavedShape = pLayerSounds->m_vSources[m_SourceIndex].m_Shape;
}

// -----

CEditorActionEditSoundSourceProp::CEditorActionEditSoundSourceProp(CEditorMap *pMap, int GroupIndex, int LayerIndex, int SourceIndex, ESoundProp Prop, int Previous, int Current) :
	CEditorActionEditLayerPropBase(pMap, GroupIndex, LayerIndex, Prop, Previous, Current), m_SourceIndex(SourceIndex)
{
	static const char *s_apNames[] = {
		"pos X",
		"pos Y",
		"loop",
		"pan",
		"time delay",
		"falloff",
		"pos env",
		"pos env offset",
		"sound env",
		"sound env offset"};
	static_assert(std::size(s_apNames) == (size_t)ESoundProp::NUM_PROPS);
	str_format(m_aDisplayText, sizeof(m_aDisplayText), "Edit sound source %d in layer %d of group %d %s property", SourceIndex, LayerIndex, GroupIndex, s_apNames[(int)Prop]);
}

void CEditorActionEditSoundSourceProp::Undo()
{
	Apply(m_Previous);
}

void CEditorActionEditSoundSourceProp::Redo()
{
	Apply(m_Current);
}

void CEditorActionEditSoundSourceProp::Apply(int Value)
{
	std::shared_ptr<CLayerSounds> pLayerSounds = std::static_pointer_cast<CLayerSounds>(m_pLayer);
	CSoundSource *pSource = &pLayerSounds->m_vSources[m_SourceIndex];

	if(m_Prop == ESoundProp::POS_X)
	{
		pSource->m_Position.x = Value;
	}
	else if(m_Prop == ESoundProp::POS_Y)
	{
		pSource->m_Position.y = Value;
	}
	else if(m_Prop == ESoundProp::LOOP)
	{
		pSource->m_Loop = Value;
	}
	else if(m_Prop == ESoundProp::PAN)
	{
		pSource->m_Pan = Value;
	}
	else if(m_Prop == ESoundProp::TIME_DELAY)
	{
		pSource->m_TimeDelay = Value;
	}
	else if(m_Prop == ESoundProp::FALLOFF)
	{
		pSource->m_Falloff = Value;
	}
	else if(m_Prop == ESoundProp::POS_ENV)
	{
		pSource->m_PosEnv = Value;
	}
	else if(m_Prop == ESoundProp::POS_ENV_OFFSET)
	{
		pSource->m_PosEnvOffset = Value;
	}
	else if(m_Prop == ESoundProp::SOUND_ENV)
	{
		pSource->m_SoundEnv = Value;
	}
	else if(m_Prop == ESoundProp::SOUND_ENV_OFFSET)
	{
		pSource->m_SoundEnvOffset = Value;
	}

	Map()->OnModify();
}

CEditorActionEditRectSoundSourceShapeProp::CEditorActionEditRectSoundSourceShapeProp(CEditorMap *pMap, int GroupIndex, int LayerIndex, int SourceIndex, ERectangleShapeProp Prop, int Previous, int Current) :
	CEditorActionEditLayerPropBase(pMap, GroupIndex, LayerIndex, Prop, Previous, Current), m_SourceIndex(SourceIndex)
{
	static const char *s_apNames[] = {
		"width",
		"height"};
	static_assert(std::size(s_apNames) == (size_t)ERectangleShapeProp::NUM_PROPS);
	str_format(m_aDisplayText, sizeof(m_aDisplayText), "Edit sound source %d in layer %d of group %d sound shape %s property", m_SourceIndex, m_LayerIndex, m_GroupIndex, s_apNames[(int)Prop]);
}

void CEditorActionEditRectSoundSourceShapeProp::Undo()
{
	Apply(m_Previous);
}

void CEditorActionEditRectSoundSourceShapeProp::Redo()
{
	Apply(m_Current);
}

void CEditorActionEditRectSoundSourceShapeProp::Apply(int Value)
{
	std::shared_ptr<CLayerSounds> pLayerSounds = std::static_pointer_cast<CLayerSounds>(m_pLayer);
	CSoundSource *pSource = &pLayerSounds->m_vSources[m_SourceIndex];

	if(m_Prop == ERectangleShapeProp::RECTANGLE_WIDTH)
	{
		pSource->m_Shape.m_Rectangle.m_Width = Value;
	}
	else if(m_Prop == ERectangleShapeProp::RECTANGLE_HEIGHT)
	{
		pSource->m_Shape.m_Rectangle.m_Height = Value;
	}

	Map()->OnModify();
}

CEditorActionEditCircleSoundSourceShapeProp::CEditorActionEditCircleSoundSourceShapeProp(CEditorMap *pMap, int GroupIndex, int LayerIndex, int SourceIndex, ECircleShapeProp Prop, int Previous, int Current) :
	CEditorActionEditLayerPropBase(pMap, GroupIndex, LayerIndex, Prop, Previous, Current), m_SourceIndex(SourceIndex)
{
	static const char *s_apNames[] = {
		"radius"};
	static_assert(std::size(s_apNames) == (size_t)ECircleShapeProp::NUM_PROPS);
	str_format(m_aDisplayText, sizeof(m_aDisplayText), "Edit sound source %d in layer %d of group %d sound shape %s property", m_SourceIndex, m_LayerIndex, m_GroupIndex, s_apNames[(int)Prop]);
}

void CEditorActionEditCircleSoundSourceShapeProp::Undo()
{
	Apply(m_Previous);
}

void CEditorActionEditCircleSoundSourceShapeProp::Redo()
{
	Apply(m_Current);
}

void CEditorActionEditCircleSoundSourceShapeProp::Apply(int Value)
{
	std::shared_ptr<CLayerSounds> pLayerSounds = std::static_pointer_cast<CLayerSounds>(m_pLayer);
	CSoundSource *pSource = &pLayerSounds->m_vSources[m_SourceIndex];

	if(m_Prop == ECircleShapeProp::CIRCLE_RADIUS)
	{
		pSource->m_Shape.m_Circle.m_Radius = Value;
	}

	Map()->OnModify();
}

// --------------------------

CEditorActionNewEmptySound::CEditorActionNewEmptySound(CEditorMap *pMap, int GroupIndex, int LayerIndex, int x, int y) :
	CEditorActionLayerBase(pMap, GroupIndex, LayerIndex), m_X(x), m_Y(y)
{
	str_format(m_aDisplayText, sizeof(m_aDisplayText), "New sound in layer %d of group %d", LayerIndex, GroupIndex);
}

void CEditorActionNewEmptySound::Undo()
{
	// Undo is simply deleting the added source
	std::shared_ptr<CLayerSounds> pLayerSounds = std::static_pointer_cast<CLayerSounds>(m_pLayer);
	pLayerSounds->m_vSources.pop_back();

	Map()->OnModify();
}

void CEditorActionNewEmptySound::Redo()
{
	std::shared_ptr<CLayerSounds> pLayerSounds = std::static_pointer_cast<CLayerSounds>(m_pLayer);
	pLayerSounds->NewSource(m_X, m_Y);

	Map()->OnModify();
}

CEditorActionNewEmptyQuad::CEditorActionNewEmptyQuad(CEditorMap *pMap, int GroupIndex, int LayerIndex, int x, int y) :
	CEditorActionLayerBase(pMap, GroupIndex, LayerIndex), m_X(x), m_Y(y)
{
	str_format(m_aDisplayText, sizeof(m_aDisplayText), "New quad in layer %d of group %d", LayerIndex, GroupIndex);
}

void CEditorActionNewEmptyQuad::Undo()
{
	// Undo is simply deleting the added quad
	std::shared_ptr<CLayerQuads> pLayerQuads = std::static_pointer_cast<CLayerQuads>(m_pLayer);
	pLayerQuads->m_vQuads.pop_back();

	Map()->OnModify();
}

void CEditorActionNewEmptyQuad::Redo()
{
	std::shared_ptr<CLayerQuads> pLayerQuads = std::static_pointer_cast<CLayerQuads>(m_pLayer);

	int Width = 64;
	int Height = 64;
	if(pLayerQuads->m_Image >= 0)
	{
		Width = Map()->m_vpImages[pLayerQuads->m_Image]->m_Width;
		Height = Map()->m_vpImages[pLayerQuads->m_Image]->m_Height;
	}

	pLayerQuads->NewQuad(m_X, m_Y, Width, Height);

	Map()->OnModify();
}

// -------------

CEditorActionNewQuad::CEditorActionNewQuad(CEditorMap *pMap, int GroupIndex, int LayerIndex) :
	CEditorActionLayerBase(pMap, GroupIndex, LayerIndex)
{
	std::shared_ptr<CLayerQuads> pLayerQuads = std::static_pointer_cast<CLayerQuads>(m_pLayer);
	m_Quad = pLayerQuads->m_vQuads[pLayerQuads->m_vQuads.size() - 1];

	str_format(m_aDisplayText, sizeof(m_aDisplayText), "New quad in layer %d of group %d", LayerIndex, GroupIndex);
}

void CEditorActionNewQuad::Undo()
{
	std::shared_ptr<CLayerQuads> pLayerQuads = std::static_pointer_cast<CLayerQuads>(m_pLayer);
	pLayerQuads->m_vQuads.pop_back();
}

void CEditorActionNewQuad::Redo()
{
	std::shared_ptr<CLayerQuads> pLayerQuads = std::static_pointer_cast<CLayerQuads>(m_pLayer);
	pLayerQuads->m_vQuads.emplace_back(m_Quad);
}

// --------------

CEditorActionMoveSoundSource::CEditorActionMoveSoundSource(CEditorMap *pMap, int GroupIndex, int LayerIndex, int SourceIndex, CPoint OriginalPosition, CPoint CurrentPosition) :
	CEditorActionLayerBase(pMap, GroupIndex, LayerIndex), m_SourceIndex(SourceIndex), m_OriginalPosition(OriginalPosition), m_CurrentPosition(CurrentPosition)
{
	str_format(m_aDisplayText, sizeof(m_aDisplayText), "Move sound source %d of layer %d in group %d", SourceIndex, LayerIndex, GroupIndex);
}

void CEditorActionMoveSoundSource::Undo()
{
	dbg_assert(m_pLayer->m_Type == LAYERTYPE_SOUNDS, "Layer type does not match a sound layer");
	std::static_pointer_cast<CLayerSounds>(m_pLayer)->m_vSources[m_SourceIndex].m_Position = m_OriginalPosition;
}

void CEditorActionMoveSoundSource::Redo()
{
	dbg_assert(m_pLayer->m_Type == LAYERTYPE_SOUNDS, "Layer type does not match a sound layer");
	std::static_pointer_cast<CLayerSounds>(m_pLayer)->m_vSources[m_SourceIndex].m_Position = m_CurrentPosition;
}
