package common.tags;

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.DataInput;
import java.io.DataInputStream;
import java.io.DataOutput;
import java.io.DataOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;

import common.collect.Lists;
import common.collect.Maps;
import common.util.Util;

import java.util.Set;
import java.util.zip.GZIPInputStream;
import java.util.zip.GZIPOutputStream;

/**
 * compressed data tree / CDT
 * (ids 0..15)
 */
public class TagObject extends Tag {
	private Map<String, Tag> tags = Maps.<String, Tag>newHashMap();

	void write(DataOutput output) throws IOException {
		for(String key : this.tags.keySet()) {
			Tag tag = this.tags.get(key);
			output.writeByte(tag.getType().getId());
			if(tag.getType() != TagType.NULL) {
				output.writeUTF(key);
				tag.write(output);
			}
		}
		output.writeByte(0);
	}

	void read(DataInput input, int depth, SizeTracker tracker) throws IOException {
		tracker.read(48);
		if(depth > 256)
			throw new IOException("Objekt ist zu komplex, die Tiefe ist größer als 256");
		this.tags.clear();
		byte id;
		while((id = input.readByte()) != 0) {
			String key = input.readUTF();
			tracker.read(28 + 2 * key.length());
			Tag tag = TagType.create(id);
			tag.read(input, depth + 1, tracker);
			if(this.tags.put(key, tag) != null)
				throw new IOException("Objekt ist ungültig, Tag '" + key + "' ist mehrfach vorhanden");
		}
	}

	public Set<String> keySet() {
		return this.tags.keySet();
	}

	TagType getType() {
		return TagType.OBJECT;
	}

	void set(String key, Tag value) {
		this.tags.put(key, value);
	}

	public void setObject(String key, TagObject value) {
		this.tags.put(key, value);
	}

	public void setStringArray(String key, String[] value) {
		this.tags.put(key, new TagStringArray(value));
	}

	public void setList(String key, List<TagObject> value) {
		this.tags.put(key, new TagList(value));
	}

	public void setBool(String key, boolean value) {
		this.tags.put(key, new TagBool(value));
	}

	public void setByte(String key, byte value) {
		this.tags.put(key, new TagByte(value));
	}

	public void setShort(String key, short value) {
		this.tags.put(key, new TagShort(value));
	}

	public void setChar(String key, char value) {
		this.tags.put(key, new TagChar(value));
	}

	public void setInt(String key, int value) {
		this.tags.put(key, new TagInt(value));
	}

	public void setLong(String key, long value) {
		this.tags.put(key, new TagLong(value));
	}

	public void setFloat(String key, float value) {
		this.tags.put(key, new TagFloat(value));
	}

	public void setDouble(String key, double value) {
		this.tags.put(key, new TagDouble(value));
	}

	public void setString(String key, String value) {
		this.tags.put(key, new TagString(value));
	}

	public void setByteArray(String key, byte[] value) {
		this.tags.put(key, new TagByteArray(value));
	}

	public void setIntArray(String key, int[] value) {
		this.tags.put(key, new TagIntArray(value));
	}

	Tag get(String key) {
		return this.tags.get(key);
	}

	private boolean has(String key, TagType type) {
		Tag tag = this.tags.get(key);
		return tag != null && tag.getType() == type;
	}

	public boolean hasBool(String key) {
		return this.has(key, TagType.BOOLEAN);
	}

	public boolean hasByte(String key) {
		return this.has(key, TagType.BYTE);
	}

	public boolean hasShort(String key) {
		return this.has(key, TagType.SHORT);
	}

	public boolean hasChar(String key) {
		return this.has(key, TagType.CHAR);
	}

	public boolean hasInt(String key) {
		return this.has(key, TagType.INT);
	}

	public boolean hasLong(String key) {
		return this.has(key, TagType.LONG);
	}

	public boolean hasFloat(String key) {
		return this.has(key, TagType.FLOAT);
	}

	public boolean hasDouble(String key) {
		return this.has(key, TagType.DOUBLE);
	}

