package de.dhbwstuttgart.typeinference.unify.model;

import de.dhbwstuttgart.parser.SourceLoc;
import de.dhbwstuttgart.server.packet.dataContainers.ISerializableData;
import de.dhbwstuttgart.server.packet.dataContainers.KeyStorage;
import de.dhbwstuttgart.server.packet.dataContainers.serialized.SerialList;
import de.dhbwstuttgart.server.packet.dataContainers.serialized.SerialMap;
import de.dhbwstuttgart.server.packet.dataContainers.serialized.SerialUUID;
import de.dhbwstuttgart.typeinference.constraints.IConstraintElement;
import de.dhbwstuttgart.typeinference.unify.UnifyContext;

import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;


/**
 * A pair which contains two types and an operator, e.q. (Integer <. a).
 * @author Florian Steurer
 */
public class UnifyPair implements IConstraintElement, ISerializableData {

	private SourceLoc location;

	/**
	 * The type on the left hand side of the pair.
	 */
	private final UnifyType lhs;
	
	/**
	 * The type on the right hand side of the pair.
	 */
	private final UnifyType rhs;
	
	/**
	 * The operator that determines the relation between the left and right hand side type.
	 */
	private PairOperator pairOp;
    
    private boolean undefinedPair = false;
    
    /**
     * Unifier/substitute that generated this pair
     * PL 2018-03-15
     */
    private Set<UnifyPair> substitution;
    
    /**
     * Base on which the the unifier is applied
     * PL 2018-03-15
     */
    private UnifyPair basePair;
    
    /**
     * For pairs a <. Theta generated in the rule reduceTphSup
     * to store the f-Bouned Elements to avoid endless recursion 
     * PL 2018-03-15
     */
    private Set<UnifyType> fBounded = new HashSet<>();
	
	private final int hashCode;
	
	/**
	 * Creates a new instance of the pair.
	 * @param lhs The type on the left hand side of the pair.
	 * @param rhs The type on the right hand side of the pair.
	 * @param op The operator that determines the relation between the left and right hand side type.
	 */
	public UnifyPair(UnifyType lhs, UnifyType rhs, PairOperator op) {
		this.lhs = lhs;
		this.rhs = rhs;
		pairOp = op;
		substitution = new HashSet<>();

		// Caching hashcode
		hashCode = 17 + 31 * lhs.hashCode() + 31 * rhs.hashCode() + 31 * pairOp.hashCode();
	}

	public UnifyPair(UnifyType lhs, UnifyType rhs, PairOperator op, SourceLoc location) {
		this(lhs, rhs, op);
		this.location = location;
	}
	
	public UnifyPair(UnifyType lhs, UnifyType rhs, PairOperator op, Set<UnifyPair> uni, UnifyPair base) {
		this(lhs, rhs, op, uni, base, new HashSet<>());
	}

	public UnifyPair(UnifyType lhs, UnifyType rhs, PairOperator op, Set<UnifyPair> uni, UnifyPair base, Set<UnifyType> fBounded) {
		this(lhs, rhs, op, uni, base, fBounded, null);
	}

	public UnifyPair(UnifyType lhs, UnifyType rhs, PairOperator op, Set<UnifyPair> uni, UnifyPair base, Set<UnifyType> fBounded, SourceLoc location) {
		this.lhs = lhs;
		this.rhs = rhs;
		pairOp = op;
		substitution = uni;
		basePair = base;
		this.location = location;
		this.fBounded = fBounded;

		// Caching hashcode
		hashCode = 17 + 31 * lhs.hashCode() + 31 * rhs.hashCode() + 31 * pairOp.hashCode();
	}

	public SourceLoc getLocation() {
		if (location != null) return location;
		else if (basePair != null) return basePair.getLocation();
		return null;
	}
		
	/**
	 * Returns the type on the left hand side of the pair.
	 */
	public UnifyType getLhsType() {
		return lhs;
	}
	
	/**
	 * Returns the type on the right hand side of the pair.
	 */
	public UnifyType getRhsType() {
		return rhs;
	}
	
	/**
	 * Returns the operator that determines the relation between the left and right hand side type.
	 */
	public PairOperator getPairOp() {
		return pairOp;
	}
	
	public void setPairOp(PairOperator po) {
		pairOp = po;
	}
	
	public void addSubstitutions(Set<UnifyPair> sup) {
		substitution.addAll(sup);
	}
	
	public void setUndefinedPair() {
		undefinedPair = true;
	}
	public Set<UnifyPair> getSubstitution() {
		return new HashSet<>(substitution);
	}
	
	public UnifyPair getBasePair() {
		return basePair;
	}
	public boolean isUndefinedPair() {
		return undefinedPair;
	}
	
	public Set<UnifyPair> getAllSubstitutions () {
    Set<UnifyPair> ret = new HashSet<>(new ArrayList<>(getSubstitution()));
		if (basePair != null) {
			ret.addAll(new ArrayList<>(basePair.getAllSubstitutions()));
		}
		return ret;
	}
	
	public Set<UnifyPair> getThisAndAllBases () {
		Set<UnifyPair> ret = getAllBases();
		ret.add(this);
		return ret;
	}
	public Set<UnifyPair> getAllBases () {
		Set<UnifyPair> ret = new HashSet<>();
		if (basePair != null) {
			ret.add(getBasePair());
			ret.addAll(basePair.getAllBases());
		}
		return ret;
	}
	
	public UnifyPair getGroundBasePair () {
		if (basePair == null) {
			return this;
		}
		if (basePair.getBasePair() == null) {
			return basePair;
		}
		else {
			return basePair.getGroundBasePair();
		}
	}
	
