/**
 *   This file is part of Skript.
 *
 *  Skript is free software: you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation, either version 3 of the License, or
 *  (at your option) any later version.
 *
 *  Skript is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with Skript.  If not, see <http://www.gnu.org/licenses/>.
 *
 *
 * Copyright 2011-2017 Peter Güttinger and contributors
 */
package ch.njol.skript.aliases;

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Function;

import org.apache.commons.lang.StringUtils;
import org.eclipse.jdt.annotation.Nullable;

import com.google.gson.Gson;

import ch.njol.skript.Skript;
import ch.njol.skript.aliases.AliasesProvider.Variation;
import ch.njol.skript.aliases.AliasesProvider.VariationGroup;
import ch.njol.skript.config.EntryNode;
import ch.njol.skript.config.Node;
import ch.njol.skript.config.SectionNode;
import ch.njol.skript.localization.ArgsMessage;
import ch.njol.skript.localization.Message;
import ch.njol.skript.localization.Noun;
import ch.njol.util.NonNullPair;

/**
 * Parses aliases.
 */
public class AliasesParser {
	
	private static final Message m_empty_name = new Message("aliases.empty name");
	private static final ArgsMessage m_invalid_variation_section = new ArgsMessage("aliases.invalid variation section");
	private static final Message m_unexpected_section = new Message("aliases.unexpected section");
	private static final Message m_useless_variation = new Message("aliases.useless variation");
	private static final ArgsMessage m_not_enough_brackets = new ArgsMessage("aliases.not enough brackets");
	private static final ArgsMessage m_too_many_brackets = new ArgsMessage("aliases.too many brackets");
	private static final ArgsMessage m_unknown_variation = new ArgsMessage("aliases.unknown variation");
	private static final ArgsMessage m_invalid_minecraft_id = new ArgsMessage("aliases.invalid minecraft id");
	private static final Message m_empty_alias = new Message("aliases.empty alias");
	
	/**
	 * Aliases provider, which takes the aliases and variations we parse.
	 */
	protected final AliasesProvider provider;
	
	/**
	 * Contains condition functions to determine when aliases should be loaded.
	 */
	private final Map<String, Function<String,Boolean>> conditions;
	
	public AliasesParser(AliasesProvider provider) {
		this.provider = provider;
		this.conditions = new HashMap<>();
	}
	
	/**
	 * Loads aliases from a section node.
	 * @param root Root section node for us to load.
	 */
	public void load(SectionNode root) {
		Skript.debug("Loading aliases node " + root.getKey() + " from " + root.getConfig().getFileName() + " (" + provider.getAliasCount() + " aliases loaded)");
		//long start = System.currentTimeMillis();
		for (Node node : root) {
			// Get key and make sure it exists
			String key = node.getKey();
			if (key == null) {
				Skript.error(m_empty_name.toString());
				continue;
			}
			
			// Section nodes are for variations
			if (node instanceof SectionNode) {
				VariationGroup vars = loadVariations((SectionNode) node);
				if (vars != null) {
					String groupName = node.getKey();
					assert groupName != null;
					provider.addVariationGroup(groupName, vars);
				} else {
					Skript.error(m_invalid_variation_section.toString(key));
				}
				continue;
			}
			
			// Sanity check
			if (!(node instanceof EntryNode)) {
				Skript.error(m_unexpected_section.toString());
				continue;
			}
			
			// Check for conditions
			if (conditions.containsKey(key)) {
				boolean success = conditions.get(key).apply(((EntryNode) node).getValue());
				if (!success) { // Failure causes ignoring rest in this section node
					Skript.debug("Condition " + key + " was NOT met; not loading more");
					return;
				}
				continue; // Do not interpret this as alias
			}
			
			// Get value (it always exists)
			String value = ((EntryNode) node).getValue();
			
			loadAlias(key, value);
		}
		
		//long time = System.currentTimeMillis() - start;
		//Skript.debug("Finished loading " + root.getKey() + " in " + (time / 1000000) + "ms");
	}
	
