/**
 * 
 */
package de.dhbwstuttgart.bytecode.genericsGenerator;

import java.util.ArrayList;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import de.dhbwstuttgart.bytecode.genericsGeneratorTypes.*;
import org.objectweb.asm.Type;

import de.dhbwstuttgart.bytecode.TPHExtractor;
import de.dhbwstuttgart.bytecode.constraint.EqualConstraint;
import de.dhbwstuttgart.bytecode.constraint.ExtendsConstraint;
import de.dhbwstuttgart.bytecode.constraint.TPHConstraint;
import de.dhbwstuttgart.bytecode.constraint.TPHConstraint.Relation;
import de.dhbwstuttgart.bytecode.utilities.ConstraintsFinder;
import de.dhbwstuttgart.bytecode.utilities.MethodAndTPH;
import de.dhbwstuttgart.bytecode.utilities.NameReplacer;

/**
 * @author fayez
 *
 */
public class GenericsGeneratorUtility {
	/**
	 * Returns a list of type placeholders names of a specified method
	 * 
	 * @param methodID The id of the method ReturnTypeMethodName(ParameterTypes)
	 * @param listOfMethodsAndTph List of all methods 
	 * @return a list of type placehoders names
	 */
	public static List<String> getMethodTPHS(String methodID, ArrayList<MethodAndTPH> listOfMethodsAndTph) {
		return listOfMethodsAndTph.stream().filter(mt->mt.getId().equals(methodID)).findAny().get().getTphs();
	}
	
	/**
	 * Returns a reverse constraint of a specified constraint which is given by its left and right 
	 * side
	 * 
	 * @param allCons List of all constraints
	 * @param left The left side of the given constraint
	 * @param right The right side of the given constraint
	 * @return An Optional object which contains the reverse constraint if it is exist
	 */
	public static Optional<TPHConstraint> getReverseConstraint(List<TPHConstraint> allCons, String left, String right) {
		return allCons.stream().filter(c->{
			return c.getRight().equals(left) && c.getLeft().equals(right);
		}).findAny();
	}
	
	/**
	 * Substitutes the old name by the new name in all constraints
	 *  
	 * @param allCons List of all constraints
	 * @param oldName The name to be replaced
	 * @param newName The new name
	 */
	public static void substituteTPH(List<TPHConstraint> allCons, String oldName, String newName) {
		allCons.stream()
		.filter(c->c.getRel()==Relation.EXTENDS)
		.forEach(c->{
			if(c.getLeft().equals(oldName))
				c.setLeft(newName);
			if(c.getRight().equals(oldName))
				c.setRight(newName);
		});
		
	}

	public static List<ConstraintsWithSameLeftSide> findConstraintsWithLeftSameLeftSide(List<TPHConstraint> allCons) {
		// finder looks for constraints that have the same left hand side
		// and put them in a list
		ConstraintsFinder finder = new ConstraintsFinder(allCons);
		List<ConstraintsWithSameLeftSide> foundCons = finder.findConstraints();
		return foundCons;
	}

	public static void simplifyConstraintsWithSameLeftSide(List<ConstraintsWithSameLeftSide> consWithSameLeftSide,
			List<TPHConstraint> allCons, List<String> methodTPHS) {
		List<TPHConstraint> eqCons = new ArrayList<>();

		for (ConstraintsWithSameLeftSide list : consWithSameLeftSide) {
			NameReplacementResult replRes = modifyNames(allCons, methodTPHS, list);
			createEqualConstraintsForNames(replRes.getName(), replRes.getOldNames(),eqCons);
			
			for (TPHConstraint c : allCons) {
				if (c.getRel() == Relation.EQUAL && replRes.getName().equals(c.getRight())) {
					eqCons.add(c);
				}
			}
			updateEqualCons(replRes, eqCons);

			TPHConstraint c = list.getConstraints().get(0);
			allCons.removeAll(list.getConstraints());
			allCons.add(c);
		}
		
	}
	
