/*
 * Copyright 2003-2017 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.smodel.event.ModelEventDispatch;
import jetbrains.mps.smodel.references.UnregisteredNodes;
import org.jetbrains.annotations.NotNull;
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.module.SRepository;

/**
 * Normal state of any node, being part of a model.
 *
 * Events are dispatched, model access ensured.
 * <p>
 * OpenAPI listeners ({@link org.jetbrains.mps.openapi.model.SNodeAccessListener}, {@link org.jetbrains.mps.openapi.model.SModelChangeListener},
 * {@link org.jetbrains.mps.openapi.model.SModelAccessListener}) are notified through {@link jetbrains.mps.smodel.event.ModelEventDispatch},
 * this class shall not depend on particular openapi.SModel implementation (e.g. SModelBase or EditableSModelBase).
 * <p>
 * Legacy listeners ({@link jetbrains.mps.smodel.event.SModelListener}, {@link jetbrains.mps.smodel.NodeReadEventsCaster} and
 * {@link jetbrains.mps.smodel.NodeReadAccessCasterInEditor} are handled here.
 * <p>
 * IMPORTANT: property/reference access shall not trigger node read. Node read is triggered once the node is obtained from the model,
 * either as children, sibling or any other navigation means.
 *
 * @author Artem Tikhomirov
 */
final class AttachedNodeOwner extends SNodeOwner {

  private final SModel myModel;
  // can be null
  private ModelEventDispatch myEventDispatch;

  public AttachedNodeOwner(@NotNull SModel model) {
    myModel = model;
  }

  /*package*/ void setEventDispatch(ModelEventDispatch dispatch) {
    // the reason why I don't care to make myEventDispatch immediately visible
    // in a multi-thread environment (i.e. one thread reads model and dispatches notifications
    // while another attaches the model to model descriptor and updates myEventDispatch) as there's
    // no contract whatsoever about what happens in this case. In a single-thread, this assignment would
    // 'happen-before' any subsequent read and we are all set.
    myEventDispatch = dispatch;
  }

  @Override
  public void assertLegalRead() {
    // FIXME explicit attach to set repository? So that it behaves exactly as it was prior to SNodeOwner?
    final SRepository repo = myModel.getRepository();
    if (repo != null) {
      repo.getModelAccess().checkReadAccess();
    }
  }

  @Override
  public void assertLegalChange() {
    final SModel model = myModel;
    if (model.isUpdateMode()) {
      return;
    }
    final SRepository repo = myModel.getRepository();
    if (repo == null) {
      return;
    }
    repo.getModelAccess().checkWriteAccess();
    // due to isCommandAction() check one can't modify an attached model from within a write action (e.g. JavaDebugEvaluate facet during make).
    // Is it right? What's the reason to have write action then?
    if (!repo.getModelAccess().isCommandAction()) {
      throw new IllegalModelChangeError("registered node can be modified only inside a command or during model loading process " + myModel);
    }
  }

  @Override
  public SModel getModel() {
    return myModel;
  }

  @Override
  public void registerNode(SNode node) {
    myModel.registerNode(node);
    // FIXME why UnregisteredNodes.put in SNode#unRegisterFromModel (#detach(SNodeOwner)) us conditioned with !isUpdateMode(), and this one is not?
    UnregisteredNodes.instance().remove(node);
  }

  @Override
  public void unregisterNode(SNode node) {
    myModel.unregisterNode(node);
    if (!myModel.isUpdateMode()) {
      UnregisteredNodes.instance().put(node);
    }
  }

  @Override
  void performUndoableAction(org.jetbrains.mps.openapi.model.SNode node, SNodeUndoableAction action) {
    myModel.performUndoableAction(action);
  }

  @Override
  /*package*/ void fireNodeRead(SNode node, boolean needUnclassified) {
    // nodeRead()
    if (myModel.isUpdateMode()) {
      return;
    }
    final ModelEventDispatch md = myEventDispatch;
    if (md != null) {
      md.fireNodeRead(node);
    }
    if (!myModel.canFireReadEvent()) {
      return;
    }
    // fireNodeReadAccess()
    NodeReadAccessCasterInEditor.fireNodeReadAccessed(node);
    if (needUnclassified) {
      // fireNodeUnclassifiedReadAccess()
      NodeReadEventsCaster.fireNodeUnclassifiedReadAccess(node);
    }
  }

