#include "MWTMXTiledMap.h"
#include "MWTMXXmlParser.h"
#include "MWTMXLayer.h"
#include <new>

using namespace cocos2d;
using namespace std;

MWTMXTiledMap *MWTMXTiledMap::create(const std::string& tmxFile)
{
	MWTMXTiledMap *ret = new (std::nothrow) MWTMXTiledMap();
	if (ret->initWithTMXFile(tmxFile))
	{
		ret->autorelease();
		return ret;
	}
	CC_SAFE_DELETE(ret);
	return nullptr;
}

MWTMXTiledMap* MWTMXTiledMap::createWithXML(const std::string& tmxString, const std::string& resourcePath)
{
	MWTMXTiledMap *ret = new (std::nothrow) MWTMXTiledMap();
	if (ret->initWithXML(tmxString, resourcePath))
	{
		ret->autorelease();
		return ret;
	}
	CC_SAFE_DELETE(ret);
	return nullptr;
}

bool MWTMXTiledMap::initWithTMXFile(const std::string& tmxFile)
{
	CCASSERT(tmxFile.size()>0, "MWTMXTiledMap: tmx file should not be empty");

	setContentSize(Size::ZERO);

	MWTMXMapInfo *mapInfo = MWTMXMapInfo::create(tmxFile);

	if (!mapInfo)
	{
		return false;
	}
	CCASSERT(!mapInfo->getTilesets().empty(), "MWTMXTiledMap: Map not found. Please check the filename.");
	buildWithMapInfo(mapInfo);

	return true;
}

bool MWTMXTiledMap::initWithXML(const std::string& tmxString, const std::string& resourcePath)
{
	setContentSize(Size::ZERO);

	MWTMXMapInfo *mapInfo = MWTMXMapInfo::createWithXML(tmxString, resourcePath);

	CCASSERT(!mapInfo->getTilesets().empty(), "MWTMXTiledMap: Map not found. Please check the filename.");
	buildWithMapInfo(mapInfo);

	return true;
}

MWTMXTiledMap::MWTMXTiledMap()
	: _mapSize(Size::ZERO)
	, _tileSize(Size::ZERO)
	, _lockUpdateCount(0)
	, _autoExpansionSize(Size::ZERO)
	, _fixArtifactsByStretchingTexel(true)
{
}

MWTMXTiledMap::~MWTMXTiledMap()
{
}

MWTMXLayer *MWTMXTiledMap::parseLayer(TMXLayerInfo *layerInfo, MWTMXMapInfo *mapInfo)
{
	auto &&tilesets = tilesetsForLayer(layerInfo, mapInfo);
	if (tilesets.empty())
	{
		return nullptr;
	}

	MWTMXLayer *layer = MWTMXLayer::create(tilesets, layerInfo, mapInfo);

	// tell the layerinfo to release the ownership of the tiles map.
	layerInfo->_ownTiles = false;
	layer->setAutoExpansionSize(_autoExpansionSize);
	layer->setFixArtifactsByStretchingTexel(_fixArtifactsByStretchingTexel);
	layer->setupTiles();

	return layer;
}

std::vector<MWTMXTilesetInfo *> MWTMXTiledMap::tilesetsForLayer(TMXLayerInfo *layerInfo, MWTMXMapInfo *mapInfo) const
{
	struct __TilesetsInLayerItem
	{
		MWTMXTilesetInfo *tileset;
		int firstGid;
		bool inLayer;
	};

	Size size = layerInfo->_layerSize;
	auto &tilesets = mapInfo->getTilesets();

	std::vector<__TilesetsInLayerItem> tilesetsInLayer;
	tilesetsInLayer.reserve(tilesets.size());
	for (auto &item : tilesets)
	{
		__TilesetsInLayerItem newItem;
		newItem.tileset = item;
		newItem.firstGid = item->_firstGid;
		newItem.inLayer = false;
		tilesetsInLayer.push_back(newItem);
	}
	std::sort(tilesetsInLayer.begin(), tilesetsInLayer.end(), [this](__TilesetsInLayerItem &lhs, __TilesetsInLayerItem &rhs) {
		return lhs.firstGid < rhs.firstGid;
	});

	for (int y = 0; y < size.height; y++)
	{
		for (int x = 0; x < size.width; x++)
		{
			int pos = static_cast<int>(x + size.width * y);
			int gid = layerInfo->_tiles[pos];

			if (gid != 0)
			{
				for (int32_t i = (int32_t)tilesetsInLayer.size() - 1; i >= 0; --i)
				{
					auto tileset = tilesetsInLayer[i];
					if ((gid & kTMXFlippedMask) >= tileset.firstGid)
					{
						tilesetsInLayer[i].inLayer = true;
						break;
					}
				}
			}
		}
	}

	std::vector<MWTMXTilesetInfo *> result;

	for (auto &item : tilesetsInLayer)
	{
		if (item.inLayer)
		{
			result.push_back(item.tileset);
		}
	}

	// if all the tiles are 0, return empty tileset
	if (result.empty())
	{
		CCLOG("cocos2d: Warning: MX Layer '%s' has no tiles.", layerInfo->_name.c_str());
	}

	return std::move(result);
}