	/**
	 * Parses block states from string input to a map.
	 * @param input Block states as used in Vanilla commands.
	 * @return Them put to a map.
	 */
	protected Map<String, String> parseBlockStates(String input) {
		Map<String,String> parsed = new HashMap<>();
		
		int comma;
		int pos = 0;
		while (pos != -1) { // Loop until we don't have more key=value pairs
			comma = input.indexOf(',', pos); // Find where next key starts
			
			// Get key=value as string
			String pair;
			if (comma == -1) {
				pair = input.substring(pos);
				pos = -1;
			} else {
				pair = input.substring(pos, comma);
				pos = comma + 1;
			}
			
			// Split pair to parts, add them to map
			String[] parts = pair.split("=");
			parsed.put(parts[0], parts[1]);
		}
		
		return parsed;
	}
	
	/**
	 * Loads variations from a section node.
	 * @param root Root node for this variation.
	 * @return Group of variations.
	 */
	@Nullable
	protected VariationGroup loadVariations(SectionNode root) {
		String name = root.getKey();
		assert name != null; // Better be so
		if (!name.startsWith("{") || !name.endsWith("}")) {
			// This is not a variation section!
			return null;
		}
		
		VariationGroup vars = new VariationGroup();
		for (Node node : root) {
			String pattern = node.getKey();
			assert pattern != null;
			List<String> keys = parseKeyPattern(pattern);
			Variation var = parseVariation(((EntryNode) node).getValue());
			
			// Put var there for all keys it matches with
			boolean useful = false;
			for (String key : keys) {
				assert key != null;
				if (key.equals("{default}")) {
					key = "";
					useful = true;
				}
				vars.put(key, var);
			}
			
			if (!useful && var.getId() == null && var.getTags().isEmpty() && var.getBlockStates().isEmpty()) {
				// Useless variation, basically
				Skript.warning(m_useless_variation.toString());
			}
		}
		
		return vars;
	}
	
	/**
	 * Parses a single variation from a string.
	 * @param item Raw variation info.
	 * @return Variation instance.
	 */
	protected Variation parseVariation(String item) {
		String trimmed = item.trim();
		assert trimmed != null;
		item = trimmed; // These could mess up following check among other things
		int firstBracket = item.indexOf('{');
		
		String id; // Id or alias
		Map<String, Object> tags;
		if (firstBracket == -1) {
			id = item;
			tags = new HashMap<>();
		} else {
			if (firstBracket == 0) {
				throw new AssertionError("missing space between id and tags in " + item);
			}
			id = item.substring(0, firstBracket - 1);
			String json = item.substring(firstBracket);
			assert json != null;
			tags = provider.parseMojangson(json);
		}
		
		// Separate block state from id
		String typeName;
		Map<String, String> blockStates;
		int stateIndex = id.indexOf('[');
		if (stateIndex != -1) {
			if (stateIndex == 0) {
				throw new AssertionError("missing id or - in " + id);
			}
			typeName = id.substring(0, stateIndex); // Id comes before block state
			String statesInput = id.substring(stateIndex + 1, id.length() - 1);
			assert statesInput != null;
			blockStates = parseBlockStates(statesInput);
		} else { // No block state, just the id
			typeName = id;
			blockStates = new HashMap<>();
		}
		
		// Variations don't always need an id
		if (typeName.equals("-")) {
			typeName = null;
		}
		
		return new Variation(typeName, typeName == null ? -1 : typeName.indexOf('-'), tags, blockStates);
	}
	
	/**
	 * A very simple stack that operates with ints only.
	 */
	private static class IntStack {
		
		/**
		 * Backing array of this stack.
		 */
		private int[] ints;
		
		/**
		 * Current position in the array.
		 */
		private int pos;
		
		public IntStack(int capacity) {
			this.ints = new int[capacity];
			this.pos = 0;
		}
		
		public void push(int value) {
			if (pos == ints.length - 1)
				enlargeArray();
			ints[pos++] = value;
		}
		
		public int pop() {
			return ints[--pos];
		}
		
		public boolean isEmpty() {
			return pos == 0;
		}
		
		private void enlargeArray() {
			int[] newArray = new int[ints.length * 2];
			System.arraycopy(ints, 0, newArray, 0, ints.length);
			this.ints = newArray;
		}

		public void clear() {
			pos = 0;
		}
	}
	