	public boolean hasString(String key) {
		return this.has(key, TagType.STRING);
	}

	public boolean hasByteArray(String key) {
		return this.has(key, TagType.BYTE_ARRAY);
	}

	public boolean hasIntArray(String key) {
		return this.has(key, TagType.INT_ARRAY);
	}

	public boolean hasObject(String key) {
		return this.has(key, TagType.OBJECT);
	}

	public boolean hasStringArray(String key) {
		return this.has(key, TagType.STRING_ARRAY);
	}

	public boolean hasList(String key) {
		return this.has(key, TagType.LIST);
	}

	public boolean getBool(String key) {
		return !this.has(key, TagType.BOOLEAN) ? false : ((TagBool)this.tags.get(key)).getBool();
	}

	public byte getByte(String key) {
		return !this.has(key, TagType.BYTE) ? 0 : ((TagByte)this.tags.get(key)).getByte();
	}

	public short getShort(String key) {
		return !this.has(key, TagType.SHORT) ? 0 : ((TagShort)this.tags.get(key)).getShort();
	}

	public char getChar(String key) {
		return !this.has(key, TagType.CHAR) ? 0 : ((TagChar)this.tags.get(key)).getChar();
	}

	public int getInt(String key) {
		return !this.has(key, TagType.INT) ? 0 : ((TagInt)this.tags.get(key)).getInt();
	}

	public long getLong(String key) {
		return !this.has(key, TagType.LONG) ? 0L : ((TagLong)this.tags.get(key)).getLong();
	}

	public float getFloat(String key) {
		return !this.has(key, TagType.FLOAT) ? 0.0f : ((TagFloat)this.tags.get(key)).getFloat();
	}

	public double getDouble(String key) {
		return !this.has(key, TagType.DOUBLE) ? 0.0 : ((TagDouble)this.tags.get(key)).getDouble();
	}

	public String getString(String key) {
		return !this.has(key, TagType.STRING) ? "" : ((TagString)this.tags.get(key)).getString();
	}

	public byte[] getByteArray(String key) {
		return !this.has(key, TagType.BYTE_ARRAY) ? new byte[0] : ((TagByteArray)this.tags.get(key)).getByteArray();
	}

	public int[] getIntArray(String key) {
		return !this.has(key, TagType.INT_ARRAY) ? new int[0] : ((TagIntArray)this.tags.get(key)).getIntArray();
	}

	public TagObject getObject(String key) {
		return !this.has(key, TagType.OBJECT) ? new TagObject() : (TagObject)this.tags.get(key);
	}

	public String[] getStringArray(String key) {
		return !this.has(key, TagType.STRING_ARRAY) ? new String[0] : ((TagStringArray)this.tags.get(key)).getStringArray();
	}

	public List<TagObject> getList(String key) {
		return !this.has(key, TagType.LIST) ? Lists.newArrayList() : ((TagList)this.tags.get(key)).getList();
	}

	public void remove(String key) {
		this.tags.remove(key);
	}
	
	private List<Entry<String, Tag>> getSortedEntries() {
		List<Entry<String, Tag>> list = Lists.newArrayList(this.tags.entrySet());
		Collections.sort(list, (e1, e2) -> e1.getValue().getType() == TagType.OBJECT && e2.getValue().getType() != TagType.OBJECT ? 1 : (
				e2.getValue().getType() == TagType.OBJECT && e1.getValue().getType() != TagType.OBJECT ? -1 : (
				(e1.getValue().getType() == TagType.LIST || e1.getValue().getType() == TagType.STRING_ARRAY) && 
				e2.getValue().getType() != TagType.LIST && e2.getValue().getType() != TagType.STRING_ARRAY ? 1 : (
				(e2.getValue().getType() == TagType.LIST || e2.getValue().getType() == TagType.STRING_ARRAY) && 
				e1.getValue().getType() != TagType.LIST && e1.getValue().getType() != TagType.STRING_ARRAY ? -1 : e1.getKey().compareTo(e2.getKey())))));
		return list;
	}

