/*
 * 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.project.structure.modules;

import jetbrains.mps.persistence.MementoImpl;
import jetbrains.mps.project.AbstractModule;
import jetbrains.mps.project.ModuleId;
import jetbrains.mps.project.facets.JavaModuleFacet;
import jetbrains.mps.project.structure.model.ModelRootDescriptor;
import jetbrains.mps.util.annotation.ToRemove;
import jetbrains.mps.util.io.ModelInputStream;
import jetbrains.mps.util.io.ModelOutputStream;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.mps.openapi.language.SLanguage;
import org.jetbrains.mps.openapi.module.SModuleFacet;
import org.jetbrains.mps.openapi.module.SModuleReference;
import org.jetbrains.mps.openapi.module.SRepository;
import org.jetbrains.mps.openapi.persistence.PersistenceFacade;

import java.io.IOException;
import java.util.Collection;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.function.Supplier;

/**
 * This class captures persistence and editing aspects of SModule. Client code shall not use this class
 * unless its purpose is to edit or persist module properties. Use SModule API (or Language/Generator/Solution/DevKit subclasses)
 * to read module dependencies and identity information.
 *
 * -----------------------------------------------------------------------------------------------------------------------------------
 * FIXME This class mixes up the persistence and editing aspects of the {@link AbstractModule} class.
 * FIXME in order to edit facets/model roots in the module a client needs to access such entities as {@link ModuleFacetDescriptor}, {@link ModelRootDescriptor} directly,
 * FIXME when he has just an {@link AbstractModule} (which leads to a low-level module#getModuleDescriptor.getFacetDescriptors().add...)
 * FIXME obviously it is wrong: a client should rather work with {@link SModuleFacet} entities in the case of editing an {@link AbstractModule}, not descriptors.
 * FIXME OTOH it cannot be a plain persistence descriptor since in order to update (more or less) any properties of an {@link AbstractModule}
 * FIXME we use such pattern in the {@code AbstractModule} as:
 * <code>
 *   AbstractModule module;
 *   var descriptor = module.getDescriptor();
 *   <change descriptor freely as we wish>
 *   module.setDescriptor(descriptor); // commit descriptor
 * </code>
 * which is needed in order to guarantee a consistency of the {@link AbstractModule} operations.
 *
 * TODO Also I would rather use in the ModuleDescriptor hierarchy composition instead of inheritance. The {@link #myDeploymentDescriptor} reference is especially repelling here.
 *
 * Road map:
 * We separate the persistence descriptor from the special editing 'handle'.
 * We ensure that the persistence descriptor reflects all the properties we find in our module persistence.
 *
 * AP
 */
public class ModuleDescriptor implements CopyableDescriptor<ModuleDescriptor>  {
  private ModuleId myId;
  private String myNamespace;
  private String myTimestamp;

  private int myModuleVersion;

  private final Collection<ModelRootDescriptor> myModelRoots = new LinkedHashSet<>();
  private final Collection<ModuleFacetDescriptor> myFacets = new LinkedHashSet<>();
  private final Collection<Dependency> myDependencies = new LinkedHashSet<>();
  private final Collection<SModuleReference> myUsedLanguages = new LinkedHashSet<>();
  private final Collection<SModuleReference> myUsedDevkits = new LinkedHashSet<>();
  private final Map<SLanguage, Integer> myLanguageVersions = new LinkedHashMap<>();
  private final Map<SModuleReference, Integer> myDependencyVersions = new LinkedHashMap<>();
  private final Collection<String> myAdditionalJavaStubPaths = new LinkedHashSet<>();
  private final Collection<String> mySourcePaths = new LinkedHashSet<>();
  private DeploymentDescriptor myDeploymentDescriptor; // FIXME must be removed

  private Throwable myLoadException;
  private boolean myUseTransientOutput;
  private boolean myHasLanguageVersions = true;
  private boolean myHasDependencyVersions = true;

  public ModuleDescriptor() {
  }

  public final ModuleId getId() {
    return myId;
  }

  public final void setId(ModuleId id) {
    myId = id;
  }

  public final String getNamespace() {
    return myNamespace;
  }

  public final void setNamespace(String namespace) {
    myNamespace = namespace;
  }