	public static void simplifyConstraintsWithSameLeftSide(List<ConstraintsWithSameLeftSide> consWithSameLeftSide,
																		  TPHExtractor tphExtractor, List<String> tphsClass, List<TPHConstraint> equalCons) {
		List<TPHConstraint> allCons = tphExtractor.allCons;
		
		for (ConstraintsWithSameLeftSide list : consWithSameLeftSide) {
			if(!stillWithSameLeftSide(list))
				continue;
			NameReplacementResult replRes = modifyNames(allCons, list, tphExtractor.ListOfMethodsAndTph,tphsClass);
			createEqualConstraintsForNames(replRes.getName(), replRes.getOldNames(),equalCons);
			
			for (TPHConstraint c : allCons) {
				if (c.getRel() == Relation.EQUAL && replRes.getName().equals(c.getRight())) {
					equalCons.add(c);
				}
			}
			updateEqualCons(replRes, equalCons);

			TPHConstraint c = list.getConstraints().get(0);
			allCons.removeAll(list.getConstraints());
			removeConstraintsWithSameLeftAndRightSide(allCons);
			allCons.add(c);
			removeDuplicate(allCons);
		}
		consWithSameLeftSide.clear();
		consWithSameLeftSide = findConstraintsWithLeftSameLeftSide(allCons);
		if(!consWithSameLeftSide.isEmpty())
			simplifyConstraintsWithSameLeftSide(consWithSameLeftSide,tphExtractor,tphsClass,equalCons);
		
	}

	private static void removeDuplicate(List<TPHConstraint> allCons) {
		List<TPHConstraint> visited = new ArrayList<>();
		List<Integer> toRemove = new ArrayList<>();
		checkForDuplicate(allCons, visited, toRemove);
		removeConstraintsByIndex(allCons, toRemove);
	}

	private static void removeConstraintsByIndex(List<TPHConstraint> allCons, List<Integer> toRemove) {
		for(int index : toRemove)
			allCons.remove(index);
	}

	private static void checkForDuplicate(List<TPHConstraint> allCons, List<TPHConstraint> visited, List<Integer> toRemove) {
		for(int i = 0;i<allCons.size();++i){
			markConstraintIfDuplicate(allCons, visited, toRemove, i);
		}
	}

	private static void markConstraintIfDuplicate(List<TPHConstraint> allCons, List<TPHConstraint> visited, List<Integer> toRemove, int i) {
		TPHConstraint constraint = allCons.get(i);
		if(containsInList(visited,constraint.getLeft(),constraint.getRight())){
			toRemove.add(i);
		} else {
			visited.add(constraint);
		}
	}

	private static boolean containsInList(List<TPHConstraint> allConstraints, String left, String right) {
		for(TPHConstraint constraint : allConstraints)
			if(constraint.getLeft().equals(left) && constraint.getRight().equals(right))
				return true;
		return false;
	}

	private static void addNewConstraintsWithSameLeftSide(ConstraintsWithSameLeftSide constraintsWithSameLeftSide, List<TPHConstraint> allCons) {
		allCons.forEach(c->{
			if (constraintsWithSameLeftSide.getConstraints().get(0).getLeft().equals(c.getLeft())){
				if(!constraintsWithSameLeftSide.getConstraints().contains(c))
					constraintsWithSameLeftSide.getConstraints().add(c);
			}
		});
	}

	private static void checkForNewConstraintsWithSameLeftSide(List<ConstraintsWithSameLeftSide> consWithSameLeftSide, List<TPHConstraint> allCons) {
		allCons.forEach(c->{
			Optional<ConstraintsWithSameLeftSide> constraintsWithSameLeftSide = getConstraintsWithSameLeftSide(consWithSameLeftSide, c.getLeft());
			constraintsWithSameLeftSide.ifPresent(cls->{
				if(!cls.getConstraints().contains(c))
					cls.getConstraints().add(c);
			});
		});
	}