	/**
	 * Parses alias key pattern using some black magic.
	 * @param name Key/name of alias.
	 * @return All strings that match aliases with this pattern.
	 */
	protected List<String> parseKeyPattern(String name) {
		List<String> versions = new ArrayList<>();
		boolean simple = true; // Simple patterns are used as-is
		
		IntStack optionals = new IntStack(4);
		IntStack choices = new IntStack(4);
		for (int i = 0; i < name.length();) {
			int c = name.codePointAt(i);
			if (c == '[') { // Start optional part
				optionals.push(i);
				simple = false;
			} else if (c == '(') { // Start choice part
				choices.push(i);
				simple = false;
			} else if (c == ']') { // End optional part
				int start;
				try {
					start = optionals.pop();
				} catch (ArrayIndexOutOfBoundsException e) {
					Skript.error(m_too_many_brackets.toString(i, ']'));
					return versions;
				}
				versions.addAll(parseKeyPattern(name.substring(0, start) + name.substring(start + 1, i) + name.substring(i + 1)));
				versions.addAll(parseKeyPattern(name.substring(0, start) + name.substring(i + 1)));
			} else if (c == ')') { // End choice part
				int start;
				try {
					start = choices.pop();
				} catch (ArrayIndexOutOfBoundsException e) {
					Skript.error(m_too_many_brackets.toString(i, ')'));
					return versions;
				}
				int optionStart = start;
				int nested = 0;
				for (int j = start + 1; j < i;) {
					c = name.codePointAt(j);
					
					if (c == '(' || c == '[') {
						nested++;
					} else if (c == ')' || c == ']') {
						nested--;
					} else if (c == '|' && nested == 0) {
						versions.addAll(parseKeyPattern(name.substring(0, start) + name.substring(optionStart + 1, j) + name.substring(i + 1)));
						optionStart = j; // Prepare for next option
					}
					
					j += Character.charCount(c);
				}
				assert nested == 0;
				versions.addAll(parseKeyPattern(name.substring(0, start) + name.substring(optionStart + 1, i) + name.substring(i + 1)));
			}
			
			i += Character.charCount(c);
		}
		
		// Make sure all groups were closed
		if (!optionals.isEmpty() || !choices.isEmpty()) {
			int errorStart;
			if (!optionals.isEmpty())
				errorStart = optionals.pop();
			else
				errorStart = choices.pop();
			char errorChar = (char) name.codePointAt(errorStart);
			
			Skript.error(m_not_enough_brackets.toString(errorStart, errorChar));
			optionals.clear();
			choices.clear();
			return versions;
		}

		// If this is a simple name, its needs to be added here
		// (all groups were added earlier)
		if (simple)
			versions.add(name);
		
		return versions;
	}
	
	protected static class PatternSlot {
		
		public final String content;
		
		public PatternSlot(String content) {
			this.content = content;
		}
	}
	
	protected static class VariationSlot extends PatternSlot {
		
		/**
		 * Variation group.
		 */
		public final VariationGroup vars;
		
		private int counter;
		
		public VariationSlot(VariationGroup vars) {
			super("");
			this.vars = vars;
		}
		
		@SuppressWarnings("null")
		public String getName() {
			return vars.keys.get(counter);
		}
		
		@SuppressWarnings("null")
		public Variation getVariation() {
			return vars.values.get(counter);
		}
		
		public boolean increment() {
			counter++;
			if (counter == vars.keys.size()) {
				counter = 0;
				return true;
			}
			return false;
		}
	}
	
