package jetbrains.mps.lang.test.matcher;

/*Generated by MPS */

import java.util.Map;
import org.jetbrains.mps.openapi.model.SNode;
import java.util.List;
import java.util.Collections;
import org.jetbrains.annotations.NotNull;
import java.util.HashMap;
import java.util.Iterator;
import java.util.ArrayList;
import java.util.HashSet;
import org.jetbrains.mps.openapi.language.SContainmentLink;
import org.jetbrains.mps.openapi.language.SReferenceLink;
import org.jetbrains.mps.openapi.model.SReference;
import org.jetbrains.mps.openapi.language.SProperty;
import org.jetbrains.mps.openapi.language.SDataType;
import java.util.Objects;

/**
 * XXX could add options to parameterize instance prior to diff. E.g. dumpDiff()/debugDiff to use in scenarios where diff().isEmpty() is used but it's handy to see true diff in case anything goes wrong
 * FIXME quite similar to {@code jetbrains.mps.smodel.SNodeMatcher}
 */
public final class NodesMatcher {
  /**
   * AFAIU, it's a map of 'structural' correspondence, which is used to ensure reference 
   * targets of nodes being matched point to the same element within the model structure.
   * Personally, I don't see why it's reasonable to keep map of a e.g. whole model just to 
   * ensure structural match provided we ensure equal structure by regular parent-child 
   * walk approach. I'd rather perform matching of reference targets (if they are from the same 
   * model; and do it only once). However, there's code that uses the map (editor tests) 
   * which  I need to refactor first.
   */
  private final Map<SNode, SNode> myMap;
  private final List<SNode> myFirst;
  private final List<SNode> mySecond;

  public NodesMatcher(SNode a, SNode b) {
    this(Collections.singletonList(a), Collections.singletonList(b));
  }

  public NodesMatcher(@NotNull List<SNode> a, @NotNull List<SNode> b) {
    myMap = new HashMap<SNode, SNode>();
    myFirst = a;
    mySecond = b;
  }

  public Map<SNode, SNode> getMap() {
    // I don't quite understand what's the map we either populate and use, or just use.
    // why matchNodes(node,node) goes through matchNodes(list,list), creates and populates map,
    // while matchNodes(node,node,map) does not populate the map
    // Once I understand what map is, I can give method better name
    return myMap;
  }

  /**
   * Collection of differences in initial nodes, in an order that corresponds to order of original collections 
   * (not necessarily identical as original collections are not necessarily of the same size)
   * 
   * Builds a map of corresponding nodes later available through {@link jetbrains.mps.lang.test.matcher.NodesMatcher#getMap() }
   * 
   * @return differences, or empty collection if all nodes match
   */
  @NotNull
  public List<NodeDifference> diff() {
    HashMap<SNode, SNode> map = new HashMap<SNode, SNode>();
    Iterator<SNode> iteratorA = myFirst.iterator();
    Iterator<SNode> iteratorB = mySecond.iterator();
    while (iteratorA.hasNext() && iteratorB.hasNext()) {
      populateMap(iteratorA.next(), iteratorB.next(), map);
    }
    myMap.clear();
    myMap.putAll(map);
    return diff(map);
  }

  @NotNull
  public List<NodeDifference> diff(Map<SNode, SNode> nodeMap) {
    ArrayList<NodeDifference> ret = new ArrayList<NodeDifference>(myFirst.size());
    Iterator<SNode> iteratorA = myFirst.iterator();
    Iterator<SNode> iteratorB = mySecond.iterator();
    MatcherImpl mi = new MatcherImpl(nodeMap);
    while (iteratorA.hasNext() && iteratorB.hasNext()) {
      mi.match(iteratorA.next(), iteratorB.next());
    }
    for (DifferenceItem di : mi.myDifferences) {
      // we can't get anything but NodeDifference elements after mi.match() execution as it replaces any DI item created with an ND
      assert di instanceof NodeDifference;
      if (di instanceof NodeDifference) {
        ret.add((NodeDifference) di);
      }
    }
    while (iteratorA.hasNext()) {
      ret.add(new NodeDifference(iteratorA.next().getPresentation(), Collections.singletonList(new UnmatchedNode())));
    }
    while (iteratorB.hasNext()) {
      ret.add(new NodeDifference(iteratorB.next().getPresentation(), Collections.singletonList(new UnmatchedNode())));
    }
    return ret;
  }

  private static void populateMap(SNode a, SNode b, Map<SNode, SNode> map) {
    if (!(a.getConcept().equals(b.getConcept()))) {
      return;
    }
    HashSet<SContainmentLink> roles = new HashSet<SContainmentLink>();
    for (SNode child : a.getChildren()) {
      roles.add(child.getContainmentLink());
    }
    for (SNode child : b.getChildren()) {
      roles.add(child.getContainmentLink());
    }
    for (SContainmentLink role : roles) {
      Iterator<? extends SNode> iterator1 = a.getChildren(role).iterator();
      Iterator<? extends SNode> iterator2 = b.getChildren(role).iterator();
      while (iterator1.hasNext() && iterator2.hasNext()) {
        populateMap(iterator1.next(), iterator2.next(), map);
      }
    }
    map.put(a, b);
  }

