/*
 * 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 jetbrains.mps.smodel.adapter.structure;

import gnu.trove.TLongObjectHashMap;
import jetbrains.mps.smodel.adapter.ids.MetaIdByDeclaration;
import jetbrains.mps.smodel.adapter.ids.MetaIdFactory;
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 jetbrains.mps.smodel.adapter.structure.concept.SConceptAdapterById;
import jetbrains.mps.smodel.adapter.structure.concept.SInterfaceConceptAdapterById;
import jetbrains.mps.smodel.adapter.structure.language.SLanguageAdapterById;
import jetbrains.mps.smodel.adapter.structure.link.SContainmentLinkAdapterById;
import jetbrains.mps.smodel.adapter.structure.property.SPropertyAdapterById;
import jetbrains.mps.smodel.adapter.structure.ref.SReferenceLinkAdapterById;
import jetbrains.mps.smodel.language.ConceptRegistry;
import jetbrains.mps.smodel.runtime.ConceptDescriptor;
import jetbrains.mps.util.NameUtil;
import jetbrains.mps.util.annotation.ToRemove;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.mps.annotations.Immutable;
import org.jetbrains.mps.openapi.language.SAbstractConcept;
import org.jetbrains.mps.openapi.language.SConcept;
import org.jetbrains.mps.openapi.language.SContainmentLink;
import org.jetbrains.mps.openapi.language.SInterfaceConcept;
import org.jetbrains.mps.openapi.language.SLanguage;
import org.jetbrains.mps.openapi.language.SProperty;
import org.jetbrains.mps.openapi.language.SReferenceLink;
import org.jetbrains.mps.openapi.module.SModuleReference;

import java.util.ArrayList;
import java.util.List;

/**
 * {@implNote} This class shall stay pure FACTORY of SXAdapter objects (i.e. instantiate them only), and shall not contradict with
 * their "proxy" aspect (i.e. implementation that needs to look for {@link ConceptDescriptor} and alike).
 * Simply put, methods of this class shall not go outside, e.g. to {@link ConceptRegistry} or
 * {@link jetbrains.mps.smodel.language.StructureRegistry}, like {@link #getConceptById(SConceptId)} does now.
 */
public abstract class MetaAdapterFactory {
  private static final TLongObjectHashMap<List<SLanguageAdapterById>> ourLanguages = new TLongObjectHashMap<>(200);

  // there are 5 thousand concepts in MPS alone, don't need to be shy, rehash would be more expensive.
  private static final TLongObjectHashMap<List<ConceptBucket>> ourConcepts = new TLongObjectHashMap<>(5000);
  private static final TLongObjectHashMap<List<InterfaceBucket>> ourInterfaces = new TLongObjectHashMap<>(1000);
  private static final TLongObjectHashMap<List<PropertyBucket>> ourProperties = new TLongObjectHashMap<>(5000);
  private static final TLongObjectHashMap<List<AssociationLinkBucket>> ourAssociations = new TLongObjectHashMap<>(1000);
  private static final TLongObjectHashMap<List<AggregationLinkBucket>> ourAggregations = new TLongObjectHashMap<>(1000);

  @NotNull
  public static SLanguage getLanguage(@NotNull SLanguageId id, @NotNull String langName) {
    // FIXME change templates to invoke getLanguage(long,long,string) instead
    return getLanguage(id.getHighBits(), id.getLowBits(), langName);
  }

  @NotNull
  public static SLanguage getLanguage(long uuidHigh, long uuidLow, String langName) {
    List<SLanguageAdapterById> list = getBucketList(ourLanguages, uuidHigh * 17 + uuidLow);
    //noinspection SynchronizationOnLocalVariableOrMethodParameter
    synchronized (list) {
      for (int i = 0, x = list.size(); i < x; i++) {
        SLanguageAdapterById rv = list.get(i);
        SLanguageId id = rv.getId();
        if (uuidHigh == id.getHighBits() && uuidLow == id.getLowBits()) {
          return rv;
        }
      }
      SLanguageAdapterById rv = new SLanguageAdapterById(MetaIdFactory.langId(uuidHigh, uuidLow), langName);
      list.add(rv);
      return rv;
    }
  }

  @NotNull
  public static SLanguage getLanguage(@NotNull SModuleReference languageModuleRef) {
    return getLanguage(MetaIdByDeclaration.ref2LangId(languageModuleRef), languageModuleRef.getModuleName());
  }

  @NotNull
  public static SConcept getConcept(SConceptId id, String conceptName) {
    SLanguageId langId = id.getLanguageId();
    return getConcept(langId.getHighBits(), langId.getLowBits(), id.getIdValue(), conceptName);
  }

