package de.dhbwstuttgart.typeinference.unify;

import de.dhbwstuttgart.typeinference.unify.model.PairOperator;
import de.dhbwstuttgart.typeinference.unify.model.PlaceholderType;
import de.dhbwstuttgart.typeinference.unify.model.UnifyPair;
import de.dhbwstuttgart.typeinference.unify.model.UnifyType;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.function.Function;
import java.util.stream.Collectors;

/**
 * A collection of capsuled (and thus static) functions to split up large algorithms in TypeUnifyTask
 */
public class TypeUnifyTaskHelper {

  /**
   * Filter all topLevelSets for those with a single element that contain only one pair:
   *   a <. theta,
   *   theta <. a or
   *   a =. theta
   */
  public static Set<Set<UnifyPair>> getSingleElementSets(ArrayList<Set<? extends Set<UnifyPair>>> topLevelSets) {
    return topLevelSets.stream()
      .filter(x -> x.size() == 1)
      .map(y -> y.stream().findFirst().get()).collect(Collectors.toSet());
  }

  /**
   * Varianzbestimmung Anfang
   * Oderconstraint, wenn entweder kein Basepair oder unterschiedliche Basepairs => oderConstraint = true;
   * Varianz =  1 => Argumentvariable
   * Varianz = -1 => Rückgabevariable
   * Varianz =  0 => unklar
   * Varianz =  2 => Operatoren oderConstraints
   */
  public static int calculateVariance(List<Set<UnifyPair>> nextSetasList) {
    Optional<Integer> xi = nextSetasList.stream().map(x -> x.stream().filter(y -> (y.getLhsType() instanceof PlaceholderType && !(y.getRhsType() instanceof PlaceholderType)))
        .filter(z -> ((PlaceholderType) z.getLhsType()).getVariance() != 0)
        .map(c -> ((PlaceholderType) c.getLhsType()).getVariance())
        .reduce((a, b) -> {
          if (a.intValue() == b.intValue()) return a;
          else return 0;
        })) //2 kommt insbesondere bei Oder-Constraints vor
      .filter(Optional::isPresent)
      .map(Optional::get)
      .findAny();

    return xi.orElse(0);
  }

  /**
   *
   */
  public static int calculateOderConstraintVariance(List<Set<UnifyPair>> nextSetAsList) {
    Optional<Integer> optVariance =
      nextSetAsList
        .getFirst()
        .stream()
        .filter(x -> x.getGroundBasePair().getLhsType() instanceof PlaceholderType &&
          !(x.getRhsType() instanceof PlaceholderType) &&
          x.getPairOp() == PairOperator.EQUALSDOT)
        .map(x ->
          ((PlaceholderType) x.getGroundBasePair().getLhsType()).getVariance())
        .reduce((n, m) -> (n != 0) ? n : m);

    //Fuer Operatorenaufrufe wird variance auf 2 gesetzt.
    //da kein Receiver existiert also kein x.getGroundBasePair().getLhsType() instanceof PlaceholderType
    //Bei Varianz = 2 werden alle Elemente des Kartesischen Produkts abgearbeitet
    return optVariance.orElse(2);
  }


  /**
   * Find the first occurrence (if any) of a UnifyPair with operator EQUALSDOT while having
   * one side equal to its base pair counterpart
   */
  public static Optional<UnifyPair> findEqualityConstrainedUnifyPair(Set<UnifyPair> nextSetElement) {
    return nextSetElement.stream().filter(x ->
        x.getPairOp()
        .equals(PairOperator.EQUALSDOT))
      .filter(x -> //Sicherstellen, dass bei a = ty a auch wirklich die gesuchte Typvariable ist
        x.getLhsType()
          .equals(x.getBasePair().getLhsType()) ||
        x.getLhsType()
          .equals(x.getBasePair().getRhsType())
    ).findFirst();
  }

