/*
 * Copyright 2003-2015 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.typesystem.inference;

import gnu.trove.THashSet;
import jetbrains.mps.errors.IRuleConflictWarningProducer;
import jetbrains.mps.lang.typesystem.runtime.CheckingRuleSet;
import jetbrains.mps.lang.typesystem.runtime.ComparisonRule_Runtime;
import jetbrains.mps.lang.typesystem.runtime.DoubleRuleSet;
import jetbrains.mps.lang.typesystem.runtime.IHelginsDescriptor;
import jetbrains.mps.lang.typesystem.runtime.InequationReplacementRule_Runtime;
import jetbrains.mps.lang.typesystem.runtime.InferenceRule_Runtime;
import jetbrains.mps.lang.typesystem.runtime.IsApplicable2Status;
import jetbrains.mps.lang.typesystem.runtime.IsApplicableStatus;
import jetbrains.mps.lang.typesystem.runtime.NonTypesystemRule_Runtime;
import jetbrains.mps.lang.typesystem.runtime.OverloadedOperationsManager;
import jetbrains.mps.lang.typesystem.runtime.RuleSet;
import jetbrains.mps.lang.typesystem.runtime.SubstituteType_Runtime;
import jetbrains.mps.lang.typesystem.runtime.SubtypingRule_Runtime;
import jetbrains.mps.smodel.language.LanguageRuntime;
import jetbrains.mps.util.Pair;
import org.apache.log4j.LogManager;
import org.apache.log4j.Logger;
import org.jetbrains.mps.openapi.model.SNode;

import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;

public class RulesManager {

  private RuleSet<InferenceRule_Runtime> myInferenceRules = new CheckingRuleSet<InferenceRule_Runtime>();
  private RuleSet<SubtypingRule_Runtime> mySubtypingRules = new RuleSet<SubtypingRule_Runtime>();
  private RuleSet<SubstituteType_Runtime> mySubstituteTypeRules = new RuleSet<SubstituteType_Runtime>();
  private DoubleRuleSet<ComparisonRule_Runtime> myComparisonRules = new DoubleRuleSet<ComparisonRule_Runtime>();
  private DoubleRuleSet<InequationReplacementRule_Runtime> myReplacementRules = new DoubleRuleSet<InequationReplacementRule_Runtime>();
  private RuleSet<NonTypesystemRule_Runtime> myNonTypeSystemRules = new CheckingRuleSet<NonTypesystemRule_Runtime>();

  private Set<IVariableConverter_Runtime> myVariableConverters = new THashSet<IVariableConverter_Runtime>();

  private OverloadedOperationsManager myOverloadedOperationsManager;

  private static final Logger LOG = LogManager.getLogger(RulesManager.class);
  private volatile boolean myNeedsLoading = false;
  private Set<LanguageRuntime> myLoadedLanguages = new HashSet<LanguageRuntime>();
  private Set<LanguageRuntime> myLanguagesToLoad = new HashSet<LanguageRuntime>();

  public RulesManager(TypeChecker typeChecker) {
    myOverloadedOperationsManager = new OverloadedOperationsManager(typeChecker);
  }

  public void loadLanguages(Iterable<LanguageRuntime> languages) {
    for (LanguageRuntime language : languages) {
      assert !myLoadedLanguages.contains(language);
      myLanguagesToLoad.add(language);
      myNeedsLoading = true;
    }
  }

  private void ensureAllRulesLoaded() {
    if (!myNeedsLoading) {
      return;
    }
    synchronized (this) {
      if (!myNeedsLoading) {
        return;
      }

      for (LanguageRuntime language : myLanguagesToLoad) {
        assert !myLoadedLanguages.contains(language);
        myLoadedLanguages.add(language);

        IHelginsDescriptor typesystem = null;
        try {
          typesystem = language.getAspect(IHelginsDescriptor.class);
        } catch (LinkageError linkageError) {
          LOG.warn("Problems with creating typesystem descriptor " + linkageError.getMessage());
        } catch (Throwable t) {
          LOG.error("Error while loading language: " + language.getNamespace(), t);
        }
        if (typesystem == null) {
          continue;
        }
        try {
          myInferenceRules.addRuleSetItem(typesystem.getInferenceRules());
          mySubtypingRules.addRuleSetItem(typesystem.getSubtypingRules());
          mySubstituteTypeRules.addRuleSetItem(typesystem.getSubstituteTypeRules());
          Set<ComparisonRule_Runtime> comparisonRule_runtimes = typesystem.getComparisonRules();
          myComparisonRules.addRuleSetItem(comparisonRule_runtimes);
          myReplacementRules.addRuleSetItem(typesystem.getEliminationRules());
          myVariableConverters.addAll(typesystem.getVariableConverters());
          myNonTypeSystemRules.addRuleSetItem(typesystem.getNonTypesystemRules());
          myOverloadedOperationsManager.addOverloadedOperationsTypeProviders(typesystem.getOverloadedOperationsTypesProviders());
        } catch (RuntimeException t) {
        }
      }

      myLanguagesToLoad = new HashSet<LanguageRuntime>();
      myNeedsLoading = false;
    }
  }

  public void unloadLanguages(Iterable<LanguageRuntime> languages) {
    for (LanguageRuntime language : languages) {
      if (myLoadedLanguages.contains(language)) {
        unloadLoadedAllLoaded();
      }
      myLanguagesToLoad.remove(language);
      myNeedsLoading = true;
    }
  }

  private void unloadLoadedAllLoaded() {

    myLanguagesToLoad.addAll(myLoadedLanguages);
    myLoadedLanguages = new HashSet<LanguageRuntime>();

    // TODO: cleanup
    myInferenceRules.clear();
    mySubtypingRules.clear();
    mySubstituteTypeRules.clear();
    myComparisonRules.clear();
    myReplacementRules.clear();
    myVariableConverters.clear();
    myOverloadedOperationsManager.clear();
    myNonTypeSystemRules.clear();
  }

  public IVariableConverter_Runtime getVariableConverter(SNode context, String role, SNode variable, boolean isAggregation) {
    ensureAllRulesLoaded();
    for (IVariableConverter_Runtime converter : myVariableConverters) {
      if (converter.isApplicable(context, role, variable, isAggregation)) return converter;
    }
    return null;
  }

  public List<Pair<InferenceRule_Runtime, IsApplicableStatus>> getInferenceRules(final SNode node) {
    ensureAllRulesLoaded();
    List<Pair<InferenceRule_Runtime, IsApplicableStatus>> result = new LinkedList<Pair<InferenceRule_Runtime, IsApplicableStatus>>();
    Set<InferenceRule_Runtime> ruleSet;
    ruleSet = myInferenceRules.getRules(node);
    for (InferenceRule_Runtime rule : ruleSet) {
      IsApplicableStatus status = rule.isApplicableAndPattern(node);
      if (status.isApplicable()) {
        result.add(new Pair<InferenceRule_Runtime, IsApplicableStatus>(rule, status));
      }
      if (rule.overrides(node, status)) {
        break;
      }
    }
    return result;
  }

  public List<Pair<NonTypesystemRule_Runtime, IsApplicableStatus>> getNonTypesystemRules(SNode node) {
    ensureAllRulesLoaded();
    List<Pair<NonTypesystemRule_Runtime, IsApplicableStatus>> result = new LinkedList<Pair<NonTypesystemRule_Runtime, IsApplicableStatus>>();
    Set<NonTypesystemRule_Runtime> ruleSet;
    ruleSet = myNonTypeSystemRules.getRules(node);
    for (NonTypesystemRule_Runtime rule : ruleSet) {
      IsApplicableStatus status = rule.isApplicableAndPattern(node);
      if (status.isApplicable()) {
        result.add(new Pair<NonTypesystemRule_Runtime, IsApplicableStatus>(rule, status));
      }
    }
    return result;
  }

  public List<Pair<SubtypingRule_Runtime, IsApplicableStatus>> getSubtypingRules(final SNode node, final boolean isWeak) {
    ensureAllRulesLoaded();
    List<Pair<SubtypingRule_Runtime, IsApplicableStatus>> result = new LinkedList<Pair<SubtypingRule_Runtime, IsApplicableStatus>>();
    for (SubtypingRule_Runtime rule : mySubtypingRules.getRules(node)) {
      if ((isWeak || !rule.isWeak())) {
        IsApplicableStatus status = rule.isApplicableAndPattern(node);
        if (status.isApplicable()) {
          result.add(new Pair<SubtypingRule_Runtime, IsApplicableStatus>(rule, status));
        }
      }
    }
    return result;
  }

  public List<Pair<SubstituteType_Runtime, IsApplicableStatus>> getSubstituteTypeRules(final SNode node) {
    ensureAllRulesLoaded();
    List<Pair<SubstituteType_Runtime, IsApplicableStatus>> result = new LinkedList<Pair<SubstituteType_Runtime, IsApplicableStatus>>();
    for (SubstituteType_Runtime rule : mySubstituteTypeRules.getRules(node)) {
      IsApplicableStatus status = rule.isApplicableAndPattern(node);
      if (status.isApplicable()) {
        result.add(new Pair<SubstituteType_Runtime, IsApplicableStatus>(rule, status));
      }
    }
    return result;
  }

  public List<Pair<ComparisonRule_Runtime, IsApplicable2Status>> getComparisonRules(final SNode node1, final SNode node2, final boolean isWeak) {
    ensureAllRulesLoaded();
    List<Pair<ComparisonRule_Runtime, IsApplicable2Status>> result = new LinkedList<Pair<ComparisonRule_Runtime, IsApplicable2Status>>();
    Set<ComparisonRule_Runtime> ruleSet = myComparisonRules.getRules(node1, node2);
    for (ComparisonRule_Runtime rule : ruleSet) {
      if (isWeak || !rule.isWeak()) {
        IsApplicable2Status status = rule.isApplicableAndPatterns(node1, node2);
        if (status.isApplicable()) {
          result.add(new Pair<ComparisonRule_Runtime, IsApplicable2Status>(rule, status));
        }
      }
    }
    return result;
  }


  public List<Pair<InequationReplacementRule_Runtime, IsApplicable2Status>> getReplacementRules(final SNode node1, final SNode node2) {
    ensureAllRulesLoaded();
    List<Pair<InequationReplacementRule_Runtime, IsApplicable2Status>> result = new LinkedList<Pair<InequationReplacementRule_Runtime, IsApplicable2Status>>();
    Set<InequationReplacementRule_Runtime> ruleSet = myReplacementRules.getRules(node1, node2);
    for (InequationReplacementRule_Runtime rule : ruleSet) {
      IsApplicable2Status status = rule.isApplicableAndPatterns(node1, node2);
      if (status.isApplicable()) {
        result.add(new Pair<InequationReplacementRule_Runtime, IsApplicable2Status>(rule, status));
      }
    }
    return result;
  }

  public SNode getOperationType(SNode operation, SNode leftOperandType, SNode rightOperandType) {
    return getOperationType(operation, leftOperandType, rightOperandType, IRuleConflictWarningProducer.NULL);
  }

  public SNode getOperationType(SNode operation, SNode leftOperandType, SNode rightOperandType, IRuleConflictWarningProducer warningProducer) {
    ensureAllRulesLoaded();
    return myOverloadedOperationsManager.getOperationType(operation, leftOperandType, rightOperandType, warningProducer);
  }
}

