/*
 * Copyright 2003-2022 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.extapi.model.GeneratableSModel;
import jetbrains.mps.persistence.MetaModelInfoProvider;
import jetbrains.mps.smodel.persistence.def.ModelPersistence;
import jetbrains.mps.util.io.ModelInputStream;
import jetbrains.mps.util.io.ModelOutputStream;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.mps.openapi.model.SModelReference;
import org.jetbrains.mps.openapi.persistence.PersistenceFacade;

import java.io.IOException;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;

/**
 * Essential meta information about persisted model (id, persistence version, associated properties, etc).
 * Its intention is to keep data that might be helpful/essential prior to model loading.
 *
 * Generally, use of header for a model persistence is optional, it comes handy
 * for partial model loading and is in use by most persistence implementations supplied by MPS.
 */
public class SModelHeader {
  /**
   * @deprecated Use {@link GeneratableSModel#DO_NOT_GENERATE} instead, header doesn't care about specific attributes
   */
  @Deprecated(since = "2021.3", forRemoval = true)
  public static final String DO_NOT_GENERATE = GeneratableSModel.DO_NOT_GENERATE;

  /*
   * Model is identified with SModelId, optional module id and has a name, these are elements we'd like to keep in header
   * for quick consideration. Although SModelReference has all these, and it seems straightforward to use it here,
   * it's not quite 'right' due to semantics attached - resolution of a model within a repository. However,
   * exposing 3 getters/setters (modelId, modelName, moduleId) is cumbersome, and SModel#getReference is @NotNull regardless of model
   * loaded/attached state, hence for the time being we decided to live with SModelReference here.
   */
  private SModelReference myModelRef = null;
  private int myPersistenceVersion = -1;
  private final Map<String, String> myOptionalProperties = new HashMap<>(4);
  private MetaModelInfoProvider myMetaInfoProvider;

  public SModelHeader() {
  }

  public int getPersistenceVersion() {
    return myPersistenceVersion;
  }

  public void setPersistenceVersion(int persistenceVersion) {
    myPersistenceVersion = persistenceVersion;
  }

  /**
   * DESIGN NOTE: SModelReference is not a persisted attribute of a model. Conceptually, reference emerges once we have
   * an object to point to. The moment we got SModelHeader there's nothing to point to yet, although one could construct
   * SModelReference with
   * @return <code>null</code> if model header is not initialized with a model reference
   */
  @Nullable
  public SModelReference getModelReference() {
    return myModelRef;
  }

  public void setModelReference(@Nullable SModelReference modelRef) {
    myModelRef = modelRef;
  }

  public Map<String, String> getOptionalProperties() {
    return Collections.unmodifiableMap(myOptionalProperties);
  }

  public String getOptionalProperty(String key) {
    return myOptionalProperties.get(key);
  }

  public void setOptionalProperty(String key, String value) {
    assert !ModelPersistence.REF.equals(key);
    // roughly following http://www.w3.org/TR/2008/PER-xml-20080205/#NT-Name
    assert key.matches("^[:A-Z_a-z][-:A-Z_a-z.0-9]*") : "bad key [" + key + "]";

    myOptionalProperties.put(key, value);
  }

  public void removeOptionalProperty(String key) {
    myOptionalProperties.remove(key);
  }

  /**
   * PROVISIONAL, DO NOT USE (unless your name starts with 'A' and you know what you're doing)
   *
   * This is per-model mechanism to alter meta-model (aka structure model) information used in persistence.
   * Generally, this mechanism shall not be in use, and <code>null</code> value is legitimate default, which means
   * native MPS mechanism of SConcept (and ConceptDescriptors) would be in use.
   * However, certain scenarios (command-line merge and ant task to convert models to binary) can't yet afford starting whole
   * MPS and thus shall rely on meta-information read from model files (which is generally sufficient to write the files back).
   *
   * For these scenarios, we used to have global {@code jetbrains.mps.persistence.ModelEnvironmentInfo}, which is global and a bit
   * outdated for modern persistence, hence it has been replaced with MetaModelInfoProvider, although this solution is provisional
   * and likely to get changed in future (perhaps, class known now as IdInfoCollector would replace it).
   */
  public void setMetaInfoProvider(@Nullable MetaModelInfoProvider mmiProvider) {
    myMetaInfoProvider = mmiProvider;
  }
  @Nullable
  public MetaModelInfoProvider getMetaInfoProvider() {
    return myMetaInfoProvider;
  }

  public static SModelHeader create(int persistenceVersion) {
    SModelHeader header = new SModelHeader();
    header.setPersistenceVersion(persistenceVersion);
    return header;
  }

  // FIXME move save and load into respective class (binary persistence)
  public void save(ModelOutputStream stream) throws IOException {
    stream.writeByte(77);
    stream.writeString(myModelRef == null ? null : PersistenceFacade.getInstance().asString(myModelRef));
    stream.writeInt(myPersistenceVersion);
    stream.writeInt(0); //version was here
    stream.writeInt(myOptionalProperties.size());
    for (Map.Entry<String, String> ss : myOptionalProperties.entrySet()) {
      stream.writeString(ss.getKey());
      stream.writeString(ss.getValue());
    }
  }

  public static SModelHeader load(ModelInputStream stream) throws IOException {
    if (stream.readByte() != 77) throw new IOException("bad stream: no model header start marker");
    SModelHeader result = new SModelHeader();
    final String s = stream.readString();
    result.setModelReference(s == null ? null : PersistenceFacade.getInstance().createModelReference(s));
    result.setPersistenceVersion(stream.readInt());
    stream.readInt(); //old model version was here
    for (int size = stream.readInt(); size > 0; size--) {
      result.setOptionalProperty(stream.readString(), stream.readString());
    }
    return result;
  }

  public SModelHeader createCopy() {
    SModelHeader copy = new SModelHeader();
    copy.myModelRef = myModelRef;
    copy.myPersistenceVersion = myPersistenceVersion;
    copy.myOptionalProperties.putAll(myOptionalProperties);
    return copy;
  }
}
