/*
 * 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.nodeEditor.cellMenu;

import jetbrains.mps.nodeEditor.SubstituteActionUtil;
import jetbrains.mps.openapi.editor.EditorContext;
import jetbrains.mps.openapi.editor.cells.EditorCell;
import jetbrains.mps.openapi.editor.cells.SubstituteAction;
import jetbrains.mps.openapi.editor.cells.SubstituteInfo;
import jetbrains.mps.smodel.IOperationContext;
import jetbrains.mps.smodel.ModelAccessHelper;
import jetbrains.mps.smodel.SModelInternal;
import jetbrains.mps.smodel.SModelOperations;
import jetbrains.mps.smodel.tempmodel.TempModuleOptions;
import jetbrains.mps.smodel.tempmodel.TemporaryModels;
import jetbrains.mps.typesystem.inference.InequalitySystem;
import jetbrains.mps.util.Computable;
import org.apache.log4j.LogManager;
import org.apache.log4j.Logger;
import org.jetbrains.mps.openapi.language.SLanguage;
import org.jetbrains.mps.openapi.model.SModel;
import org.jetbrains.mps.openapi.model.SNode;

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;

/**
 * Author: Sergey Dmitriev.
 * Time: Oct 29, 2003 2:17:38 PM
 */
public abstract class AbstractNodeSubstituteInfo implements SubstituteInfo {
  private static final Logger LOG = LogManager.getLogger(AbstractNodeSubstituteInfo.class);

  private static SModel ourModelForTypechecking = null;

  public static SModel getModelForTypechecking() {
    return ourModelForTypechecking;
  }

  private List<SubstituteAction> myCachedActionList;
  private Map<String, List<SubstituteAction>> myPatternsToActionListsCache = new HashMap<String, List<SubstituteAction>>();
  private Map<String, List<SubstituteAction>> myStrictPatternsToActionListsCache = new HashMap<String, List<SubstituteAction>>();
  private String myOriginalText;
  private EditorContext myEditorContext;

  public AbstractNodeSubstituteInfo(EditorContext editorContext) {
    myEditorContext = editorContext;
  }

  public EditorContext getEditorContext() {
    return myEditorContext;
  }

  public IOperationContext getOperationContext() {
    return myEditorContext.getOperationContext();
  }

  @Override
  public void setOriginalText(String text) {
    myOriginalText = text;
  }

  @Override
  public String getOriginalText() {
    return myOriginalText;
  }

  protected abstract List<SubstituteAction> createActions();

  @Override
  public void invalidateActions() {
    myCachedActionList = null;
    myPatternsToActionListsCache.clear();
    myStrictPatternsToActionListsCache.clear();
  }

  @Override
  public boolean hasExactlyNActions(final String pattern, final boolean strictMatching, final int n) {
    return new ModelAccessHelper(myEditorContext.getRepository()).runReadAction(new Computable<Boolean>() {
      @Override
      public Boolean compute() {
        int count = 0;
        for (SubstituteAction action : getActionsFromCache(pattern, strictMatching)) {
          if (strictMatching ? action.canSubstituteStrictly(pattern) : action.canSubstitute(pattern)) {
            count++;
          }

          if (count > n) return false;
        }

        return n == count;
      }
    });
  }

  protected InequalitySystem getInequalitiesSystem(EditorCell contextCell) {
    return null;
  }

