/*
 * Copyright 2003-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 org.jetbrains.mps.openapi.model;

import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.mps.openapi.language.SConcept;
import org.jetbrains.mps.openapi.module.SModule;
import org.jetbrains.mps.openapi.module.SRepository;
import org.jetbrains.mps.openapi.persistence.DataSource;
import org.jetbrains.mps.openapi.persistence.ModelRoot;

/**
 * Represents a model. Models are loaded lazily when needed.
 */
public interface SModel {
  SRepository getRepository();

  /**
   * Returns the id of the model valid within the containing module.
   */
  @NotNull
  SModelId getModelId();

  /**
   * This is an equivalent to getModelReference().getModelName
   * The returned name of the model may include a stereotype, such as 'generator' or 'tests', separated by the '@' character,
   * e.g. jetbrains.mps.sample.generator.main@generator
   * @deprecated Prefer {@link #getName()} with access to distinct fractions of the name
   */
  @Deprecated
  String getModelName();

  /**
   * This is an equivalent to <code>getModelReference().getName()</code>
   * @return name of the model in its full glory
   */
  @NotNull
  SModelName getName();

  @NotNull
  SModelReference getReference();

  ModelRoot getModelRoot();

  /**
   * Retrieves the owning module
   * TODO: fix remove SModule!
   */
  SModule getModule();

  /**
   * Returns a collection of root nodes. Root nodes are all nodes added to model using addRootNode.
   * todo VP: should be immutable collection? Currently it isn't.
   */
  Iterable<SNode> getRootNodes();

  /**
   * Instantiates an SNode of the given concept, suitable for use in this model.
   */
  SNode createNode(@NotNull SConcept concept);

  /**
   * Instantiates an SNode of the given concept, suitable for use in this model, optionally specifying an id for the node.
   * If no <code>nodeId</code> was supplied, the call is identical to {@link #createNode(org.jetbrains.mps.openapi.language.SConcept)}
   */
  SNode createNode(@NotNull SConcept concept, @Nullable SNodeId nodeId);

  /**
   * Adds the node as a root to this model.
   * Each node in the underlying subtree becomes connected to this model and returns it from the getModel() method.
   *
   * @throws jetbrains.mps.smodel.IllegalModelChangeError
   *          when invoked on a read-only model or outside of a valid command.
   */
  void addRootNode(SNode node);

  /**
   * Removes the whole subtree from the model.
   *
   * @throws jetbrains.mps.smodel.IllegalModelChangeError
   *          when invoked on a read-only model or outside of a valid command.
   */
  void removeRootNode(SNode node);

  SNode getNode(SNodeId id);

  /**
   * The data source which this model was loaded from.
   */
  @NotNull
  DataSource getSource();

  /**
   * No changes are permitted.
   * For read-only models all modification operations always throw {@link jetbrains.mps.smodel.IllegalModelChangeError}.
   */
  boolean isReadOnly();

  /**
   * The model is fully loaded into memory.
   */
  boolean isLoaded();

  /**
   * When owning a read action lock, this method will fully load the model from the storage.
   * Does nothing if already loaded.
   * The load() method is called automatically on a not-loaded model whenever elements from it are being resolved.
   * Problems can be retrieved later by the {@link #getProblems()} method.
   */
  void load();

  /**
   * The list of persistence-specific model problems (like syntax or I/O errors).
   * Returns empty list if this model is not loaded yet.
   */
  @NotNull
  Iterable<Problem> getProblems();

  /**
   * When owning a write action lock, this method will discard the in-memory representation of the model.
   * A modified model is first saved into the storage so that the changes are preserved.
   */
  void unload();

  void addModelListener(SModelListener l);

  void removeModelListener(SModelListener l);

  /**
   * This method will be removed after 3.3 release.
   * @deprecated use {@link #addAccessListener(SNodeAccessListener)}
   */
  @Deprecated
  void addAccessListener(SModelAccessListener l);

  /**
   * This method will be removed after 3.3 release.
   * @deprecated use {@link #removeAccessListener(SNodeAccessListener)}
   */
  @Deprecated
  void removeAccessListener(SModelAccessListener l);

  /**
   * @param l listener to add, tolerates <code>null</code>
   * @since 3.3
   */
  void addAccessListener(SNodeAccessListener l);

  /**
   * @param l listener to remove, tolerates <code>null</code>
   * @since 3.3
   */
  void removeAccessListener(SNodeAccessListener l);

  /**
   * As {@link org.jetbrains.mps.openapi.model.SNode} API suggests, any model could be modified. However, it's up to model's
   * implementation to decide whether to send notifications about changes or not.
   * Attaching a change listener to a model doesn't guarantee changes get dispatched to the listener,
   * unless it's a model that explicitly states that as part of its contract (e.g. {@link org.jetbrains.mps.openapi.model.EditableSModel}).
   *
   * Note, there's no guarantee about listener uniqueness, i.e. it's unspecified what happens if the same listener instance is added twice.
   *
   * @param l listener to add, tolerates <code>null</code>
   * @since 3.3
   */
  void addChangeListener(SNodeChangeListener l);

  /**
   * @param l listener to remove, tolerates <code>null</code> and unknown (not registered) listeners
   * @since 3.3
   */
  void removeChangeListener(SNodeChangeListener l);


  /**
   * Represents a problem with the persistence.
   */
  interface Problem {

    enum Kind {
      Load,
      Save
    }

    /**
     * Returns whether it was a save or load problem. Save problems can arise when an AST content
     * doesn't fit into the persistence format.
     */
    Kind getKind();

    /**
     * When line and column are available, returns the location of the stream, where the problem occurred,
     * or just plain text description of the location otherwise.
     */
    String getLocation();

    int getColumn();

    int getLine();

    String getText();

    /**
     * Errors usually cause model to be partially loaded, so it cannot be saved back to the storage later.
     */
    boolean isError();

    /**
     * The incomplete node (when available) for load problems, or a node which caused troubles during save operation.
     */
    SNode getNode();
  }
}