  /**
   * 
   * @deprecated use cons with args and {@link jetbrains.mps.lang.test.matcher.NodesMatcher#diff() }
   */
  @Deprecated
  public NodeDifference match(SNode a, SNode b) {
    MatcherImpl mi = new MatcherImpl(myMap);
    mi.match(a, b);
    return (mi.myDifferences.isEmpty() ? null : ((NodeDifference) mi.myDifferences.get(0)));
  }

  /*package*/ static class MatcherImpl {
    /*package*/ final List<DifferenceItem> myDifferences = new ArrayList<DifferenceItem>();
    private final Map<SNode, SNode> myMap;

    /*package*/ MatcherImpl(Map<SNode, SNode> nodeMap) {
      myMap = nodeMap;
    }

    /*package*/ void match(SNode a, SNode b) {
      final int before = myDifferences.size();
      if (matchConcepts(a, b)) {
        matchProperties(a, b);
        matchReferences(a, b);
        matchChildren(a, b);
      }
      final int after = myDifferences.size();
      if (after != before) {
        assert after > before;
        NodeDifference nd = new NodeDifference(a.getPresentation(), new ArrayList<DifferenceItem>(myDifferences.subList(before, after)));
        // i>before, not >=, as we replace element @before with nd afterwards
        for (int i = after - 1; i > before; i--) {
          myDifferences.remove(i);
        }
        myDifferences.set(before, nd);
      }
    }

    private boolean matchConcepts(SNode a, SNode b) {
      if (a.getConcept().equals(b.getConcept())) {
        return true;
      }
      myDifferences.add(new ConceptDifference(a.getConcept(), b.getConcept()));
      return false;
    }

    private void matchReferences(SNode a, SNode b) {
      HashSet<SReferenceLink> roles = new HashSet<SReferenceLink>();
      for (SReference nextReference : a.getReferences()) {
        roles.add(nextReference.getLink());
      }
      for (SReference nextReference : b.getReferences()) {
        roles.add(nextReference.getLink());
      }
      for (SReferenceLink role : roles) {
        SReference reference1 = a.getReference(role);
        SNode referenceTarget1 = null;
        if (reference1 != null) {
          referenceTarget1 = reference1.getTargetNode();
        }

        SReference reference2 = b.getReference(role);
        SNode referenceTarget2 = null;
        if (reference2 != null) {
          referenceTarget2 = reference2.getTargetNode();
        }

        if (myMap.containsKey(referenceTarget1)) {
          if (myMap.get(referenceTarget1) != referenceTarget2) {
            myDifferences.add(new ReferenceDifference(role, true, myMap.get(referenceTarget1), referenceTarget2));
          }
        } else {
          if (referenceTarget1 != referenceTarget2) {
            myDifferences.add(new ReferenceDifference(role, false, referenceTarget1, referenceTarget2));
          }
        }
      }
    }

    private static int countElements(Iterator<?> it) {
      int counter = 0;
      while (it.hasNext()) {
        it.next();
        counter++;
      }
      return counter;
    }

    private void matchChildren(SNode a, SNode b) {
      HashSet<SContainmentLink> roles = new HashSet<SContainmentLink>();
      for (SNode child : a.getChildren()) {
        roles.add(child.getContainmentLink());
      }
      for (SNode child : b.getChildren()) {
        roles.add(child.getContainmentLink());
      }
      for (SContainmentLink role : roles) {
        Iterable<? extends SNode> children1 = a.getChildren(role);
        Iterable<? extends SNode> children2 = b.getChildren(role);
        int size1 = countElements(children1.iterator());
        int size2 = countElements(children2.iterator());
        if (size1 != size2) {
          myDifferences.add(new ChildrenCountDifference(role, size1, size2));
          continue;
        }

        Iterator<? extends SNode> iterator1 = children1.iterator();
        Iterator<? extends SNode> iterator2 = children2.iterator();
        while (iterator1.hasNext() && iterator2.hasNext()) {
          match(iterator1.next(), iterator2.next());
        }
      }
    }

    private void matchProperties(SNode a, SNode b) {
      HashSet<SProperty> properties = new HashSet<SProperty>();
      for (SProperty property : a.getProperties()) {
        properties.add(property);
      }
      for (SProperty property : b.getProperties()) {
        properties.add(property);
      }
      for (SProperty property : properties) {
        SDataType type = property.getType();
        Object pa = type.fromString(a.getProperty(property));
        Object pb = type.fromString(b.getProperty(property));
        if (!(Objects.equals(pa, pb))) {
          myDifferences.add(new PropertyDifference(property, pa, pb));
        }
      }
    }

  }

}
