/*
 * Copyright 2003-2012 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.idea.java.psi;

import com.intellij.openapi.components.ProjectComponent;
import com.intellij.openapi.editor.Editor;
import com.intellij.openapi.fileEditor.FileEditorManager;
import com.intellij.openapi.project.Project;
import com.intellij.psi.*;
import com.intellij.refactoring.rename.inplace.InplaceRefactoring;
import com.intellij.util.messages.MessageBusConnection;
import jetbrains.mps.ide.platform.watching.ReloadAction;
import jetbrains.mps.ide.platform.watching.ReloadManagerComponent;
import jetbrains.mps.idea.java.psi.JavaPsiListener.PsiEvent;
import jetbrains.mps.idea.core.psi.impl.MPSPsiNodeBase;
import org.jetbrains.annotations.NotNull;

import java.util.*;

/**
 * danilla 11/12/12
 */

public class PsiChangesWatcher implements ProjectComponent {

  private final Object LOCK = new Object();

  private Project myProject;
  private PsiManager myPsiManager;

  private Set<JavaPsiListener> myListeners = new HashSet<JavaPsiListener>();
  // this is to allow listeners call code that adds/removes another listeners
  // e.g. modelRoot (listener) calls update() that attaches models, which in turn start to listen PSI on attach
  // note: it all happens in one same thread, so LOCK doesn't work as a guard
  private boolean isNotifying = false;
  private Set<JavaPsiListener> toBeAdded = new HashSet<JavaPsiListener>();
  private Set<JavaPsiListener> toBeRemoved = new HashSet<JavaPsiListener>();

  private MessageBusConnection connection;
  private PsiTreeChangeListener myOwnPsiListener = new OwnPsiListener();

  private ReloadManagerComponent myReloadManager;

  PsiChangesWatcher(Project p, ReloadManagerComponent reloadManager) {
    myProject = p;
    myReloadManager = reloadManager;
  }

  @Override
  public void initComponent() {
    myPsiManager = PsiManager.getInstance(myProject);
  }

  @Override
  public void disposeComponent() {
    myListeners.clear();
  }

  @NotNull
  @Override
  public String getComponentName() {
    return "JavaStubPsiChangesWatcher";
  }

  @Override
  public void projectOpened() {
    // Q: check for listeners.notEmpty and MPS facet?
    myPsiManager.addPsiTreeChangeListener(myOwnPsiListener);
  }

  @Override
  public void projectClosed() {
    myPsiManager.removePsiTreeChangeListener(myOwnPsiListener);
  }

  public void addListener(JavaPsiListener listener) {
    synchronized (LOCK) {
      if (!isNotifying) {
        myListeners.add(listener);
      } else {
        toBeAdded.add(listener);
      }

      // in case it was previously removed during this notification loop
      toBeRemoved.remove(listener);
    }
  }

  public void removeListener(JavaPsiListener listener) {
    synchronized (LOCK) {
      if (!isNotifying) {
        myListeners.remove(listener);
      } else {
        toBeRemoved.add(listener);
      }
      // in case it was pending-added during this notification loop
      toBeAdded.remove(listener);
    }
  }

  // called by PsiChangeProcessor
  /* package */ void notifyListeners(PsiEvent event) {
    synchronized (LOCK) {
      try {
        isNotifying = true;
        for (JavaPsiListener l : myListeners) {
          l.psiChanged(event);
        }
      } finally {

        isNotifying = false;
        // handling pending additions/removals which have been added/removed as a result
        // of previous listener invocations
        if (!toBeAdded.isEmpty()) {
          myListeners.addAll(toBeAdded);
        }
        if (!toBeRemoved.isEmpty()) {
          myListeners.removeAll(toBeRemoved);
        }

        toBeAdded.clear();
        toBeRemoved.clear();
      }
    }
  }

  private class OwnPsiListener extends PsiTreeChangeAdapter {
    @Override
    public void childAdded(final PsiTreeChangeEvent event) {
      if (isFromMPSPsiProvider(event)) return;

      myReloadManager.runReload(PsiChangeProcessor.class, new ReloadAction<PsiChangeProcessor>() {
        @Override
        public void runAction(PsiChangeProcessor p) {
          p.childAdded(event);
        }
      });
    }

    @Override
    public void childRemoved(final PsiTreeChangeEvent event) {
      if (isFromMPSPsiProvider(event)) return;

      myReloadManager.runReload(PsiChangeProcessor.class, new ReloadAction<PsiChangeProcessor>() {
        @Override
        public void runAction(PsiChangeProcessor p) {
          p.childRemoved(event);
        }
      });
    }

    @Override
    public void childReplaced(final PsiTreeChangeEvent event) {
      if (isFromMPSPsiProvider(event)) return;

      Editor editor = FileEditorManager.getInstance(myProject).getSelectedTextEditor();
      if (editor != null) {
        InplaceRefactoring ir = editor.getUserData(InplaceRefactoring.INPLACE_RENAMER);
        if (ir != null) {
          return;
        }
      }

      myReloadManager.runReload(PsiChangeProcessor.class, new ReloadAction<PsiChangeProcessor>() {
        @Override
        public void runAction(PsiChangeProcessor p) {
          p.childReplaced(event);
        }
      });
    }

    @Override
    public void childrenChanged(final PsiTreeChangeEvent event) {
      if (isFromMPSPsiProvider(event)) return;

      myReloadManager.runReload(PsiChangeProcessor.class, new ReloadAction<PsiChangeProcessor>() {
        @Override
        public void runAction(PsiChangeProcessor p) {
          p.childrenChanged(event);
        }
      });
    }

    @Override
    public void childMoved(@NotNull final PsiTreeChangeEvent event) {
      if (isFromMPSPsiProvider(event)) return;

      myReloadManager.runReload(PsiChangeProcessor.class, new ReloadAction<PsiChangeProcessor>() {
        @Override
        public void runAction(PsiChangeProcessor p) {
          p.childMoved(event);
        }
      });
    }

    @Override
    public void propertyChanged(@NotNull final PsiTreeChangeEvent event) {
      if (isFromMPSPsiProvider(event)) return;

      myReloadManager.runReload(PsiChangeProcessor.class, new ReloadAction<PsiChangeProcessor>() {
        @Override
        public void runAction(PsiChangeProcessor p) {
          p.propertyChanged(event);
        }
      });
    }

    private boolean isFromMPSPsiProvider(PsiTreeChangeEvent event) {
      PsiElement parent = event.getParent();
      return (parent instanceof MPSPsiNodeBase);
    }
  }
}
