package StevenDimDoors.mod_pocketDim.dungeon.pack;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Pattern;

import StevenDimDoors.mod_pocketDim.util.BaseConfigurationProcessor;
import StevenDimDoors.mod_pocketDim.util.ConfigurationProcessingException;
import StevenDimDoors.mod_pocketDim.util.WeightedContainer;

import com.google.common.base.CharMatcher;
import com.google.common.base.Splitter;
import com.google.common.primitives.Ints;

public class DungeonPackConfigReader extends BaseConfigurationProcessor<DungeonPackConfig>
{
	private interface ILineProcessor
	{
		public void process(String line, DungeonPackConfig config) throws ConfigurationProcessingException;
	}

	//Note: These constants aren't static so that the memory will be released once
	//we're done using it an instance. These aren't objects that we need to hold
	//onto throughout the lifetime of MC, only at loading time.
	
	private final int CONFIG_VERSION = 1;
	private final int LOOKAHEAD_LIMIT = 1024;
	private final int MAX_PRODUCT_WEIGHT = 10000;
	private final int MIN_PRODUCT_WEIGHT = 1;
	private final int DEFAULT_PRODUCT_WEIGHT = 100;
	private final int MAX_DUNGEON_PACK_WEIGHT = 10000;
	private final int MIN_DUNGEON_PACK_WEIGHT = 1;
	private final int MAX_DUPLICATE_SEARCH_LEVELS = 5;
	private final int MIN_DUPLICATE_SEARCH_LEVELS = 0;
	private final int DEFAULT_DUNGEON_PACK_WEIGHT = 100;
	private final int MAX_CONDITION_LENGTH = 20;
	private final int MAX_PRODUCT_COUNT = MAX_CONDITION_LENGTH;
	private final String COMMENT_MARKER = "##";

	private final Pattern DUNGEON_TYPE_PATTERN = Pattern.compile("[A-Za-z0-9_\\-]{1,20}");

	private final Splitter WHITESPACE_SPLITTER = Splitter.on(CharMatcher.WHITESPACE).omitEmptyStrings();
	private final String SETTING_SEPARATOR = "=";
	private final String RULE_SEPARATOR = "->";
	private final String WEIGHT_SEPARATOR = "#";

	public DungeonPackConfigReader() { }

	@Override
	public DungeonPackConfig readFromStream(InputStream inputStream) throws ConfigurationProcessingException
	{
		BufferedReader reader = null;
		try
		{
			DungeonPackConfig config = new DungeonPackConfig();
			reader = new BufferedReader(new InputStreamReader(inputStream));

			//Check the config format version
			int version = readVersion(reader);
			if (version != CONFIG_VERSION)
			{
				throw new ConfigurationProcessingException("The dungeon pack config has an incompatible version.");
			}

			config.setTypeNames(new ArrayList<String>());
			config.setRules(new ArrayList<DungeonChainRuleDefinition>());

			//Read the dungeon types
			if (findSection("Types", reader))
			{
				processLines(reader, config, new DungeonTypeProcessor());
			}

			//Load default settings
			config.setAllowDuplicatesInChain(true);
			config.setAllowPackChangeIn(true);
			config.setAllowPackChangeOut(true);
			config.setDistortDoorCoordinates(false);
			config.setPackWeight(DEFAULT_DUNGEON_PACK_WEIGHT);
			config.setDuplicateSearchLevels(MIN_DUPLICATE_SEARCH_LEVELS);
			
			//Read the settings section
			if (findSection("Settings", reader))
			{
				processLines(reader, config, new DungeonSettingsParser());
			}

			//Read the rules section
			if (findSection("Rules", reader))
			{
				processLines(reader, config, new RuleDefinitionParser());
			}

			return config;
		}
		catch (ConfigurationProcessingException ex)
		{
			throw ex;
		}
		catch (Exception ex)
		{
			throw new ConfigurationProcessingException("An unexpected error occurred while trying to read the configuration file.", ex);
		}
		finally
		{
			if (reader != null)
			{
				try
				{
					reader.close();
				}
				catch (IOException ex) { }
			}
		}
	}

