package speiger.src.coreengine.utils.io.dataTag.special;

import java.io.IOException;
import java.util.List;
import java.util.StringJoiner;
import java.util.function.Consumer;

import speiger.src.collections.objects.lists.ObjectArrayList;
import speiger.src.collections.objects.maps.impl.hash.Object2ObjectLinkedOpenHashMap;
import speiger.src.collections.objects.maps.interfaces.Object2ObjectMap;
import speiger.src.collections.objects.maps.interfaces.Object2ObjectMap.Entry;
import speiger.src.collections.objects.sets.ObjectSet;
import speiger.src.collections.objects.utils.ObjectSets;
import speiger.src.collections.objects.utils.maps.Object2ObjectMaps;
import speiger.src.coreengine.utils.io.dataTag.DataTag;
import speiger.src.coreengine.utils.io.dataTag.compression.DataTagLimiter;
import speiger.src.coreengine.utils.io.dataTag.compression.TagCompressor;
import speiger.src.coreengine.utils.io.dataTag.compression.TagDecompressor;
import speiger.src.coreengine.utils.io.dataTag.simple.PrimitiveTag;
import speiger.src.coreengine.utils.io.dataTag.simple.StringTag;
import speiger.src.coreengine.utils.io.streams.ExtendedDataInput;
import speiger.src.coreengine.utils.io.streams.ExtendedDataOutput;

public class MapTag implements IMapTag
{
	Object2ObjectMap<String, DataTag> tags = new Object2ObjectLinkedOpenHashMap<String, DataTag>();
	
	public MapTag()
	{
	}

	public MapTag(String id, DataTag tag)
	{
		if(tag instanceof ListTag) putList(id, tag.cast());
		else if(tag instanceof MapTag) putMap(id, tag.cast());
		else putTag(id, tag);
	}
	
	@Override
	public int getId()
	{
		return 17;
	}
	
	@Override
	public String getTypeName()
	{
		return "Map";
	}
	
	@Override
	public long countBytes(long input)
	{
		input += 4;
		for(Entry<String, DataTag> entry : Object2ObjectMaps.fastIterable(tags))
		{
			input += 1;
			input = entry.getValue().countBytes(input);
		}
		return input;
	}
	
	@Override
	public void addKeys(Consumer<String> keys)
	{
		for(Entry<String, DataTag> entry : Object2ObjectMaps.fastIterable(tags))
		{
			keys.accept(entry.getKey());
			entry.getValue().addKeys(keys);
		}
	}
	
	@Override
	public void write(ExtendedDataOutput output, TagCompressor compressor) throws IOException
	{
		output.writeVarInt(tags.size());
		for(Entry<String, DataTag> entry : Object2ObjectMaps.fastIterable(tags))
		{
			output.writeByte(entry.getValue().getId());
			compressor.writeKey(entry.getKey());
			entry.getValue().write(output, compressor);
		}
	}
	
	@Override
	public void read(ExtendedDataInput input, TagDecompressor decompressor, int depthLimit, DataTagLimiter limiter) throws IOException
	{
		if(depthLimit >= 512)
		{
			throw new IllegalStateException("Depth Limit Exceeded");
		}
		int size = input.readVarInt();
		limiter.countBytes(4 + size);
		for(int i = 0;i < size;i++)
		{
			byte type = input.readByte();
			String id = decompressor.readKey();
			limiter.countBytes(4 + (id.length() * 2));
			tags.put(id, decompressor.readSubTag(type, id, depthLimit + 1));
		}
	}
	
	@Override
	public MapTag copy()
	{
		MapTag tag = new MapTag();
		for(Entry<String, DataTag> entry : Object2ObjectMaps.fastIterable(tags))
		{
			tag.tags.put(entry.getKey(), entry.getValue().copy());
		}
		return tag;
	}
	
	@Override
	public void putTag(String id, DataTag tag)
	{
		if(tag == null) return;
		tags.put(id, tag);
	}
	
	public boolean contains(String id)
	{
		return tags.containsKey(id);
	}
	
	public boolean contains(String id, int tagType)
	{
		DataTag tag = tags.get(id);
		return tag != null && ((tagType == 99 && tag instanceof PrimitiveTag) || tag.getId() == tagType);
	}
	
	@Override
	public DataTag get(String id)
	{
		return tags.get(id);
	}
	
	public DataTag remove(String id)
	{
		return tags.remove(id);
	}
	
	public boolean isEmpty()
	{
		return tags.isEmpty();
	}
	
	public int size()
	{
		return tags.size();
	}
	
	public ObjectSet<String> keySet()
	{
		return ObjectSets.unmodifiable(tags.keySet());
	}
	
	public ObjectSet<Entry<String, DataTag>> entrySet()
	{
		return ObjectSets.unmodifiable(tags.object2ObjectEntrySet());
	}
	
	@Override
	public int hashCode()
	{
		return tags.hashCode();
	}
	
	@Override
	public boolean equals(Object obj)
	{
		return obj instanceof MapTag && ((MapTag)obj).tags.equals(tags);
	}
	
	@Override
	public String toString()
	{
		StringJoiner joiner = new StringJoiner(",", "{", "}");
		List<String> keys = new ObjectArrayList<>(tags.keySet());
		keys.sort(null);
		for(int i = 0, m = keys.size();i < m;i++)
		{
			joiner.add(StringTag.quoteAndEscapeBuilder(keys.get(i)).append(":").append(tags.get(keys.get(i))).toString());
		}
		return joiner.toString();
	}
	
	@Override
	public String prettyPrint(String indent, int depth)
	{
		if(tags.isEmpty())
		{
			return "{}";
		}
		List<String> keys = new ObjectArrayList<>(tags.keySet());
		keys.sort(null);
		StringBuilder builder = new StringBuilder().append("{");
		String s = DataTag.repeate(indent, depth+1);
		if(!indent.isEmpty())
		{
			builder.append('\n');
		}
		for(int i = 0,m=keys.size();i<m;i++)
		{
			builder.append(s).append(KEY_HIGHLIGHT).append(keys.get(i)).append(RESET_HIGHLIGHT).append(": ").append(tags.get(keys.get(i)).prettyPrint(indent, depth+1));
			if(i + 1 < m)
			{
				builder.append(",").append(indent.isEmpty() ? " " : '\n');
			}
		}
		if(!indent.isEmpty())
		{
			builder.append('\n').append(DataTag.repeate(indent, depth));
		}
		builder.append("}");
		return builder.toString();
	}
}
