/*
 * Copyright 2003-2016 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.rules;

import gnu.trove.THashSet;
import jetbrains.mps.languageScope.LanguageScope;
import jetbrains.mps.smodel.NodeReadAccessCasterInEditor;
import org.jetbrains.mps.openapi.language.SAbstractConcept;
import org.jetbrains.mps.openapi.language.SConcept;
import org.jetbrains.mps.openapi.model.SNode;
import jetbrains.mps.util.Computable;
import jetbrains.mps.util.Pair;
import jetbrains.mps.util.Triplet;

import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedList;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;

/**
 * User: fyodor
 * Date: 8/31/12
 */
public abstract class DoubleTermRules<K> {

  private ConcurrentHashMap<Object, Set<K>> myCachedRules = new ConcurrentHashMap<Object, Set<K>>();

  public Set<K> lookupRules(SNode leftTerm, SNode rightTerm) {

    final LanguageScope langScope = LanguageScope.getCurrent();

    final SAbstractConcept leftConcept = leftTerm.getConcept();
    final SAbstractConcept rightConcept = rightTerm.getConcept();

    final Object compoundKey = new Triplet<Object, SAbstractConcept, SAbstractConcept>(langScope, leftConcept, rightConcept);

    Set<K> cachedRules = myCachedRules.get(compoundKey);
    if (cachedRules != null) return cachedRules;

    return NodeReadAccessCasterInEditor.runReadTransparentAction(new Computable<Set<K>>() {
      @Override
      public Set<K> compute() {
        Set<K> computedRules = computeRules(leftConcept, rightConcept, langScope);
        myCachedRules.put(compoundKey, computedRules);

        return computedRules;
      }
    });
  }

  public void purgeCache() {
    myCachedRules.clear();
  }

  private Set<K> computeRules(SAbstractConcept leftConcept, SAbstractConcept rightConcept, LanguageScope langScope) {
    THashSet<K> result = new THashSet<K>();

    LinkedList<Pair<SAbstractConcept, SAbstractConcept>> queue = new LinkedList<Pair<SAbstractConcept, SAbstractConcept>>();
    queue.add(new Pair<SAbstractConcept, SAbstractConcept>(leftConcept, rightConcept));
    for (SConcept leftSuperConcept : allSuperConcepts(leftConcept)) {
      queue.add(new Pair<SAbstractConcept, SAbstractConcept>(leftSuperConcept, rightConcept));
    }

    while (!queue.isEmpty()) {
      Pair<SAbstractConcept, SAbstractConcept> nextConceptPair = queue.remove();
      for (K applicableRule : allForConceptPair(nextConceptPair.o1, nextConceptPair.o2, langScope)) {
        result.add(applicableRule);
      }

      for (SConcept rightSuperConcept : allSuperConcepts(nextConceptPair.o2)) {
        queue.add(new Pair<SAbstractConcept, SAbstractConcept>(nextConceptPair.o1, rightSuperConcept));
      }
    }

    return Collections.unmodifiableSet(result);
  }

  // all super-concepts provided supplied concept is not an interface; including BaseConcept
  private Iterable<SConcept> allSuperConcepts(SAbstractConcept concept) {
    // I have no idea why interface concepts are not considered - computeRules(iface1, iface2) would
    // look for applicable rules anyway (queue is initialized regardless of concept kind), just won't
    // traverse concept hierarchy like it does for computeRules(concept1, concept2).
    if (!(concept instanceof SConcept)) {
      return Collections.emptyList();
    }
    // not sure there's much sense in BaseConcept among return values, left as it used to be.
    SConcept c = ((SConcept) concept).getSuperConcept();
    ArrayList<SConcept> rv = new ArrayList<SConcept>();
    while (c != null) {
      rv.add(c);
      c = c.getSuperConcept();
    }
    return rv;
  }

  abstract protected Iterable<K> allForConceptPair(SAbstractConcept leftConcept, SAbstractConcept rightConcept, LanguageScope langScope);

}
