package org.geogebra.common.util;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.TreeSet;
import org.geogebra.common.kernel.Construction;
import org.geogebra.common.kernel.Macro;
import org.geogebra.common.kernel.StringTemplate;
import org.geogebra.common.kernel.algos.AlgoMacro;
import org.geogebra.common.kernel.arithmetic.ExpressionNodeEvaluator;
import org.geogebra.common.kernel.arithmetic.ExpressionValue;
import org.geogebra.common.kernel.arithmetic.Inspecting;
import org.geogebra.common.kernel.geos.GeoElement;
import org.geogebra.common.kernel.geos.GeoPoint;
import org.geogebra.common.kernel.geos.Test;
import org.geogebra.common.util.debug.Log;
/**
* @author Christoph
*
*/
public class GeoAssignment extends Assignment {
/**
* Possible values for CheckOperations
*/
public final static String[] CHECK_OPERATIONS = { "==", "AreEqual",
"AreCongruent" };
private static final long TIMEOUT = (long) 1E4;
private String checkOp;
private Inspecting geoInspector;
private Macro macro;
private GeoElement[] solutionObjects;
private int callsToEqual, callsToCheckTypes;
private Test[] inputTypes;
/**
* The possible InputTypes for this Assignment
*/
HashSet<Test> uniqueInputTypes;
private TreeSet<GeoElement> randomizeablePredecessors;
private Construction cons;
/**
* @param macro
* the macro (user defined tool) corresponding to the assignment
*/
public GeoAssignment(Macro macro) {
super(macro.getKernel());
cons = kernel.getConstruction();
this.macro = macro;
inputTypes = macro.getInputTypes();
uniqueInputTypes = new HashSet<Test>(Arrays.asList(inputTypes));
randomizeablePredecessors = new TreeSet<GeoElement>();
checkOp = "AreEqual";
geoInspector = new Inspecting() {
@Override
public boolean check(ExpressionValue v) {
return ((GeoElement) v).isLabelSet()
&& uniqueInputTypes.contains(Test.getSpecificTest(v));
}
};
}
@Override
public Result checkAssignment() {
res = Result.UNKNOWN;
if (isValid()) {
callsToEqual = 0;
callsToCheckTypes = 0;
boolean oldSilentMode = cons.getKernel().isSilentMode();
cons.getKernel().setSilentMode(true);
TreeSet<GeoElement> possibleOutputGeos = new TreeSet<GeoElement>(
Collections.reverseOrder());
// find all possible inputgeos and all outputgeos that match the
// type of
// the macro
TreeSet<GeoElement> sortedSet = cons
.getGeoSetNameDescriptionOrder();
Iterator<GeoElement> it = sortedSet.iterator();
while (it.hasNext()) {
GeoElement geo = it.next();
TreeSet<GeoElement> allPredecessors = geo.getAllPredecessors();
if (!allPredecessors.isEmpty()) {
for (GeoElement macroOut : macro.getMacroOutput()) {
if (macroOut.getClass().equals(geo.getClass())) {
possibleOutputGeos.add(geo);
}
}
}
}
if (macro.getMacroOutput().length > possibleOutputGeos.size()) {
res = Result.WRONG_OUTPUT_TYPE;
} else {
checkCorrectness(possibleOutputGeos);
}
Log.debug("Checking on " + macro.getToolName()
+ " completed. Comparisons of Objects: " + callsToEqual);
Log.debug("Checking on " + macro.getToolName()
+ " completed. Checked types of Objects: "
+ callsToCheckTypes);
cons.getKernel().setSilentMode(oldSilentMode);
}
return res;
}
private void checkCorrectness(TreeSet<GeoElement> possibleOutputGeos) {
PermutationOfGeOElementsUtil outputPermutationUtil = new PermutationOfGeOElementsUtil(
possibleOutputGeos.toArray(new GeoElement[0]),
macro.getMacroOutput().length);
GeoElement[] possibleOutputPermutation = outputPermutationUtil.next();
TreeSet<Result> partRes = new TreeSet<Result>();
long startTime = System.currentTimeMillis();
double macroCons = 0;
while (possibleOutputPermutation != null && res != Result.CORRECT
&& System.currentTimeMillis() < startTime + TIMEOUT) {
if (!areOutputTypesOK(possibleOutputPermutation,
macro.getMacroOutput())) {
possibleOutputPermutation = outputPermutationUtil.next();
continue;
}
TreeSet<GeoElement> possibleInputGeos = getAllPredecessors(
possibleOutputPermutation, geoInspector);
if (possibleInputGeos.size() < macro.getInputTypes().length) {
res = Result.NOT_ENOUGH_INPUTS;
} else {
macroCons += checkPermutationsOfInputs(
possibleOutputPermutation, partRes,
possibleInputGeos);
}
possibleOutputPermutation = outputPermutationUtil.next();
}
Log.debug(macro.getCommandName() + ":"
+ (System.currentTimeMillis() - startTime) + "," + macroCons);
}
private static boolean areOutputTypesOK(
GeoElement[] possibleOutputPermutation,
GeoElement[] macroOutput) {
if (possibleOutputPermutation.length != macroOutput.length) {
return false;
}
for (int i = 0; i < possibleOutputPermutation.length; i++) {
if (!Test.canSet(possibleOutputPermutation[i], macroOutput[i])) {
return false;
}
}
return true;
}
private double checkPermutationsOfInputs(
GeoElement[] possibleOutputPermutation, TreeSet<Result> partRes,
TreeSet<GeoElement> possibleInputGeos) {
boolean isTypeCheckNeeded = uniqueInputTypes.size() > 1;
GeoElement[] input;
PermutationOfGeOElementsUtil inputPermutationUtil = new PermutationOfGeOElementsUtil(
possibleInputGeos.toArray(new GeoElement[0]),
macro.getInputTypes().length);
input = inputPermutationUtil.next();
boolean solutionFound = false;
double ret = 0;
while (input != null && !solutionFound) {
partRes.clear();
if (!isTypeCheckNeeded || areTypesOK(input)) {
double d = macro.getKernel().getApplication()
.getMillisecondTime();
AlgoMacro algoMacro = new AlgoMacro(cons, null, macro, input,
false);
ret += macro.getKernel().getApplication()
.getMillisecondTime() - d;
GeoElement[] macroOutput = algoMacro.getOutput();
for (int i = 0; i < possibleOutputPermutation.length
&& (!partRes.contains(Result.WRONG)); i++) {
checkEqualityOfGeos(input, macroOutput[i],
possibleOutputPermutation, i, partRes);
}
algoMacro.remove();
solutionFound = !partRes.contains(Result.WRONG)
&& !partRes.contains(Result.WRONG_AFTER_RANDOMIZE)
&& partRes.contains(Result.CORRECT);
} else if (res != Result.WRONG_AFTER_RANDOMIZE
&& res != Result.WRONG) {
res = Result.WRONG_INPUT_TYPES;
}
if (partRes.contains(Result.WRONG)
&& res != Result.WRONG_AFTER_RANDOMIZE) {
res = Result.WRONG;
} else if (partRes.contains(Result.WRONG_AFTER_RANDOMIZE)) {
res = Result.WRONG_AFTER_RANDOMIZE;
Log.debug("Objects wrong after Randomize: "
+ toString(possibleOutputPermutation));
Log.debug("Objects used as inputs: " + toString(input));
} else if (partRes.contains(Result.CORRECT)) {
res = Result.CORRECT;
solutionObjects = possibleOutputPermutation;
Log.debug("Objects found to be the Solution: "
+ toString(solutionObjects));
Log.debug("Objects used as inputs: " + toString(input));
}
input = inputPermutationUtil.next();
}
return ret;
}
private void checkEqualityOfGeos(GeoElement[] input, GeoElement macroOutput,
GeoElement possibleOutput[], int i, TreeSet<Result> partRes) {
// TODO Check if we really need to call adjustMoveableOutputs with all
// possibleOutputs ie.the array
boolean mayAdjustMoveableOutputs = adjustMoveableOutputs(macroOutput,
possibleOutput);
if ("AreEqual".equals(checkOp)) {
// GeoElement root = new AlgoAreEqual(cons, macroOutput,
// possibleOutput[i]).getOutput()[0];
//
// AlgoProve algoProve = new AlgoProve(cons, null, root);
partRes.add(macroOutput.isEqual(possibleOutput[i]) ? Result.CORRECT
: Result.WRONG);
// partRes.add(algoProve.getGeoBoolean().getBoolean() ?
// Result.CORRECT
// : Result.WRONG);
} else if ("==".equals(checkOp)) {
partRes.add(ExpressionNodeEvaluator.evalEquals(macro.getKernel(),
macroOutput, possibleOutput[i]).getBoolean()
? Result.CORRECT : Result.WRONG);
} else if ("AreCongruent".equals(checkOp)) {
partRes.add((macroOutput.isCongruent(possibleOutput[i]).boolVal())
? Result.CORRECT : Result.WRONG);
}
callsToEqual++;
int j = 0;
if (partRes.contains(Result.CORRECT)) {
Log.debug("randomizing...");
while (j < input.length
&& !partRes.contains(Result.WRONG_AFTER_RANDOMIZE)) {
if (input[j].isRandomizable()) {
mayAdjustMoveableOutputs = doProbabilisticChecking(input[j],
macroOutput, possibleOutput, i, partRes,
mayAdjustMoveableOutputs);
} else {
input[j].addRandomizablePredecessorsToSet(
randomizeablePredecessors);
for (int k = 0; k < randomizeablePredecessors.size()
&& !partRes.contains(
Result.WRONG_AFTER_RANDOMIZE); k++) {
mayAdjustMoveableOutputs = doProbabilisticChecking(
randomizeablePredecessors.pollFirst(),
macroOutput, possibleOutput, i, partRes,
mayAdjustMoveableOutputs);
}
}
j++;
}
}
}
private boolean doProbabilisticChecking(GeoElement geoToRandomize,
GeoElement macroOutput, GeoElement[] possibleOutput, int i,
TreeSet<Result> partRes, boolean mayAdjustMoveableOutputs) {
boolean mayAdjustMoveableOutputsL = mayAdjustMoveableOutputs;
GeoElement saveInput;
saveInput = geoToRandomize.copy();
geoToRandomize.randomizeForProbabilisticChecking();
geoToRandomize.updateCascade();
if (mayAdjustMoveableOutputs) {
mayAdjustMoveableOutputsL = adjustMoveableOutputs(macroOutput,
possibleOutput);
}
if ("AreEqual".equals(checkOp)) {
partRes.add(macroOutput.isEqual(possibleOutput[i]) ? Result.CORRECT
: Result.WRONG_AFTER_RANDOMIZE);
} else if ("==".equals(checkOp)) {
partRes.add(ExpressionNodeEvaluator.evalEquals(macro.getKernel(),
macroOutput, possibleOutput[i]).getBoolean()
? Result.CORRECT : Result.WRONG_AFTER_RANDOMIZE);
} else if ("AreCongruent".equals(checkOp)) {
partRes.add((macroOutput.isCongruent(possibleOutput[i]).boolVal())
? Result.CORRECT : Result.WRONG_AFTER_RANDOMIZE);
}
callsToEqual++;
geoToRandomize.set(saveInput);
geoToRandomize.updateCascade();
return mayAdjustMoveableOutputsL;
}
/**
* If some macro outputs are moveable (eg. point on path), push them close
* to the corresponding possible outputs (within given path/region
* constraint)
*
* @param macroOutput
* sample macro output
* @param possibleOutput
* possible outputs
* @return whether an output was changeable
*/
private static boolean adjustMoveableOutputs(GeoElement macroOutput,
GeoElement[] possibleOutput) {
boolean ret = false;
AlgoMacro algo = (AlgoMacro) macroOutput.getParentAlgorithm();
int size = algo.getOutputLength();
for (int i = 0; i < size; i++) {
if (algo.isChangeable(algo.getOutput(i))
&& possibleOutput[i] instanceof GeoPoint) {
GeoPoint pt = (GeoPoint) possibleOutput[i];
algo.setCoords((GeoPoint) algo.getOutput(i), pt.getX(),
pt.getY(), pt.getZ());
ret = true;
}
}
return ret;
}
private static TreeSet<GeoElement> getAllPredecessors(
GeoElement[] possibleOutputPermutation, Inspecting geoInspector) {
TreeSet<GeoElement> possibleInputGeos = new TreeSet<GeoElement>();
for (int i = 0; i < possibleOutputPermutation.length; i++) {
possibleOutputPermutation[i].addPredecessorsToSet(possibleInputGeos,
geoInspector);
}
for (int i = 0; i < possibleOutputPermutation.length; i++) {
possibleInputGeos.remove(possibleOutputPermutation[i]);
}
return possibleInputGeos;
}
private boolean areTypesOK(GeoElement[] input) {
boolean typesOK = true; // we assume that types are OK
int k = 0;
while (k < input.length && typesOK) {
callsToCheckTypes++;
if (inputTypes[k].check(input[k])) {
typesOK = true;
} else {
typesOK = false;
}
k++;
}
return typesOK;
}
private static String toString(GeoElement[] elements) {
StringBuilder solObj = new StringBuilder();
for (GeoElement g : elements) {
if (solObj.length() > 0) {
solObj.append(", ");
}
solObj.append(g.toString(StringTemplate.defaultTemplate));
}
return solObj.toString();
}
/**
* @return the icon file name of the user defined tool corresponding to this
* assignment
*/
@Override
public String getIconFileName() {
return macro.getIconFileName();
}
/**
* @return the Name of the Tool corresponding to this Assignment
*/
public String getToolName() {
return macro.getToolName();
}
/**
* @return the user defined tool corresponding to the assignment
*/
public Macro getTool() {
return macro;
}
/*
* (non-Javadoc)
*
* @see org.geogebra.common.util.Assignment#getAssignmentXML()
*/
@Override
public String getAssignmentXML() {
StringBuilder sb = new StringBuilder();
sb.append("\t<assignment toolName=\"");
StringUtil.encodeXML(sb, macro.getToolName());
sb.append("\" commandName=\"");
StringUtil.encodeXML(sb, macro.getCommandName());
sb.append("\" checkOperation=\"");
StringUtil.encodeXML(sb, getCheckOperation());
sb.append("\">\n");
getAssignmentXML(sb);
return sb.toString();
}
/**
* @param newTool
* the Macro which should be used to checking
*/
public void setMacro(Macro newTool) {
macro = newTool;
}
/**
* @return a String representing the operator/method used by this assignment
* to check correctness. One of "==", "AreEqual", "AreCongruent"
*/
public String getCheckOperation() {
return checkOp;
}
/**
* @param checkOp
* The operator/method which should be used for checking this
* Assignment. One of { "==", "AreEqual", "AreCongruent" }
*/
public void setCheckOperation(String checkOp) {
this.checkOp = checkOp;
}
@Override
public Result[] possibleResults() {
return Result.values();
}
@Override
public String getDisplayName() {
return getToolName();
}
@Override
public boolean isValid() {
return kernel.getMacro(getTool().getCommandName()) != null;
}
}
// Eyal Schneider
// http://stackoverflow.com/a/2799190
/**
* Utility Class to permute the array of GeoElements
*
* @author Eyal Schneider, http://stackoverflow.com/a/2799190
* @author Adaption: Christoph Stadlbauer
*/
class PermutationOfGeOElementsUtil {
private GeoElement[] arr;
private int[] permSwappings;
/**
* @param arr
* the Array with the Elements to be permuted
*/
public PermutationOfGeOElementsUtil(GeoElement[] arr) {
this(arr, arr.length);
}
/**
* @param arr
* the Array with the Elements to be permuted
* @param permSize
* the Elements k < arr.length of the array you need to permute
*/
public PermutationOfGeOElementsUtil(GeoElement[] arr, int permSize) {
// this.arr = arr.clone();
this.arr = new GeoElement[arr.length];
System.arraycopy(arr, 0, this.arr, 0, arr.length);
this.permSwappings = new int[permSize];
for (int i = 0; i < permSwappings.length; i++) {
permSwappings[i] = i;
}
}
/**
* @return the next permutation of the array if exists, null otherwise
*/
public GeoElement[] next() {
if (arr == null) {
return null;
}
GeoElement[] res = new GeoElement[permSwappings.length];
System.arraycopy(arr, 0, res, 0, permSwappings.length);
// GeoElement[] res = Arrays.copyOf(arr, permSwappings.length);
// Prepare next
int i = permSwappings.length - 1;
while (i >= 0 && permSwappings[i] == arr.length - 1) {
swap(i, permSwappings[i]); // Undo the swap represented by
// permSwappings[i]
permSwappings[i] = i;
i--;
}
if (i < 0) {
arr = null;
} else {
int prev = permSwappings[i];
swap(i, prev);
int next = prev + 1;
permSwappings[i] = next;
swap(i, next);
}
return res;
}
private void swap(int i, int j) {
GeoElement tmp = arr[i];
arr[i] = arr[j];
arr[j] = tmp;
}
}