/*
 * Copyright 2003-2011 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.util.Computable;
import org.jetbrains.mps.openapi.model.SModelReference;
import org.jetbrains.mps.openapi.model.SNode;
import org.jetbrains.mps.openapi.model.SNodeId;

import java.util.Stack;

public class NodeReadAccessCasterInEditor {
  private static ThreadLocal<ListenersContainer> ourListenersContainer = new ThreadLocal<ListenersContainer>();

  public static void setCellBuildNodeReadAccessListener(NodeReadAccessInEditorListener listener) {
    getListenersContainer().addListener(listener);
  }

  public static void removeCellBuildNodeAccessListener() {
    ListenersContainer listenersContainer = ourListenersContainer.get();
    assert listenersContainer != null : "Removing not existing listener";
    listenersContainer.removeListener();
    if (listenersContainer.canBeDisposed()) {
      ourListenersContainer.set(null);
    }
  }

  public static void fireNodeReadAccessed(SNode node) {
    ListenersContainer listenersContainer = ourListenersContainer.get();
    if (listenersContainer != null) {
      listenersContainer.fireNodeReadAccessed(node);
    }
  }

  public static void firePropertyReadAccessed(SNode node, String propertyName, boolean propertyExistenceCheck) {
    ListenersContainer listenersContainer = ourListenersContainer.get();
    if (listenersContainer != null) {
      listenersContainer.firePropertyReadAccessed(node, propertyName, propertyExistenceCheck);
    }
  }

  public static void fireReferenceTargetReadAccessed(SNode sourceNode, SModelReference targetModelReference,
      SNodeId targetNodeId) {
    ListenersContainer listenersContainer = ourListenersContainer.get();
    if (listenersContainer != null) {
      listenersContainer.fireReferenceTargetReadAccessed(sourceNode, targetModelReference, targetNodeId);
    }
  }

  public static NodeReadAccessInEditorListener getReadAccessListener() {
    ListenersContainer listenersContainer = ourListenersContainer.get();
    return listenersContainer == null ? null : listenersContainer.getActiveListener();
  }

  public static boolean areEventsBlocked() {
    ListenersContainer listenersContainer = ourListenersContainer.get();
    return listenersContainer != null && listenersContainer.areEventsBlocked();
  }

  public static void setEventsBlocked(boolean blocked) {
    ListenersContainer listenersCotainer = getListenersContainer();
    listenersCotainer.setEventsBlocked(blocked);
    if (listenersCotainer.canBeDisposed()) {
      ourListenersContainer.set(null);
    }
  }

  public static void runReadTransparentAction(Runnable r) {
    boolean wereBlocked = areEventsBlocked();
    boolean wereBlockedNonEditor = NodeReadEventsCaster.areEventsBlocked();
    NodeReadEventsCaster.setEventsBlocked(true);
    setEventsBlocked(true);
    try {
      r.run();
    } finally {
      setEventsBlocked(wereBlocked);
      NodeReadEventsCaster.setEventsBlocked(wereBlockedNonEditor);
    }
  }

  public static <T> T runReadTransparentAction(final Computable<T> c) {
    boolean wereBlocked = areEventsBlocked();
    boolean wereBlockedNonEditor = NodeReadEventsCaster.areEventsBlocked();
    NodeReadEventsCaster.setEventsBlocked(true);
    setEventsBlocked(true);
    try {
      return c.compute();
    } finally {
      setEventsBlocked(wereBlocked);
      NodeReadEventsCaster.setEventsBlocked(wereBlockedNonEditor);
    }
  }

  public static<T> T runCleanPropertyAccessAction(Computable<T> propertyAccess) {
    ListenersContainer listenersContainer = ourListenersContainer.get();
    boolean wasCleanAccessMode = false;
    if (listenersContainer != null) {
      wasCleanAccessMode = listenersContainer.isCleanPropertyAccessMode();
      listenersContainer.setCleanPropertyAccessMode(true);
    }
    try {
      return propertyAccess.compute();
    } finally {
      if (listenersContainer != null) {
        listenersContainer.setCleanPropertyAccessMode(wasCleanAccessMode);
      }
    }
  }

  private static ListenersContainer getListenersContainer() {
    ListenersContainer listeners = ourListenersContainer.get();
    if (listeners == null) {
      listeners = new ListenersContainer();
      ourListenersContainer.set(listeners);
    }
    return listeners;
  }

  static class ListenersContainer {
    private Stack<NodeReadAccessInEditorListener> myListenersStack = new Stack<NodeReadAccessInEditorListener>();
    private boolean myEventsBlocked;
    private boolean myCleanPropertyAccessMode;
    private boolean myPropertyReadEventsSuppressed;

    public void addListener(NodeReadAccessInEditorListener listener) {
      assert listener != null : "Trying to add null as a listener";
      myListenersStack.push(listener);
    }

    public void removeListener() {
      assert !myListenersStack.isEmpty() : "Trying to remove listener from an empty Listeners stack";
      NodeReadAccessInEditorListener listener = myListenersStack.pop();
      if (!myListenersStack.isEmpty()) {
        myListenersStack.peek().addNodesToDependOn(listener.getNodesToDependOn());
        myListenersStack.peek().addRefTargetsToDependOn(listener.getRefTargetsToDependOn());
      }
    }

    public boolean canBeDisposed() {
      return !myEventsBlocked && myListenersStack.isEmpty();
    }

    public NodeReadAccessInEditorListener getActiveListener() {
      return myListenersStack.isEmpty() ? null : myListenersStack.peek();
    }

    public boolean areEventsBlocked() {
      return myEventsBlocked;
    }

    public void setEventsBlocked(boolean eventsBlocked) {
      myEventsBlocked = eventsBlocked;
    }

    public boolean isCleanPropertyAccessMode() {
      return myCleanPropertyAccessMode;
    }

    public void setCleanPropertyAccessMode(boolean cleanPropertyAccess) {
      myCleanPropertyAccessMode = cleanPropertyAccess;
    }

    public void fireNodeReadAccessed(SNode node) {
      if (myEventsBlocked || myListenersStack.isEmpty()) return;
      myListenersStack.peek().nodeUnclassifiedReadAccess(node);
    }

    public void firePropertyReadAccessed(SNode node, String propertyName, boolean propertyExistenceCheck) {
      if (myEventsBlocked || myPropertyReadEventsSuppressed || myListenersStack.isEmpty()) return;

      NodeReadAccessInEditorListener listener = myListenersStack.peek();
      myPropertyReadEventsSuppressed = true;
      try {
        if (myCleanPropertyAccessMode) {
          listener.propertyCleanReadAccess(node, propertyName);
        } else if (propertyExistenceCheck) {
          listener.propertyExistenceAccess(node, propertyName);
        } else {
          listener.propertyDirtyReadAccess(node, propertyName);
        }
      } finally {
        myPropertyReadEventsSuppressed = false;
      }
    }

    public void fireReferenceTargetReadAccessed(SNode sourceNode, SModelReference targetModelReference,
        SNodeId targetNodeId) {
      if (myEventsBlocked || myListenersStack.isEmpty()) return;
      myListenersStack.peek().addRefTargetToDependOn(new jetbrains.mps.smodel.SNodePointer(targetModelReference, targetNodeId));
    }
  }
}