  public final SModuleReference getModuleReference() {
    return PersistenceFacade.getInstance().createModuleReference(getId(), getNamespace());
  }

  public final String getTimestamp() {
    return myTimestamp;
  }

  public final void setTimestamp(String timestamp) {
    myTimestamp = timestamp;
  }

  public boolean getCompileInMPS() {
    throw new UnsupportedOperationException();
  }

  public final Collection<ModelRootDescriptor> getModelRootDescriptors() {
    return myModelRoots;
  }

  public final Collection<ModuleFacetDescriptor> getModuleFacetDescriptors() {
    return myFacets;
  }

  /**
   * PROVISIONAL API, DO NOT USE OUTSIDE OF MPS
   * When facet is added/replaced in a module, we need to register it with persistence (module descriptor).
   * <p/>
   * With this methods, we keep facet persistence management in a single place (rather than
   * <code>facet.save(new Memento())</code> scattered around)
   */
  public final void addFacetDescriptor(@NotNull SModuleFacet facet) {
    removeFacetDescriptor(facet);
    myFacets.add(new ModuleFacetDescriptor(facet.getFacetType(), new MementoImpl()));
  }

  /**
   * PROVISIONAL API, DO NOT USE OUTSIDE OF MPS
   * Forget persistence information for the given facet
   */
  public final void removeFacetDescriptor(@NotNull SModuleFacet facet) {
    final String facetType = facet.getFacetType();
    for (Iterator<ModuleFacetDescriptor> it = myFacets.iterator(); it.hasNext(); ) {
      ModuleFacetDescriptor facetDescriptor = it.next();
      if (facetType.equals(facetDescriptor.getType())) {
        it.remove();
        break;
      }
    }
  }

  /**
   * PROVISIONAL API, DO NOT USE OUTSIDE OF MPS
   * Push facet settings into persistence.
   * If there's no descriptor for the facet, it's ignored (use {@link #addFacetDescriptor(SModuleFacet)} first)
   * This behavior (no update for missing descriptors) is important as facet removal is three-fold - we remove descriptor first,
   * while facet at the module still active, then we update persistent values of existing facets, and then reload module with new descriptor.
   * If we would update missing descriptors, we would effectively resurrect removed descriptors.
   * <p/>
   * It's not clear whether this code shall be part of AbstractModule#save or not. It seems reasonable to
   * push facet settings into persistence on module save, OTOH, the way module settings are edited/updated (i.e. with
   * changes into descriptor and AbstractModule.setModuleDescriptor() + AM.save(), see ModulePropertiesConfigurable)
   * makes me feel update in the descriptor, not in the module, is better option (after all, it's ModuleDescriptor that is responsible
   * for editing of module settings) - it's sort of changes snapshot, applied with a single setModuleDescriptor operation, rather than sequence
   * of SModule changes.
   */
  public final void updateFacetDescriptor(@NotNull SModuleFacet facet) {
    for (ModuleFacetDescriptor facetDescriptor : myFacets) {
      if (facetDescriptor.getType().equals(facet.getFacetType())) {
        facet.save(facetDescriptor.getMemento());
        break;
      }
    }
  }

  public final Collection<Dependency> getDependencies() {
    return myDependencies;
  }

  /**
   * @deprecated Now, used languages of a module are derived from models it owns, and thus
   * this list is generally empty. Although generally there might be modules that keep their used languages, and ModuleDescriptor
   * may keep the list, it shall be the list of SLanguage anyway.
   */
  @Deprecated
  @ToRemove(version = 3.3)
  public final Collection<SModuleReference> getUsedLanguages() {
    return myUsedLanguages;
  }

  public final Map<SLanguage, Integer> getLanguageVersions() {
    return myLanguageVersions;
  }

  public final Map<SModuleReference, Integer> getDependencyVersions() {
    return myDependencyVersions;
  }

  public final Collection<SModuleReference> getUsedDevkits() {
    return myUsedDevkits;
  }