	private static Optional<ConstraintsWithSameLeftSide> getConstraintsWithSameLeftSide(List<ConstraintsWithSameLeftSide> consWithSameLeftSide, String left) {
		return consWithSameLeftSide.stream().filter(csl->isContainedInConsWithLeftSide(csl,left)).findAny();
	}

	private static boolean isContainedInConsWithLeftSide(ConstraintsWithSameLeftSide csl, String left) {
		return csl.getConstraints().get(0).getLeft().equals(left);
	}

	private static boolean stillWithSameLeftSide(ConstraintsWithSameLeftSide constraints) {
		removeUnvalidConstraints(constraints);
		return constraints.getConstraints().size()>1;
	}

	private static void removeUnvalidConstraints(ConstraintsWithSameLeftSide constraints) {
		List<TPHConstraint> toRemove = new ArrayList<>();
		constraints.getConstraints().forEach(c->{
			if(c.getLeft().equals(c.getRight()))
				toRemove.add(c);
		});
		if(!toRemove.isEmpty())
			constraints.getConstraints().removeAll(toRemove);
	}

	private static void removeConstraintsWithSameLeftAndRightSide(List<TPHConstraint> allCons) {
		List<TPHConstraint> toRemove = new ArrayList<>();
		allCons.forEach(c->{
			if(c.getLeft().equals(c.getRight()))
				toRemove.add(c);
		});
		allCons.removeAll(toRemove);
	}

	private static void createEqualConstraintsForNames(String name, List<String> equalNames, List<TPHConstraint> eqCons) {
		// create an equal constraint for each value in repres
		for (String eName : equalNames) {
			EqualConstraint ec = new EqualConstraint(eName, name);
			eqCons.add(ec);
		}
	}

	/**
	 * @param allCons
	 * @param methodTPHS
	 * @param list
	 * @return
	 */
	public static NameReplacementResult modifyNames(List<TPHConstraint> allCons, List<String> methodTPHS,
			ConstraintsWithSameLeftSide list) {
		// generate a new name and replace the right hand side for each constraint
		// in list with the new name
		NameReplacer replacer = new NameReplacer(list.getConstraints(), allCons, methodTPHS);
		// new name -> [all old names]
		NameReplacementResult replRes = replacer.replaceNames();
		return replRes;
	}
	
	public static NameReplacementResult modifyNames(List<TPHConstraint> allCons, ConstraintsWithSameLeftSide list, List<MethodAndTPH> methodAndTPHs, List<String> tphsClass) {
		// generate a new name and replace the right hand side for each constraint
		// in list with the new name
		NameReplacer replacer = new NameReplacer(list.getConstraints(), allCons, methodAndTPHs, tphsClass);
		// new name -> [all old names]
		NameReplacementResult replRes = replacer.replaceNames();
		return replRes;
	}
	
	public static void updateEqualCons(NameReplacementResult nameReplacementResult, List<TPHConstraint> eqCons) {
		List<String> oldNames = nameReplacementResult.getOldNames();
		String newName = nameReplacementResult.getName();
		for (TPHConstraint c : eqCons) {
//			if(oldNames.contains(c.getLeft()))
//				c.setLeft(newName);
			if (oldNames.contains(c.getRight()))
				c.setRight(newName);
		}

	}

	public static void modifyRelationForConstraintsInCycle(List<Cycle> cycles) {
		cycles.stream().map(lc->lc.getConstraints()).flatMap(e->e.stream()).forEach(c->c.setRel(Relation.EQUAL));
	}

