using System;
using System.Collections.Generic;
using System.IO;
using System.IO.Compression;
using MonoGame.Extended.Tilemaps.Parsers;
namespace MonoGame.Extended.Tilemaps.Tiled;
///
/// Decodes tile data from various Tiled encodings (CSV, XML, Base64, gzip, zlib).
///
internal static class TiledDataDecoder
{
// Flip flag constants from Tiled specification
private const uint FLIPPED_HORIZONTALLY_FLAG = 0x80000000;
private const uint FLIPPED_VERTICALLY_FLAG = 0x40000000;
private const uint FLIPPED_DIAGONALLY_FLAG = 0x20000000;
private const uint FLIP_MASK = 0xE0000000;
///
/// Decodes tile data from various formats.
///
/// The tile layer data XML.
/// The layer width in tiles.
/// The layer height in tiles.
/// A 2D array of tiles.
public static TilemapTile[,] DecodeTileData(TiledTileLayerDataXml data, int width, int height)
{
if (data == null)
{
return new TilemapTile[width, height];
}
// Handle infinite maps (chunked data)
if (data.Chunks != null && data.Chunks.Count > 0)
{
return DecodeChunkedData(data, width, height);
}
// Handle regular data
return data.Encoding switch
{
null => DecodeXmlData(data.Tiles, width, height),
"csv" => DecodeCsvData(data.Value, width, height),
"base64" => DecodeBase64Data(data.Value, data.Compression, width, height),
_ => throw new TilemapParseException($"Unsupported tile data encoding: '{data.Encoding}'")
};
}
private static TilemapTile[,] DecodeXmlData(List tiles, int width, int height)
{
TilemapTile[,] result = new TilemapTile[width, height];
if (tiles == null || tiles.Count == 0)
{
return result;
}
for (int i = 0; i < tiles.Count && i < width * height; i++)
{
(int gid, TilemapTileFlipFlags flags) = ExtractFlipFlags(tiles[i].Gid);
int x = i % width;
int y = i / width;
result[x, y] = new TilemapTile(gid, flags);
}
return result;
}
private static TilemapTile[,] DecodeCsvData(string csv, int width, int height)
{
TilemapTile[,] result = new TilemapTile[width, height];
if (string.IsNullOrWhiteSpace(csv))
{
return result;
}
string[] values = csv.Split(new[] { ',', '\n', '\r', ' ', '\t' }, StringSplitOptions.RemoveEmptyEntries);
for (int i = 0; i < values.Length && i < width * height; i++)
{
if (!uint.TryParse(values[i].Trim(), out uint rawGid))
{
throw new TilemapParseException($"Invalid tile GID in CSV data: '{values[i]}'");
}
(int gid, TilemapTileFlipFlags flags) = ExtractFlipFlags(rawGid);
int x = i % width;
int y = i / width;
result[x, y] = new TilemapTile(gid, flags);
}
return result;
}
private static TilemapTile[,] DecodeBase64Data(string base64, string compression, int width, int height)
{
if (string.IsNullOrWhiteSpace(base64))
{
return new TilemapTile[width, height];
}
byte[] bytes = Convert.FromBase64String(base64.Trim());
// Decompress if needed
if (!string.IsNullOrEmpty(compression))
{
bytes = compression.ToLowerInvariant() switch
{
"gzip" => DecompressGzip(bytes),
"zlib" => DecompressZlib(bytes),
_ => throw new TilemapParseException($"Unsupported compression format: '{compression}'")
};
}
// Parse uint32 array (little-endian)
TilemapTile[,] result = new TilemapTile[width, height];
int tileCount = width * height;
if (bytes.Length < tileCount * 4)
{
throw new TilemapParseException($"Insufficient tile data: expected {tileCount * 4} bytes, got {bytes.Length}");
}
for (int i = 0; i < tileCount; i++)
{
uint rawGid = BitConverter.ToUInt32(bytes, i * 4);
(int gid, TilemapTileFlipFlags flags) = ExtractFlipFlags(rawGid);
int x = i % width;
int y = i / width;
result[x, y] = new TilemapTile(gid, flags);
}
return result;
}
private static TilemapTile[,] DecodeChunkedData(TiledTileLayerDataXml data, int width, int height)
{
TilemapTile[,] result = new TilemapTile[width, height];
foreach (TiledChunkXml chunk in data.Chunks)
{
// Decode chunk data (always CSV for chunks based on Tiled spec)
TilemapTile[,] chunkTiles = DecodeCsvData(chunk.Value, chunk.Width, chunk.Height);
// Copy chunk tiles into result at correct position
for (int cy = 0; cy < chunk.Height; cy++)
{
for (int cx = 0; cx < chunk.Width; cx++)
{
int worldX = chunk.X + cx;
int worldY = chunk.Y + cy;
// Skip tiles outside map bounds
if (worldX < 0 || worldX >= width || worldY < 0 || worldY >= height)
{
continue;
}
result[worldX, worldY] = chunkTiles[cx, cy];
}
}
}
return result;
}
private static (int gid, TilemapTileFlipFlags flags) ExtractFlipFlags(uint rawGid)
{
// Extract GID (clear flip flags)
int gid = (int)(rawGid & ~FLIP_MASK);
// Extract flip flags
TilemapTileFlipFlags flags = TilemapTileFlipFlags.None;
if ((rawGid & FLIPPED_HORIZONTALLY_FLAG) != 0)
{
flags |= TilemapTileFlipFlags.FlipHorizontally;
}
if ((rawGid & FLIPPED_VERTICALLY_FLAG) != 0)
{
flags |= TilemapTileFlipFlags.FlipVertically;
}
if ((rawGid & FLIPPED_DIAGONALLY_FLAG) != 0)
{
flags |= TilemapTileFlipFlags.FlipDiagonally;
}
return (gid, flags);
}
private static byte[] DecompressGzip(byte[] data)
{
try
{
using var input = new MemoryStream(data);
using var gzip = new GZipStream(input, CompressionMode.Decompress);
using var output = new MemoryStream();
gzip.CopyTo(output);
return output.ToArray();
}
catch (Exception ex)
{
throw new TilemapParseException("Failed to decompress gzip data", ex);
}
}
private static byte[] DecompressZlib(byte[] data)
{
try
{
// Zlib format = 2-byte header + DEFLATE stream + 4-byte Adler-32 checksum
// Skip the 2-byte zlib header and 4-byte checksum at the end
if (data.Length < 6)
{
throw new TilemapParseException($"Invalid zlib data: too short ({data.Length} bytes)");
}
using var input = new MemoryStream(data, 2, data.Length - 6);
using var deflate = new DeflateStream(input, CompressionMode.Decompress);
using var output = new MemoryStream();
deflate.CopyTo(output);
return output.ToArray();
}
catch (Exception ex)
{
throw new TilemapParseException("Failed to decompress zlib data", ex);
}
}
}