	private int readVersion(BufferedReader reader) throws ConfigurationProcessingException, IOException
	{
		String firstLine = reader.readLine();
		String[] parts = firstLine.split("\\s", 0);
		Integer version = null;

		if (parts.length == 2 && parts[0].equalsIgnoreCase("version"))
		{
			version = Ints.tryParse(parts[1]);
		}

		if (version == null)
		{
			throw new ConfigurationProcessingException("Could not parse the config format version.");
		}
		return version;
	}

	private void processLines(BufferedReader reader, DungeonPackConfig config, ILineProcessor processor) throws IOException, ConfigurationProcessingException
	{
		String line;

		while (reader.ready())
		{
			reader.mark(LOOKAHEAD_LIMIT);
			line = reader.readLine();
			if (!line.startsWith(COMMENT_MARKER))
			{
				line = line.trim();
				if (line.length() > 0)
				{
					if (line.endsWith(":"))
					{
						//Consider this line a section header, reset the reader to undo consuming it
						reader.reset();
						break;
					}
					else
					{
						processor.process(line, config);
					}
				}
			}
		}
	}

	private boolean findSection(String name, BufferedReader reader) throws IOException, ConfigurationProcessingException
	{
		boolean found = false;
		boolean matched = false;
		String line = null;
		String label = name + ":";

		//Find the next section header
		//Ignore blank lines and comment lines, stop for headers, and throw an exception for anything else
		while (!found && reader.ready())
		{
			reader.mark(LOOKAHEAD_LIMIT);
			line = reader.readLine();
			if (!line.startsWith(COMMENT_MARKER))
			{
				line = line.trim();
				if (line.length() > 0)
				{
					if (line.endsWith(":"))
					{
						//Consider this line a section header
						found = true;
						matched = line.equalsIgnoreCase(label);
					}
					else
					{
						//This line is invalid
						throw new ConfigurationProcessingException("The dungeon pack config has an incorrect line where a section was expected: " + line);
					}
				}
			}
		}

		//Check if the header matches the one we're looking for.
		//If it doesn't match, undo consuming the line so it can be read later.
		if (found && !matched)
		{
			reader.reset();
		}
		return found;
	}

	private class DungeonTypeProcessor implements ILineProcessor
	{
		@Override
		public void process(String line, DungeonPackConfig config) throws ConfigurationProcessingException
		{
			List<String> typeNames = config.getTypeNames();
			
			//Check if the dungeon type has a name that meets our restrictions
			if (DUNGEON_TYPE_PATTERN.matcher(line).matches())
			{
				//Ignore duplicate dungeon types
				line = line.toUpperCase();
				if (!typeNames.contains(line))
				{
					typeNames.add(line);
				}
			}
			else
			{
				throw new ConfigurationProcessingException("The dungeon pack config has a dungeon type with illegal characters in its name: " + line);
			}
		}
	}
	
	private class DungeonSettingsParser implements ILineProcessor
	{
		@Override
		public void process(String line, DungeonPackConfig config) throws ConfigurationProcessingException
		{
			//The various settings that we support will be hardcoded here.
			//In the future, if we get more settings, then this should be
			//refactored to use a more lookup-driven approach.
			
			boolean valid = true;
			String[] settingParts = line.split(SETTING_SEPARATOR, 2);
			if (settingParts.length == 2)
			{
				try
				{
					String name = settingParts[0].trim();
					String value = settingParts[1].trim();
					if (name.equalsIgnoreCase("AllowDuplicatesInChain"))
					{
						config.setAllowDuplicatesInChain(parseBoolean(value));
					}
					else if (name.equalsIgnoreCase("AllowPackChangeOut"))
					{
						config.setAllowPackChangeOut(parseBoolean(value));
					}
					else if (name.equalsIgnoreCase("AllowPackChangeIn"))
					{
						config.setAllowPackChangeIn(parseBoolean(value));
					}
					else if (name.equalsIgnoreCase("DistortDoorCoordinates"))
					{
						config.setDistortDoorCoordinates(parseBoolean(value));
					}
					else if (name.equalsIgnoreCase("PackWeight"))
					{
						int weight = Integer.parseInt(value);
						if (weight >= MIN_DUNGEON_PACK_WEIGHT && weight <= MAX_DUNGEON_PACK_WEIGHT)
						{
							config.setPackWeight(weight);
						}
						else
						{
							valid = false;
						}
					}
					else if (name.equalsIgnoreCase("DuplicateSearchLevels"))
					{
						int levels = Integer.parseInt(value);
						if (levels >= MIN_DUPLICATE_SEARCH_LEVELS && levels <= MAX_DUPLICATE_SEARCH_LEVELS)
						{
							config.setDuplicateSearchLevels(levels);
						}
						else
						{
							valid = false;
						}
					}
					else
					{
						valid = false;
					}
				}
				catch (Exception e)
				{
					valid = false;
				}
			}
			else
			{
				valid = false;
			}
			
			if (!valid)
			{
				throw new ConfigurationProcessingException("The dungeon pack config has an invalid setting: " + line);
			}
		}
	}