  @Override
  public List<SubstituteAction> getSmartMatchingActions(final String pattern, final boolean strictMatching, EditorCell contextCell) {
    // TODO make this thread local maybe?
    ourModelForTypechecking = TemporaryModels.getInstance().create(false, false, TempModuleOptions.forDefaultModule());
    for (SLanguage l : SModelOperations.getAllLanguageImports(getEditorContext().getModel())) {
      ((SModelInternal) ourModelForTypechecking).addLanguage(l);
    }

    try {
      final InequalitySystem inequalitiesSystem = getInequalitiesSystem(contextCell);

      List<SubstituteAction> substituteActionList = getMatchingActions(pattern, strictMatching);
      if (inequalitiesSystem == null) return substituteActionList;

      List<SubstituteAction> result = new ArrayList<SubstituteAction>();
      for (SubstituteAction nodeSubstituteAction : substituteActionList) {
        try {
          SNode type = nodeSubstituteAction.getActionType(pattern);
          if (type != null && inequalitiesSystem.satisfies(type)) {
            result.add(nodeSubstituteAction);
          }
        } catch (Throwable th) {
          LOG.error("Exception on checking smart matching conditions for action: " + (nodeSubstituteAction == null ? "null" : nodeSubstituteAction.getClass()),
              th);
        }
      }
      return result;
    } catch (RuntimeException rte) {
      LOG.error("Exception while building SmartMatchingActions list", rte);
      return new ArrayList<>();
    } finally {
      TemporaryModels.getInstance().dispose(ourModelForTypechecking);
      ourModelForTypechecking = null;
    }
  }

  @Override
  public List<SubstituteAction> getMatchingActions(final String pattern, final boolean strictMatching) {
    return new ModelAccessHelper(myEditorContext.getRepository()).runReadAction(new Computable<List<SubstituteAction>>() {
      @Override
      public List<SubstituteAction> compute() {
        List<SubstituteAction> actionsFromCache = getActionsFromCache(pattern, strictMatching);
        ArrayList<SubstituteAction> result = new ArrayList<SubstituteAction>(actionsFromCache.size());
        for (SubstituteAction item : actionsFromCache) {
          try {
            if (strictMatching ? item.canSubstituteStrictly(pattern) : SubstituteActionUtil.canSubstitute(item, pattern)) {
              result.add(item);
            }
          } catch (Throwable th) {
            LOG.error("Exception on calling canSubstitute on a substitute action " + (item == null ? "null" : item.getClass()), th);
          }
        }
        putActionsToCache(pattern, strictMatching, result);
        result.trimToSize();
        return result;
      }
    });
  }

  private List<SubstituteAction> getActions() {
    if (myCachedActionList == null) {
      try {
        myCachedActionList = createActions();
      } catch (Throwable th) {
        LOG.error("Exception while creating substitute actions in " + this.getClass(), th);
        return new LinkedList<SubstituteAction>();
      }
    }
    return myCachedActionList;
  }

  private void putActionsToCache(String pattern, boolean strictMatching, List<SubstituteAction> actions) {
    List<SubstituteAction> actionsCopy = new ArrayList<SubstituteAction>(actions);
    if (strictMatching) {
      myStrictPatternsToActionListsCache.put(pattern, actionsCopy);
    } else {
      myPatternsToActionListsCache.put(pattern, actionsCopy);
    }
  }

  private List<SubstituteAction> getActionsFromCache(String pattern, boolean strictMatching) {
    if (pattern == null) {
      return Collections.unmodifiableList(getActions());
    }
    if (!strictMatching) {
      if (pattern.isEmpty()) {
        if (myPatternsToActionListsCache.containsKey(pattern)) {
          return Collections.unmodifiableList(myPatternsToActionListsCache.get(pattern));
        }
      } else {
        for (; pattern.length() > 0; pattern = pattern.substring(0, pattern.length() - 1)) {
          if (myPatternsToActionListsCache.containsKey(pattern)) {
            return Collections.unmodifiableList(myPatternsToActionListsCache.get(pattern));
          }
        }
      }
    } else {
      if (myStrictPatternsToActionListsCache.containsKey(pattern)) {
        return Collections.unmodifiableList(myStrictPatternsToActionListsCache.get(pattern));
      } else if (myPatternsToActionListsCache.containsKey(pattern)) {
        return Collections.unmodifiableList(myPatternsToActionListsCache.get(pattern));
      }
    }
    return Collections.unmodifiableList(getActions());
  }
}
