/*
 * Copyright 2012-2016 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.jetpad.model.event;


import jetbrains.jetpad.base.Registration;
import jetbrains.jetpad.base.ThrowableHandlers;

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

/**
 * Reusable container for listeners.
 * It supports:
 * - managing listeners
 * - firing events
 */
public class Listeners<ListenerT> {
  private List<Object> myListeners = null;
  private int myFireDepth;
  private int myListenersCount;

  public boolean isEmpty() {
    return myListeners == null || myListeners.isEmpty();
  }

  public Registration add(final ListenerT l) {
    if (isEmpty()) {
      beforeFirstAdded();
    }

    if (myFireDepth > 0) {
      myListeners.add(new ListenerOp<>(l, true));
    } else {
      if (myListeners == null) {
        myListeners = new ArrayList<>(1);
      }
      myListeners.add(l);
      myListenersCount++;
    }
    return new Registration() {
      @Override
      protected void doRemove() {
        if (myFireDepth > 0) {
          myListeners.add(new ListenerOp<>(l, false));
        } else {
          myListeners.remove(l);
          myListenersCount--;
        }

        if (isEmpty()) {
          afterLastRemoved();
        }
      }
    };
  }

  public void fire(final ListenerCaller<ListenerT> h) {
    if (isEmpty()) return;
    beforeFire();
    try {
      Iterator<ListenerT> it = listenersIterator();
      while (it.hasNext()) {
        ListenerT l = it.next();
        try {
          h.call(l);
        } catch (Throwable t) {
          ThrowableHandlers.handle(t);
        }
      }
    } finally {
      afterFire();
    }
  }

  private Iterator<ListenerT> listenersIterator() {
    final int size = myListenersCount;
    return new Iterator<ListenerT>() {
      private int myIndex = 0;

      private void moveToNext() {
        while (myIndex < size && isRemoved((ListenerT) myListeners.get(myIndex))) {
          myIndex++;
        }
      }

      @Override
      public boolean hasNext() {
        moveToNext();
        return myIndex < size;
      }

      @Override
      public ListenerT next() {
        if (myIndex >= size) {
          throw new IllegalStateException();
        }
        return (ListenerT) myListeners.get(myIndex++);
      }

      @Override
      public void remove() {
        throw new UnsupportedOperationException();
      }
    };
  }

  private boolean isRemoved(ListenerT l) {
    int size = myListeners.size();
    for (int i = myListenersCount; i < size; i++) {
      ListenerOp<ListenerT> op = (ListenerOp<ListenerT>) myListeners.get(i);
      if (!op.add && op.listener == l) return true;
    }
    return false;
  }

  protected void beforeFirstAdded() {
  }

  protected void afterLastRemoved() {
  }

  private void beforeFire() {
    myFireDepth++;
  }

  private void afterFire() {
    myFireDepth--;
    if (myFireDepth == 0) {
      List<Object> opsList = myListeners.subList(myListenersCount, myListeners.size());
      Object[] ops = opsList.toArray();
      opsList.clear();
      for (Object o : ops) {
        ListenerOp<ListenerT> op = (ListenerOp<ListenerT>) o;
        if (op.add) {
          myListeners.add(op.listener);
          myListenersCount++;
        } else {
          myListeners.remove(op.listener);
          myListenersCount--;
        }
      }
    }
  }

  int size() {
    return myListeners == null ? 0 : myListeners.size();
  }

  private static class ListenerOp<ListenerT> {
    final ListenerT listener;
    final boolean add;

    ListenerOp(ListenerT listener, boolean add) {
      this.listener = listener;
      this.add = add;
    }
  }
}
@@ -635,16 +635,57 @@
 vent;%0A%0A%0A
+import jetbrains.jetpad.base.Disposable;%0A
 import j
@@ -768,16 +768,42 @@
 dlers;%0A%0A
+import java.io.Closeable;%0A
 import j
@@ -822,16 +822,46 @@
 ayList;%0A
+import java.util.Collections;%0A
 import j
@@ -1043,16 +1043,217 @@
 nerT%3E %7B%0A
+  private static final Firing EMPTY = new Firing() %7B%0A    @Override%0A    public void close() %7B%0A    %7D%0A%0A    @Override%0A    public Iterator iterator() %7B%0A      return Collections.emptyIterator();%0A    %7D%0A  %7D;%0A%0A
   privat
@@ -2178,134 +2178,44 @@
     
-if (isEmpty()) return;%0A    beforeFire();%0A    try %7B%0A      Iterator%3CListenerT%3E it = listenersIterator();%0A      while (it.hasNext
+try (Firing%3CListenerT%3E firing = fire
 ()) 
@@ -2214,34 +2214,37 @@
 fire()) %7B%0A      
-  
+for (
 ListenerT l = it
@@ -2243,20 +2243,19 @@
 T l 
-= it.next();
+: firing) %7B
 %0A   
@@ -2384,19 +2384,326 @@
    %7D
- finally
+%0A  %7D%0A%0A  public Firing%3CListenerT%3E fire() %7B%0A    if (isEmpty()) %7B%0A      return (Firing%3CListenerT%3E) EMPTY;%0A    %7D%0A%0A    beforeFire();%0A    return new Firing%3CListenerT%3E() %7B%0A      @Override%0A      public Iterator%3CListenerT%3E iterator() %7B%0A        return listenersIterator();%0A      %7D%0A%0A      @Override%0A      public void close()
  %7B%0A
+  
     
@@ -2717,25 +2717,34 @@
 Fire();%0A    
-%7D
+  %7D%0A    %7D;
 %0A  %7D%0A%0A  priv
@@ -4512,24 +4512,125 @@
 ize();%0A  %7D%0A%0A
+  public interface Firing%3CListenerT%3E extends Closeable, Iterable%3CListenerT%3E %7B%0A    void close();%0A  %7D%0A%0A
   private st

introduce Firing