  /**
   * Paths to extra jar files needed to compile and run given module, generally empty unless a module has some peculiar dependencies on existing java libraries.
   * As of today, these come from {@code <stubModelEntry path=""/>} in a module descriptor.
   * according to {@code LanguageDescriptorPersistence}, legacy entries were {@code classPath} and {@code runtimeClassPath}
   * FIXME WHY DOES IT USE String for File location, which FS shall I use to resolve these locations?
   */
  public final Collection<String> getAdditionalJavaStubPaths() {
    return myAdditionalJavaStubPaths;
  }

  /**
   * Additional source files to compile along with module's own generated output.
   * Though, uses are bit odd:
   *  - There's unused {@link AbstractModule#getSourcePaths()}
   *  - JavaModuleFacet manifests these {@link JavaModuleFacet#getAdditionalSourcePaths()}, likely using module descriptor just as a storage (it's what JMF does anyway)
   *  - Make respects these to compile a module
   */
  public final Collection<String> getSourcePaths() {
    return mySourcePaths;
  }

  @Nullable
  public final DeploymentDescriptor getDeploymentDescriptor() {
    return myDeploymentDescriptor;
  }

  public final void setDeploymentDescriptor(DeploymentDescriptor deploymentDescriptor) {
    myDeploymentDescriptor = deploymentDescriptor;
  }

  public boolean updateModelRefs(SRepository repository) {
    return false;
  }

  public boolean updateModuleRefs(SRepository repository) {
    RefUpdateUtil uu = new RefUpdateUtil(repository);
    return RefUpdateUtil.composeUpdates(
      uu.updateModuleRefs(myUsedLanguages),
      uu.updateModuleRefs(myUsedDevkits),
      uu.updateDependencies(myDependencies)
    );
  }

  public Throwable getLoadException() {
    return myLoadException;
  }

  public void setLoadException(Throwable loadException) {
    myLoadException = loadException;
  }

  public boolean isUseTransientOutput() {
    return myUseTransientOutput;
  }

  public void setUseTransientOutput(boolean useTransientOutput) {
    myUseTransientOutput = useTransientOutput;
  }

  protected int getHeaderMarker() {
    return 0x73048111;
  }

  public void save(ModelOutputStream stream) throws IOException {
    stream.writeInt(getHeaderMarker());
    stream.writeModuleID(myId);
    stream.writeString(myNamespace);
    stream.writeString(myTimestamp);

    stream.writeInt(myModelRoots.size());
    for (ModelRootDescriptor root : myModelRoots) {
      root.save(stream);
    }

    stream.writeInt(myFacets.size());
    for (ModuleFacetDescriptor facet : myFacets) {
      facet.save(stream);
    }

    stream.writeInt(myDependencies.size());
    for (Dependency dep : myDependencies) {
      dep.save(stream);
    }

    stream.writeInt(myUsedLanguages.size());
    for (SModuleReference ref : myUsedLanguages) {
      stream.writeModuleReference(ref);
    }

    stream.writeInt(myUsedDevkits.size());
    for (SModuleReference ref : myUsedDevkits) {
      stream.writeModuleReference(ref);
    }

    stream.writeStrings(myAdditionalJavaStubPaths);
    stream.writeStrings(mySourcePaths);

    stream.writeByte(myDeploymentDescriptor != null ? 0x1 : 0x70);
    if (myDeploymentDescriptor != null) {
      myDeploymentDescriptor.save(stream);
    }

    stream.writeBoolean(myUseTransientOutput);
    stream.writeInt(myModuleVersion);

    stream.writeByte(0x3a);
  }

