package de.dhbwstuttgart.typeinference.unify.model;

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.typeinference.unify.UnifyContext;
import java.util.Set;

import de.dhbwstuttgart.typeinference.unify.interfaces.IFiniteClosure;
import de.dhbwstuttgart.typeinference.unify.interfaces.UnifyTypeVisitor;

/**
 * A reference type e.q. Integer or List<T>.
 * @author Florian Steurer
 *
 */
public class ReferenceType extends UnifyType implements ISerializableData {

	/**
	 * The buffered hashCode
	 */
	private final int hashCode;

	/**
	 * gibt an, ob der ReferenceType eine generische Typvariable ist
	 */
	private final boolean genericTypeVar;


	public <T> UnifyType accept(UnifyTypeVisitor<T> visitor, T ht) {
		return visitor.visit(this, ht);
	}

	public ReferenceType(String name, boolean genericTypeVar) {
		super(name, new TypeParams());
		hashCode = 31 + 17 * typeName.hashCode() + 17 * typeParams.hashCode();
		this.genericTypeVar = genericTypeVar;
	}

	public ReferenceType(String name, UnifyType... params) {
		super(name, new TypeParams(params));
		hashCode = 31 + 17 * typeName.hashCode() + 17 * typeParams.hashCode();
		genericTypeVar = false;
	}

	public ReferenceType(String name, TypeParams params) {
		super(name, params);
		hashCode = 31 + 17 * typeName.hashCode() + 17 * typeParams.hashCode();
		genericTypeVar = false;
	}

	private ReferenceType(String name, TypeParams params, boolean genericTypeVar) {
		super(name, params);
		hashCode = 31 + 17 * typeName.hashCode() + 17 * typeParams.hashCode();
		this.genericTypeVar = genericTypeVar;
	}

	public boolean isGenTypeVar() {
		return genericTypeVar;
	}

	@Override
	Set<UnifyType> smArg(IFiniteClosure fc, Set<UnifyType> fBounded) {
		return fc.smArg(this, fBounded);
	}

	@Override
	Set<UnifyType> grArg(IFiniteClosure fc, Set<UnifyType> fBounded) {
		return fc.grArg(this, fBounded);
	}

	@Override
	UnifyType apply(Unifier unif) {
		TypeParams newParams = typeParams.apply(unif);

		if (newParams.hashCode() == typeParams.hashCode() && newParams.equals(typeParams))
			return this;

		return new ReferenceType(typeName, newParams);
	}

	@Override
	public UnifyType setTypeParams(TypeParams newTp) {
		if (newTp.hashCode() == typeParams.hashCode() && newTp.equals(typeParams))
			return this; // reduced the amount of objects created
		return new ReferenceType(typeName, newTp);
	}

	@Override
	public int hashCode() {
		return hashCode;
	}

	@Override
	public boolean equals(Object obj) {
		if (!(obj instanceof ReferenceType))
			return false;

		if (obj.hashCode() != this.hashCode())
			return false;

		ReferenceType other = (ReferenceType) obj;

		if (!other.getName().equals(typeName))
			return false;

		return other.getTypeParams().equals(typeParams);
	}

	@Override
	public SerialMap toSerial(KeyStorage keyStorage) {
		SerialMap serialized = new SerialMap();
		serialized.put("name", this.typeName);
		serialized.put("params", SerialList.fromMapped(this.typeParams.get(), param -> param.toSerial(keyStorage)));
		serialized.put("isGenericTypeVar", this.genericTypeVar);

		// create the wrapper and put this as the object
		var serializedWrapper = super.toSerial(keyStorage);
		serializedWrapper.put("object", serialized);
		return serializedWrapper;
	}

	public static ReferenceType fromSerial(SerialMap data, UnifyContext context) {
		var name = data.getValue("name").getOf(String.class);
		var params = data.getList("params").assertListOfMaps().stream().map(
			paramData -> UnifyType.fromSerial(paramData, context)).toList();
		var isGenericTypeVar = data.getValue("isGenericTypeVar").getOf(Boolean.class);
		return new ReferenceType(name, new TypeParams(params), isGenericTypeVar);
	}
}