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

import jetbrains.mps.logging.Logger;
import jetbrains.mps.smodel.legacy.ConceptMetaInfoConverter;
import jetbrains.mps.util.InternUtil;
import jetbrains.mps.util.WeakSet;
import jetbrains.mps.util.annotation.ToRemove;
import org.apache.log4j.Level;
import org.apache.log4j.LogManager;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.mps.annotations.Immutable;
import org.jetbrains.mps.openapi.language.SReferenceLink;
import org.jetbrains.mps.openapi.model.SModel;
import org.jetbrains.mps.openapi.model.SModelReference;
import org.jetbrains.mps.openapi.model.SNode;
import org.jetbrains.mps.openapi.model.SNodeId;
import org.jetbrains.mps.openapi.model.SNodeReference;

import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.Stack;

public abstract class SReference implements org.jetbrains.mps.openapi.model.SReference {
  public static final SReference[] EMPTY_ARRAY = new SReference[0];
  private static final Set<SReference> ourErrorReportedRefs = new WeakSet<SReference>();
  private final static ThreadLocal<Boolean> ourLoggingOff = new ThreadLocal<Boolean>() {
    @Override
    protected Boolean initialValue() {
      return false;
    }
  };
  protected final SNode mySourceNode; // made protected only for assert in DynamicReference
  private SReferenceLink myRoleId;
  private volatile String myResolveInfo;

  /**
   * role must be "genuine", interned
   */
  @Deprecated
  protected SReference(String role, SNode sourceNode) {
    mySourceNode = sourceNode;
    assert sourceNode != null;
    myRoleId = ((ConceptMetaInfoConverter) sourceNode.getConcept()).convertAssociation(role);
  }

  protected SReference(SReferenceLink role, SNode sourceNode) {
    myRoleId = role;
    mySourceNode = sourceNode;
  }

  public static SReference create(SReferenceLink id, SNode sourceNode, SNode targetNode) {
    if (sourceNode.getModel() != null && targetNode.getModel() != null) {
      // 'mature' reference
      return new StaticReference(id, sourceNode, targetNode.getModel().getReference(), targetNode.getNodeId(), targetNode.getName());
    }
    return new StaticReference(id, sourceNode, targetNode);
  }

  @Deprecated
  @ToRemove(version = 3.2)
  public static SReference create(String role, SNode sourceNode, SNode targetNode) {
    if (sourceNode.getModel() != null && targetNode.getModel() != null) {
      // 'mature' reference
      return new StaticReference(role, sourceNode, targetNode.getModel().getReference(), targetNode.getNodeId(), targetNode.getName());
    }
    return new StaticReference(role, sourceNode, targetNode);
  }

  public static SReference create(SReferenceLink role, SNode sourceNode, SModelReference targetModelReference, SNodeId targetNodeId) {
    return new StaticReference(role, sourceNode, targetModelReference, targetNodeId, null);
  }
  public static SReference create(SReferenceLink role, SNode sourceNode, SModelReference targetModelReference, SNodeId targetNodeId, String resolveInfo) {
    return new StaticReference(role, sourceNode, targetModelReference, targetNodeId, resolveInfo);
  }

  public static SReference create(SReferenceLink role, SNode sourceNode, SNodeReference pointer, String resolveInfo) {
    return create(role, sourceNode, pointer.getModelReference(), pointer.getNodeId(), resolveInfo);
  }

  @Deprecated
  @ToRemove(version = 3.2)
  public static SReference create(String role, SNode sourceNode, SModelReference targetModelReference, SNodeId targetNodeId) {
    return new StaticReference(role, sourceNode, targetModelReference, targetNodeId, null);
  }

  @Deprecated
  @ToRemove(version = 3.3)
  public static SReference create(String role, SNode sourceNode, SModelReference targetModelReference, SNodeId targetNodeId, String resolveInfo) {
    return new StaticReference(role, sourceNode, targetModelReference, targetNodeId, resolveInfo);
  }

  @Deprecated
  @ToRemove(version = 3.3)
  public static SReference create(String role, SNode sourceNode, SNodeReference pointer, String resolveInfo) {
    return create(role, sourceNode, pointer.getModelReference(), pointer.getNodeId(), resolveInfo);
  }

