package common.world;

import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import java.util.Map.Entry;
import java.util.function.Function;

import common.block.Block;
import common.collect.ImmutableMap;
import common.collect.ImmutableSet;
import common.collect.ImmutableTable;
import common.collect.Iterables;
import common.collect.Maps;
import common.collect.Sets;
import common.collect.StandardTable;
import common.collect.Table;
import common.init.BlockRegistry;
import common.properties.Property;
import common.util.IdMap;

public class State {
	private static final Function<Entry<Property, Comparable>, String> MAP_ENTRY_TO_STRING = 
		new Function<Entry<Property, Comparable>, String>() {
			public String apply(Entry<Property, Comparable> entry) {
				if(entry == null) {
					return "<NULL>";
				}
				else {
					Property prop = entry.getKey();
					return prop.getName() + "=" + prop.getName(entry.getValue());
				}
			}
	};
	private static final Map<String, State> NAME_MAP = Maps.newLinkedHashMap();
	public static final IdMap<State> ID_MAP = new IdMap();

	private final Block block;
	private final ImmutableMap<Property, Comparable> properties;
	private ImmutableTable<Property, Comparable, State> map;
	private ImmutableSet<Property> saved;
	private String id;
	
	private static Set<Property> getSavedProperties(Block block) {
		Set<Property> stored = Sets.newHashSet();
		Map<Property, Comparable> map = Maps.newHashMap();
		for(int z = 0; z < 16; z++) {
			State sub = block.getStateFromMeta(z);
			for(Entry<Property, Comparable> entry : sub.getProperties().entrySet()) {
				if(map.containsKey(entry.getKey())) {
					if(!map.get(entry.getKey()).equals(entry.getValue()))
						stored.add(entry.getKey());
				}
				else {
					map.put(entry.getKey(), entry.getValue());
				}
			}
		}
		for(int z = 0; z < 16; z++) {
			State sub = block.getStateFromMeta(z);
			Map<Property, Comparable> smap = sub.getProperties();
			for(Property prop : map.keySet()) {
				if(!smap.containsKey(prop))
					stored.add(prop);
			}
		}
		return stored;
	}
	
	private static String filterProperties(State state, Set<Property> stored) {
		StringBuilder sb = new StringBuilder(BlockRegistry.getNameFromBlock(state.getBlock()));
		for(Entry<Property, Comparable> entry : state.getProperties().entrySet()) {
			if(stored.contains(entry.getKey()))
				sb.append(',').append(entry.getKey().getName()).append('=').append(entry.getKey().getName(entry.getValue()));
		}
		return sb.toString();
	}

	public static State getState(String name, State def) {
		if(name == null)
			return def;
		State state = NAME_MAP.get(name);
		if(state != null)
			return state;
		int idx = name.indexOf(",");
		Block block = BlockRegistry.REGISTRY.byNameExact(idx >= 0 ? name.substring(0, idx) : name);
		return block != null ? block.getState() : def;
	}

	public State(Block block, ImmutableMap<Property, Comparable> properties) {
		this.block = block;
		this.properties = properties;
	}

	public <T extends Comparable<T>> State cycleProperty(Property<T> property) {
		Collection<T> values = property.getStates();
		T value = this.getValue(property);
		Iterator<T> iter = values.iterator();
		while(iter.hasNext()) {
			if(iter.next().equals(value)) {
				if(iter.hasNext())
					return this.withProperty(property, (T)iter.next());
				return this.withProperty(property, (T)values.iterator().next());
			}
		}
		return this.withProperty(property, (T)iter.next());
	}

	public Collection<Property> getPropertyNames() {
		return Collections.<Property>unmodifiableCollection(this.properties.keySet());
	}
	
	public Collection<Property> getSavedProperties() {
		return this.saved;
	}
	
	public String getId() {
		return this.id;
	}

	public <T extends Comparable<T>> T getValue(Property<T> property) {
		if(!this.properties.containsKey(property)) {
			throw new IllegalArgumentException("Cannot get property " + property + " as it does not exist in " + this.block);
		}
		else {
			return (T)((Comparable)property.getType().cast(this.properties.get(property)));
		}
	}

	public <T extends Comparable<T>, V extends T> State withProperty(Property<T> property, V value) {
		if(!this.properties.containsKey(property)) {
			throw new IllegalArgumentException("Cannot set property " + property + " as it does not exist in " + this.block);
		}
		else if(!property.getStates().contains(value)) {
			throw new IllegalArgumentException("Cannot set property " + property + " to " + value + " on block "
					+ BlockRegistry.getNameFromBlock(this.block) + ", it is not an allowed value");
		}
		else {
			return (State)(this.properties.get(property) == value ? this : (State)this.map.get(property, value));
		}
	}

	public ImmutableMap<Property, Comparable> getProperties() {
		return this.properties;
	}

	public Block getBlock() {
		return this.block;
	}

	public boolean equals(Object obj) {
		return this == obj;
	}

	public int hashCode() {
		return this.properties.hashCode();
	}

	public String toString() {
		StringBuilder sb = new StringBuilder();
		sb.append(BlockRegistry.getNameFromBlock(this.getBlock()));
		if(!this.getProperties().isEmpty()) {
			sb.append("[");
			Iterable<String> iter = Iterables.transform(this.getProperties().entrySet(), MAP_ENTRY_TO_STRING);
			boolean flag = false;
			for(String s : iter) {
				if(flag)
					sb.append(",");
				else
					flag = true;
				sb.append(s);
			}
//			COMMA_JOINER.appendTo(sb, );
			sb.append("]");
		}
		return sb.toString();
	}

	public void buildTable(Map<Map<Property, Comparable>, State> map) {
		if(this.map != null)
			throw new IllegalStateException();
		Table<Property, Comparable, State> table = new StandardTable<Property, Comparable, State>();
		for(Property<? extends Comparable> prop : this.properties.keySet()) {
			for(Comparable value : prop.getStates()) {
				if(value != this.properties.get(prop)) {
					Map<Property, Comparable> props = Maps.<Property, Comparable>newHashMap(this.properties);
					props.put(prop, value);
					table.put(prop, value, map.get(props));
				}
			}
		}
		this.map = ImmutableTable.<Property, Comparable, State>copyOf(table);
	}
	
	public void buildSaved() {
		if(this.saved != null)
			throw new IllegalStateException();
		ID_MAP.put(this, BlockRegistry.getIdFromBlock(this.block) << 4 | this.block.getMetaFromState(this));
		this.saved = ImmutableSet.copyOf(getSavedProperties(this.block));
		NAME_MAP.put(this.id = filterProperties(this, this.saved), this);
	}
}