  @NotNull
  public static SConcept getConcept(long uuidHigh, long uuidLow, long concept, String conceptName) {
    List<ConceptBucket> bucketList = getBucketList(ourConcepts, bucketKey(uuidHigh, uuidLow, concept));
    //noinspection SynchronizationOnLocalVariableOrMethodParameter
    synchronized (bucketList) {
      for (int i = 0, x = bucketList.size(); i < x; i++) {
        ConceptBucket rv = bucketList.get(i);
        if (rv.isBucketFor(uuidHigh, uuidLow, concept)) {
          return rv.get();
        }
      }
      ConceptBucket b = new ConceptBucket(uuidHigh, uuidLow, concept, conceptName);
      bucketList.add(b);
      return b.get();
    }
  }

  public static SConcept getConcept(@NotNull SLanguage language, long concept, @NotNull String shortConceptName) {
    final SLanguageId langId = MetaIdHelper.getLanguage(language);
    return getConcept(langId.getHighBits(), langId.getLowBits(), concept, NameUtil.conceptFQNameFromNamespaceAndShortName(language.getQualifiedName(), shortConceptName));
  }

  @NotNull
  public static SInterfaceConcept getInterfaceConcept(SConceptId id, String conceptName) {
    SLanguageId langId = id.getLanguageId();
    return getInterfaceConcept(langId.getHighBits(), langId.getLowBits(), id.getIdValue(), conceptName);
  }

  @NotNull
  public static SInterfaceConcept getInterfaceConcept(long uuidHigh, long uuidLow, long concept, String conceptName) {
    List<InterfaceBucket> bucketList = getBucketList(ourInterfaces, bucketKey(uuidHigh, uuidLow, concept));
    //noinspection SynchronizationOnLocalVariableOrMethodParameter
    synchronized (bucketList) {
      for (int i = 0, x = bucketList.size(); i < x; i++) {
        InterfaceBucket rv = bucketList.get(i);
        if (rv.isBucketFor(uuidHigh, uuidLow, concept)) {
          return rv.get();
        }
      }
      InterfaceBucket b = new InterfaceBucket(uuidHigh, uuidLow, concept, conceptName);
      bucketList.add(b);
      return b.get();
    }
  }

  public static SInterfaceConcept getInterfaceConcept(@NotNull SLanguage language, long concept, @NotNull String shortConceptName) {
    final SLanguageId langId = MetaIdHelper.getLanguage(language);
    return getInterfaceConcept(langId.getHighBits(), langId.getLowBits(), concept, NameUtil.conceptFQNameFromNamespaceAndShortName(language.getQualifiedName(), shortConceptName));
  }

  @NotNull
  public static SProperty getProperty(SPropertyId id, String propName) {
    SConceptId cid = id.getConceptId();
    SLanguageId langId = cid.getLanguageId();
    return getProperty(langId.getHighBits(), langId.getLowBits(), cid.getIdValue(), id.getIdValue(), propName);
  }

  @NotNull
  public static SProperty getProperty(long uuidHigh, long uuidLow, long concept, long prop, String propName) {
    List<PropertyBucket> bucketList = getBucketList(ourProperties, bucketKey(uuidHigh, uuidLow, concept, prop));
    //noinspection SynchronizationOnLocalVariableOrMethodParameter
    synchronized (bucketList) {
      for (int i = 0, x = bucketList.size(); i < x; i++) {
        PropertyBucket rv = bucketList.get(i);
        if (rv.isBucketFor(uuidHigh, uuidLow, concept, prop)) {
          return rv.get();
        }
      }
      PropertyBucket b = new PropertyBucket(uuidHigh, uuidLow, concept, prop, propName);
      bucketList.add(b);
      return b.get();
    }
  }

  public static SProperty getProperty(@NotNull SAbstractConcept concept, long prop, String propName) {
    final SConceptId cid = MetaIdHelper.getConcept(concept);
    SLanguageId langId = cid.getLanguageId();
    return getProperty(langId.getHighBits(), langId.getLowBits(), cid.getIdValue(), prop, propName);
  }

  @NotNull
  public static SReferenceLink getReferenceLink(SReferenceLinkId id, String refName) {
    SConceptId cid = id.getConceptId();
    SLanguageId langId = cid.getLanguageId();
    return getReferenceLink(langId.getHighBits(), langId.getLowBits(), cid.getIdValue(), id.getIdValue(), refName);
  }

  @NotNull
  public static SReferenceLink getReferenceLink(long uuidHigh, long uuidLow, long concept, long link, String refName) {
    List<AssociationLinkBucket> bucketList = getBucketList(ourAssociations, bucketKey(uuidHigh, uuidLow, concept, link));
    //noinspection SynchronizationOnLocalVariableOrMethodParameter
    synchronized (bucketList) {
      for (int i = 0, x = bucketList.size(); i < x; i++) {
        AssociationLinkBucket rv = bucketList.get(i);
        if (rv.isBucketFor(uuidHigh, uuidLow, concept, link)) {
          return rv.get();
        }
      }
      AssociationLinkBucket b = new AssociationLinkBucket(uuidHigh, uuidLow, concept, link, refName);
      bucketList.add(b);
      return b.get();
    }
  }

