package server;

import de.dhbwstuttgart.core.ConsoleInterface;
import de.dhbwstuttgart.core.JavaTXCompiler;
import de.dhbwstuttgart.core.JavaTXServer;
import de.dhbwstuttgart.environment.CompilationEnvironment;
import de.dhbwstuttgart.server.SocketClient;
import de.dhbwstuttgart.server.packet.SetAutoclosePacket;
import de.dhbwstuttgart.server.packet.UnifyRequestPacket;
import de.dhbwstuttgart.syntaxtree.ClassOrInterface;
import de.dhbwstuttgart.syntaxtree.factory.ASTFactory;
import de.dhbwstuttgart.syntaxtree.factory.UnifyTypeFactory;
import de.dhbwstuttgart.typeinference.constraints.ConstraintSet;
import de.dhbwstuttgart.typeinference.constraints.Pair;
import de.dhbwstuttgart.typeinference.result.ResultSet;
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.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.io.File;
import java.io.IOException;
import java.io.Writer;
import java.nio.file.Path;
import java.util.HashSet;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.stream.Collectors;
import targetast.TestCodegen;
import static org.junit.jupiter.api.Assertions.*;

public class ServerTest {

  public ServerTest() {
    ConsoleInterface.unifyServerUrl = Optional.of("ws://localhost:5000");
  }

  //@Test
  public void checkServer_Scalar() throws IOException, ClassNotFoundException {
    compareLocalAndServerResult("Scalar.jav");
  }

  //@Test
  public void checkServer_Matrix() throws IOException, ClassNotFoundException {
    compareLocalAndServerResult("Matrix.jav");
  }

  protected void compareLocalAndServerResult(final String filename) throws IOException, ClassNotFoundException {
    File file = Path.of(TestCodegen.path.toString(), filename).toFile();

    // get information from the compiler
    JavaTXCompiler compiler = new JavaTXCompiler(List.of(file));
    compiler.parseAll();

    // NOW: simulate the call to method typeInference. Once via server and once locally
    // if everything works, they should neither interfere with each other nor differ in their result

    // get the values from the compiler
    PlaceholderRegistry placeholderRegistry = JavaTXCompiler.defaultClientPlaceholderRegistry; //new PlaceholderRegistry();
    ConstraintSet<Pair> cons = compiler.getConstraints(file);
    ConstraintSet<UnifyPair> unifyCons = UnifyTypeFactory.convert(compiler, cons, placeholderRegistry);
    unifyCons = unifyCons.map(ServerTest::distributeInnerVars);

    FiniteClosure finiteClosure = UnifyTypeFactory.generateFC(
      ServerTest.getAllClasses(compiler, file).stream().toList(),
      Logger.NULL_LOGGER,
      compiler.classLoader,
      compiler,
      placeholderRegistry
    );
    UnifyTaskModel usedTasks = new UnifyTaskModel();



    // create the server
    JavaTXServer server = new JavaTXServer(5000);
    try (ExecutorService executor = Executors.newSingleThreadExecutor()) {
      // run the server in a separate thread
      executor.submit(server::listen);
    }

    // run the unification on the server
    PlaceholderRegistry prCopy = JavaTXCompiler.defaultClientPlaceholderRegistry.deepClone();
    UnifyResultModel urm = new UnifyResultModel(cons, finiteClosure);
    UnifyContext context = new UnifyContext(Logger.NULL_LOGGER, true, urm, usedTasks, prCopy);
    SocketClient.execute(SetAutoclosePacket.create());
    List<ResultSet> serverResult = SocketClient.executeAndGet(
      UnifyRequestPacket.create(finiteClosure, cons, unifyCons, context.placeholderRegistry())
    ).getResultSet(context);

    // close the server
    server.forceStop();

    // run the unification on the client (do this second, because it changes the initial placeholder registry)
    UnifyResultListenerImpl li = new UnifyResultListenerImpl();
    urm.addUnifyResultListener(li);
    TypeUnify.unifyParallel(unifyCons.getUndConstraints(), unifyCons.getOderConstraints(), finiteClosure, context);
    List<ResultSet> clientResult = li.getResults();


    // create the bytecode from both results
    var sf = compiler.sourceFiles.get(file);
    var serverBytecode = compiler.generateBytecode(sf, serverResult);
    var localBytecode = compiler.generateBytecode(sf, clientResult);

    // test if the generated code is the same
    for (var serverEntry : serverBytecode.entrySet()) {
      var serverBytes = serverEntry.getValue();
      var localBytes = localBytecode.get(serverEntry.getKey());
      assertArrayEquals(serverBytes, localBytes);
    }
  }


  protected static UnifyPair distributeInnerVars(UnifyPair x) {
    UnifyType lhs, rhs;
    if (((lhs = x.getLhsType()) instanceof PlaceholderType) && ((rhs = x.getRhsType()) instanceof PlaceholderType) && (((PlaceholderType) lhs).isInnerType() || ((PlaceholderType) rhs).isInnerType())) {
      ((PlaceholderType) lhs).setInnerType(true);
      ((PlaceholderType) rhs).setInnerType(true);
    }
    return x;
  }

  protected static Set<ClassOrInterface> getAllClasses(JavaTXCompiler compiler, File file)
    throws ClassNotFoundException, IOException
  {
    var sf = compiler.sourceFiles.get(file);
    Set<ClassOrInterface> allClasses = new HashSet<>();
    allClasses.addAll(compiler.getAvailableClasses(sf));
    allClasses.addAll(sf.getClasses());
    var newClasses = CompilationEnvironment.loadDefaultPackageClasses(sf.getPkgName(), file, compiler).stream().map(ASTFactory::createClass).collect(Collectors.toSet());
    for (var clazz : newClasses) {
      // Don't load classes that get recompiled
      if (sf.getClasses().stream().anyMatch(nf -> nf.getClassName().equals(clazz.getClassName())))
        continue;
      if (allClasses.stream().noneMatch(old -> old.getClassName().equals(clazz.getClassName())))
        allClasses.add(clazz);
    }
    return allClasses;
  }
}