	public static List<NameReplacementResult> substituteTPHSFormCycle(List<TPHConstraint> allCons, List<Cycle> cycles, TPHExtractor tphExtractor) {
		List<NameReplacementResult> results = new ArrayList<>();
		cycles.forEach(lc->{
			Set<String> names = getNamesFromCycle(lc);
			String newName = names.stream().findFirst().get();
			
			List<String> equalNames = new ArrayList<>(names);
			
			Stream<ArrayList<String>> tphsOfMethods = tphExtractor.ListOfMethodsAndTph.stream().map(m->m.getTphs());
			Stream<ArrayList<String>> localTphsOfMethods = tphExtractor.ListOfMethodsAndTph.stream().map(m->m.getLocalTphs());
			
			replaceOldNames(newName, equalNames, tphsOfMethods);
			replaceOldNames(newName, equalNames, localTphsOfMethods);
			
			NameReplacementResult res = new NameReplacementResult(newName, equalNames);
			results.add(res);
			substituteAll(allCons,names,newName);
		});
		return results;
	}
	
	/**
	 * @param newName
	 * @param names
	 * @param tphsOfMethods
	 */
	public static void replaceOldNames(final String newName, final List<String> names, Stream<ArrayList<String>> tphsOfMethods) {
		tphsOfMethods.forEach(tphsMethod->{
			if(tphsMethod.removeAll(names))
				tphsMethod.add(newName);
		});
	}
	
	public static void substituteAll(List<TPHConstraint> allCons, Set<String> names, String newName) {
		allCons.stream()
		.filter(c-> c.getRel()==Relation.EXTENDS)
		.forEach(c->{
			if(names.contains(c.getLeft())) {
				c.setLeft(newName);
			} else if(names.contains(c.getRight())) {
				c.setRight(newName);
			}
		});
	}

	public static Set<String> getNamesFromCycle(Cycle lc) {
		Set<String> names = new HashSet<>();
		lc.getConstraints().forEach(c->names.add(c.getLeft()));
		return names;
	}

	public static void createResults(List<GenericsGeneratorResult> genericsGeneratorResults,
			List<NameReplacementResult> nameReplacementResults) {
		nameReplacementResults.forEach(n->{
			Set<String> equals = new HashSet<>(n.getOldNames());
			TPHConstraint cons = new ExtendsConstraint(n.getName(), Type.getInternalName(Object.class));
			GenericsGeneratorResult ggRes = new GenericsGeneratorResult(cons, equals);
			genericsGeneratorResults.add(ggRes);
		});
	}

	public static void removeEqualConstraints(List<TPHConstraint> allCons) {
		List<TPHConstraint> equalConstraints = getEqualConstraints(allCons);
		allCons.removeAll(equalConstraints);
	}

	public static List<TPHConstraint> getEqualConstraints(List<TPHConstraint> allCons) {
		return allCons.stream().filter(c->c.getRel()==Relation.EQUAL).collect(Collectors.toList());
	}

	public static List<NameReplacementResult> createNameReplacementResultsFromEqualConstraints(
			List<TPHConstraint> equalCons) {
		List<NameReplacementResult> nameReplacementResults = new ArrayList<>();
		equalCons.forEach(c->{
			if(isInNameReplacementResults(nameReplacementResults,c.getRight())) {
				getNameReplacementByName(nameReplacementResults,c.getRight()).getOldNames().add(c.getLeft());
			} else {
				List<String> oldNames = new ArrayList<>();
				oldNames.add(c.getLeft());
				NameReplacementResult nrRes = new NameReplacementResult(c.getRight(), oldNames);
				nameReplacementResults.add(nrRes);
			}
		});
		return nameReplacementResults;
	}
	
	private static NameReplacementResult getNameReplacementByName(List<NameReplacementResult> nameReplacementResults,
			String name) {
		return nameReplacementResults.stream().filter(n->n.getName().equals(name)).findFirst().get();
	}

	public static boolean isInNameReplacementResults(List<NameReplacementResult> nameReplacementResults,String name){
		if(nameReplacementResults.isEmpty())
			return false;
		return nameReplacementResults.stream().map(r->r.getName()).filter(n->name.equals(n)).findFirst().isPresent();
	}

	public static Optional<TPHConstraint> getConstraintByLeftSide(List<TPHConstraint> allCons, String tph) {
		return allCons.stream().filter(c->c.getLeft().equals(tph)).findFirst();
	}