  public static SReferenceLink getReferenceLink(@NotNull SAbstractConcept concept, long link, String linkName) {
    final SConceptId cid = MetaIdHelper.getConcept(concept);
    SLanguageId langId = cid.getLanguageId();
    return getReferenceLink(langId.getHighBits(), langId.getLowBits(), cid.getIdValue(), link, linkName);
  }

  @NotNull
  public static SContainmentLink getContainmentLink(SContainmentLinkId id, String linkName) {
    SConceptId cid = id.getConceptId();
    SLanguageId langId = cid.getLanguageId();
    return getContainmentLink(langId.getHighBits(), langId.getLowBits(), cid.getIdValue(), id.getIdValue(), linkName);
  }

  @NotNull
  public static SContainmentLink getContainmentLink(long uuidHigh, long uuidLow, long concept, long link, String linkName) {
    List<AggregationLinkBucket> bucketList = getBucketList(ourAggregations, bucketKey(uuidHigh, uuidLow, concept, link));
    //noinspection SynchronizationOnLocalVariableOrMethodParameter
    synchronized (bucketList) {
      for (int i = 0, x = bucketList.size(); i < x; i++) {
        AggregationLinkBucket rv = bucketList.get(i);
        if (rv.isBucketFor(uuidHigh, uuidLow, concept, link)) {
          return rv.get();
        }
      }
      AggregationLinkBucket b = new AggregationLinkBucket(uuidHigh, uuidLow, concept, link, linkName);
      bucketList.add(b);
      return b.get();
    }
  }

    public static SContainmentLink getContainmentLink(@NotNull SAbstractConcept concept, long link, String linkName) {
    final SConceptId cid = MetaIdHelper.getConcept(concept);
      SLanguageId langId = cid.getLanguageId();
    return getContainmentLink(langId.getHighBits(), langId.getLowBits(), cid.getIdValue(), link, linkName);
  }

  @NotNull
  public static SAbstractConcept getAbstractConcept(ConceptDescriptor descriptor) {
    if (descriptor.isInterfaceConcept()) {
      return getInterfaceConcept(descriptor.getId(), descriptor.getConceptFqName());
    } else {
      return getConcept(descriptor.getId(), descriptor.getConceptFqName());
    }
  }

  @Deprecated
  @ToRemove(version = 3.4)
  /**
   * For internal use only.
   * This method is used in deprecated methods that accept SConceptId parameter, but were replaced with methods
   * that accept SAbstractConcept.
   * See MPS-24098. It was caused by such a deprecated method doing MAF.getConcept() with an id of interface concept.
   * In this case, ConstraintsRegistry was filled with a SConcept, which was a "fake instance" of interface concept and was
   * "implementing" this interface, but when getting constraints for this interface, this "SConcept" constraints were obtained,
   * which caused a stack overflow at last.
   */
  public static SAbstractConcept getConceptById(SConceptId id){
    // FIXME the main defect of this method is that it goes to ConceptRegistry.getInstance()
    //       and denies 'factory' nature of MAF. It's tempting to use this method e.g.
    //       from SReferenceLinkAdapter.getTargetConcept(), but there shall be distinction between
    //       instantiation of SXAdapter (what MAF) does, and its implementation (its "proxy" aspect)
    //       that may access ConceptRegistry and alike to fulfil the contract. MAF doing the same
    //       becomes dangerous.
    return MetaAdapterFactory.getAbstractConcept(ConceptRegistry.getInstance().getConceptDescriptor(id));
  }

  @Immutable
  private static class LangKey {
    private final SLanguageId myId;
    private final String myName;

    public LangKey(SLanguageId id, String name) {
      myId = id;
      myName = name;
    }

    @Override
    public int hashCode() {
      return myId.hashCode() * 31 + myName.hashCode();
    }

    @Override
    public boolean equals(Object obj) {
      if (obj instanceof LangKey) {
        LangKey o = (LangKey) obj;
        return myId.equals(o.myId) && myName.equals(o.myName);
      }
      return false;
    }
  }

  private static <T> List<T> getBucketList(TLongObjectHashMap<List<T>> map, long bucketKey) {
    //noinspection SynchronizationOnLocalVariableOrMethodParameter
    synchronized (map) {
      List<T> rv = map.get(bucketKey);
      if (rv == null) {
        // I don't expect too much collisions
        map.put(bucketKey, rv = new ArrayList<T>(4));
      }
      return rv;
    }
  }

