package de.dhbwstuttgart.typeinference.constraints;

import de.dhbwstuttgart.server.packet.dataContainers.ISerializableData;
import de.dhbwstuttgart.server.packet.dataContainers.KeyStorage;
import de.dhbwstuttgart.server.packet.dataContainers.serialized.ISerialNode;
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.unify.UnifyContext;
import de.dhbwstuttgart.typeinference.unify.model.UnifyPair;

import java.util.HashSet;
import java.util.Optional;
import java.util.Set;
import java.util.function.Function;
import java.util.stream.Collectors;

public class Constraint<A extends IConstraintElement> extends HashSet<A> implements Comparable<Constraint<A>>, ISerializableData {
	private static final long serialVersionUID = 1L;
	private boolean isInherited = false;//wird beides nur für die Method-Constraints benoetigt
	private boolean isImplemented = false;
	
	/*
	 * wird verwendet um bei der Codegenerierung die richtige Methoden - Signatur
	 * auszuwaehlen 
	 */
	/*private*/ Set<A> methodSignatureConstraint = new HashSet<>();
	
	private Constraint<A> extendConstraint = null;
	
	public Constraint() {
		super();
	}

  public Constraint(int initialCapacity) {
    super(initialCapacity);
  }

	public Constraint(boolean isInherited, boolean isImplemented) {
		this.isInherited = isInherited;
		this.isImplemented = isImplemented;
	}
	
	public Constraint(boolean isInherited, boolean isImplemented, Constraint<A> extendConstraint, Set<A> methodSignatureConstraint) {
		this.isInherited = isInherited;
		this.isImplemented = isImplemented;
		this.extendConstraint = extendConstraint;
		this.methodSignatureConstraint = methodSignatureConstraint;
	}
	
	public void setIsInherited(boolean isInherited) {
		this.isInherited = isInherited;
	}
	
	public boolean isInherited() {
		return isInherited;
	}
	
	public boolean isImplemented() {
		return isImplemented;
	}
	
	public Constraint<A> getExtendConstraint() {
		return extendConstraint;
	}
	
	public void setExtendConstraint(Constraint<A> c) {
		extendConstraint = c;
	}
	
	public Set<A> getmethodSignatureConstraint() {
		return methodSignatureConstraint;
	}
	
	public void setmethodSignatureConstraint(Set<A> c) {
		methodSignatureConstraint = c;
	}

  public <B extends IConstraintElement> Constraint<B> createdMapped(Function<A,B> mapper) {
    Constraint<B> result = new Constraint<>(this.size());
    for (A element : this) {
      result.add(mapper.apply(element));
    }
    return result;
  }

	public String toString() {
    return super.toString() + "\nisInherited = " + isInherited
      + " isOveridden = " + isImplemented
      + " msc[" + methodSignatureConstraint.size() + "] = " + methodSignatureConstraint
      //" + extendsContraint: "  + (extendConstraint != null ? extendConstraint.toStringBase() : "null" )
      + "\n";
	}
	
	public String toStringBase() {
		return super.toString();
	}


  @Override
  public int compareTo(Constraint<A> o) {
    return this.toString().compareTo(o.toString());
  }

  private String serialUUID = null;

  @Override
  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("isInherited", isInherited);
      serialized.put("isImplemented", isImplemented);
      serialized.put("extendedConstraint", extendConstraint == null ? null :
        extendConstraint.toSerial(keyStorage));
      Function<A, ISerialNode> pairMapper = pair -> {
        if (pair instanceof Pair simplePair) return simplePair.toSerial(keyStorage);
        if (pair instanceof UnifyPair unifyPair) return unifyPair.toSerial(keyStorage);
        throw new RuntimeException("No serialization is supported for type " + pair.getClass().getName());
      };
      serialized.put("methodSignatureConstraint", methodSignatureConstraint == null ? null :
        SerialList.fromMapped(methodSignatureConstraint, pairMapper));
      serialized.put("setElements", SerialList.fromMapped(this, pairMapper));
    }

    // return only the unique key
    return new SerialUUID(uuid);
  }

  public static <T extends IConstraintElement> Constraint<T> fromSerial(SerialUUID serialUUID, UnifyContext context, Class<T> target, KeyStorage keyStorage) {
    String uuid = serialUUID.uuid;

    if (!keyStorage.isAlreadyUnserialized(uuid)) {
      Constraint<T> constraint = new Constraint<>();
      // immediately add the object to the context to prevent infinite recursion
      keyStorage.putUnserialized(uuid, constraint);

      // retrieve the serialized data und start unserializing it
      SerialMap data = keyStorage.getSerialized(uuid);
      constraint.isInherited = data.getValue("isInherited").getOf(Boolean.class);
      constraint.isImplemented = data.getValue("isImplemented").getOf(Boolean.class);
      constraint.extendConstraint = Optional.ofNullable(data.getUUIDOrNull("extendedConstraint"))
        .map(v -> Constraint.fromSerial(v, context, target, keyStorage))
        .orElse(null);

      // to convert the maps back to elements, we sadly have to do some assumptions about the generic types...
      Function<ISerialNode, T> pairUnmapper = pairData -> {
        if (target == Pair.class && pairData instanceof SerialMap pairMap) {
          return (T) Pair.fromSerial(pairMap, context);
        }
        if (target == UnifyPair.class && pairData instanceof SerialUUID pairUUID) {
          return (T) UnifyPair.fromSerial(pairUUID, context, keyStorage);
        }
        throw new RuntimeException("No serialization is supported for target type " + target.getName());
      };

      constraint.methodSignatureConstraint =
        Optional.ofNullable(data.getListOrNull("methodSignatureConstraint"))
          .map(l -> l.stream().map(pairUnmapper).collect(Collectors.toSet()))
          .orElse(null);
      constraint.addAll(
        data.getList("setElements")
          .stream().map(pairUnmapper).toList());
    }

    return keyStorage.getUnserialized(uuid, Constraint.class);
  }
}