  public void load(ModelInputStream stream) throws IOException {
    if (stream.readInt() != getHeaderMarker()) throw new IOException("bad stream: no module descriptor start marker");
    myId = stream.readModuleID();
    myNamespace = stream.readString();
    myTimestamp = stream.readString();

    myModelRoots.clear();
    for (int size = stream.readInt(); size > 0; size--) {
      myModelRoots.add(ModelRootDescriptor.load(stream));
    }

    myFacets.clear();
    for (int size = stream.readInt(); size > 0; size--) {
      myFacets.add(ModuleFacetDescriptor.load(stream));
    }

    myDependencies.clear();
    for (int size = stream.readInt(); size > 0; size--) {
      Dependency dep = new Dependency();
      dep.load(stream);
      myDependencies.add(dep);
    }

    myUsedLanguages.clear();
    for (int size = stream.readInt(); size > 0; size--) {
      myUsedLanguages.add(stream.readModuleReference());
    }

    myUsedDevkits.clear();
    for (int size = stream.readInt(); size > 0; size--) {
      myUsedDevkits.add(stream.readModuleReference());
    }

    myAdditionalJavaStubPaths.clear();
    myAdditionalJavaStubPaths.addAll(stream.readStrings());

    mySourcePaths.clear();
    mySourcePaths.addAll(stream.readStrings());

    byte b = stream.readByte();
    if (b == 0x1) {
      myDeploymentDescriptor = new DeploymentDescriptor();
      myDeploymentDescriptor.load(stream);
    } else if (b == 0x70) {
      myDeploymentDescriptor = null;
    } else {
      throw new IOException("broken stream");
    }

    myUseTransientOutput = stream.readBoolean();
    myModuleVersion = stream.readInt();

    if (stream.readByte() != 0x3a) throw new IOException("bad stream: no module descriptor end marker");
  }

  /**
   * @deprecated needed it for migration (3.1->3.2)
   */
  @ToRemove(version = 3.4)
  @Deprecated
  public final void setHasLanguageVersions(boolean hasLanguageVersions) {
    myHasLanguageVersions = hasLanguageVersions;
  }

  /**
   * @deprecated needed it for migration (3.2->3.3)
   */
  @ToRemove(version = 3.4)
  @Deprecated
  public final void setHasDependencyVersions(boolean hasDependencyVersions) {
    myHasDependencyVersions = hasDependencyVersions;
  }

  @Deprecated
  public final boolean hasLanguageVersions() {
    return myHasLanguageVersions;
  }

  @ToRemove(version = 3.4)
  @Deprecated
  public final boolean hasDependencyVersions() {
    return myHasDependencyVersions;
  }

  public final int getModuleVersion() {
    return myModuleVersion;
  }

  public final void setModuleVersion(int version) {
    myModuleVersion = version;
  }

  /**
   * utility method to help subclasses implementing the {@link #copy()} method
   *
   * @param concreteConstructor the module descriptor constructor to put the copy inside
   */
  protected final <T extends ModuleDescriptor> T copy0(@NotNull Supplier<T> concreteConstructor) {
    T descriptorCopy = concreteConstructor.get();
    descriptorCopy.setId(getId());
    descriptorCopy.setNamespace(getNamespace());
    descriptorCopy.setTimestamp(getTimestamp());
    descriptorCopy.setModuleVersion(getModuleVersion());
    Copyable.deepCopy(getModelRootDescriptors(), descriptorCopy.getModelRootDescriptors());
    Copyable.deepCopy(getModuleFacetDescriptors(), descriptorCopy.getModuleFacetDescriptors());
    Copyable.deepCopy(getDependencies(), descriptorCopy.getDependencies());
    descriptorCopy.getUsedLanguages().addAll(getUsedLanguages());
    descriptorCopy.getUsedDevkits().addAll(getUsedDevkits());
    descriptorCopy.getLanguageVersions().putAll(getLanguageVersions());
    descriptorCopy.getDependencyVersions().putAll(getDependencyVersions());
    descriptorCopy.getAdditionalJavaStubPaths().addAll(getAdditionalJavaStubPaths());
    descriptorCopy.getSourcePaths().addAll(getSourcePaths());
    copyDeploymentDescriptor(descriptorCopy);
    descriptorCopy.setLoadException(getLoadException());
    descriptorCopy.setUseTransientOutput(isUseTransientOutput());
    descriptorCopy.setHasDependencyVersions(hasDependencyVersions());
    descriptorCopy.setHasLanguageVersions(hasLanguageVersions());
    return descriptorCopy;
  }

  private <T extends ModuleDescriptor> void copyDeploymentDescriptor(T descriptorCopy) {
    DeploymentDescriptor deploymentDescriptor = getDeploymentDescriptor();
    if (deploymentDescriptor != null) {
      deploymentDescriptor = deploymentDescriptor.copy();
    }
    descriptorCopy.setDeploymentDescriptor(deploymentDescriptor);
  }

  @NotNull
  @Override
  public ModuleDescriptor copy() {
    return copy0(ModuleDescriptor::new);
  }
}