  // bucket key doesn't need to be unique for a concept, just rare enough to get less collisions
  // if necessary, may add hashcode of concept name if we'd like to distinguish concepts with new and old name (as it used to be with MPS 3.4)
  private static long bucketKey(long highBits, long lowBits, long concept) {
    return concept * 11 + lowBits + highBits * 17;
  }

  private static long bucketKey(long highBits, long lowBits, long concept, long feature) {
    return feature * 19 + concept * 11 + lowBits + highBits * 17;
  }

  /*
   * Original idea was to have ConceptBucket and to keep props and links in there. However,
   * access to features (say, MAF.getProperty) doesn't give any hint whether it comes through interface or a class concept.
   * Possible mitigation is to have generic ConceptBucketBase without prop/link fields, with 3 subclasses (for InterfaceConcept,
   * ClassConcept and AbstractConcept). Latter would receive all prop/link collections and would be used for corresponding MAF accessors
   * when exact concept kind is unknown. For the time being, however, I decided to try with distinct map per feature as it results in less
   * indirection (map of list vs map of object with map/list), OTOH might be fruitful to try aforementioned approach as it gives a chance for
   * further optimization based on limited number of features per concept (i.e. may stick to plain/sorted arrays and manage them in AbstractConceptBucket
   * plain arrays could be used with copy-on-write semantics and with access synchronized only when changed).
   */
  static class AbstractConceptBucket {
    private final long myLanguageHighBits;
    private final long myLanguageLowBits;
    private final long myConceptId;

    /*package*/ AbstractConceptBucket(long highBits, long lowBits, long concept) {
      myLanguageHighBits = highBits;
      myLanguageLowBits = lowBits;
      myConceptId = concept;
    }

    /*package*/ final boolean isBucketFor(long highBits, long lowBits, long concept) {
      return myLanguageHighBits == highBits && myLanguageLowBits == lowBits && myConceptId == concept;
    }
  }

  static final class ConceptBucket extends AbstractConceptBucket {
    private final SConceptAdapterById myConcept;

    /*package*/ ConceptBucket(long highBits, long lowBits, long concept, String name) {
      super(highBits, lowBits, concept);
      myConcept = new SConceptAdapterById(MetaIdFactory.conceptId(highBits, lowBits, concept), name);
    }

    public SConcept get() {
      return myConcept;
    }
  }

  static final class InterfaceBucket extends AbstractConceptBucket {
    private final SInterfaceConceptAdapterById myConcept;

    InterfaceBucket(long highBits, long lowBits, long concept, String name) {
      super(highBits, lowBits, concept);
      myConcept = new SInterfaceConceptAdapterById(MetaIdFactory.conceptId(highBits, lowBits, concept), name);
    }

    public SInterfaceConcept get() {
      return myConcept;
    }
  }


  static abstract class StructuralFeatureBucket {
    private final long myLanguageHighBits;
    private final long myLanguageLowBits;
    private final long myConceptId;
    private final long myFeatureId;

    /*package*/ StructuralFeatureBucket(long highBits, long lowBits, long concept, long feature) {
      myLanguageHighBits = highBits;
      myLanguageLowBits = lowBits;
      myConceptId = concept;
      myFeatureId = feature;
    }

    /*package*/ final boolean isBucketFor(long highBits, long lowBits, long concept, long feature) {
      return myLanguageHighBits == highBits && myLanguageLowBits == lowBits && myConceptId == concept && myFeatureId == feature;
    }
  }

  static final class PropertyBucket extends StructuralFeatureBucket {
    private final SPropertyAdapterById myProperty;

    /*package*/ PropertyBucket(long highBits, long lowBits, long concept, long prop, String name) {
      super(highBits, lowBits, concept, prop);
      myProperty = new SPropertyAdapterById(MetaIdFactory.propId(highBits, lowBits, concept, prop), name);
    }

    /*package*/ SProperty get() {
      return myProperty;
    }
  }

  static final class AssociationLinkBucket extends StructuralFeatureBucket {
    private final SReferenceLinkAdapterById myLink;

    /*package*/ AssociationLinkBucket(long highBits, long lowBits, long concept, long link, String name) {
      super(highBits, lowBits, concept, link);
      myLink = new SReferenceLinkAdapterById(MetaIdFactory.refId(highBits, lowBits, concept, link), name);
    }

    /*package*/ SReferenceLink get() {
      return myLink;
    }
  }

  static final class AggregationLinkBucket extends StructuralFeatureBucket {
    private final SContainmentLinkAdapterById myLink;

    /*package*/ AggregationLinkBucket(long highBits, long lowBits, long concept, long link, String name) {
      super(highBits, lowBits, concept, link);
      myLink = new SContainmentLinkAdapterById(MetaIdFactory.linkId(highBits, lowBits, concept, link), name);
    }

    /*package*/ SContainmentLinkAdapterById get() {
      return myLink;
    }
  }
}