  @Deprecated
  @ToRemove(version = 3.3)
  public static SReference create(String role, SNode sourceNode, SNode targetNode, String resolveInfo) {
    SReference ref = create(role, sourceNode, targetNode);
    ref.setResolveInfo(resolveInfo);
    return ref;
  }

  /**
   * @return Whether logging was really disabled by this call, i.e. it wasn't already disabled before
   */
  public static boolean disableLogging() {
    boolean wasOff = ourLoggingOff.get();
    ourLoggingOff.set(true);
    return !wasOff;
  }

  public static void enableLogging() {
    ourLoggingOff.set(false);
  }

  public static SNode getTargetNodeSilently(org.jetbrains.mps.openapi.model.SReference ref) {
    boolean needToEnableLogging = false;
    try {
      needToEnableLogging = disableLogging();
      return ref.getTargetNode();
    } finally {
      if (needToEnableLogging) {
        enableLogging();
      }
    }
  }

  @Override
  @Deprecated
  @ToRemove(version = 3.2)
  public String getRole() {
    return myRoleId.getRoleName();
  }

  @Deprecated
  @ToRemove(version = 3.2)
  public void setRole(String newRole) {
    myRoleId = ((ConceptMetaInfoConverter) mySourceNode.getConcept()).convertAssociation(newRole);
  }

  @Override
  public SReferenceLink getLink() {
    return myRoleId;
  }

  @Override
  public SNode getSourceNode() {
    return mySourceNode;
  }

  //-------------------------

  @Override
  public final SNode getTargetNode() {
    return getTargetNode_internal();
  }

  @Override
  public SNodeReference getTargetNodeReference() {
    return new SNodePointer(getTargetSModelReference(), getTargetNodeId());
  }

  //-------- factory methods -----------

  @Override
  @Nullable
  public abstract SModelReference getTargetSModelReference();

  @Deprecated
  /**
   * Use method in SReferenceBase class, as when you change ref, you know what ref it is
   * @Deprecated in 3.0
   */
  public abstract void setTargetSModelReference(@NotNull SModelReference targetModelReference);

  @Override
  @Nullable
  public SNodeId getTargetNodeId() {
    SNode targetNode = getTargetNode();
    return targetNode == null ? null : targetNode.getNodeId();
  }

  public void makeDirect() {

  }

  public boolean makeIndirect() {
    return false;
  }

  public String getResolveInfo() {
    return myResolveInfo;
  }

  //-------- error logging -----------

  public void setResolveInfo(String info) {
    myResolveInfo = InternUtil.intern(info);
  }

  protected abstract SNode getTargetNode_internal();

  /**
   * prints error to log
   * @param onlyWarn if true then warning must be printed out. Must be true almost always.
   */
  protected final void error(String message, boolean onlyWarn, ProblemDescription... problems) {
    if (!ourLoggingOff.get()) {
      //skip errors in java stubs because they can have reference to classes that doesn't present in the class path
      SModel model = getSourceNode().getModel();
      if (model != null && SModelStereotype.isStubModel(model)) {
        return;
      }

      synchronized (ourErrorReportedRefs) {
        if (!ourErrorReportedRefs.contains(this)) {
          ourErrorReportedRefs.add(this);

          String msg = String.format("Could not resolve reference '%s' from %s.", getRole(), getSourceNode());
          msg += "\n" + getSourceNode().getReference();
          if (message != null) {
            msg += "\n" + " -- " + message;
          }
          // fixme AP: multiline log messages is a bad style
          Logger log = Logger.wrap(LogManager.getLogger(this.getClass()));
          if (onlyWarn) log.warning(msg); else log.error(msg);
          if (problems != null) {
            for (ProblemDescription pd : problems) {
              if (onlyWarn) log.warning(pd.getMessage(), pd.getNode()); else log.error(pd.getMessage(), pd.getNode());
            }
          }
        }
      }
    }
  }

  @Immutable
  public static final class ProblemDescription {

    private final SNodeReference myNode;
    private final String myMessage;

    public ProblemDescription(@NotNull SNodeReference node, @NotNull String message) {
      myNode = node;
      myMessage = message;
    }

    @NotNull
    public SNodeReference getNode() {
      return myNode;
    }

    public String getMessage() {
      return myMessage;
    }
  }
}
