package me.despawningbone.arithtrain;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.TreeMap;
import java.util.concurrent.ThreadLocalRandom;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * The utility class for expression evaluation and generation.<br>
 * The generator can be instantiated with different configurable values, allowing for flexible expression generation.<br>
 * The evaluator can also be statically accessed in this class, since it does not have configurable values.  
 * <p>
 * <i>Intended for use in the main arithmetic trainer, but third-party programs may also utilize this class as an expression generation and evaluation library if needed.</i>
 * @author despawningbone
 */

public class ExpressionGenerator {
	
	/**
	 * A weighted representation of the operators' weighted chance to appear in generation.
	 * <p>
	 * <b>Note: This is a universal value across all generators!</b>
	 */
	public final static TreeMap<Integer, String> operators = new TreeMap<>();
	
	public int bracketChance, operatorCount, origin, bound;
	
	/**
	 * Instantiates a generator with specifications according to the configuration passed in the parameters.<br> 
	 * 
	 * @param bracketChance chance for brackets to appear (value should be from 0-100)
	 * @param operatorCount how many operators the generated expression will have
	 * @param origin lower bound of the generated numbers in the expressions (inclusive)
	 * @param bound upper bound of the generated numbers in the expressions (inclusive)
	 */
	public ExpressionGenerator(int bracketChance, int operatorCount, int origin, int bound) {
		this.bracketChance = bracketChance;
		this.operatorCount = operatorCount;
		this.origin = origin;
		this.bound = bound;
	}
	
	
	/**
	 * Generates a direct {@link ExpressionGenerator#stepEval(List)} parsable expression along with the steps needed to solve according to the generator object's specifications. <br>
	 * Note that as real number operations can be extremely difficult to calculate mentally, the arithmetic trainer will only generate integers.
	 *   
	 * @return a generated expression string
	 */
	public List<String> generate() {   //generates only integers 
		ThreadLocalRandom ran = ThreadLocalRandom.current(); 
		int startingValue = ran.nextInt(origin, bound);  //start it with a value so it wont be invalid
		String exp = String.valueOf(startingValue);
		for(int i = 0; i < operatorCount; i++) {
			int opnd = ran.nextInt(origin, bound);
			String operator = operators.floorEntry(ran.nextInt(100)).getValue();  //choose one randomly 
			if(ran.nextBoolean()) {  //randomly choose to put in front or behind the old expression
				exp += operator + opnd;
			} else {
				exp = opnd + operator + exp;
			}
			if(ran.nextInt(100) + 1 < bracketChance) {  //add brackets
				exp = "(" + exp + ")";
			}
		}
		
		try { 
			List<String> steps = stepEval(new ArrayList<String>(Arrays.asList(exp)));  //validate if whole expression is valid; might induce performance overhead but its the quickest way to do so
			//no need evaluateWithSteps() coz the generated expression format can be directly parsed without reformatting 
			return steps;  
		} catch (IllegalArgumentException e) {
			return generate();  //generate a new one until theres no divide by zero anymore
		}
	}
	
	/**
	 * Compares the current expression generator with another and determines if the current generator has strictly more difficult specifications than the other.<br>
	 * The comparing criteria is as follows:
	 * <ul>
	 * 	<li>Higher bracket chance</li>
	 * 	<li>Higher operator count</li>
	 * 	<li>Larger numeric range</li>
	 * </ul><p>
	 * All of the above must be true in order for the current generator to be considered harder.<br>
	 * @param gen the generator to be compared with
	 * @return true if the current generator is harder, false otherwise
	 */
	public boolean isHarder(ExpressionGenerator gen) {
		return this.bracketChance >= gen.bracketChance && this.operatorCount >= gen.operatorCount && (this.bound - this.origin) > (gen.bound - gen.origin); 
	}
	
	
	
