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

import jetbrains.mps.util.annotation.ToRemove;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.mps.openapi.event.SNodeAddEvent;
import org.jetbrains.mps.openapi.event.SNodeReadEvent;
import org.jetbrains.mps.openapi.event.SNodeRemoveEvent;
import org.jetbrains.mps.openapi.event.SPropertyChangeEvent;
import org.jetbrains.mps.openapi.event.SPropertyReadEvent;
import org.jetbrains.mps.openapi.event.SReferenceChangeEvent;
import org.jetbrains.mps.openapi.event.SReferenceReadEvent;
import org.jetbrains.mps.openapi.language.SContainmentLink;
import org.jetbrains.mps.openapi.language.SProperty;
import org.jetbrains.mps.openapi.language.SReferenceLink;
import org.jetbrains.mps.openapi.model.EditableSModel;
import org.jetbrains.mps.openapi.model.SModel;
import org.jetbrains.mps.openapi.model.SModelAccessListener;
import org.jetbrains.mps.openapi.model.SModelChangeListener;
import org.jetbrains.mps.openapi.model.SNode;
import org.jetbrains.mps.openapi.model.SNodeAccessListener;
import org.jetbrains.mps.openapi.model.SNodeChangeListener;
import org.jetbrains.mps.openapi.model.SReference;

import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;

/**
 * Facility to track model listeners and to dispatch events.
 * Responsible for OpenAPI listeners only (i.e. not about legacy and soon-to-cease {@link jetbrains.mps.smodel.event.SModelListener}).
 * Keeping this separate from SModelBase gives flexibility in implementing SModel.
 * @author Artem Tikhomirov
 */
public final class ModelEventDispatch {
  // model we dispatch events for
  private final SModel myModel;
  // same as myModel, casted to EditableSModel for convenience, or null if myModel is not editable
  private final EditableSModel myEditableSModel;
  private final List<SNodeAccessListener> myAccessListeners = new CopyOnWriteArrayList<SNodeAccessListener>();
  private final List<SNodeChangeListener> myChangeListeners = new CopyOnWriteArrayList<SNodeChangeListener>();
  @ToRemove(version = 3.3)
  private final List<LegacyNodeAccessListener> myLegacyReadListeners = new CopyOnWriteArrayList<LegacyNodeAccessListener>();
  @ToRemove(version = 3.3)
  private final List<LegacyNodeChangeListener> myLegacyChangeListeners = new CopyOnWriteArrayList<LegacyNodeChangeListener>();

  public ModelEventDispatch(@NotNull SModel model) {
    myModel = model;
    myEditableSModel = model instanceof EditableSModel ? (EditableSModel) model : null;
  }

  @ToRemove(version = 3.3)
  public void addAccessListener(@Nullable SModelAccessListener l) {
    if (l == null) {
      return;
    }
    LegacyNodeAccessListener wrap = new LegacyNodeAccessListener(l);
    addAccessListener(wrap);
    myLegacyReadListeners.add(wrap);
  }

  @ToRemove(version = 3.3)
  public void removeAccessListener(@Nullable SModelAccessListener l) {
    if (l == null) {
      return;
    }
    LegacyNodeAccessListener wrap = null;
    for (LegacyNodeAccessListener w : myLegacyReadListeners) {
      if (w.wraps(l)) {
        wrap = w;
        break;
      }
    }
    if (wrap == null) {
      return;
    }
    myLegacyReadListeners.remove(wrap);
    removeAccessListener(wrap);
  }

  public void addAccessListener(@Nullable SNodeAccessListener l) {
    if (l != null) {
      myAccessListeners.add(l);
    }
  }

  public void removeAccessListener(@Nullable SNodeAccessListener l) {
    if (l != null) {
      myAccessListeners.remove(l);
    }
  }

