public class BiomeNameHelper {
	
	public static final int MAX_REG_ID = 39;
	public static final List<Integer> VANILLA_MUT = Arrays.asList(129, 130, 131, 132, 133, 134, 140, 149, 151, 155, 156, 157, 158, 160, 162, 163, 164, 165, 166, 167);
	
	

	private static Map<String, BiomeGenBase> biomeMap;
	private static Set<BiomeGenBase> biomeSet;
	
	private final static Map<String, String> nameSimplificationMap = new HashMap<String, String>();
	
	static {
		nameSimplificationMap.put("biomesoplenty.biomes", "BOP");
		nameSimplificationMap.put("extrabiomes.module.summa.biome", "EBXL");
		nameSimplificationMap.put("twilightforest.biomes", "TF");
		nameSimplificationMap.put("bwg4.biomes", "BWG4");
		nameSimplificationMap.put("highlands.biome", "HL");
		nameSimplificationMap.put("net.tropicraft.world.biomes", "TROP");
		nameSimplificationMap.put("xolova.blued00r.divinerpg.generation", "DRPG");
	}
	
	//TODO This better - recognize vanilla biomes some other way
	public static boolean isVanilla(BiomeGenBase biome) {
		return biome.biomeID <= MAX_REG_ID || VANILLA_MUT.contains(biome.biomeID);
	}
	
	
	public static String getBiomeName(BiomeGenBase biome) {
		String name = ((isVanilla(biome)) ? "Vanilla" : biome.getClass().getName()) + '.' + biome.biomeName;
		for (Entry<String,String> entry : nameSimplificationMap.entrySet())
			if (name.startsWith(entry.getKey()))
				return entry.getValue() + '.' + biome.biomeName;
		return name;
	}

	public static BiomeGenBase getBiome(String biomeName) {
		return biomeMap.get(biomeName);
	}

	public static void initBiomeMap() {
		if (biomeMap != null)
			return;
		
		biomeMap = new HashMap<String, BiomeGenBase>();
		biomeSet = new HashSet<BiomeGenBase>();

		for (BiomeGenBase biome : BiomeGenBase.getBiomeGenArray()) {
			if (biome != null) {
				biomeMap.put(getBiomeName(biome), biome);
				biomeSet.add(biome);
			}
		}
	}

	public static Set<BiomeGenBase> getAllBiomes() {
		return biomeSet;
	}

	public static Set<String> getAllBiomeNames() {
		return biomeMap.keySet();
	}
} public final class MSCLogFormatter extends Formatter {
	
	public final static List<String> log = new ArrayList<String>();
	
	private String lineSep = System.getProperty("line.separator");
	private SimpleDateFormat dateFormat = new SimpleDateFormat("MM-dd HH:mm:ss");

	public String format(LogRecord record) {
		StringBuilder sb = new StringBuilder();

		sb.append(dateFormat.format(Long.valueOf(record.getMillis())));
		sb.append(" [" + record.getLevel().getLocalizedName() + "] ");
		sb.append(record.getMessage());
		
		log.add(sb.toString());
		
		sb.append(lineSep);
		
		Throwable thr = record.getThrown();

		if (thr != null) {
			StringWriter thrDump = new StringWriter();
			thr.printStackTrace(new PrintWriter(thrDump));
			sb.append(thrDump.toString());
		}

		return sb.toString();
	}
} @Mod(modid = "MSC2", name="Mob Spawn Controls 2", dependencies = "after:*", version=MobSpawnControls.VERSION)
public class MobSpawnControls{

	public static final String VERSION = "1.2.1";
	public static final PacketPipeline DISPATCHER = new PacketPipeline();
	
	@SidedProxy(clientSide = "com.mcf.davidee.msc.forge.ClientProxy", serverSide = "com.mcf.davidee.msc.forge.CommonProxy")
	public static CommonProxy proxy;
	@Instance("MSC2")
	public static MobSpawnControls instance;

	private static FileHandler logHandler = null;
	private static Logger logger = Logger.getLogger("MobSpawnControls");


	public static Logger getLogger() {
		return logger;
	}

	private SpawnConfiguration config, defaultConfig;
	private SpawnFreqTicker ticker;


	@EventHandler 
	public void preInit(FMLPreInitializationEvent event) {
		ModMetadata modMeta = event.getModMetadata();
		modMeta.authorList = Arrays.asList(new String[] { "Davidee" });
		modMeta.autogenerated = false;
		modMeta.credits = "Thanks to Mojang, Forge, and all your support.";
		modMeta.description = "Allows you to control the vanilla spawning system.";
		modMeta.url = "http://www.minecraftforum.net/topic/1909304-/";
	}

	@EventHandler 
	public void load(FMLInitializationEvent event) {
		logger.setLevel(Level.ALL);

		try {
			File logfile = new File(proxy.getMinecraftDirectory(),"MobSpawnControls.log");
			if ((logfile.exists() || logfile.createNewFile()) && logfile.canWrite() && logHandler == null)	{
				logHandler = new FileHandler(logfile.getPath());
				logHandler.setFormatter(new MSCLogFormatter());
				logger.addHandler(logHandler);
			}
		} catch (SecurityException e) {
			e.printStackTrace();
		} catch (IOException e) {
			e.printStackTrace();
		}

		logger.info("Mob Spawn Controls initializing...");

		ticker = new SpawnFreqTicker();
		DISPATCHER.initialize();
	}

	@EventHandler 
	public void modsLoaded(FMLPostInitializationEvent event) {
		BiomeClassLoader.loadBiomeClasses();
		logger.info("Generating default creature type map...");
		MobHelper.populateDefaultMap();
		logger.info("Mapping biomes...");
		BiomeNameHelper.initBiomeMap();
		logger.info("Creating default spawn configuration...");
		defaultConfig = new SpawnConfiguration(new File(new File(proxy.getMinecraftDirectory(),"config"), "default_msc"));
		defaultConfig.load();
		defaultConfig.save();
	}
	
	@SubscribeEvent
	public void onServerTick(ServerTickEvent event) {
		ticker.tick();
	}

	@EventHandler  
	public void serverStarting(FMLServerStartingEvent event) {
		MinecraftServer server = event.getServer();
		WorldServer world = server.worldServerForDimension(0);
		if (world != null && world.getSaveHandler() instanceof SaveHandler){
			logger.info("Creating spawn configuration for World \"" + world.getSaveHandler().getWorldDirectoryName() + "\"");
			config = new SpawnConfiguration(new File(((SaveHandler)world.getSaveHandler()).getWorldDirectory(),"msc"), defaultConfig);
			config.load();
			config.save();
		}
	}

	@EventHandler 
	public void serverStopping(FMLServerStoppingEvent event) {
		logger.info("Unloading spawn configuration");
		MSCLogFormatter.log.clear();
		config = null;
	}

	public SpawnConfiguration getConfigNoThrow() {
		return config;
	}

