/*
 * Copyright 2003-2011 JetBrains s.r.o.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package jetbrains.mps.newTypesystem;

import gnu.trove.THashSet;
import jetbrains.mps.lang.pattern.util.IMatchModifier;
import jetbrains.mps.lang.pattern.util.MatchingUtil;
import jetbrains.mps.lang.smodel.generator.smodelAdapter.SNodeOperations;
import jetbrains.mps.lang.typesystem.runtime.HUtil;
import jetbrains.mps.newTypesystem.state.Equations;
import jetbrains.mps.newTypesystem.state.State;
import jetbrains.mps.smodel.SModelUtil_new;
import jetbrains.mps.smodel.SNodeUtil;
import jetbrains.mps.typesystem.inference.EquationInfo;
import jetbrains.mps.typesystemEngine.util.LatticeUtil;
import jetbrains.mps.util.Pair;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.mps.openapi.model.SNode;
import org.jetbrains.mps.openapi.model.SReference;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;

public class TypesUtil {

  public static boolean isVariable(SNode node) {
    return HUtil.isRuntimeTypeVariable(node);
  }

  public static boolean canBeVariable(SNode node) {
    return isVariable(node) || LatticeUtil.isMeet(node) && hasVariablesInside(node);
  }

  public static boolean hasVariablesInside(SNode node) {
    if (node == null) {
      return false;
    }
    if (TypesUtil.isVariable(node)) {
      return true;
    }
    for (SNode child : node.getChildren()) {
      if (TypesUtil.hasVariablesInside(child)) {
        return true;
      }
    }
    for (SNode referent : getNodeReferents(node)) {
      if (referent != null && TypesUtil.isVariable(referent)) {
        return true;
      }
    }
    return false;
  }

  public static int depth(SNode sNode) {
    int childDepth = 0;
    for (SNode child : sNode.getChildren()) {
      int depth = depth(child);
      if (childDepth < depth) {
        childDepth = depth;
      }
    }
    if (!SNodeOperations.isInstanceOf(sNode, SNodeUtil.concept_SNodeType)) return childDepth + 1;
    if (sNode.getReference(SNodeUtil.ref_SNodeType_concept) == null) return childDepth + 1;
    return childDepth + 2;
  }


  public static List<SNode> getVariables(SNode node, State state) {
    List<SNode> result = new LinkedList<SNode>();
    getVariablesInside(node, result, state);
    return result;
  }

  private static void getVariablesInside(SNode node, List<SNode> result, State state) {
    if (state != null) {
      node = state.getRepresentative(node);
    }
    if (node == null) {
      return;
    }
    if (isVariable(node)) {
      result.add(node);
      return;
    }
    for (SNode child : node.getChildren()) {
      getVariablesInside(child, result, state);
    }
    for (SNode referent : getNodeReferents(node)) {
      if (state != null) {
        referent = state.getRepresentative(referent);
      }
      if (referent != null && isVariable(referent)) {
        result.add(referent);
      }
    }
  }

  @NotNull
  public static List<SNode> getNodeReferents(@NotNull SNode node) {
    if (node == null) return Collections.emptyList();
    final List<SNode> result = new ArrayList<SNode>();
    for (SReference ref : node.getReferences()) {
      result.add(ref.getTargetNode());
    }
    return result;
  }

  public static boolean match(SNode left, SNode right) {
    return match(left, right, null);
  }

  public static boolean match(SNode left, SNode right, /*out*/ Collection<Pair<SNode, SNode>> matchingPairs) {
    if (left == right) return true;
    if (left == null || right == null) return false;

    if (TypesUtil.isVariable(left) || TypesUtil.isVariable(right)) {
      if (matchingPairs != null) {
        matchingPairs.add(new Pair<SNode, SNode>(left, right));
      }
      return true;
    }

    MatchingNodesCollector matchingNodesCollector = new MatchingNodesCollector();
    boolean match = MatchingUtil.matchNodes(left, right, matchingNodesCollector, false);
    if (match && matchingPairs != null) {
      matchingPairs.addAll(matchingNodesCollector.myMatchingPairs);
    }

    return match;
  }

  public static boolean matchExpandingJoinAndMeet(SNode left, SNode right, /*out*/ Collection<Pair<SNode, SNode>> matchingPairs) {
    if (match(left, right, matchingPairs)) return true;

    if (LatticeUtil.isJoin(left)) {
      for (SNode arg : LatticeUtil.getJoinArguments(left)) {
        if (match(arg, right, matchingPairs)) return true;
      }
    }
    if (LatticeUtil.isMeet(left)) {
      for (SNode arg : LatticeUtil.getMeetArguments(left)) {
        if (!match(arg, right, matchingPairs)) return false;
      }
      return true;
    }
    if (LatticeUtil.isJoin(right)) {
      for (SNode arg : LatticeUtil.getJoinArguments(right)) {
        if (match(left, arg, matchingPairs)) return true;
      }
    }
    if (LatticeUtil.isMeet(right)) {
      for (SNode arg : LatticeUtil.getMeetArguments(right)) {
        if (!match(left, arg, matchingPairs)) return false;
      }
      return true;
    }

    return false;
  }

  @Deprecated
  public static boolean match(SNode left, SNode right, Equations equations, @Nullable EquationInfo info) {
    THashSet<Pair<SNode, SNode>> matchingPairs = new THashSet<Pair<SNode, SNode>>();
    boolean match = match(left, right, matchingPairs);
    if (match && equations != null) {
      equations.addEquations(matchingPairs, info);
    }
    return match;
  }

  public static SNode cleanupMeet(SNode type) {
    // Dirty hack to avoid meet type to appear inside fully reified type
    Set<SNode> newArgs = new THashSet<SNode>();
    final List<SNode> arguments = LatticeUtil.getMeetArguments(type);
    boolean addTheRest = false;
    for (SNode arg : arguments) {
      if (arg != null && (addTheRest || !SNodeOperations.isInstanceOf(arg, SNodeUtil.concept_VoidType))) {
        newArgs.add(arg);
      } else {
        addTheRest = true;
      }
    }
    if (newArgs.size() != arguments.size()) {
      type = LatticeUtil.meetNodes(newArgs);
    }
    return type;
  }

  private static class MatchingNodesCollector implements IMatchModifier {
    private final Set<Pair<SNode, SNode>> myMatchingPairs = new THashSet<Pair<SNode, SNode>>();

    @Override
    public boolean accept(SNode node1, SNode node2) {
      return TypesUtil.isVariable(node1) || TypesUtil.isVariable(node2);
    }

    @Override
    public boolean acceptList(List<SNode> nodes1, List<SNode> nodes2) {
      return false;
    }

    @Override
    public void performAction(SNode node1, SNode node2) {
      if (myMatchingPairs != null) {
        myMatchingPairs.add(new Pair<SNode, SNode>(node1, node2));
      }
    }

    @Override
    public void performGroupAction(List<SNode> nodes1, List<SNode> nodes2) {
    }
  }

  public static SNode createRuntimeErrorType() {
    return SModelUtil_new.instantiateConceptDeclaration(SNodeUtil.concept_RuntimeErrorType, null, null, false);
  }

}