  @ToRemove(version = 3.3)
  public void addChangeListener(SModelChangeListener l) {
    if (l == null) {
      return;
    }
    LegacyNodeChangeListener wrap = new LegacyNodeChangeListener(l);
    myLegacyChangeListeners.add(wrap);
    addChangeListener(wrap);
  }

  @ToRemove(version = 3.3)
  public void removeChangeListener(SModelChangeListener l) {
    if (l == null) {
      return;
    }
    LegacyNodeChangeListener wrap = null;
    for (LegacyNodeChangeListener w : myLegacyChangeListeners) {
      if (w.wraps(l)) {
        wrap = w;
        break;
      }
    }
    if (wrap == null) {
      return;
    }
    myLegacyChangeListeners.remove(wrap);
    removeChangeListener(wrap);
  }

  public void addChangeListener(SNodeChangeListener l) {
    if (l == null) {
      return;
    }
    myChangeListeners.add(l);
  }

  public void removeChangeListener(SNodeChangeListener l) {
    if (l == null) {
      return;
    }
    myChangeListeners.remove(l);
  }

  public void fireNodeRead(SNode node) {
    if (myAccessListeners.isEmpty()) {
      return;
    }
    final SNodeReadEvent event = new SNodeReadEvent(node);
    for (SNodeAccessListener l : myAccessListeners) {
      l.nodeRead(event);
    }
  }

  public void fireReferenceRead(SNode node, SReferenceLink reference) {
    if (myAccessListeners.isEmpty()) {
      return;
    }
    final SReferenceReadEvent event = new SReferenceReadEvent(node, reference);
    for (SNodeAccessListener l : myAccessListeners) {
      l.referenceRead(event);
    }
  }

  public void firePropertyRead(SNode node, SProperty property) {
    if (myAccessListeners.isEmpty()) {
      return;
    }
    final SPropertyReadEvent event = new SPropertyReadEvent(node, property);
    for (SNodeAccessListener l : myAccessListeners) {
      l.propertyRead(event);
    }
  }

  public void fireReferenceChange(SNode node, SReferenceLink role, SReference oldValue, SReference newValue) {
    markEditableModelChanged();
    if (myChangeListeners.isEmpty()) {
      return;
    }
    final SReferenceChangeEvent event = new SReferenceChangeEvent(myModel, node, role, oldValue, newValue);
    for (SNodeChangeListener l : myChangeListeners) {
      l.referenceChanged(event);
    }
  }

  public void firePropertyChange(SNode node, SProperty property, String oldValue, String newValue) {
    markEditableModelChanged();
    if (myChangeListeners.isEmpty()) {
      return;
    }
    final SPropertyChangeEvent event = new SPropertyChangeEvent(myModel, node, property, oldValue, newValue);
    for (SNodeChangeListener l : myChangeListeners) {
      l.propertyChanged(event);
    }
  }

  public void fireNodeAdd(SNode node, SContainmentLink role, SNode child) {
    markEditableModelChanged();
    if (myChangeListeners.isEmpty()) {
      return;
    }
    final SNodeAddEvent event = role == null ? new SNodeAddEvent(myModel, child) : new SNodeAddEvent(myModel, node, child, role);
    for (SNodeChangeListener l : myChangeListeners) {
      l.nodeAdded(event);
    }
  }

  public void fireNodeRemove(SNode node, SContainmentLink role, SNode child) {
    markEditableModelChanged();
    if (myChangeListeners.isEmpty()) {
      return;
    }
    final SNodeRemoveEvent event = role == null ? new SNodeRemoveEvent(myModel, child) : new SNodeRemoveEvent(myModel, node, child, role);
    for (SNodeChangeListener l : myChangeListeners) {
      l.nodeRemoved(event);
    }
  }

  // instead of EditableSModelBase attaching a change listener to itself to update its 'changed' state,
  // we update this state from event dispatcher
  private void markEditableModelChanged() {
    if (myEditableSModel != null) {
      myEditableSModel.setChanged(true);
    }
  }
}
