package de.dhbwstuttgart.server.packet;

import com.fasterxml.jackson.annotation.JsonIgnore;
import de.dhbwstuttgart.core.ConsoleInterface;
import de.dhbwstuttgart.server.ServerTaskLogger;
import de.dhbwstuttgart.server.SocketFuture;
import de.dhbwstuttgart.server.SocketServer;
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.SerialValue;
import de.dhbwstuttgart.typeinference.constraints.ConstraintSet;
import de.dhbwstuttgart.typeinference.constraints.Pair;
import de.dhbwstuttgart.typeinference.unify.PlaceholderRegistry;
import de.dhbwstuttgart.typeinference.unify.TypeUnify;
import de.dhbwstuttgart.typeinference.unify.UnifyContext;
import de.dhbwstuttgart.typeinference.unify.UnifyResultListenerImpl;
import de.dhbwstuttgart.typeinference.unify.UnifyResultModel;
import de.dhbwstuttgart.typeinference.unify.UnifyTaskModel;
import de.dhbwstuttgart.typeinference.unify.interfaces.IFiniteClosure;
import de.dhbwstuttgart.typeinference.unify.model.FiniteClosure;
import de.dhbwstuttgart.typeinference.unify.model.PlaceholderType;
import de.dhbwstuttgart.typeinference.unify.model.UnifyPair;
import de.dhbwstuttgart.typeinference.unify.model.UnifyType;
import de.dhbwstuttgart.util.Logger;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.concurrent.ForkJoinPool;
import org.java_websocket.WebSocket;

/**
 * A packet to send all required data for the unification algorithm to the server and request the unification
 */
public class UnifyRequestPacket implements IClientToServerPacket<UnifyResultPacket> {

  public SerialMap finiteClosure;
  public SerialMap constraintSet;
  public SerialMap unifyConstraintSet;
  public SerialMap serialKeyStorage;
  public SerialValue<?> placeholders;
  public SerialList<SerialMap> factoryplaceholders;
  public String futureId;
  public int logLevel;

  @JsonIgnore
  private KeyStorage keyStorage = new KeyStorage();
  @JsonIgnore
  private boolean keyStorageLoaded = false;

  public static UnifyRequestPacket create(
    FiniteClosure finiteClosure,
    ConstraintSet<Pair> constraintSet,
    ConstraintSet<UnifyPair> unifyConstraintSet,
    PlaceholderRegistry placeholderRegistry
  ) {
    UnifyRequestPacket packet = new UnifyRequestPacket();
    // store constraint and finite closure
    packet.finiteClosure = finiteClosure.toSerial(packet.keyStorage);
    packet.constraintSet = constraintSet.toSerial(packet.keyStorage);
    packet.unifyConstraintSet = unifyConstraintSet.toSerial(packet.keyStorage);
    // store placeholder registry
    var serialRegistry = placeholderRegistry.toSerial(packet.keyStorage);
    packet.placeholders = serialRegistry.getValue("ph");
    packet.factoryplaceholders = serialRegistry.getList("factoryPh").assertListOfMaps();
    // store referenced objects separately
    packet.serialKeyStorage = packet.keyStorage.toSerial(packet.keyStorage);
    packet.logLevel = ConsoleInterface.logLevel.getValue();
    return packet;
  }


  @JsonIgnore
  public void loadKeyStorage(UnifyContext context) {
    if (!keyStorageLoaded) {
      keyStorageLoaded = true;
      keyStorage = KeyStorage.fromSerial(this.serialKeyStorage, context);
    }
  }

  @JsonIgnore
  private FiniteClosure retrieveFiniteClosure(UnifyContext context) {
    this.loadKeyStorage(context);
    return FiniteClosure.fromSerial(this.finiteClosure, context, keyStorage);
  }

  @JsonIgnore
  private ConstraintSet<Pair> retrieveConstraintSet(UnifyContext context) {
    this.loadKeyStorage(context);
    return ConstraintSet.fromSerial(this.constraintSet, context, Pair.class, keyStorage);
  }

  @JsonIgnore
  private ConstraintSet<UnifyPair> retrieveUnifyConstraintSet(UnifyContext context) {
    this.loadKeyStorage(context);
    return ConstraintSet.fromSerial(this.unifyConstraintSet, context, UnifyPair.class, keyStorage);
  }

  @JsonIgnore
  public void onHandle(WebSocket webSocket, SocketServer socketServer) {
    socketServer.sendMessage(webSocket, "You requested a unify! Please wait until I calculated everything...");
    socketServer.log(webSocket, "Client requested a unification. Starting now...");

    try {
      var placeholderRegistry = new PlaceholderRegistry();
      ArrayList<String> existingPlaceholders = (ArrayList) this.placeholders.getOf(ArrayList.class);
      existingPlaceholders.forEach(placeholderRegistry::addPlaceholder);

      Logger logger = new ServerTaskLogger(
        webSocket,
        socketServer,
        Logger.LogLevel.fromValue(
          Math.max(this.logLevel, Logger.LogLevel.INFO.getValue())
        )
      );
      var unifyContext = new UnifyContext(logger, true,
        new UnifyResultModel(new ConstraintSet<>(), new FiniteClosure(new HashSet<>(), logger, placeholderRegistry)),
        new UnifyTaskModel(), ForkJoinPool.commonPool(), placeholderRegistry
      );

      this.factoryplaceholders.stream()
        .map(p -> (PlaceholderType)UnifyType.fromSerial(p, unifyContext))
        .forEach(placeholderRegistry.UnifyTypeFactory_PLACEHOLDERS::add);


      // start the unification algorithm from the received data
      IFiniteClosure finiteClosure = this.retrieveFiniteClosure(unifyContext);
      ConstraintSet<Pair> constraintSet = this.retrieveConstraintSet(unifyContext);
      ConstraintSet<UnifyPair> unifyConstraintSet = this.retrieveUnifyConstraintSet(unifyContext);

      var resultModel = new UnifyResultModel(constraintSet, finiteClosure);
      UnifyResultListenerImpl resultListener = new UnifyResultListenerImpl();
      resultModel.addUnifyResultListener(resultListener);

      TypeUnify.unifyParallel(
        unifyConstraintSet.getUndConstraints(),
        unifyConstraintSet.getOderConstraints(),
        finiteClosure,
        unifyContext.newWithResultModel(resultModel)
      );

      var resultSets = resultListener.getResults();

      socketServer.log(webSocket, "Finished unification");
      socketServer.sendMessage(webSocket, "Unification finished. Found " + resultSets.size() + " result sets");

      if (webSocket.isOpen()) {
        UnifyResultPacket resultPacket = UnifyResultPacket.create(resultSets, futureId);
        webSocket.send(PacketContainer.serialize(resultPacket));
      }
    } catch (Exception e) {
      SocketServer.logger.exception(e);
      socketServer.log(webSocket, e.getMessage());
    }
  }

  @JsonIgnore
  public SocketFuture<UnifyResultPacket> getFuture() {
    var future = new SocketFuture<>(List.of(UnifyResultPacket.class));
    futureId = future.futureId;
    return future;
  }

}