	/**
	 * Returns a textual representation of the specifications defined in this generator object.<br>
	 * @return an array-like representation of the generator
	 */
	@Override
	public String toString() {  //allow people to get the config values via toString() 
		return "ExpGen{bracket=" + this.bracketChance + ",opCount=" + this.operatorCount + ",range=" + this.origin + "-" + this.bound + "}";
	}
	
	
	/**
	 * Lints and evaluates a string expression, and returns the evaluated result with steps.<br>
	 *   
	 * @param exp the expression to be evaluated
	 * @return an array storing the steps involved in evaluating the result
	 */
	public static List<String> evaluateWithSteps(String exp) {  //wrapper for the stepEval function to make it easier to understand externally, and also acts as an expression  linter
		if(exp == null || Pattern.compile("[^+\\-*/%().0-9E ]").matcher(exp).find()) {   //checking for anything other than the operators supported
			throw new IllegalArgumentException("Invalid expression!");
		}
		
		Pattern pattern = Pattern.compile("\\(([0-9.E]*)\\)|\\((-*[0-9.E]*)\\)"); //remove unnecessary brackets, eg 1-(-6) -> 1--6 so splitting later on will not break
		exp = exp.replaceAll("\\s", "")  //DONE remove all spaces
				.replaceAll("\\)([0-9(])", ")*$1").replaceAll("([0-9)])\\(", "$1*("); //add back *s between brackets so it wont break, but the steps will include it
		Matcher match = pattern.matcher(exp);  
		while(match.find()) {  //to remove multiple brackets if present, eg ((x)) 
			exp = match.replaceAll("$1$2");
			match = pattern.matcher(exp);
		}
		
		return stepEval(new ArrayList<String>(Arrays.asList(exp
				.replaceAll("(?<![0-9)])[+]+", "")  //remove + for positive indicators
				.replaceAll("(?<![0-9)])[-]((-{4})*)-", ""))));  // make consecutive negative signs become one, excluding x--y because it can be x-(-x) 
	}
	
	
	/**
	 * An internal recursive function that evaluates the last expression in the given list step by step until a single numeric value is yielded.<br>
	 * <p>
	 * This method is not expected to be used alone! As a result, it has the private modifier to prevent outside access.<br>
	 * It is strongly recommended that third-party programs should utilize {@link ExpressionGenerator#evaluateWithSteps(String)} instead of accessing this method through reflection.<br>
	 * @param steps a list containing the expression(s)
	 * @return a list containing the expressions including the newly evaluated one
	 */
	private static List<String> stepEval(List<String> steps) {
		String newestExp = steps.get(steps.size() - 1);
		
		if(newestExp.matches("-{0,1}[0-9.]*(?:E-{0,1})?[0-9.]*")) return steps;   //stopping anchor; only case where - sign should exist is at the first char or after E for negative
		
		String[] splits = newestExp.split("(?<=[^0-9.E])(?=(?<![^0-9]-))(?=(?<!^-))|(?=[^0-9.E])(?<=[^E])");   //split with delimiter, aka expressions; second group defines - without preceding number as negative indicator instead of operator, and 3rd group checks the start of line; last group escapes negative E notations (e.g. E-2) 
		boolean isPrior = false, sameLevel = true; int index = splits.length - 1; 
		for(int i = 0; i < splits.length; i++) {
			String split = splits[i];
			if(split.matches("[^0-9.E]*")) {  //if its an operand we dont need to do anything
				if(split.equals("(")) {
					isPrior = false;  //because parentheses preceed any operators, so remove other priorities
					sameLevel = false;
				} else if(!isPrior && (split.equals("*") || split.equals("/") || split.equals("%"))) { //only 3 with elevated priorities
					isPrior = true;
					index = i;
				} else if(!isPrior && (!sameLevel || !(splits[index].equals("+") || splits[index].equals("-"))) && (split.equals("+") || split.equals("-"))) {  //as its not prioritized, only when there is no prioritized operator will it be evaluated; if it is in the same bracket, evaluate the first operator
					index = i;
					sameLevel = true;
				} else if(split.equals(")")) {
					isPrior = true; //because parentheses are of highest priority
				}
			}
			
		}
		
		/*
		 * start eval
		 */
		
		String op = splits[index];
		try {
			double x = Double.parseDouble(splits[index - 1]);
			double y = Double.parseDouble(splits[index + 1]);
			double val = 0;
			switch(op) {
			case "+": val = x + y;
				break;
			case "-": val = x - y;
				break;
			case "*": val = x * y;
				break;
			case "/": val = x / y;
				break;
			case "%": val = x % y;   //apparently in java its remainder, not modulus
				break;
			}
			splits[index - 1] = "";  //wipe the old values
			splits[index + 1] = "";
			splits[index] = String.valueOf(val);  //put the new value in 
			if(index + 2 < splits.length && index - 2 >= 0) {
				if(splits[index + 2].equals(")") && splits[index - 2].equals("(")) {  //throws ArrayIndexOutOfBoundsException theres inbalanced parentheses
					splits[index + 2] = "";  //wipes used parentheses
					splits[index - 2] = ""; 
				}
			}
		} catch (NumberFormatException | ArrayIndexOutOfBoundsException e) {
			throw new IllegalArgumentException("Invalid expression!");  //this should only throw if there is 2 operators at the same place, result reaches infinity or undefined, or if there is unfinished operators at the ends
		}
		
		steps.add(String.join("", splits));
		return stepEval(steps);
	}
	
}