  @Override
  /*package*/ void firePropertyRead(SNode node, SProperty p, String value, boolean hasProperty) {
    // propertyRead();
    if (myModel.isUpdateMode()) {
      return;
    }
    final ModelEventDispatch md = myEventDispatch;
    if (md != null) {
      md.firePropertyRead(node, p);
    }
    //firePropertyReadAccessInEditor();
    //fireNodePropertyReadAccess();
    if (!myModel.canFireReadEvent()) {
      return;
    }
    final String propertyName = p.getName();
    NodeReadAccessCasterInEditor.firePropertyReadAccessed(node, propertyName, hasProperty);
    NodeReadEventsCaster.fireNodePropertyReadAccess(node, propertyName, value);
  }

  /**
   * @param link not null
   * @param target may be null
   */
  @Override
  /*package*/ void fireReferenceRead(SNode node, SReferenceLink link, SNode target) {
    if (myModel.isUpdateMode()) {
      return;
    }
    // referenceRead()
    final ModelEventDispatch md = myEventDispatch;
    if (md != null) {
      md.fireReferenceRead(node, link);
    }
    // fireNodeReferentReadAccess();
    if (myModel.canFireReadEvent()) {
      NodeReadEventsCaster.fireNodeReferentReadAccess(node, link.getRoleName(), target);
    }
  }

  @Override
  /*package*/ void firePropertyChange(SNode node, SProperty property, String oldValue, String newValue) {
    if (myModel.isUpdateMode()) {
      return;
    }
    myModel.firePropertyChangedEvent(node, property, oldValue, newValue);
    //propertyChanged(property, oldValue, newValue);
    final ModelEventDispatch md = myEventDispatch;
    if (md != null) {
      md.firePropertyChange(node, property, oldValue, newValue);
    }
  }

  @Override
  /*package*/ void fireReferenceChange(SNode node, SReferenceLink l, org.jetbrains.mps.openapi.model.SReference oldRef, org.jetbrains.mps.openapi.model.SReference newRef) {
    if (myModel.isUpdateMode()) {
      return;
    }
    if (oldRef != null) {
      myModel.fireReferenceRemovedEvent(oldRef);
    }
    if (newRef != null) {
      myModel.fireReferenceAddedEvent(newRef);
    }
    // referenceChanged(l, oldRef, newRef);
    final ModelEventDispatch md = myEventDispatch;
    if (md != null) {
      md.fireReferenceChange(node, l, oldRef, newRef);
    }
  }

  @Override
  /*package*/ void fireNodeAdd(SNode node, SContainmentLink role, SNode child, SNode anchor) {
    if (node == null && role == null) {
      // root
      final ModelEventDispatch md = myEventDispatch;
      if (md != null) {
        md.fireNodeAdd(null, null, child);
      }
      myModel.fireRootAddedEvent(child);
      return;
    }
    if (myModel.isUpdateMode()) {
      return;
    }
    myModel.fireChildAddedEvent(node, role, child, anchor);
    //nodeAdded(role, child);
    final ModelEventDispatch md = myEventDispatch;
    if (md != null) {
      md.fireNodeAdd(node, role, child);
    }
  }

  @Override
  void fireBeforeNodeRemove(SNode node, SContainmentLink role, SNode child, SNode anchor) {
    if (node == null && role == null) {
      myModel.fireBeforeRootRemovedEvent(child);
    } else {
      myModel.fireBeforeChildRemovedEvent(node, role, child, anchor);
    }
  }

  @Override
  /*package*/ void fireNodeRemove(SNode node, SContainmentLink role, SNode child, SNode anchor) {
    if (node == null && role == null) {
      final ModelEventDispatch md = myEventDispatch;
      if (md != null) {
        md.fireNodeRemove(null, null, child);
      }
      myModel.fireRootRemovedEvent(child);
      return;
    }
    if (myModel.isUpdateMode()) {
      return;
    }
    myModel.fireChildRemovedEvent(node, role, child, anchor);
    //nodeRemoved(child, role);
    final ModelEventDispatch md = myEventDispatch;
    if (md != null) {
      md.fireNodeRemove(node, role, child);
    }
  }
}
