/*
 * Copyright 2003-2014 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.persistence.registry;

import jetbrains.mps.smodel.adapter.ids.MetaIdHelper;
import jetbrains.mps.smodel.adapter.ids.SConceptId;
import jetbrains.mps.smodel.adapter.ids.SContainmentLinkId;
import jetbrains.mps.smodel.adapter.ids.SLanguageId;
import jetbrains.mps.smodel.adapter.ids.SPropertyId;
import jetbrains.mps.smodel.adapter.ids.SReferenceLinkId;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.mps.openapi.language.SConcept;
import org.jetbrains.mps.openapi.language.SContainmentLink;
import org.jetbrains.mps.openapi.language.SProperty;
import org.jetbrains.mps.openapi.language.SReferenceLink;

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;

/**
 * Fraction of a structure model, sufficient to serialize/de-serialize given model/nodes.
 *
 * No thread safety is ensured, deemed to be populated from a single thread.
 * @author Artem Tikhomirov
 */
public class IdInfoRegistry {
  private final HashMap<SLanguageId, LangInfo> myLanguagesInUse;
  /*
   * In fact, just a view of myLanguagesInUse.values().getConceptsInUse().select(it -> map(it.getConceptId(), it))
   */
  private final HashMap<SConceptId, ConceptInfo> myRegistry;

  public IdInfoRegistry() {
    myRegistry = new HashMap<SConceptId, ConceptInfo>();
    myLanguagesInUse = new HashMap<SLanguageId, LangInfo>();
  }

  /**
   * @return ordered set of languages known to this registry
   */
  public List<LangInfo> getLanguagesInUse() {
    ArrayList<LangInfo> rv = new ArrayList<LangInfo>(myLanguagesInUse.values());
    Collections.sort(rv);
    return rv;
  }

  /**
   * Records a language (unless already known) in the collector
   * @return utility object that keeps language information essential for persistence
   */
  @NotNull
  public LangInfo registerLanguage(@NotNull SLanguageId lang, @NotNull String languageName) {
    LangInfo langInfo = myLanguagesInUse.get(lang);
    if (langInfo == null) {
      myLanguagesInUse.put(lang, langInfo = new LangInfo(lang, languageName));
    }
    return langInfo;
  }

  /**
   * Records a concept (unless already known) in the collector.
   * NOTE, concept's language has to be registered beforehand.
   * @return utility object that keeps concept information essential for persistence
   * @throws java.lang.IllegalArgumentException if concept to register comes from unknown language
   */
  @NotNull
  public ConceptInfo registerConcept(@NotNull SConceptId concept, @NotNull String conceptName) throws IllegalArgumentException {
    if (!knows(concept.getLanguageId())) {
      throw new IllegalArgumentException(String.format("Concept %s comes from a language not yet registered here", concept));
    }
    ConceptInfo ci = myRegistry.get(concept);
    if (ci != null) {
      return ci;
    }
    LangInfo li = myLanguagesInUse.get(concept.getLanguageId());
    ci = new ConceptInfo(concept, conceptName);
    li.register(ci);
    myRegistry.put(concept, ci);
    return ci;
  }

  public boolean knows(@NotNull SLanguageId lang) {
    return myLanguagesInUse.containsKey(lang);
  }

  public boolean knows(@NotNull SConceptId concept) {
    return myRegistry.containsKey(concept);
  }

  @Nullable
  public ConceptInfo get(@NotNull SConceptId concept) {
    return myRegistry.get(concept);
  }

  public ConceptInfo find(@NotNull SConcept concept) {
    final SConceptId id = MetaIdHelper.getConcept(concept);
    assert id != null; // original ModelWriter9.saveNode assumed this
    assert myRegistry.containsKey(id); // the way IdInfoCollector is built shall ensure concept of any node in a model is registered
    return myRegistry.get(id);
  }
  public PropertyInfo find(@NotNull SProperty property) {
    SPropertyId id = MetaIdHelper.getProperty(property);
    assert id != null;
    return myRegistry.get(id.getConceptId()).find(id);
  }
  public AssociationLinkInfo find(@NotNull SReferenceLink link) {
    SReferenceLinkId id = MetaIdHelper.getAssociation(link);
    assert id != null;
    return myRegistry.get(id.getConceptId()).find(id);
  }
  public AggregationLinkInfo find(@NotNull SContainmentLink link) {
    SContainmentLinkId id = MetaIdHelper.getAggregation(link);
    assert id != null;
    return myRegistry.get(id.getConceptId()).find(id);
  }

  /**
   * Treat set of known meta-objects as closed and assign string index values to each and every meta-object registered
   * @see ConceptInfo#getIndex()
   * @param indexEncoder translates internal identity integer value into an index for textual persistence
   */
  public void initializeIndexValues(@NotNull IndexEncoder indexEncoder) {
    HashSet<String> usedConceptIndexes = new HashSet<String>();
    HashSet<String> usedPropertyIndexes = new HashSet<String>();
    HashSet<String> usedAssociationIndexes = new HashSet<String>();
    HashSet<String> usedAggregationIndexes = new HashSet<String>();
    // iterate from language to ensure the same order (and same hash conflict resolution result) for subsequent runs
    for (LangInfo langInfo : getLanguagesInUse()) {
      for (ConceptInfo ci : langInfo.getConceptsInUse()) {
        fill(usedConceptIndexes, ci, indexEncoder);
        for (PropertyInfo pi : ci.getPropertiesInUse()) {
          fill(usedPropertyIndexes, pi, indexEncoder);
        }
        for (AssociationLinkInfo li : ci.getAssociationsInUse()) {
          fill(usedAssociationIndexes, li, indexEncoder);
        }
        for (AggregationLinkInfo li : ci.getAggregationsInUse()) {
          fill(usedAggregationIndexes, li, indexEncoder);
        }
      }
    }
  }

  private static void fill(HashSet<String> usedIndexes, BaseInfo bi, IndexEncoder indexEncoder) {
    int v = bi.internalKey();
    String s;
    do {
      s = indexEncoder.index(v);
      v++;
    } while (!usedIndexes.add(s));
    bi.setIndex(s);
  }

  public interface IndexEncoder {
    String index(int key);
  }
}