	/**
	 * wenn in einem  Paar bestehend aus 2 Typvariablen eine nicht wildcardtable ist,
	 * so beide auf nicht wildcardtable setzen
	 */
	public void disableCondWildcards() {
		if (lhs instanceof PlaceholderType && rhs instanceof PlaceholderType
			&& (!((PlaceholderType)lhs).isWildcardable() || !((PlaceholderType)rhs).isWildcardable()))
		{
			((PlaceholderType)lhs).disableWildcardtable();
			((PlaceholderType)rhs).disableWildcardtable();
		}
			
	}
	
	public boolean wrongWildcard() {
		return lhs.wrongWildcard() || rhs.wrongWildcard();
	}
	
	public Set<UnifyType> getfBounded() {
		return this.fBounded;
	}
	
	@Override
	public boolean equals(Object obj) {
		if(!(obj instanceof UnifyPair))
			return false;
		
		if(obj.hashCode() != this.hashCode())
			return false;

		UnifyPair other = (UnifyPair) obj;
		
		if (isUndefinedPair()) {
			if (other.getBasePair() != basePair || (other.getBasePair() == null && basePair == null)) { 
				return false;
			}
			
			if (!other.getBasePair().equals(basePair) ||
				!other.getAllSubstitutions().equals(getAllSubstitutions())) {
				return false;
			}
		}

		
		return other.getPairOp() == pairOp 
				&& other.getLhsType().equals(lhs) 
				&& other.getRhsType().equals(rhs);
	}

	@Override
	public int hashCode() {
		return hashCode;
	}
	
	@Override
	public String toString() {
		String ret = "";
		if (lhs instanceof PlaceholderType) {
			ret = Integer.valueOf(((PlaceholderType)lhs).getVariance()).toString() + " "
					+ "WC: " + ((PlaceholderType)lhs).isWildcardable()
					+ ", IT: " + ((PlaceholderType)lhs).isInnerType();
		}
		if (rhs instanceof PlaceholderType) {
			ret = ret + ", " + Integer.valueOf(((PlaceholderType)rhs).getVariance()).toString() + " "
					+ "WC: " + ((PlaceholderType)rhs).isWildcardable()
					+ ", IT: " + ((PlaceholderType)rhs).isInnerType();
		}
		var res =  "(UP: " + lhs + " " + pairOp + " " + rhs + ", " + ret + ")"; //+ ", [" + getfBounded().toString()+ "])";
		var location = this.getLocation();
		if (location != null) {
			res += "@" + location.line() + " in " + location.file();
		}

		return res;
	}

	/*
	public List<? extends PlaceholderType> getInvolvedPlaceholderTypes() {
		ArrayList<PlaceholderType> ret = new ArrayList<>();
		ret.addAll(lhs.getInvolvedPlaceholderTypes());
		ret.addAll(rhs.getInvolvedPlaceholderTypes());
		return ret;
	}
	*/
	private String serialUUID = null;

	public SerialUUID toSerial(KeyStorage keyStorage) {
		final String uuid = serialUUID == null ? keyStorage.getIdentifier() : serialUUID;
		if (serialUUID == null) serialUUID = uuid;

		if (!keyStorage.isAlreadySerialized(uuid)) {
			SerialMap serialized = new SerialMap();
			keyStorage.putSerialized(uuid, serialized);
			serialized.put("lhs", this.lhs.toSerial(keyStorage));
			serialized.put("rhs", this.rhs.toSerial(keyStorage));
			serialized.put("op", this.pairOp.toString());
			serialized.put("basePair", this.basePair == null ? null : this.basePair.toSerial(keyStorage));
			serialized.put("location", this.location == null ? null : this.location.toSerial(keyStorage));
			serialized.put("substitution", SerialList.fromMapped(this.substitution, unifyPair -> unifyPair.toSerial(keyStorage)));
			serialized.put("fBounded", SerialList.fromMapped(this.fBounded, fbounded -> fbounded.toSerial(keyStorage)));
		}
		return new SerialUUID(uuid);
	}

	public static UnifyPair fromSerial(SerialUUID serialUUID, UnifyContext context, KeyStorage keyStorage) {
		String uuid = serialUUID.uuid;
		if (!keyStorage.isAlreadyUnserialized(uuid)) {
			SerialMap data = keyStorage.getSerialized(uuid);
			SerialMap lhsData = data.getMap("lhs");
			SerialMap rhsData = data.getMap("rhs");
			String opData = data.getValue("op").getOf(String.class);

			UnifyPair pair = new UnifyPair(
				UnifyType.fromSerial(lhsData, context),
				UnifyType.fromSerial(rhsData, context),
				PairOperator.fromString(opData)
			);
			// put the object into the storage before unserializing basePair recursively
			keyStorage.putUnserialized(uuid, pair);

			SerialList<SerialUUID> substitutionData = data.getList("substitution").assertListOfUUIDs();
			SerialList<SerialMap> fBoundedData = data.getList("fBounded").assertListOfMaps();
			SerialUUID basePairData = data.getUUIDOrNull("basePair");
			SerialMap locationData = data.getMapOrNull("location");

			pair.substitution = substitutionData.stream().map(substData -> UnifyPair.fromSerial(substData, context, keyStorage)).collect(Collectors.toSet());
			pair.fBounded = fBoundedData.stream().map(fBoundData -> UnifyType.fromSerial(fBoundData, context)).collect(Collectors.toSet());
			if (basePairData != null) {
				pair.basePair = UnifyPair.fromSerial(basePairData, context, keyStorage);
			}
			if (locationData != null) {
				pair.location = SourceLoc.fromSerial(locationData);
			}
		}
		return keyStorage.getUnserialized(uuid, UnifyPair.class);
	}
}