void MWTMXTiledMap::buildWithMapInfo(MWTMXMapInfo* mapInfo)
{
	_mapSize = mapInfo->getMapSize();
	_tileSize = mapInfo->getTileSize();
	_mapOrientation = mapInfo->getOrientation();

	_objectGroups = mapInfo->getObjectGroups();

	_properties = mapInfo->getProperties();

	_tileProperties = mapInfo->getTileProperties();

	int idx = 0;

	auto& layers = mapInfo->getLayers();
	for (const auto &layerInfo : layers) {
		if (layerInfo->_visible)
		{
			MWTMXLayer *child = parseLayer(layerInfo, mapInfo);

			if (child == nullptr) continue;

			addChild(child, idx, idx);

			_layers.push_back(child);

			// update content size with the max size
			const Size& childSize = child->getContentSize();
			Size currentSize = this->getContentSize();
			currentSize.width = std::max(currentSize.width, childSize.width);
			currentSize.height = std::max(currentSize.height, childSize.height);
			this->setContentSize(currentSize);

			idx++;
		}
	}
}

MWTMXLayer *MWTMXTiledMap::getLayer(const std::string &layerName) const
{
	CCASSERT(layerName.size() > 0, "Invalid layer name!");

	for (auto &layer : _layers)
	{
		if (layer)
		{
			if (layerName.compare(layer->getLayerName()) == 0)
			{
				return layer;
			}
		}
	}

	// layer not found
	return nullptr;
}

TMXObjectGroup *MWTMXTiledMap::getObjectGroup(const std::string& groupName) const
{
	CCASSERT(groupName.size() > 0, "Invalid group name!");

	if (_objectGroups.size()>0)
	{
		TMXObjectGroup* objectGroup = nullptr;
		for (auto iter = _objectGroups.cbegin(); iter != _objectGroups.cend(); ++iter)
		{
			objectGroup = *iter;
			if (objectGroup && objectGroup->getGroupName() == groupName)
			{
				return objectGroup;
			}
		}
	}

	// objectGroup not found
	return nullptr;
}

Value MWTMXTiledMap::getProperty(const std::string& propertyName) const
{
	if (_properties.find(propertyName) != _properties.end())
		return _properties.at(propertyName);

	return Value();
}

Value MWTMXTiledMap::getPropertiesForGID(int GID) const
{
	if (_tileProperties.find(GID) != _tileProperties.end())
		return _tileProperties.at(GID);

	return Value();
}

std::string MWTMXTiledMap::getDescription() const
{
	return StringUtils::format("<MWTMXTiledMap | Tag = %d, Layers = %d", _tag, static_cast<int>(_children.size()));
}

void MWTMXTiledMap::setAutoExpansionSize(const Size &size)
{
	_autoExpansionSize = size;
	for (auto &layer : _layers)
	{
		layer->setAutoExpansionSize(size);
	}
}

void MWTMXTiledMap::lockUpdate()
{
	++_lockUpdateCount;
	for (auto &layer : _layers)
	{
		layer->lockUpdate();
	}
}

void MWTMXTiledMap::unlockUpdate()
{
	--_lockUpdateCount;
	for (auto &layer : _layers)
	{
		layer->unlockUpdate();
	}
}

void MWTMXTiledMap::setFixArtifactsByStretchingTexel(bool value)
{
	_fixArtifactsByStretchingTexel = value;
	for (auto &layer : _layers)
	{
		layer->setFixArtifactsByStretchingTexel(value);
	}
}

void MWTMXTiledMap::setLoadSpriteFrameFunction(const std::function<SpriteFrame *(const std::string &, int32_t)> &func)
{
	for (auto &layer : _layers)
	{
		layer->setLoadSpriteFrameFunction(func);
	}
}

void MWTMXTiledMap::removeLayer(const std::string &layerName)
{
	CCASSERT(layerName.size() > 0, "Invalid layer name!");

	MWTMXLayer *layer;
	for (auto iter = _layers.begin(); iter != _layers.end(); )
	{
		layer = *iter;
		if (layerName == layer->getLayerName())
		{
			iter = _layers.erase(iter);
			this->removeChild(layer, true);
			return;
		}
		else 
		{
			++iter;
		}
	}
}

Rect MWTMXTiledMap::getScale9Rect(const Vec2 &center)
{
	RectInt ret{ 99999, -1, 99999, -1 };
	int cx = center.x;
	int cy = center.y;
	bool found = false;
	for (auto &layer : _layers)
	{
		RectInt layerRect = layer->_scale9RectForThumbnail;
		if (layerRect.x1 <= cx && layerRect.y1 <= cy && layerRect.x2 >= cx && layerRect.y2 >= cy)
		{
			if (layerRect.x1 < ret.x1) ret.x1 = layerRect.x1;
			if (layerRect.y1 < ret.x1) ret.y1 = layerRect.y1;
			if (layerRect.x2 < ret.x2) ret.x2 = layerRect.x2;
			if (layerRect.y2 < ret.y2) ret.y2 = layerRect.y2;
			found = true;
		}
	}

	if (found)
	{
		return Rect(ret.x1, ret.y1, ret.x2 - ret.x1 + 1, ret.y2 - ret.y1 + 1);
	}
	return Rect(-1.f, -1.f, 0.f, 0.f);
}