	public static Optional<TPHConstraint> getConstraintByRightSide(List<TPHConstraint> allCons, String tph) {
		return allCons.stream().filter(c->c.getRight().equals(tph)).findFirst();
	}

	public static boolean isTPHInGenericGeneratorResult(final List<GenericsGeneratorResult> constraints, final String tph) {
		return constraints.stream().filter(g->g.getConstraint().getLeft().equals(tph)).findFirst().isPresent();
	}

	/**
	 * @param allCons
	 * @param tphsMethod
	 * @param tphsClass 
	 * @param visitedTPHs
	 * @param tph
	 * @return
	 */
	public static LinkedList<String> createLinkedListForTPHsInRelation(final List<TPHConstraint> allCons,
			List<String> tphsMethod, List<String> tphsClass, final Set<String> visitedTPHs, String tph) {
		
		final LinkedList<String> tphsInRel = new LinkedList<>();
		List<String> tphsList = new ArrayList<>(tphsMethod);
		tphsList.addAll(tphsClass);
		
		boolean isNextSuperTypeFound = findNextSuperTyp(allCons,tphsList ,visitedTPHs, tph, tphsInRel);
		if(!isNextSuperTypeFound)
			findNextSubType(allCons, tphsList,visitedTPHs, tph, tphsInRel);
		return tphsInRel;
	}

	public static boolean findNextSubType(List<TPHConstraint> allCons, List<String> tphsList, Set<String> visitedTPHs, String tph,
			LinkedList<String> tphsInRel) {
		Optional<TPHConstraint> con = getConstraintByRightSide(allCons, tph);
		while (con.isPresent()) {
			String left = con.get().getLeft();
			String right = con.get().getRight();
			
			addTPHsToListFromLeft(tphsInRel, left, right);
			markAsVisited(visitedTPHs, left);
			
			if(tphsList.contains(left))
				return true;
	
			con = getConstraintByRightSide(allCons, left);
		}
		return false;
	}

	public static void addTPHsToListFromLeft(LinkedList<String> tphsInRel, String left, String right) {
		if (tphsInRel.isEmpty())
			tphsInRel.add(right);
	
		tphsInRel.addFirst(left);
	}

	/**
	 * @param allCons
	 * @param tphsList 
	 * @param visitedTPHs
	 * @param tph
	 * @param tphsInRel
	 * @return
	 */
	public static boolean findNextSuperTyp(final List<TPHConstraint> allCons, List<String> tphsList, final Set<String> visitedTPHs, String tph,
			final LinkedList<String> tphsInRel) {
		Optional<TPHConstraint> con = getConstraintByLeftSide(allCons, tph);
		
		markAsVisited(visitedTPHs, tph);
		
		while (con.isPresent()) {
			String left = con.get().getLeft();
			String right = con.get().getRight();
			addTPHsToList(tphsInRel, left, right);
	
			if(tphsList.contains(right))
				return true;
			
			markAsVisited(visitedTPHs, right);
	
			con = getConstraintByLeftSide(allCons, right);
		}
		return false;
	}

	public static void markAsVisited(Set<String> visitedTPHs, String left) {
		visitedTPHs.add(left);
	}

	/**
	 * @param visitedTPHs
	 * @param right
	 * @param left
	 */
	public static void markAsVisited(final Set<String> visitedTPHs, String left, String right) {
		visitedTPHs.add(left);
		visitedTPHs.add(right);
	}

	/**
	 * @param tphsInRel
	 * @param right
	 * @param left
	 */
	public static void addTPHsToList(final LinkedList<String> tphsInRel, String left, String right) {
		if (tphsInRel.isEmpty())
			tphsInRel.add(left);
		tphsInRel.add(right);
	}