	/**
	 * Parses all possible variations from given name.
	 * @param name Name which might contain variations.
	 * @return Map of variations.
	 */
	protected Map<String, Variation> parseKeyVariations(String name) {
		/**
		 * Variation name start.
		 */
		int varStart = -1;
		
		/**
		 * Variation name end.
		 */
		int varEnd = 0;
		
		/**
		 * Variation slots in this name.
		 */
		List<PatternSlot> slots = new ArrayList<>();
		
		// Compute variation slots
		for (int i = 0; i < name.length();) {
			int c = name.codePointAt(i);
			if (c == '{') { // Found variation name start
				varStart = i;
				String part = name.substring(varEnd, i);
				assert part != null;
				slots.add(new PatternSlot(part));
			} else if (c == '}') { // Found variation name end
				if (varStart == -1) { // Or just invalid syntax
					Skript.error(m_not_enough_brackets.toString());
					continue;
				}

				// Extract variation name from full name
				String varName = name.substring(varStart, i + 1);
				assert varName != null;
				// Get variations for that id and hope they exist
				VariationGroup vars = provider.getVariationGroup(varName);
				if (vars == null) {
					Skript.error(m_unknown_variation.toString(varName));
					continue;
				}
				slots.add(new VariationSlot(vars));
				
				// Variation name finished
				varStart = -1;
				varEnd = i + 1;
			}
			
			i += Character.charCount(c);
		}
		
		// Handle last non-variation slot
		String part = name.substring(varEnd);
		assert part != null;
		slots.add(new PatternSlot(part));
		
		if (varStart != -1) { // A variation was not properly finished
			Skript.error(m_not_enough_brackets.toString());
		}
		
		/**
		 * All possible variations by patterns of them.
		 */
		Map<String, Variation> variations = new LinkedHashMap<>();
		
		if (slots.size() == 1) {
			// Fast path: no variations
			PatternSlot slot = slots.get(0);
			if (!(slot instanceof VariationSlot)) {
				variations.put(fixName(name), new Variation(null, -1, Collections.emptyMap(), Collections.emptyMap()));
				return variations;
			}
			// Otherwise we have only one slot, which is variation. Weird, isn't it?
		}
		
		// Create all permutations caused by variations
		while (true) {
			/**
			 * Count of pattern slots in this key pattern.
			 */
			int count = slots.size();
			
			/**
			 * Slot index of currently manipulated variation.
			 */
			int incremented = 0;
			
			/**
			 * This key pattern.
			 */
			StringBuilder pattern = new StringBuilder();
			
			// Variations replace or add to these after each other
			
			/**
			 * Minecraft id. Can be replaced by subsequent variations.
			 */
			String id = null;
			
			/**
			 * Where to insert id of alias that uses this variation.
			 */
			int insertPoint = -1;
			
			/**
			 * Tags by their names. All variations can add and overwrite them.
			 */
			Map<String, Object> tags = new HashMap<>();
			
			/**
			 * Block states. All variations can add and overwrite them.
			 */
			Map<String, String> states = new HashMap<>();
			
			// Construct alias name and variations
			for (int i = 0; i < count; i++) {
				PatternSlot slot = slots.get(i);
				if (slot instanceof VariationSlot) { // A variation
					VariationSlot varSlot = (VariationSlot) slot;
					pattern.append(varSlot.getName());
					Variation var = varSlot.getVariation();
					String varId = var.getId();
					if (varId != null)
						id = varId;
					if (var.getInsertPoint() != -1)
						insertPoint = var.getInsertPoint();
						
					tags.putAll(var.getTags());
					states.putAll(var.getBlockStates());
					
					if (i == incremented) { // This slot is manipulated now
						if (varSlot.increment())
							incremented++; // And it flipped from max to 0 again
					}
				} else { // Just text
					if (i == incremented) // We can't do that
						incremented++; // But perhaps next slot can
					pattern.append(slot.content);
				}
			}
			
			// Put variation to map which we will return
			variations.put(fixName(pattern.toString()), new Variation(id, insertPoint, tags, states));
			
			// Check if we're finished with permutations
			if (incremented == count) {
				break; // Indeed, get out now!
			}
		}
		
		return variations;
	}
	