  /**
   *  Find all unifyPairs, that associate the identified type variable of origPair with any concrete type. That means:
   *  If "a = type" is in origPair, then we get all UnifyPairs that contain either "a < typeA" or "typeB < a"
   */
  public static Set<UnifyPair> findConstraintsWithSameTVAssociation(UnifyPair origPair, Set<Set<UnifyPair>> singleElementSets) {
    UnifyType tyVar = origPair.getLhsType();
    if (!(tyVar instanceof PlaceholderType)) {
      tyVar = origPair.getRhsType();
    }

    UnifyType tyVarEF = tyVar;
    return singleElementSets.stream()
      .map(xx ->
        xx.iterator().next())
      .filter(x ->
            (x.getLhsType().equals(tyVarEF) && !(x.getRhsType() instanceof PlaceholderType))
            ||
            (x.getRhsType().equals(tyVarEF) && !(x.getLhsType() instanceof PlaceholderType))
      )
      .collect(Collectors.toCollection(HashSet::new));
  }

  /**
   *
   */
  public static boolean doesFirstNextSetHasSameBase(List<Set<UnifyPair>> nextSetAsList) {
    if (nextSetAsList.isEmpty()) {
      return false;
    }

    UnifyPair firstBasePair = null;

    for (var unifyPair : nextSetAsList.getFirst().stream().toList()) {
      var basePair = unifyPair.getBasePair();

      // if any base pair is null, there is NOT always the same base!
      if (basePair == null) {
        return false;
      }

      if (firstBasePair == null) {
        firstBasePair = basePair;
      }
      else if (!basePair.equals(firstBasePair)) {
        return false;
      }
    }

    return true;
  }

  /**
   *  Extracts data from every element in the nested lists of results. What data depends on the given
   *  extractor function
   */
  public static Set<UnifyPair> collectFromThreadResult (
    Set<Set<UnifyPair>> currentThreadResult,
    Function<UnifyPair, Set<UnifyPair>> extractor
  ) {
    return currentThreadResult.stream()
      .map(b ->
        b.stream()
          .map(extractor)
          .reduce((y, z) -> {
            y.addAll(z);
            return y;
          })
          .orElse(new HashSet<>()))
      .reduce((y, z) -> {
        y.addAll(z);
        return y;
      })
      .orElse(new HashSet<>());
  }

  /**
   *  Extract a list of PlaceholderTypes from a set of pairs, such that each resulting element:
   *  - Is the LHS of a pair
   *  - Is a PlaceholderType
   *  - has a basePair Side that is a PlaceholderType with the same name
   */
  public static List<PlaceholderType> extractMatchingPlaceholderTypes(Set<UnifyPair> pairs) {
    return pairs.stream()
      .filter(x -> {
        UnifyType lhs = x.getLhsType();
        UnifyType baseLhs = x.getBasePair().getLhsType();
        UnifyType baseRhs = x.getBasePair().getRhsType();
        return (lhs instanceof PlaceholderType) &&
          ((baseLhs instanceof PlaceholderType && lhs.getName().equals(baseLhs.getName())) ||
            (baseRhs instanceof PlaceholderType && lhs.getName().equals(baseRhs.getName())));
      })
      .map(x -> (PlaceholderType) x.getLhsType())
      .collect(Collectors.toCollection(ArrayList::new));
  }


  public static Set<UnifyPair> occursCheck(final Set<UnifyPair> eq) {
    Set<UnifyPair> ocurrPairs = new HashSet<>(eq.size());
    for (UnifyPair x : eq) {
      UnifyType lhs = x.getLhsType();
      UnifyType rhs = x.getRhsType();
      if (lhs instanceof PlaceholderType lhsPlaceholder &&
        !(rhs instanceof PlaceholderType) &&
        rhs.getTypeParams().occurs(lhsPlaceholder))
      {
        x.setUndefinedPair();
        ocurrPairs.add(x);
      }
    }
    return ocurrPairs;
  }


  public static <T> HashSet<T> getPresizedHashSet(int minElements) {
    if (minElements < 16) return new HashSet<>();
    // HashSet and HashMap will resize at 75% load, so we account for that by multiplying with 1.5
    int n = (int)(minElements * 1.5);
    return new HashSet<>(n);
  }

  public static <S,T> HashMap<S,T> getPresizedHashMap(int minElements) {
    if (minElements < 16) return new HashMap<>();
    // HashSet and HashMap will resize at 75% load, so we account for that by multiplying with 1.5
    int n = (int)(minElements * 1.5);
    return new HashMap<>(n);
  }

}