	private class RuleDefinitionParser implements ILineProcessor
	{
		@Override
		public void process(String definition, DungeonPackConfig config) throws ConfigurationProcessingException
		{
			String[] ruleParts;
			String[] productParts;
			String ruleCondition;
			String ruleProduct;
			ArrayList<String> condition;
			ArrayList<WeightedContainer<String>> products;
			List<String> typeNames = config.getTypeNames();

			ruleParts = definition.toUpperCase().split(RULE_SEPARATOR, -1);
			if (ruleParts.length != 2)
			{
				throw new ConfigurationProcessingException("The dungeon pack config has an invalid rule: " + definition);
			}

			ruleCondition = ruleParts[0];
			ruleProduct = ruleParts[1];
			condition = new ArrayList<String>();
			products = new ArrayList<WeightedContainer<String>>();

			for (String typeName : WHITESPACE_SPLITTER.split(ruleCondition))
			{
				if (isKnownDungeonType(typeName, typeNames))
				{
					condition.add(typeName);
				}
				else
				{
					throw new ConfigurationProcessingException("The dungeon pack config has a rule condition with an unknown dungeon type: " + typeName);
				}

				if (condition.size() > MAX_CONDITION_LENGTH)
				{
					throw new ConfigurationProcessingException("The dungeon pack config has a rule condition that is too long: " + definition);
				}
			}

			for (String product : WHITESPACE_SPLITTER.split(ruleProduct))
			{
				Integer weight;
				String typeName;

				productParts = product.split(WEIGHT_SEPARATOR, -1);
				if (productParts.length > 2 || productParts.length == 0)
				{
					throw new ConfigurationProcessingException("The dungeon pack config has a rule with an invalid product: " + product);
				}

				typeName = productParts[0];
				if (isKnownDungeonType(typeName, typeNames))
				{
					if (productParts.length > 1)
					{
						weight = Ints.tryParse(productParts[1]);
						if (weight == null  || (weight > MAX_PRODUCT_WEIGHT) || (weight < MIN_PRODUCT_WEIGHT))
						{
							throw new ConfigurationProcessingException("The dungeon pack config has a rule with an invalid product weight: " + product);
						}
					}
					else
					{
						weight = DEFAULT_PRODUCT_WEIGHT;
					}
					products.add(new WeightedContainer<String>(typeName, weight));
				}
				else
				{
					throw new ConfigurationProcessingException("The dungeon pack config has an unknown dungeon type in a rule: " + typeName);
				}

				if (products.size() > MAX_PRODUCT_COUNT)
				{
					throw new ConfigurationProcessingException("The dungeon pack config has a rule with too many products: " + definition);
				}
			}
			if (products.isEmpty())
			{
				throw new ConfigurationProcessingException("The dungeon pack config has a rule with no products: " + definition);
			}
			
			config.getRules().add( new DungeonChainRuleDefinition(condition, products) );
		}
	}

	private static boolean isKnownDungeonType(String typeName, List<String> typeNames)
	{
		return typeName.equals(DungeonType.WILDCARD_TYPE.Name) || typeNames.contains(typeName);
	}

	private static boolean parseBoolean(String value)
	{
		if (value.equalsIgnoreCase("true"))
			return true;
		if (value.equalsIgnoreCase("false"))
			return false;
		throw new IllegalArgumentException("The boolean value must be either \"true\" or \"false\", ignoring case.");
	}

	@Override
	public boolean canWrite()
	{
		return false;
	}

	@Override
	public void writeToStream(OutputStream outputStream, DungeonPackConfig data) throws ConfigurationProcessingException
	{
		throw new UnsupportedOperationException("DungeonPackConfigReader does not support writing.");
	}
}