	public SpawnConfiguration getConfig() throws RuntimeException {
		if (config != null)
			return config;
		throw new RuntimeException("MSC: Null Spawn Config");
	}


} public class ModConfig {

	private final File saveFile;
	private final List<Class<? extends EntityLiving>> entityClasses;

	public final ModContainer container;
	public final ModConfigParser parser;
	public final String fileName, configName;

	private List<BiomeGroup> groups;
	private CreatureTypeMap typeMap;
	private SpawnMap active, _default;
	private boolean readOnly = false;
	private int lineNum;
	
	public int weight, min, max;

	//Constructor called to create the default config
	public ModConfig(ModContainer container, File dir) {
		this.container = container;
		configName = (container == null) ? "Vanilla" : container.getModId();
		fileName = configName.replaceAll("[\\\\/:*?\"<>|]", "");
		saveFile = new File(dir, fileName + ".cfg");
		entityClasses = ModEntityRecognizer.getEntityClasses(container);

		groups = new ArrayList<BiomeGroup>();
		typeMap = new CreatureTypeMap(entityClasses);
		_default = new SpawnMap(entityClasses);
		active = new SpawnMap(_default);
		weight = 8;
		min = max = 4;
		
		parser = new ModConfigParser(this);
	}

	//Constructor called to create the world config
	public ModConfig(ModContainer container, File folder, ModConfig c) {
		this.container = container;
		configName = (container == null) ? "Vanilla" : container.getModId();
		fileName = configName.replaceAll("[\\\\/:*?\"<>|]", "");
		saveFile = new File(folder, fileName + ".cfg");
		entityClasses = c.entityClasses;

		//Copy everything over from the default config
		groups = new ArrayList<BiomeGroup>();
		for (BiomeGroup g : c.groups)
			groups.add(g.clone());
		typeMap = new CreatureTypeMap(c.typeMap);
		_default = new SpawnMap(c.active);
		active = new SpawnMap(_default);
		weight = c.weight;
		min = c.min;
		max = c.max;
		
		parser = new ModConfigParser(this);
	}

	public SpawnMap getSpawnMap() {
		return active;
	}

	public CreatureTypeMap getTypeMap() {
		return typeMap;
	}
	
	public List<BiomeGroup> getBiomeGroups() {
		return groups;
	}

	//Not sure if this will ever be used
	//If so, perhaps reset the groups and creature type map
	//to that of the default container?
	public void reset() {
		typeMap.generateDefault(entityClasses);
		active = new SpawnMap(_default);
	}

	public void save() {
		if (readOnly)
			return;
		BufferedWriter writer = null;
		try {
			writer = new BufferedWriter(new FileWriter(saveFile));
			Utils.writeLine(writer, "# Mob Spawn Controls Configuration for \""
					+ configName + "\"");
			Utils.writeLine(writer, "#");
			Utils.writeLine(writer, "?ReadOnly=" + readOnly);
			Utils.writeLine(writer, "#");
			Utils.writeLine(writer, "?ItemWeight=" + weight);
			Utils.writeLine(writer, "?MinCount=" + min);
			Utils.writeLine(writer, "?MaxCount=" + max);
			Utils.writeLine(writer, "#");
			Utils.writeLine(writer, "# Creature Type Configuration");

			for (EnumCreatureType type : EnumCreatureType.values())
				Utils.writeLine(writer, getTypeString(type));

			Utils.writeLine(writer, getTypeString(null));
			Utils.writeLine(writer, "#");
			Utils.writeLine(writer, "# Biome Groupings");
			//Write groups
			for (BiomeGroup group : groups)
				Utils.writeLine(writer, '&' + group.toString());
			Utils.writeLine(writer, "#");
			Utils.writeLine(writer, "# Spawn Settings");
			Utils.writeLine(writer, "#");
			//Write config
			active.writeConfig(writer, configName + '.');

		} catch (IOException e) {
			MobSpawnControls.getLogger().throwing("ModConfig", "save", e);
		} finally {
			if (writer != null) {
				try {
					writer.close();
				} catch (IOException e) { }
			}
		}
	}

	public void load() {
		if (!saveFile.exists())
			return;

		BufferedReader reader = null;
		try {
			reader = new BufferedReader(new FileReader(saveFile));
			groups.clear();
			active.resetGroupsList();
			lineNum = 1;
			while (reader.ready()) {
				String line = reader.readLine();
				if (!line.startsWith("#")) { //Lines starting with # are comments
					if (line.startsWith("~")) //Type setting
						setTypeSettings(line.substring(1));
					else if (line.startsWith("?")) //Variable setting
						readVariable(line.substring(1));
					else if (line.startsWith("&")) //Group Setting
						readGroup(line.substring(1));
					else //Assume biome setting
						readBiomeSettings(line);
				}
				++lineNum;
			}
		} catch (IOException e) {
			MobSpawnControls.getLogger().throwing("ModConfig", "load", e);
		} finally {
			if (reader != null) {
				try {
					reader.close();
				} catch (IOException e) { }
			}
		}
	}

	//Current groups cannot reference groups that have not yet been read.
	//This is not so much a bug as it is a safety measure, which prevents groups from
	//referencing each other in a chain and causing a recursion crash
	private void readGroup(String line) {
		BiomeGroup group = parser.readBiomeGroup(line);
		
		if (group == null) 
			warn(parser.log.get(0), '&' + line);
		else {
			for (String s : parser.log)
				warn(s, '&' + line);
			
			groups.add(group);
			active.addGroup(group);
		}
	}

	private void readBiomeSettings(String line) {
		parser.readBiomeSettings(line);
		for (String s : parser.log)
			warn(s, line);
	}

	//Should really implement a map for this
	private void readVariable(String line) {
		int index = line.indexOf('=');
		if (index != -1) {
			String var = line.substring(0, index);
			String value = line.substring(index + 1);
			if (var.equalsIgnoreCase("readonly"))
				readOnly = "true".equalsIgnoreCase(value);
			if (var.equalsIgnoreCase("itemweight"))
				weight = Utils.parseIntDMinMax(value, weight, 1, 100);
			if (var.equalsIgnoreCase("mincount"))
				min = Utils.parseIntDMinMax(value, min, 1, 10);
			if (var.equalsIgnoreCase("maxcount")) {
				max = Utils.parseIntDMinMax(value, max, 1, 15);
				if (min > max)
					max = min;
			}
		} else
			warn("Expected character '='", "?" + line);
	}

	//Used to parse Creature Type settings
	private void setTypeSettings(String line) {
		parser.setTypeSettings(line);
		for (String s : parser.log)
			warn(s, '~' + line);
	}

	private void warn(String message, String line) {
		MobSpawnControls.getLogger().warning(message + " in " + fileName + ".cfg:" + lineNum + " \"" + line + "\"");
	}
	
	public BiomeGroup getBiomeGroup(String name) {
		for (BiomeGroup group : groups)
			if (group.getName().equalsIgnoreCase(name))
				return group;
		return null;
	}

	//Used to write creature type config
	private String getTypeString(EnumCreatureType type) {
		String s = "~" + (type == null ? "UNKNOWN" : type) + ":";
		List<String> names = getEntityNames(type);
		for (int i = 0; i < names.size(); ++i) {
			s += names.get(i);
			if (i != names.size() - 1) //Last one does not get a comma
				s += ",";
		}
		return s;
	}

	//Return the simplified Entity name
	public String getEntityName(Class<? extends EntityLiving> cls) {
		String s = (String) EntityList.classToStringMapping.get(cls);
		return (container == null) ? s : s.replace(configName + ".", "");
	}
	
	public Class<? extends EntityLiving> getEntityClass(String s, EnumCreatureType type) {
		Class<? extends EntityLiving> clazz = getEntityClass(s);
		if (clazz != null && typeMap.get(clazz) != type) //Mapped to different type
			clazz = null;
		return clazz;
	}

	@SuppressWarnings({ "rawtypes", "unchecked" })
	public Class<? extends EntityLiving> getEntityClass(String s) {
		//Check for vanilla and Forge-registered entities
		String entityName = s;
		if (container != null)
			entityName = configName + "." + s;

		Class c = (Class) EntityList.stringToClassMapping.get(entityName);
		if (ModEntityRecognizer.isValidEntityClass(c)) {
			if (entityClasses.contains(c)) 
				return c;
		}

		//Check for non Forge-registered entities
		if (container != null) { //Don't check again for vanilla
			c = (Class) EntityList.stringToClassMapping.get(s);
			if (ModEntityRecognizer.isValidEntityClass(c)) {
				if (entityClasses.contains(c)) 
					return c;
			}
		}
		return null;
	}


	//Methods used for networking/gui
	public List<String> getEntityNames(EnumCreatureType type) {
		List<String> strings = new ArrayList<String>();
		List<Class<? extends EntityLiving>> cls = typeMap.getEntitiesOfType(type);
		for (Class<? extends EntityLiving> c : cls)
			strings.add(getEntityName(c));
		Collections.sort(strings);
		return strings;
	}

	public void processGroupCommand(String command) {
		String[] args = command.split(",");
		String id = args[0];
		if (id.equalsIgnoreCase("ren")) { //Rename group
			String old = args[1], newN = args[2];
			for (Iterator<BiomeGroup> it = groups.iterator(); it.hasNext();) {
				BiomeGroup cur = it.next();
				if (cur.getName().equalsIgnoreCase(old)) {
					cur.setName(newN);
					break;
				}
			}
			active.renameGroup(old, newN);
		}
		if (id.equalsIgnoreCase("del")) { //Delete group
			String name = args[1];
			for (Iterator<BiomeGroup> it = groups.iterator(); it.hasNext();)
				if (it.next().getName().equalsIgnoreCase(name)) {
					it.remove();
					break;
				}
			active.removeGroup(name);
		}
		if (id.equalsIgnoreCase("add")) { //Add group
			String name = args[1];
			BiomeGroup group = new BiomeGroup(name);
			groups.add(group);
			active.addGroup(group);
		}
	}

} public class SpawnSettings {

	private File file;
	private boolean masterEnabled, readOnly, canEdit;
	private int[] defaultCaps, spawnCaps;
	private int creatureFreq;

	public SpawnSettings (File folder) {
		file = new File(folder,"Settings.properties");
		defaultCaps = new int[] {EnumCreatureType.monster.getMaxNumberOfCreature(),
				EnumCreatureType.creature.getMaxNumberOfCreature(), 
				EnumCreatureType.ambient.getMaxNumberOfCreature(),
				EnumCreatureType.waterCreature.getMaxNumberOfCreature()
		};
		spawnCaps = Arrays.copyOf(defaultCaps, defaultCaps.length);
		creatureFreq = 400;
		canEdit = true;
	}

	public SpawnSettings(File folder, SpawnSettings other) {
		file = new File(folder,"Settings.properties");
		defaultCaps = Arrays.copyOf(other.spawnCaps, other.spawnCaps.length);
		spawnCaps = Arrays.copyOf(defaultCaps, defaultCaps.length);
		masterEnabled = other.masterEnabled;
		creatureFreq = other.creatureFreq;
		canEdit = other.canEdit;
	}

	public boolean isMasterEnabled() {
		return masterEnabled;
	}
	
	public int getCreatureFrequency() {
		return creatureFreq;
	}
	
	public boolean canEdit() {
		return canEdit;
	}

	public void reset() {
		masterEnabled = false;
		spawnCaps = Arrays.copyOf(defaultCaps, defaultCaps.length);
		for (EnumCreatureType type : EnumCreatureType.values())
			SpawnCapHelper.setSpawnCap(type, spawnCaps[type.ordinal()]);
	}

	public void save() {
		BufferedWriter writer = null;
		if (readOnly)
			return;
		try {
			writer = new BufferedWriter(new FileWriter(file));
			Utils.writeLine(writer,"# Mob Spawn Controls Settings");
			Utils.writeLine(writer,"#");
			Utils.writeLine(writer,"# This file is used to keep track of various MSC settings");
			Utils.writeLine(writer,"#");
			Utils.writeLine(writer,"# readOnly=(true/false)");
			Utils.writeLine(writer,"#");
			Utils.writeLine(writer, "readOnly=" + readOnly);
			Utils.writeLine(writer,"#");
			Utils.writeLine(writer,"# canEditInGame=(true/false)");
			Utils.writeLine(writer,"#");
			Utils.writeLine(writer, "canEditInGame=" + canEdit);
			Utils.writeLine(writer,"#");
			Utils.writeLine(writer,"# masterEnabled=(true/false)");
			Utils.writeLine(writer,"#");
			Utils.writeLine(writer, "masterEnabled=" + masterEnabled);
			Utils.writeLine(writer,"#");
			Utils.writeLine(writer,"# creatureSpawnFrequency=(int)");
			Utils.writeLine(writer,"#");
			Utils.writeLine(writer, "creatureSpawnFrequency=" + creatureFreq);
			Utils.writeLine(writer,"#");
			Utils.writeLine(writer,"# Cap.(monster/creature/waterCreature/ambient)=(int)");
			Utils.writeLine(writer,"#");
			Utils.writeLine(writer,"Cap.monster=" + spawnCaps[0]);
			Utils.writeLine(writer,"Cap.creature=" + spawnCaps[1]);
			Utils.writeLine(writer,"Cap.ambient=" + spawnCaps[2]);
			Utils.writeLine(writer,"Cap.waterCreature=" + spawnCaps[3]);
			Utils.writeLine(writer,"#");
		}
		catch(IOException e){
			MobSpawnControls.getLogger().throwing("SpawnSettings", "save", e);
		}
		finally{
			if (writer != null){
				try { writer.close(); }
				catch(IOException e){ }
			}
		}
	}

	public void load() {
		if (file.exists()){
			BufferedReader reader = null;
			try{
				reader = new BufferedReader(new FileReader(file));
				int num = 1;
				while(reader.ready()){
					String line = reader.readLine();
					if (!line.startsWith("#")){
						int index = line.indexOf('=');
						if (index == -1)
							warn("Expected character '='",num,line);
						else
							setVariable(line.substring(0,index),line.substring(index+1),num);
					}
					++num;
				}
			}
			catch(IOException e){
				MobSpawnControls.getLogger().throwing("SpawnSettings", "load", e);
			}
			finally{
				if (reader != null){
					try { reader.close(); } 
					catch(IOException e) { }
				}
			}
		}
		for (EnumCreatureType type : EnumCreatureType.values())
			SpawnCapHelper.setSpawnCap(type, spawnCaps[type.ordinal()]);
	}

	private void warn(String message, int num, String line) {
		MobSpawnControls.getLogger().warning(message + " in Settings.properties:" + num + " \"" + line + "\"");
	}

	//TODO Perhaps map values?
	private void setVariable(String key, String value, int line) {
		if (key.equalsIgnoreCase("masterenabled"))
			masterEnabled = "true".equalsIgnoreCase(value);
		else if (key.equalsIgnoreCase("readonly"))
			readOnly = "true".equalsIgnoreCase(value);
		else if (key.equalsIgnoreCase("creaturespawnfrequency"))
			creatureFreq = Utils.parseIntDMinMax(value, 400, 1, 400);
		else if (key.equalsIgnoreCase("caneditingame"))
			canEdit = "true".equalsIgnoreCase(value);
		else if (key.startsWith("Cap.")){
			key = key.substring(4);
			EnumCreatureType type = MobHelper.typeOf(key);
			if (type == null)
				warn("Invalid Creature Type \"" + key + "\"", line, "Cap." + key + "=" + value);
			else{
				try {
					spawnCaps[type.ordinal()] = Utils.parseIntWithMinMax(value, 1, 200);
				}
				catch(NumberFormatException e){
					warn(key + " is not a valid integer!", line, "Cap." + key + "=" + value);
				}
			}

		}
		else
			warn("Unknown variable setting \"" + key + "\"", line, key + "="+ value);
	}

	public MSCPacket createPacket() {
		return MSCPacket.getPacket(PacketType.SETTINGS, readOnly, masterEnabled, spawnCaps, creatureFreq);
	}

	public void readPacket(SettingsPacket pkt) {
		readOnly = pkt.readOnly;
		masterEnabled = pkt.masterEnabled;
		spawnCaps = pkt.caps;
		creatureFreq = pkt.creatureFreq;
		for (EnumCreatureType type : EnumCreatureType.values())
			SpawnCapHelper.setSpawnCap(type, spawnCaps[type.ordinal()]);
		save();
	}


} public class BiomeGroup {

	private String name;
	private Set<BiomeOperand> ops;

	public BiomeGroup(String name) {
		this.name = name;
		ops = new LinkedHashSet<BiomeOperand>();
	}

	public BiomeGroup(String name, Set<BiomeOperand> ops) {
		this.name = name;
		this.ops = ops;
	}

	public Set<BiomeGenBase> evaluate() {
		Set<BiomeGenBase> biomes = new HashSet<BiomeGenBase>();
		for (BiomeOperand op : ops) {
			switch (op.operator) {
			case ADD:
			case ALL:
				biomes.addAll(op.getBiomes());
				break;
			case SUBTRACT:
				biomes.removeAll(op.getBiomes());
				break;
			}
		}
		return biomes;
	}

	public String getName() {
		return name;
	}

	public void setName(String name) {
		this.name = name;
	}

	public void setOps(Set<BiomeOperand> ops) {
		this.ops = ops;
	}

	public String toString() {
		String str = name + "=";
		for (Iterator<BiomeOperand> it = ops.iterator(); it.hasNext();) {
			str += it.next().toString();
			if (it.hasNext())
				str += ',';
		}
		return str;
	}

	public BiomeGroup clone() {
		Set<BiomeOperand> newOps = new LinkedHashSet<BiomeOperand>();
		for (BiomeOperand op : ops)
			newOps.add(op.clone());
		return new BiomeGroup(name, newOps);
	}
} public class SpawnMap {

	private List<GroupSetting> groups;
	private Map<BiomeGenBase,BiomeSetting> biomeMap;
	private SpawnList master;

	public SpawnMap(List<Class<? extends EntityLiving>> validClasses) {
		groups = new ArrayList<GroupSetting>();
		biomeMap = new TreeMap<BiomeGenBase,BiomeSetting>(new BiomeComparator());
		master = new SpawnList();

		for (BiomeGenBase biome : BiomeNameHelper.getAllBiomes()){
			if (biome != null)
				biomeMap.put(biome, new BiomeSetting(biome,validClasses));
		}
	}

	public SpawnMap(SpawnMap other) {
		groups = new ArrayList<GroupSetting>();
		biomeMap = new TreeMap<BiomeGenBase,BiomeSetting>(new BiomeComparator());
		master = new SpawnList(other.master);

		for (GroupSetting gs : other.groups)
			groups.add(new GroupSetting(gs));
		for (Entry<BiomeGenBase,BiomeSetting> entry : other.biomeMap.entrySet())
			biomeMap.put(entry.getKey(), new BiomeSetting(entry.getValue()));
	}

	public void evaluate() {
		for (BiomeSetting s : biomeMap.values())
			s.prepareForMerge();
		for (GroupSetting s : groups)
			s.mergeInto(biomeMap);
	}

	public void sync(boolean masterEnabled) {
		if (masterEnabled){
			for (BiomeGenBase biome : biomeMap.keySet()){
				for (EnumCreatureType type : EnumCreatureType.values()){
					BiomeReflector.reflectList(biome, type).addAll(master.getSpawnList(type));
				}
			}
		}
		else{
			for (BiomeSetting s : biomeMap.values())
				s.sync();
		}
	}

	public SpawnList getMasterList() {
		return master;
	}

	public void addGroup(BiomeGroup group) {
		groups.add(new GroupSetting(group));
	}
	
	public MSCPacket getEvaluatedBiomePacket(String mod, String biome) {
		String[][] entities = new String[4][];
		SpawnList spawnList = biomeMap.get(BiomeNameHelper.getBiome(biome)).getActiveList();
		for (EnumCreatureType type : EnumCreatureType.values()) {
			int index = type.ordinal();
			
			List<SpawnListEntry> entries = spawnList.getSpawnList(type);
			entities[index] = new String[entries.size()];
			for (int i = 0; i < entries.size(); ++i)
				entities[index][i] = SpawnList.entryToString(entries.get(i)).replace(mod, "");
			Arrays.sort(entities[index]);
		}
		return MSCPacket.getPacket(PacketType.EVALUATED_BIOME, mod, biome, entities);
	}
	
	public MSCPacket getEvaluatedGroupPacket(String mod, String group) {
		List<String> biomeNames = new ArrayList<String>();
		BiomeGroup biomeGroup = getGroup(group);
		for (BiomeGenBase biome : biomeGroup.evaluate()) 
			biomeNames.add(BiomeNameHelper.getBiomeName(biome));
		Collections.sort(biomeNames);
		return MSCPacket.getPacket(PacketType.EVALUATED_GROUP, mod, group, 
				biomeNames.toArray(new String[0]));
	}
	
	@SuppressWarnings("unchecked")
	public MSCPacket getBiomeSettingPacket(String mod, String biome, ModConfig config) {
		EntityEntry[][] spawning = new EntityEntry[4][];
		String[][] notSpawning = new String[4][];
		SpawnList spawnList =  biome.equalsIgnoreCase("master") ? master : (biome.contains(".") ?
				  biomeMap.get(BiomeNameHelper.getBiome(biome)).getBiomeSettings() : getSettings(biome).getSpawnList());
		
		for (EnumCreatureType type : EnumCreatureType.values()) {
			int index = type.ordinal();
			List<EntityEntry> entries = new ArrayList<EntityEntry>();
			List<String> disabled = new ArrayList<String>();
			
			for (SpawnListEntry e : spawnList.getSpawnList(type)) {
				String entityName = config.getEntityName(e.entityClass);
				entries.add(new EntityEntry(entityName, e.itemWeight, e.minGroupCount, e.maxGroupCount));
			}
			
			for (Class<? extends EntityLiving> c : SpawnList.getDisabledEntities(spawnList.getSpawnList(type), config.getTypeMap().getEntitiesOfType(type))) 
				disabled.add(config.getEntityName(c));
			
			spawning[index] = entries.toArray(new EntityEntry[0]);
			notSpawning[index] = disabled.toArray(new String[0]);
		}
		
		return MSCPacket.getPacket(PacketType.BIOME_SETTING, mod, biome, spawning, notSpawning);
	}

	public MSCPacket getEntitySettingPacket(String mod, String entity, EnumCreatureType type, Class<? extends EntityLiving> entityClass) {
		List<BiomeEntry> spawning = new ArrayList<BiomeEntry>();
		List<String> notSpawning = new ArrayList<String>();

		for (GroupSetting s : groups) {
			SpawnListEntry e = s.getSpawnList().getEntityEntry(entityClass, type);
			if (e == null)
				notSpawning.add(s.group.getName());
			else 
				spawning.add(new BiomeEntry(s.group.getName(), e.itemWeight, e.minGroupCount, e.maxGroupCount));
		}

		for (Entry<BiomeGenBase, BiomeSetting> entry : biomeMap.entrySet()) {
			String biomeName = BiomeNameHelper.getBiomeName(entry.getKey());
			SpawnListEntry e = entry.getValue().getBiomeSettings().getEntityEntry(entityClass, type);
			if (e == null)
				notSpawning.add(biomeName);
			else
				spawning.add(new BiomeEntry(biomeName, e.itemWeight, e.minGroupCount, e.maxGroupCount));
		}

		return MSCPacket.getPacket(PacketType.ENTITY_SETTING, mod, entity, 
				spawning.toArray(new BiomeEntry[0]), notSpawning.toArray(new String[0]));
	}

	public void setEntitySettings(ModConfig config, Class<? extends EntityLiving> entityClass, EnumCreatureType type, BiomeEntry[] entries) {
		for (GroupSetting s : groups)
			s.getSpawnList().removeEntityEntry(entityClass, type);
		for (BiomeSetting s : biomeMap.values())
			s.getBiomeSettings().removeEntityEntry(entityClass, type);
		
		for (BiomeEntry e : entries) {
			SpawnListEntry newEntry = new SpawnListEntry(entityClass, e.weight, e.min, e.max);
			
			if (e.biome.contains(".")) { //Biome 
				BiomeGenBase biome = BiomeNameHelper.getBiome(e.biome);
				biomeMap.get(biome).getBiomeSettingsForEdit().getSpawnList(type).add(newEntry);
			}
			else { //Group
				BiomeGroup group = config.getBiomeGroup(e.biome);
				getSettings(group).getSpawnList().getSpawnList(type).add(newEntry);
			}
		}
	}

	public void setBiomeSection(BiomeGenBase biome, EnumCreatureType type, List<SpawnListEntry> entries) {
		List<SpawnListEntry> list = (biome == null) ? master.getSpawnList(type) : biomeMap.get(biome).getBiomeSettingsForEdit().getSpawnList(type);
		list.clear();
		for (SpawnListEntry entry : entries)
			list.add(entry);
	}

	public void setGroupSection(BiomeGroup group, EnumCreatureType type, List<SpawnListEntry> entries) {
		List<SpawnListEntry> list = getSettings(group).getSpawnList().getSpawnList(type);
		list.clear();
		for (SpawnListEntry entry : entries)
			list.add(entry);
	}

	public void removeGroup(String name) {
		for (Iterator<GroupSetting> it = groups.iterator(); it.hasNext();)
			if (it.next().group.getName().equalsIgnoreCase(name)){
				it.remove();
				break;
			}
	}

	public void renameGroup(String oldN, String newN) {
		for (Iterator<GroupSetting> it = groups.iterator(); it.hasNext(); ) {
			GroupSetting cur = it.next();
			if (cur.group.getName().equalsIgnoreCase(oldN)){
				cur.group.setName(newN);
				break;
			}
		}
	}

	public void parseGroup(String line) {
		int index = line.indexOf('=');
		String group = line.substring(0,index), def = line.substring(index+1);
		Set<BiomeOperand> ops = new LinkedHashSet<BiomeOperand>();
		if (!def.isEmpty()){
			String[] args = def.split(",");
			for (String s : args){
				BiomeOperator op = BiomeOperator.operatorOf(s.charAt(0));
				if (op == BiomeOperator.ALL)
					ops.add(new BiomeOperand());
				else{
					String name = s.substring(1);
					if (name.indexOf('.') != -1)
						ops.add(new BiomeOperand(op,BiomeNameHelper.getBiome(name)));
					else
						ops.add(new BiomeOperand(op,getGroup(name)));
				}
			}
		}
		getGroup(group).setOps(ops);
	}

	private BiomeGroup getGroup(String group) throws IllegalArgumentException {
		for (GroupSetting s : groups)
			if (s.group.getName().equalsIgnoreCase(group))
				return s.group;
		throw new IllegalArgumentException(group + " is not a valid group");
	}

	public void resetGroupsList() {
		groups.clear();
	}

	public void writeConfig(BufferedWriter writer, String textToIgnore) throws IOException {
		for (EnumCreatureType type : EnumCreatureType.values())
			Utils.writeLine(writer,  "Master~" + type + ":" + SpawnList.entriesToString(master.getSpawnList(type),textToIgnore));

		Utils.writeLine(writer,"#");
		for (GroupSetting gs : groups)
			gs.write(writer, textToIgnore);

		Utils.writeLine(writer,"#");
		for (BiomeSetting s : biomeMap.values())
			s.write(writer, textToIgnore);
	}

	private GroupSetting getSettings(BiomeGroup group) {
		for (GroupSetting s : groups)
			if (s.group == group)
				return s;
		MobSpawnControls.getLogger().severe("Could not find group \"" + group + "\"");
		throw new RuntimeException("MSC: Unknown group \"" + group.getName() + "\"");
	}
	
	private GroupSetting getSettings(String group) {
		return getSettings(getGroup(group));
	}

	private static class BiomeComparator implements Comparator<BiomeGenBase> {
		public int compare(BiomeGenBase a, BiomeGenBase b) {
			return BiomeNameHelper.getBiomeName(a).compareTo(BiomeNameHelper.getBiomeName(b));
		}
	}

} public class BiomeListPacket extends MSCPacket{
	
	public boolean evalRequest;
	public String mod;
	public String[] groups, biomes;

	@Override
	public MSCPacket readData(Object... data) {
		evalRequest = (Boolean) data[0];
		mod = (String) data[1];
		groups = (String[]) data[2];
		biomes = (String[]) data[3];
		return this;
	}
	
	@Override
	public void encodeInto(ChannelHandlerContext ctx, ByteBuf to) throws IOException { 
		to.writeBoolean(evalRequest);
		writeString(mod, to);
		writeStringArray(groups, to);
		writeStringArray(biomes, to);
	}

	@Override
	public void decodeFrom(ChannelHandlerContext ctx, ByteBuf from) throws IOException {
		evalRequest = from.readBoolean();
		mod = readString(from);
		groups = readStringArray(from);
		biomes = readStringArray(from);
	}

	@Override
	public void execute(MSCPacketHandler handler, EntityPlayer player) {
		handler.handleBiomeList(this, player);
	}

} public class CreatureTypePacket extends MSCPacket {

	public String mod;
	public String creatureType;
	public String[] mobs;

	@Override
	public MSCPacket readData(Object... data) {
		mod = (String) data[0];
		creatureType = (String) data[1];
		mobs = (String[]) data[2];
		return this;
	}
	
	@Override
	public void encodeInto(ChannelHandlerContext ctx, ByteBuf to) throws IOException { 
		writeString(mod, to);
		writeString(creatureType, to);
		writeStringArray(mobs, to);
	}

	@Override
	public void decodeFrom(ChannelHandlerContext ctx, ByteBuf from) throws IOException {
		mod = readString(from);
		creatureType = readString(from);
		mobs = readStringArray(from);
	}

	@Override
	public void execute(MSCPacketHandler handler, EntityPlayer player) {
		handler.handleCreatureType(this, player);
	}

} public class DebugPacket extends MSCPacket {

	public String[] log;
	
	@Override
	public MSCPacket readData(Object... data) {
		log = (String[]) data[0];
		return this;
	}
	
	@Override
	public void encodeInto(ChannelHandlerContext ctx, ByteBuf to) throws IOException { 
		writeStringArray(log, to);
	}

	@Override
	public void decodeFrom(ChannelHandlerContext ctx, ByteBuf from) throws IOException {
		log = readStringArray(from);
	}

	@Override
	public void execute(MSCPacketHandler handler, EntityPlayer player) {
		handler.handleDebug(this, player);
	}

} public class EntityListPacket extends MSCPacket{

	public String mod;
	public String[][] entities;

	@Override
	public MSCPacket readData(Object... data) {
		mod = (String) data[0];
		entities = (String[][]) data[1];
		return this;
	}
	
	@Override
	public void encodeInto(ChannelHandlerContext ctx, ByteBuf to) throws IOException { 
		writeString(mod, to);
		for (int i = 0; i < 4; ++i)
			writeStringArray(entities[i], to);
	}

	@Override
	public void decodeFrom(ChannelHandlerContext ctx, ByteBuf from) throws IOException {
		mod = readString(from);
		entities = new String[4][];
		for (int i = 0; i < 4; ++i)
			entities[i] = readStringArray(from);
	}

	@Override
	public void execute(MSCPacketHandler handler, EntityPlayer player) {
		handler.handleEntityList(this, player);
	}

} public class GroupsPacket extends MSCPacket {

	public String mod;
	public String[] groups;
	public String[] biomeNames;

	@Override
	public MSCPacket readData(Object... data) {
		mod = (String) data[0];
		groups = (String[]) data[1];
		biomeNames = (String[]) data[2];
		return this;
	}
	
	@Override
	public void encodeInto(ChannelHandlerContext ctx, ByteBuf to) throws IOException { 
		writeString(mod, to);
		writeStringArray(groups, to);
		writeStringArray(biomeNames, to);
	}

	@Override
	public void decodeFrom(ChannelHandlerContext ctx, ByteBuf from) throws IOException {
		mod = readString(from);
		groups = readStringArray(from);
		biomeNames = readStringArray(from);
	}

	@Override
	public void execute(MSCPacketHandler handler, EntityPlayer player) {
		handler.handleGroups(this, player);
	}

} public abstract class MSCPacket extends AbstractPacket {
	
	public static final String PACKET_ID = "msc2";

	public enum PacketType{

		HANDSHAKE(HSPacket.class),
		ACCESS_DENIED(ADPacket.class),
		REQUEST(RequestPacket.class),
		MOD_LIST(ModListPacket.class),
		GROUPS(GroupsPacket.class),
		CREATURE_TYPE(CreatureTypePacket.class),
		BIOME_LIST(BiomeListPacket.class),
		BIOME_SETTING(BiomeSettingPacket.class),
		ENTITY_LIST(EntityListPacket.class),
		ENTITY_SETTING(EntitySettingPacket.class),
		SETTINGS(SettingsPacket.class),
		EVALUATED_BIOME(EvaluatedBiomePacket.class),
		EVALUATED_GROUP(EvaluatedGroupPacket.class),
		DEBUG(DebugPacket.class);

		private Class<? extends MSCPacket> packetType;

		private PacketType(Class<? extends MSCPacket> cls) {
			this.packetType = cls;
		}

		private MSCPacket make() {
			try {
				return packetType.newInstance();
			} catch (Exception e) {
				MobSpawnControls.getLogger().severe("Error during packet creation");
				throw new RuntimeException(e);
			}
		}
		
		public static int ordinalForClass(Class<? extends MSCPacket> cls) throws IllegalArgumentException {
			PacketType[] values = PacketType.values();
			for (int i = 0; i < values.length; ++i)
				if (values[i].packetType == cls)
					return i;
			throw new IllegalArgumentException("Unknown class " + cls);
		}
	}

	public abstract MSCPacket readData(Object... data);

	//Packet creation
	public static MSCPacket getRequestPacket(PacketType type) {
		return getPacket(PacketType.REQUEST, UnsignedBytes.checkedCast(type.ordinal()));
	}

	public static MSCPacket getRequestPacket(PacketType type, String data) {
		return getPacket(PacketType.REQUEST, UnsignedBytes.checkedCast(type.ordinal()), data);
	}

	public static MSCPacket getPacket(PacketType type, Object... data) {
		return type.make().readData(data);
	}
	
	//Helper methods for the PacketPipeline
	public static MSCPacket readPacket(ChannelHandlerContext ctx, ByteBuf payload) throws IOException {
		int type = UnsignedBytes.toInt(payload.readByte());
		PacketType pType = PacketType.values()[type];
		MSCPacket packet = pType.make();
		packet.decodeFrom(ctx, payload.slice());
		return packet;
	}
	
	public static void writePacket(MSCPacket pkt, ChannelHandlerContext ctx, ByteBuf to) throws IOException {
		to.writeByte(UnsignedBytes.checkedCast(PacketType.ordinalForClass(pkt.getClass())));
		pkt.encodeInto(ctx, to);
	}
	
	
	//String utility methods
	public static void writeString(String str, ByteBuf buffer) {
		ByteBufUtils.writeUTF8String(buffer, str);
	}
	
	public static String readString(ByteBuf buffer) {
		return ByteBufUtils.readUTF8String(buffer);
	}

	public static void writeStringArray(String[] arr, ByteBuf to) {
		to.writeInt(arr.length);
		for (String s : arr)
			writeString(s, to);
	}

	public static String[] readStringArray(ByteBuf from) {
		String[] arr = new String[from.readInt()];
		for (int i = 0; i < arr.length; ++i)
			arr[i] = readString(from);
		return arr;
	}
} public enum PacketType{

	HANDSHAKE(HSPacket.class),
	ACCESS_DENIED(ADPacket.class),
	REQUEST(RequestPacket.class),
	MOD_LIST(ModListPacket.class),
	GROUPS(GroupsPacket.class),
	CREATURE_TYPE(CreatureTypePacket.class),
	BIOME_LIST(BiomeListPacket.class),
	BIOME_SETTING(BiomeSettingPacket.class),
	ENTITY_LIST(EntityListPacket.class),
	ENTITY_SETTING(EntitySettingPacket.class),
	SETTINGS(SettingsPacket.class),
	EVALUATED_BIOME(EvaluatedBiomePacket.class),
	EVALUATED_GROUP(EvaluatedGroupPacket.class),
	DEBUG(DebugPacket.class);

	private Class<? extends MSCPacket> packetType;

	private PacketType(Class<? extends MSCPacket> cls) {
		this.packetType = cls;
	}

	private MSCPacket make() {
		try {
			return packetType.newInstance();
		} catch (Exception e) {
			MobSpawnControls.getLogger().severe("Error during packet creation");
			throw new RuntimeException(e);
		}
	}
		
	public static int ordinalForClass(Class<? extends MSCPacket> cls) throws IllegalArgumentException {
		PacketType[] values = PacketType.values();
		for (int i = 0; i < values.length; ++i)
			if (values[i].packetType == cls)
				return i;
		throw new IllegalArgumentException("Unknown class " + cls);
	}
} public class ModListPacket extends MSCPacket {

	public String[] mods;

	@Override
	public MSCPacket readData(Object... data) {
		mods = (String[]) data[0];
		return this;
	}
	
	@Override
	public void encodeInto(ChannelHandlerContext ctx, ByteBuf to) throws IOException { 
		writeStringArray(mods, to);
	}

	@Override
	public void decodeFrom(ChannelHandlerContext ctx, ByteBuf from) throws IOException {
		mods = readStringArray(from);
	}

	@Override
	public void execute(MSCPacketHandler handler, EntityPlayer player) {
		handler.handleModList(this, player);
	}

} public class BiomeSettingPacket extends MSCPacket {

	public String mod;
	public String biome;
	public EntityEntry[][] entries;
	public String[][] disabled;

	@Override
	public MSCPacket readData(Object... data) {
		mod = (String) data[0];
		biome = (String) data[1];
		entries = (EntityEntry[][]) data[2];
		disabled = (String[][]) data[3];
		return this;
	}
	
	@Override
	public void encodeInto(ChannelHandlerContext ctx, ByteBuf to) throws IOException {
		writeString(mod, to);
		writeString(biome, to);
		for (int i = 0; i < 4; ++i) {
			EntityEntry[] entities = entries[i];
			to.writeInt(entities.length);
			
			for (EntityEntry e : entities) {
				writeString(e.entity, to);
				to.writeInt(e.weight);
				to.writeInt(e.min);
				to.writeInt(e.max);
			}
		}
		for (int i = 0; i < 4; ++i)
			writeStringArray(disabled[i], to);
	}

	@Override
	public void decodeFrom(ChannelHandlerContext ctx, ByteBuf from) throws IOException { 
		mod = readString(from);
		biome = readString(from);
		entries = new EntityEntry[4][];
		for (int i = 0; i < 4; ++i) {
			entries[i] = new EntityEntry[from.readInt()];
			for (int j = 0; j < entries[i].length; ++j)
				entries[i][j] = new EntityEntry(readString(from), from.readInt(), from.readInt(), from.readInt());
		}
		disabled = new String[4][];
		for (int i = 0; i < 4; ++i)
			disabled[i] = readStringArray(from);
	}

	public EntityEntry[][] getOrderedEntries() {
		EntityEntry[][] allEntries = new EntityEntry[4][];
		for (int index = 0; index < 4; ++index) {
			EntityEntry[] curEntries = new EntityEntry[entries[index].length + disabled[index].length];
			
			for (int i = 0; i < entries[index].length; ++i)
				curEntries[i] = entries[index][i];
			for (int i = 0; i < disabled[index].length; ++i)
				curEntries[entries[index].length + i] = new EntityEntry(disabled[index][i]);
			Arrays.sort(curEntries, new Comparator<EntityEntry>() {
				public int compare(EntityEntry a, EntityEntry b) {
					return a.entity.compareTo(b.entity);
				}
			});
			allEntries[index] = curEntries;
		}
		return allEntries;
	}

	@Override
	public void execute(MSCPacketHandler handler, EntityPlayer player) {
		handler.handleBiomeSetting(this, player);
	}

	public static class EntityEntry {

		public String entity;
		public int weight;
		public int min;
		public int max;

		public EntityEntry(String entity) {
			this.entity = entity;
			min = 4;
			max = 4;
		}

		public EntityEntry(String entity, int w, int min, int max) {
			this.entity = entity;
			this.weight = w;
			this.min = min;
			this.max = max;
		}

	}

} public static class EntityEntry {

	public String entity;
	public int weight;
	public int min;
	public int max;

	public EntityEntry(String entity) {
		this.entity = entity;
		min = 4;
		max = 4;
	}

	public EntityEntry(String entity, int w, int min, int max) {
		this.entity = entity;
		this.weight = w;
		this.min = min;
		this.max = max;
	}

} public class EntitySettingPacket extends MSCPacket {

	public String mod;
	public String entity;
	public BiomeEntry[] entries;
	public String[] disabled;
	
	@Override
	public MSCPacket readData(Object... data) {
		mod = (String) data[0];
		entity = (String) data[1];
		entries = (BiomeEntry[]) data[2];
		disabled = (String[]) data[3];
		return this;
	}
	
	@Override
	public void encodeInto(ChannelHandlerContext ctx, ByteBuf to) throws IOException { 
		writeString(mod, to);
		writeString(entity, to);
		
		to.writeInt(entries.length);
		for (BiomeEntry e : entries) {
			writeString(e.biome, to);
			to.writeInt(e.weight);
			to.writeInt(e.min);
			to.writeInt(e.max);
		}
		writeStringArray(disabled, to);
	}

	@Override
	public void decodeFrom(ChannelHandlerContext ctx, ByteBuf from) throws IOException { 
		mod = readString(from);
		entity = readString(from);
		
		entries = new BiomeEntry[from.readInt()];
		for (int i = 0; i < entries.length; ++i)
			entries[i] = new BiomeEntry(readString(from), from.readInt(), from.readInt(), from.readInt());
		disabled = readStringArray(from);
	}
	
	public BiomeEntry[] getOrderedEntries() {
		BiomeEntry[] allEntries = new BiomeEntry[entries.length + disabled.length];
		for (int i = 0; i < entries.length; ++i)
			allEntries[i] = entries[i];
		for (int i = 0; i < disabled.length; ++i)
			allEntries[entries.length + i] = new BiomeEntry(disabled[i]);
		Arrays.sort(allEntries, new Comparator<BiomeEntry>() {
			public int compare(BiomeEntry a, BiomeEntry b) {
				return a.biome.compareTo(b.biome);
			}
		});
		return allEntries;
	}

	@Override
	public void execute(MSCPacketHandler handler, EntityPlayer player) {
		handler.handleEntitySetting(this, player);
	}

	public static class BiomeEntry {
		public String biome;
		public int weight;
		public int min;
		public int max;
		
		public BiomeEntry (String biome) {
			this.biome = biome;
			min = 4;
			max = 4;
		}
		
		public BiomeEntry(String biome, int w, int min, int max) {
			this.biome = biome;
			this.weight = w;
			this.min = min;
			this.max = max;
		}
		
	}
} public class EvaluatedBiomePacket extends MSCPacket {
	
	public String mod;
	public String biome;
	public String[][] entities;

	@Override
	public MSCPacket readData(Object... data) {
		mod = (String) data[0];
		biome = (String) data[1];
		entities = (String[][]) data[2];
		return this;
	}
	
	@Override
	public void encodeInto(ChannelHandlerContext ctx, ByteBuf to) throws IOException { 
		writeString(mod, to);
		writeString(biome, to);
		for (int i = 0; i < 4; ++i)
			writeStringArray(entities[i], to);
	}

	@Override
	public void decodeFrom(ChannelHandlerContext ctx, ByteBuf from) throws IOException { 
		mod = readString(from);
		biome = readString(from);
		entities = new String[4][];
		for (int i = 0; i < 4; ++i)
			entities[i] = readStringArray(from);
	}
	
	@Override
	public void execute(MSCPacketHandler handler, EntityPlayer player) {
		handler.handleEvaluatedBiome(this, player);
	}

} public class EvaluatedGroupPacket extends MSCPacket {
	
	public String mod;
	public String group;
	public String[] biomes;
	
	@Override
	public MSCPacket readData(Object... data) {
		mod = (String) data[0];
		group = (String) data[1];
		biomes = (String[]) data[2];
		return this;
	}
	
	@Override
	public void encodeInto(ChannelHandlerContext ctx, ByteBuf to) throws IOException {
		writeString(mod, to);
		writeString(group, to);
		writeStringArray(biomes, to);
	}

	@Override
	public void decodeFrom(ChannelHandlerContext ctx, ByteBuf from) throws IOException { 
		mod = readString(from);
		group = readString(from);
		biomes = readStringArray(from);
	}
	
	@Override
	public void execute(MSCPacketHandler handler, EntityPlayer player) {
		handler.handleEvaluatedGroup(this, player);
	}
	
} public class SettingsPacket extends MSCPacket {

	public boolean readOnly;
	public boolean masterEnabled;
	public int[] caps;
	public int creatureFreq;

	@Override
	public MSCPacket readData(Object... data) {
		readOnly = (Boolean) data[0];
		masterEnabled = (Boolean) data[1];
		caps = (int[]) data[2];
		creatureFreq = (Integer) data[3];
		return this;
	}
	
	@Override
	public void encodeInto(ChannelHandlerContext ctx, ByteBuf to) throws IOException { 
		to.writeBoolean(readOnly);
		to.writeBoolean(masterEnabled);
		for (int i = 0; i < caps.length; ++i)
			to.writeInt(caps[i]);
		to.writeInt(creatureFreq);
	}

	@Override
	public void decodeFrom(ChannelHandlerContext ctx, ByteBuf from) throws IOException { 
		readOnly = from.readBoolean();
		masterEnabled = from.readBoolean();
		caps = new int[4];
		for (int i = 0; i < caps.length; ++i)
			caps[i] = from.readInt();
		creatureFreq = from.readInt();
	}

	@Override
	public void execute(MSCPacketHandler handler, EntityPlayer player) {
		handler.handleSettings(this, player);
	}

} public class CreatureTypeMap {
	private Map<Class<? extends EntityLiving>, EnumCreatureType> map;
	
	public CreatureTypeMap() {
		map = new HashMap<Class<? extends EntityLiving>, EnumCreatureType>();
	}

	public CreatureTypeMap(List<Class<? extends EntityLiving>> list) {
		map = new HashMap<Class<? extends EntityLiving>, EnumCreatureType>();
		generateDefault(list);
	}
	
	public CreatureTypeMap(CreatureTypeMap otherMap) {
		map = new HashMap<Class<? extends EntityLiving>, EnumCreatureType>();
		for (Class<? extends EntityLiving> cls : otherMap.map.keySet())
			map.put(cls, otherMap.map.get(cls));
	}

	
	public List<Class<? extends EntityLiving>> getEntitiesOfType(EnumCreatureType type) {
		List<Class<? extends EntityLiving>> list = new ArrayList<Class<? extends EntityLiving>>();
		for (Entry<Class<? extends EntityLiving>, EnumCreatureType> entry : map.entrySet()) 
			if (entry.getValue() == type)
				list.add(entry.getKey());
		return list;
	}

	public void generateDefault(List<Class<? extends EntityLiving>> list) {
		map.clear();
		for (Class<? extends EntityLiving> cls : list) 
			map.put(cls, MobHelper.getDefaultMobType(cls));
	}

	public void setType(EnumCreatureType type, List<Class<? extends EntityLiving>> cls) {
		for (Entry<Class<? extends EntityLiving>, EnumCreatureType> entry : map.entrySet()) 
			if (entry.getValue() == type)
				entry.setValue(null);
		
		for (Class<? extends EntityLiving> c : cls)
			map.put(c, type);
	}
	
	public void set(Class<? extends EntityLiving> cls, EnumCreatureType type) {
		map.put(cls, type);
	}

	public EnumCreatureType get(Class<? extends EntityLiving> cls) {
		return map.get(cls);
	}

} public class MobHelper {
	
	private static CreatureTypeMap defaultMap = new CreatureTypeMap();
	
	@SuppressWarnings({ "rawtypes", "unchecked" })
	public static void populateDefaultMap(){
		for (Class c: (Set<Class>)EntityList.classToStringMapping.keySet()){
			
			if (ModEntityRecognizer.isValidEntityClass(c)){
				boolean found = false;
				outer:
				for (BiomeGenBase biome: BiomeGenBase.getBiomeGenArray()){ 
					if (biome != null){
						for (EnumCreatureType type : EnumCreatureType.values()){ //Inner For
							for (SpawnListEntry entry : BiomeReflector.reflectList(biome, type)){
								if (entry.entityClass == c){
									defaultMap.set(c, type);
									found = true;
									break outer;
								}
							}
						} // End Inner For
					}
					
				} //End Outer For
				
				if (!found)
					defaultMap.set(c, null);
				
			}
			
		}
	}
	
	public static EnumCreatureType getDefaultMobType(Class<? extends EntityLiving> cls) {
		EnumCreatureType type = defaultMap.get(cls);
		if (type == null) {
			if (EntityAmbientCreature.class.isAssignableFrom(cls))
				return EnumCreatureType.ambient;
			if (EntityWaterMob.class.isAssignableFrom(cls))
				return EnumCreatureType.waterCreature;
			if (IMob.class.isAssignableFrom(cls))
				return EnumCreatureType.monster;
			if (EntityCreature.class.isAssignableFrom(cls))
				return EnumCreatureType.creature;
		}
		return type;
	}

	public static EnumCreatureType typeOf(String s) {
		for (EnumCreatureType t : EnumCreatureType.values())
			if (t.toString().equalsIgnoreCase(s))
				return t;
		return null;
	}
}
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import net.minecraft.entity.EntityLiving;
import net.minecraft.entity.EnumCreatureType;
import net.minecraft.entity.player.EntityPlayer;
import net.minecraft.entity.player.EntityPlayerMP;
import net.minecraft.world.biome.BiomeGenBase;
import net.minecraft.world.biome.BiomeGenBase.SpawnListEntry;
import com.mcf.davidee.msc.BiomeNameHelper;
import com.mcf.davidee.msc.MSCLogFormatter;
import com.mcf.davidee.msc.MobSpawnControls;
import com.mcf.davidee.msc.config.ModConfig;
import com.mcf.davidee.msc.config.SpawnSettings;
import com.mcf.davidee.msc.grouping.BiomeGroup;
import com.mcf.davidee.msc.grouping.SpawnMap;
import com.mcf.davidee.msc.packet.BiomeListPacket;
import com.mcf.davidee.msc.packet.CreatureTypePacket;
import com.mcf.davidee.msc.packet.DebugPacket;
import com.mcf.davidee.msc.packet.EntityListPacket;
import com.mcf.davidee.msc.packet.GroupsPacket;
import com.mcf.davidee.msc.packet.MSCPacket;
import com.mcf.davidee.msc.packet.MSCPacket.PacketType;
import com.mcf.davidee.msc.packet.ModListPacket;
import com.mcf.davidee.msc.packet.settings.BiomeSettingPacket;
import com.mcf.davidee.msc.packet.settings.BiomeSettingPacket.EntityEntry;
import com.mcf.davidee.msc.packet.settings.EntitySettingPacket;
import com.mcf.davidee.msc.packet.settings.EvaluatedBiomePacket;
import com.mcf.davidee.msc.packet.settings.EvaluatedGroupPacket;
import com.mcf.davidee.msc.packet.settings.SettingsPacket;
import com.mcf.davidee.msc.spawning.CreatureTypeMap;
import com.mcf.davidee.msc.spawning.MobHelper;
package com.mcf.davidee.msc.network;




public class ServerPacketHandler extends MSCPacketHandler {

	@Override
	public void handleSettings(SettingsPacket pkt, EntityPlayer player) {
		SpawnSettings settings = MobSpawnControls.instance.getConfig().getSettings();
		boolean wasMasterEnabled = settings.isMasterEnabled();
		settings.readPacket(pkt);
		if (wasMasterEnabled != settings.isMasterEnabled()) //Resync 
			MobSpawnControls.instance.getConfig().sync();
	}

	@Override
	protected boolean hasPermission(EntityPlayer player) {
		return MobSpawnControls.instance.getConfig().canPlayerEdit(player);
	}

	public void handleAccessDenied(EntityPlayer player) { }
	public void handleModList(ModListPacket pkt, EntityPlayer player) { }
	public void handleBiomeList(BiomeListPacket pkt, EntityPlayer player) { }
	public void handleEntityList(EntityListPacket pkt, EntityPlayer player) { } 
	public void handleDebug(DebugPacket pkt, EntityPlayer player){ }
	public void handleEvaluatedBiome(EvaluatedBiomePacket packet, EntityPlayer player) { }
	public void handleEvaluatedGroup(EvaluatedGroupPacket packet, EntityPlayer player) { }