	public String toString() {
		StringBuilder sb = new StringBuilder("{");
		for(Entry<String, Tag> entry : this.getSortedEntries()) {
			if(sb.length() != 1)
				sb.append(',');
			sb.append(entry.getKey()).append(':').append(entry.getValue().toString());
		}
		return sb.append('}').toString();
	}

	String toString(int indent, int depth) {
		StringBuilder sb = new StringBuilder("{");
		String prefix = Util.repeatString(' ', indent * (depth + 1));
		for(Entry<String, Tag> entry : this.getSortedEntries()) {
			if(sb.length() != 1)
				sb.append(',');
			sb.append('\n').append(prefix).append(entry.getKey()).append(": ").append(entry.getValue().toString(indent, depth + 1));
		}
		return sb.append('\n').append(Util.repeatString(' ', indent * depth)).append('}').toString();
	}

	public boolean isEmpty() {
		return this.tags.isEmpty();
	}

	public TagObject copy() {
		TagObject tag = new TagObject();
		for(String s : this.tags.keySet()) {
			tag.set(s, this.tags.get(s).copy());
		}
		return tag;
	}

	public boolean equals(Object other) {
		return super.equals(other) && this.tags.entrySet().equals(((TagObject)other).tags.entrySet());
	}

	public int hashCode() {
		return super.hashCode() ^ this.tags.hashCode();
	}

	/**
	 * Merges this tag with the given compound. Any sub-compounds are
	 * merged using the same methods, other types of tags are overwritten from the
	 * given compound.
	 */
	public void merge(TagObject other) {
		for(String key : other.tags.keySet()) {
			Tag tag = other.tags.get(key);
			if(tag.getType() == TagType.OBJECT) {
				if(this.has(key, TagType.OBJECT)) {
					TagObject comp = this.getObject(key);
					comp.merge((TagObject)tag);
				}
				else {
					this.set(key, tag.copy());
				}
			}
			else {
				this.set(key, tag.copy());
			}
		}
	}
	
	public String format(int indent) {
		return indent == 0 ? this.toString() : this.toString(indent, 0);
	}

	public static void write(TagObject tag, DataOutput out) throws IOException {
		tag.write(out);
	}

	public static TagObject read(DataInput in) throws IOException {
		return read(in, SizeTracker.INFINITE);
	}

	public static TagObject read(DataInput in, int limit) throws IOException {
		return read(in, new SizeTracker(limit));
	}

	private static TagObject read(DataInput in, SizeTracker tracker) throws IOException {
		TagObject tag = new TagObject();
		tag.read(in, 0, tracker);
		return tag;
	}

	public static void writeGZip(TagObject tag, File file) throws IOException {
		DataOutputStream out = new DataOutputStream(new BufferedOutputStream(new GZIPOutputStream(new FileOutputStream(file))));
		try {
			TagObject.write(tag, out);
		}
		finally {
			out.close();
		}
	}

	public static TagObject readGZip(File file) throws IOException {
		DataInputStream in = new DataInputStream(new BufferedInputStream(new GZIPInputStream(new FileInputStream(file))));
		TagObject tag;
		try {
			tag = TagObject.read(in, SizeTracker.INFINITE);
		}
		finally {
			in.close();
		}
		return tag;
	}

	public static boolean compare(TagObject tag1, TagObject tag2) {
		return compareTags(tag1, tag2);
	}
	
	private static boolean compareTags(Tag tag1, Tag tag2) {
		if(tag1 == tag2 || tag1 == null)
			return true;
		else if(tag2 == null || !tag1.getClass().equals(tag2.getClass()))
			return false;
		else if(tag1 instanceof TagObject) {
			TagObject comp1 = (TagObject)tag1;
			TagObject comp2 = (TagObject)tag2;
			for(String key : comp1.keySet()) {
				Tag tag = comp1.get(key);
				if(!compareTags(tag, comp2.get(key)))
					return false;
			}
			return true;
		}
		return tag1.equals(tag2);
	}
	
	public static TagObject parse(String tag) throws IllegalArgumentException {
		return TagInterpreter.parseTag(tag.trim());
	}
}
