content: package com.comphenix.protocol.wrappers.nbt.io;
  
  import java.nio.ByteBuffer;
  import java.nio.IntBuffer;
  import java.util.ArrayList;
  import java.util.HashMap;
  import java.util.List;
  import java.util.Map;
  import java.util.Set;
  
  import org.bukkit.configuration.ConfigurationSection;
  import org.bukkit.configuration.file.YamlConfiguration;
  
  import com.comphenix.protocol.wrappers.nbt.NbtBase;
  import com.comphenix.protocol.wrappers.nbt.NbtCompound;
  import com.comphenix.protocol.wrappers.nbt.NbtFactory;
  import com.comphenix.protocol.wrappers.nbt.NbtList;
  import com.comphenix.protocol.wrappers.nbt.NbtType;
  import com.comphenix.protocol.wrappers.nbt.NbtVisitor;
  import com.comphenix.protocol.wrappers.nbt.NbtWrapper;
  import com.google.common.primitives.Ints;
  
  /**
   * Serialize and deserialize NBT information from a configuration section.
   * <p>
   * Note that data types may be internally preserved by modifying the serialized name. This may
   * be visible to the end-user.
   * 
   * @author Kristian
   */
  public class NbtConfigurationSerializer {
  	/**
  	 * The default delimiter that is used to store the data type in YAML.
  	 */
  	public static final String TYPE_DELIMITER = "$";
  	
  	/**
  	 * A standard YAML serializer.
  	 */
  	public static final NbtConfigurationSerializer DEFAULT = new NbtConfigurationSerializer();
  
  	private String dataTypeDelimiter;
  
  	/**
  	 * Construct a serializer using {@link #TYPE_DELIMITER} as the default delimiter.
  	 */
  	public NbtConfigurationSerializer() {
  		this.dataTypeDelimiter = TYPE_DELIMITER;
  	}
  	
  	/**
  	 * Construct a serializer using the given value as a delimiter.
  	 * @param dataTypeDelimiter - the local data type delimiter.
  	 */
  	public NbtConfigurationSerializer(String dataTypeDelimiter) {
  		this.dataTypeDelimiter = dataTypeDelimiter;
  	}
  
  	/**
  	 * Retrieve the current data type delimiter.
  	 * @return The current data type delimiter.
  	 */
  	public String getDataTypeDelimiter() {
  		return dataTypeDelimiter;
  	}
  
  	/**
  	 * Write the content of a NBT tag to a configuration section.
  	 * @param <TType> Type
  	 * @param value - the NBT tag to write.
  	 * @param destination - the destination section.
  	 */
  	public <TType> void serialize(NbtBase<TType> value, final ConfigurationSection destination) {
  		value.accept(new NbtVisitor() {
  			private ConfigurationSection current = destination;
  			
  			// The current list we're working on
  			private List<Object> currentList;
  			
  			// Store the index of a configuration section that works like a list
  			private Map<ConfigurationSection, Integer> workingIndex = new HashMap<>();
  			
  			@Override
  			public boolean visitEnter(NbtCompound compound) {
  				current = current.createSection(compound.getName());
  				return true;
  			}
  			
  			@Override
  			public boolean visitEnter(NbtList<?> list) {
  				Integer listIndex = getNextIndex();
  				String name = getEncodedName(list, listIndex);
  				
  				if (list.getElementType().isComposite()) {
  					// Use a configuration section to store this list
  					current = current.createSection(name);
  					workingIndex.put(current, 0);
  				} else {
  					currentList = new ArrayList<>();
  					current.set(name, currentList);
  				}
  				return true;
  			}
  			
  			@Override
  			public boolean visitLeave(NbtCompound compound) {
  				current = current.getParent();
  				return true;
  			}
  			
  			@Override
  			public boolean visitLeave(NbtList<?> list) {
  				// Write the list to the configuration section
  				if (currentList != null) {
  					// Save and reset the temporary list
  					currentList = null;
  				} else {
  					// Go up a level
  					workingIndex.remove(current);
  					current = current.getParent();
  				}
  				return true;
  			}
  			
  			@Override
  			public boolean visit(NbtBase<?> node) {
  				// Are we working on a list?
  				if (currentList == null) {
  					Integer listIndex = getNextIndex();
  					String name = getEncodedName(node, listIndex);
  					
  					// Save member
  					current.set(name, fromNodeValue(node));
  					
  				} else {
  					currentList.add(fromNodeValue(node));
  				}
  				return true;
  			}
  			
  			private Integer getNextIndex() {
  				Integer listIndex = workingIndex.get(current);
  				
  				if (listIndex != null)
  					return workingIndex.put(current, listIndex + 1);
  				else
  					return null;
  			}
  			
  			// We need to store the data type somehow
  			private String getEncodedName(NbtBase<?> node, Integer index) {
  				if (index != null)
  					return index + dataTypeDelimiter + node.getType().getRawID();
  				else
  					return node.getName() + dataTypeDelimiter + node.getType().getRawID();
  			}
  			
  			private String getEncodedName(NbtList<?> node, Integer index) {
  				if (index != null)
  					return index + dataTypeDelimiter + node.getElementType().getRawID();
  				else
  					return node.getName() + dataTypeDelimiter + node.getElementType().getRawID();
  			}
  		});
  	}
  	
  	/**
  	 * Read a NBT tag from a root configuration.
  	 * @param <TType> Type
  	 * @param root - configuration that contains the NBT tag.
  	 * @param nodeName - name of the NBT tag.
  	 * @return The read NBT tag.
  	 */
  	@SuppressWarnings("unchecked")
  	public <TType> NbtWrapper<TType> deserialize(ConfigurationSection root, String nodeName) {
  		return (NbtWrapper<TType>) readNode(root, nodeName);
  	}
  	
  	/**
  	 * Read a NBT compound from a root configuration.
  	 * @param root - configuration that contains the NBT compound.
  	 * @param nodeName - name of the NBT compound.
  	 * @return The read NBT compound.
  	 */
  	public NbtCompound deserializeCompound(YamlConfiguration root, String nodeName) {
  		return (NbtCompound) readNode(root, nodeName);
  	}
  	
  	/**
  	 * Read a NBT compound from a root configuration.
  	 * @param <T> Type
  	 * @param root - configuration that contains the NBT compound.
  	 * @param nodeName - name of the NBT compound.
  	 * @return The read NBT compound.
  	 */
  	@SuppressWarnings("unchecked")
  	public <T> NbtList<T> deserializeList(YamlConfiguration root, String nodeName) {
  		return (NbtList<T>) readNode(root, nodeName);
  	}
  	
  	@SuppressWarnings("unchecked")
  	private NbtWrapper<?> readNode(ConfigurationSection parent, String name) {
  		String[] decoded = getDecodedName(name);
  		Object node = parent.get(name);
  		NbtType type = NbtType.TAG_END;
  		
  		// It's possible that the caller isn't aware of the encoded name itself
  		if (node == null) {
  			for (String key : parent.getKeys(false)) {
  				decoded = getDecodedName(key);
  				
  				// Great
  				if (decoded[0].equals(name)) {
  					node = parent.get(decoded[0]);
  					break;
  				}
  			}
  			
  			// Inform the caller of the problem
  			if (node == null) {
  				throw new IllegalArgumentException("Unable to find node " + name + " in " + parent);
  			}
  		}
  		
  		// Attempt to decode a NBT type
  		if (decoded.length > 1) {
  			type = NbtType.getTypeFromID(Integer.parseInt(decoded[1]));
  		}
  		
  		// Is this a compound?
  		if (node instanceof ConfigurationSection) {
  			// Is this a list of a map?
  			if (type != NbtType.TAG_END) {
  				NbtList<Object> list = NbtFactory.ofList(decoded[0]);
  				ConfigurationSection section = (ConfigurationSection) node;
  				List<String> sorted = sortSet(section.getKeys(false));
  				
  				// Read everything in order
  				for (String key : sorted) {
  					NbtBase<Object> base = (NbtBase<Object>) readNode(section, key.toString());
  					base.setName(NbtList.EMPTY_NAME);
  					list.getValue().add(base);
  				}
  				return (NbtWrapper<?>) list;
  			
  			} else {
  				NbtCompound compound = NbtFactory.ofCompound(decoded[0]);
  				ConfigurationSection section = (ConfigurationSection) node;
  				
  				// As above
  				for (String key : section.getKeys(false))
  					compound.put(readNode(section, key));
  				return (NbtWrapper<?>) compound;
  			}
  			
  		} else {
  			// We need to know
  			if (type == NbtType.TAG_END) {
  				throw new IllegalArgumentException("Cannot find encoded type of " + decoded[0] + " in " + name);
  			}
  
  			if (node instanceof List) {
  				NbtList<Object> list = NbtFactory.ofList(decoded[0]);
  				list.setElementType(type);
  				
  				for (Object value : (List<Object>) node) {
  					list.addClosest(toNodeValue(value, type));
  				}
  				
  				// Add the list
  				return (NbtWrapper<?>) list;
  			
  			} else {
  				// Normal node
  				return NbtFactory.ofWrapper(type, decoded[0], toNodeValue(node, type));
  			}
  		}
  	}
  	
  	private List<String> sortSet(Set<String> unsorted) {
  		// Convert to integers
  		final List<String> sorted = new ArrayList<>(unsorted);
  
  		// Parse the name and sort.
  		sorted.sort((o1, o2) -> Ints.compare(Integer.parseInt(getDecodedName(o1)[0]), Integer.parseInt(getDecodedName(o2)[0])));
  		return sorted;
  	}
  	
  	// Ensure that int arrays are converted to byte arrays
  	private Object fromNodeValue(NbtBase<?> base) {
  		if (base.getType() == NbtType.TAG_INT_ARRAY)
  			return toByteArray((int[]) base.getValue());
  		else
  			return base.getValue();
  	}
  	
  	// Convert them back
  	public Object toNodeValue(Object value, NbtType type) {
  		if (type == NbtType.TAG_INT_ARRAY)
  			return toIntegerArray((byte[]) value);
  		else
  			return value;
  	}
  	
  	/**
  	 * Convert an integer array to an equivalent byte array.
  	 * @param data - the integer array with the data.
  	 * @return An equivalent byte array.
  	 */
  	private static byte[] toByteArray(int[] data) {
          ByteBuffer byteBuffer = ByteBuffer.allocate(data.length * 4);
          IntBuffer intBuffer = byteBuffer.asIntBuffer();
          
          intBuffer.put(data);
          return byteBuffer.array();
  	}
  	
  	/**
  	 * Convert a byte array to the equivalent integer array.
  	 * <p>
  	 * Note that the number of byte elements are only perserved if the byte size is a multiple of four.
  	 * @param data - the byte array to convert.
  	 * @return The equivalent integer array.
  	 */
  	private static int[] toIntegerArray(byte[] data) {
  		IntBuffer source = ByteBuffer.wrap(data).asIntBuffer();
  		IntBuffer copy = IntBuffer.allocate(source.capacity());
  		
  		copy.put(source);
  		return copy.array();
  	}
  	
  	private static String[] getDecodedName(String nodeName) {
  		int delimiter = nodeName.lastIndexOf('$');
  		
  		if (delimiter > 0)
  			return new String[] { nodeName.substring(0, delimiter), nodeName.substring(delimiter + 1) };
  		else
  			return new String[] { nodeName };
  	}
  }
  