	public static void removeNotLocalTphs(final LinkedList<String> tphsInRel, final List<String> localTphs,
			List<String> tphsClass) {
	
		while (!localTphs.contains(tphsInRel.peek())) {
			tphsInRel.removeFirst();
		}
	
		String last = tphsInRel.peekLast();
		while (!localTphs.contains(last) && !tphsClass.contains(last)) {
			tphsInRel.removeLast();
			last = tphsInRel.peekLast();
		}
	}

	/**
	 * @param nameReplacementResults
	 * @param subType
	 * @return
	 */
	public static Set<String> createEqualSet(final List<NameReplacementResult> nameReplacementResults, String subType) {
		Optional<NameReplacementResult> equals = nameReplacementResults.stream()
				.filter(n -> n.getName().equals(subType)).findFirst();
	
		Set<String> equalSet = equals.isPresent() ? new HashSet<>(equals.get().getOldNames()) : new HashSet<>();
		return equalSet;
	}

	public static String getNextClassTph(List<String> tphsClass, LinkedList<String> tphsInRel) {
		
		for(int i = 1;i<tphsInRel.size();++i) {
			String next = tphsInRel.get(i);
			if(tphsClass.contains(next))
				return next;
		}
		
		return tphsInRel.getLast();
	}

	public static void addTPHsToClassTPHs(final List<GenericsGeneratorResult> constraints, final List<String> tphsClass) {
		
		List<String> lefts = constraints.stream().map(r->r.getConstraint()).map(c->c.getLeft()).collect(Collectors.toList());
		List<String> rights = constraints.stream().map(r->r.getConstraint()).map(c->c.getRight()).collect(Collectors.toList());
		List<String> leftsAndRights = new ArrayList<>(lefts);
		leftsAndRights.addAll(rights);
		List<String> shouldBeClassTphs = leftsAndRights.stream().distinct().collect(Collectors.toList());
		
		for(String tph : shouldBeClassTphs) {
			if(!tphsClass.contains(tph))
				tphsClass.add(tph);
		}
	}

	public static void removeClassTPHsFromMethodTPHs(List<String> tphsClass, TPHExtractor tphExtractor) {
		
		tphExtractor.ListOfMethodsAndTph.stream().map(m->m.getTphs()).forEach(tphs->{
			tphs.removeAll(tphsClass);
		});
	
		tphExtractor.ListOfMethodsAndTph.stream().map(m->m.getLocalTphs()).forEach(tphs->{
			tphs.removeAll(tphsClass);
		});
		
	}

	public static LinkedList<String> createLinkedListForTPHsInRelationClass(List<TPHConstraint> allCons,
			List<String> tphsClass, List<String> methodTPHs, Set<String> visitedTPHs, String tph) {
		final LinkedList<String> tphsInRel = new LinkedList<>();
		boolean isNextSuperTypeFound = findNextSuperTyp(allCons,tphsClass ,visitedTPHs, tph, tphsInRel);
		if(!tphsInRel.isEmpty() && !isNextSuperTypeFound && !methodTPHs.contains(tphsInRel.getLast()))
			findNextSubType(allCons, tphsClass,visitedTPHs, tph, tphsInRel);
		return tphsInRel;
	}

	public static boolean isInResult(String first, List<GenericsGeneratorResult> constraints) {
		return constraints.stream().map(r->r.getConstraint()).filter(c->c.getLeft().equals(first)).findFirst().isPresent();
	}

	public static Optional<GenericsGeneratorResult> getGenericGeneratorResult(List<GenericsGeneratorResult> list,
			String tph) {
		return list.stream().filter(g->g.getConstraint().getLeft().equals(tph)).findFirst();
	}

	public static void removeGenericGeneratorResult(List<GenericsGeneratorResult> genericsGenResults,
			GenericsGeneratorResult ggResult) {
		genericsGenResults.remove(ggResult);
	}

	public static Optional<MethodAndTPH> getMethodAndTphs(ArrayList<MethodAndTPH> listOfMethodsAndTph, String id) {
		return listOfMethodsAndTph.stream().filter(m->m.getId().equals(id)).findFirst();
	}
}