	/**
	 * Loads an alias with given name (key pattern) and data (material id and tags).
	 * @param name Name of alias.
	 * @param data Data of alias.
	 */
	protected void loadAlias(String name, String data) {
		//Skript.debug("Loading alias: " + name + " = " + data);
		List<String> patterns = parseKeyPattern(name);
		
		// Create all variations now (might need them many times in future)
		Map<String, Variation> variations = new LinkedHashMap<>();
		for (String pattern : patterns) {
			assert pattern != null;
			variations.putAll(parseKeyVariations(pattern));
		}
		
		// Complex list parsing to avoid commas inside tags
		int start = 0; // Start of next substring
		int indexStart = 0; // Start of next comma lookup
		while (start - 1 != data.length()) {
			int comma = data.indexOf(',', indexStart);
			if (comma == -1) { // No more items than this
				if (indexStart == 0) { // Nothing was loaded, so no commas at all
					String item = data.trim();
					assert item != null;
					loadSingleAlias(variations, item);
					break;
				} else {
					comma = data.length();
				}
			}
			
			int bracketOpen = data.indexOf('{', indexStart);
			int bracketClose = data.indexOf('}', bracketOpen);
			if (comma < bracketClose && comma > bracketOpen) {
				// Inside tags, comma lookup goes to end of tags
				indexStart = bracketClose;
				continue;
			}
			
			// Not inside tags, so process the item
			String item = data.substring(start, comma).trim();
			assert item != null;
			loadSingleAlias(variations, item);
			
			// Set up for next item
			start = comma + 1;
			indexStart = start;
		}
	}
	
	/**
	 * Gets singular and plural forms for given name. This might work
	 * slightly differently from {@link Noun#getPlural(String)}, to ensure
	 * it meets specification of aliases.
	 * @param name Name to get forms from.
	 * @return Singular form, plural form.
	 */
	protected NonNullPair<String, String> getAliasPlural(String name) {
		int marker = name.indexOf('¦');
		if (marker == -1) { // No singular/plural forms
			String trimmed = name.trim();
			assert trimmed != null;
			return new NonNullPair<>(trimmed, trimmed);
		}
		int pluralEnd = -1;
		for (int i = marker; i < name.length(); i++) {
			int c = name.codePointAt(i);
			if (Character.isWhitespace(c)) {
				pluralEnd = i;
				break;
			}
			
			i += Character.charCount(c);
		}
		
		// No whitespace after marker, so creating forms is simple
		if (pluralEnd == -1) {
			String singular = name.substring(0, marker);
			String plural = singular + name.substring(marker + 1);
			
			singular = singular.trim();
			plural = plural.trim();
			assert singular != null;
			assert plural != null;
			return new NonNullPair<>(singular, plural);
		}
		
		// Need to stitch both singular and plural together
		String base = name.substring(0, marker);
		String singular = base + name.substring(pluralEnd);
		String plural = base + name.substring(marker + 1);
		
		singular = singular.trim();
		plural = plural.trim();
		assert singular != null;
		assert plural != null;
		return new NonNullPair<>(singular, plural);
	}
	
	protected void loadSingleAlias(Map<String, Variation> variations, String item) {
		Variation base = parseVariation(item); // Share parsing code with variations
		
		for (Map.Entry<String, Variation> entry : variations.entrySet()) {
			String name = entry.getKey();
			assert name != null;
			Variation var = entry.getValue();
			assert var != null;
			Variation merged = base.merge(var);
			
			String id = merged.getId();
			// For null ids, we just spit a warning
			// They should have not gotten this far
			if (id == null) {
				Skript.warning(m_empty_alias.toString());
			} else {
				// Intern id to save some memory
				id = id.toLowerCase().intern();
				assert id != null;
				try {
					// Create singular and plural forms of the alias
					NonNullPair<String, Integer> plain = Noun.stripGender(name, name); // Name without gender and its gender token
					NonNullPair<String, String> forms = getAliasPlural(plain.getFirst()); // Singular and plural forms
					
					// Add alias to provider
					provider.addAlias(new AliasesProvider.AliasName(forms.getFirst(), forms.getSecond(), plain.getSecond()),
							id, merged.getTags(), merged.getBlockStates());
				} catch (InvalidMinecraftIdException e) { // Spit out a more useful error message
					Skript.error(m_invalid_minecraft_id.toString(e.getId()));
				}
			}
		}
	}
	
	/**
	 * Fixes an alias name by trimming it and removing all extraneous spaces
	 * between the words.
	 * @param name Name to be fixed.
	 * @return Name fixed.
	 */
	protected String fixName(String name) {
		String result = StringUtils.normalizeSpace(name);
		
		int i = result.indexOf('¦');
		
		if (i != -1 && Character.isWhitespace(result.codePointBefore(i)))
			result = result.substring(0, i - 1) + result.substring(i);
		return result;
	}
	
	public void registerCondition(String name, Function<String, Boolean> condition) {
		conditions.put(name, condition);
	}
}