/*
 * 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.concept;

import jetbrains.mps.smodel.Language;
import jetbrains.mps.smodel.LanguageAspect;
import jetbrains.mps.smodel.SNodeUtil;
import jetbrains.mps.smodel.adapter.ids.SContainmentLinkId;
import jetbrains.mps.smodel.adapter.ids.SPropertyId;
import jetbrains.mps.smodel.adapter.ids.SReferenceLinkId;
import jetbrains.mps.smodel.adapter.structure.FormatException;
import jetbrains.mps.smodel.adapter.structure.MetaAdapterFactory;
import jetbrains.mps.smodel.adapter.structure.link.InvalidContainmentLink;
import jetbrains.mps.smodel.adapter.structure.link.SContainmentLinkAdapterById;
import jetbrains.mps.smodel.adapter.structure.property.InvalidProperty;
import jetbrains.mps.smodel.adapter.structure.property.SPropertyAdapterById;
import jetbrains.mps.smodel.adapter.structure.ref.InvalidReferenceLink;
import jetbrains.mps.smodel.adapter.structure.ref.SReferenceLinkAdapterById;
import jetbrains.mps.smodel.language.ConceptRegistry;
import jetbrains.mps.smodel.legacy.ConceptMetaInfoConverter;
import jetbrains.mps.smodel.runtime.ConceptDescriptor;
import jetbrains.mps.smodel.runtime.ConceptPresentation;
import jetbrains.mps.smodel.runtime.LinkDescriptor;
import jetbrains.mps.smodel.runtime.PropertyDescriptor;
import jetbrains.mps.smodel.runtime.ReferenceDescriptor;
import jetbrains.mps.util.NameUtil;
import jetbrains.mps.util.annotation.ToRemove;
import org.apache.log4j.Logger;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.mps.openapi.language.SAbstractConcept;
import org.jetbrains.mps.openapi.language.SAbstractLink;
import org.jetbrains.mps.openapi.language.SContainmentLink;
import org.jetbrains.mps.openapi.language.SProperty;
import org.jetbrains.mps.openapi.language.SReferenceLink;
import org.jetbrains.mps.openapi.model.SModel;
import org.jetbrains.mps.openapi.model.SNode;
import org.jetbrains.mps.openapi.model.SNodeReference;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;

/**
 * Common ancestor of adapter classes {@link SConceptAdapter} and {@link SInterfaceConceptAdapter}.
 * It serves as an adapter from ConceptDescriptor to SAbstractConcept and the base class for all the implementations
 * of the {@link SAbstractConcept}.
 * The common idea is that on every client request it looks for the proper {@link ConceptDescriptor}
 * in the special registry and redirects the client request to it.
 * So it has only an id (string or smth else) as its state.
 * <p/>
 * One calls the SAbstractConcept instance <\it>valid</\it> if and only if its {@link ConceptDescriptor} is present
 * ({@link #getConceptDescriptor()} != null).
 * <p/>
 * Whenever the descriptor is absent (the concept instance is NOT valid) the "fail-safe" behavior is provided:
 * please look at each method individually to acknowledge the contract.
 * <p/>
 * NB: If a client of this API wants to distinguish the case when the concept is invalid, he/she
 * needs to use the method {@link #isValid()}. (!)
 * <p/>
 * Currently a lot of "hacks" introduced to fix some common cases (e.g. not valid concept still is a subconcept of the BaseConcept).
 * Also there is an editor issue when an instance of abstract concept (interface concept) might be created.
 * (E.g. the method {@link #isSubConceptOf(SAbstractConcept)} works not as expected for such concepts)
 */
public abstract class SAbstractConceptAdapter implements SAbstractConcept, ConceptMetaInfoConverter {
  public static final String ID_DELIM = ":";

  protected String myFqName;

  protected SAbstractConceptAdapter(String fqName) {
    myFqName = fqName;
  }

  /**
   * @return the backing {@link ConceptDescriptor}
   */
  @Nullable
  public abstract ConceptDescriptor getConceptDescriptor();

  /**
   * a helper method to get a declaration node for this concept
   * in the case of the legacy concept resolving (by string id)
   */
  protected abstract SNode findInModel(SModel structureModel);

  @Nullable
  @Override
  public SNodeReference getSourceNode() {
    ConceptDescriptor d = getConceptDescriptor();
    if (d == null) {
      return null;
    }
    return d.getSourceNode();
  }

  @NotNull
  @Override
  public String getName() {
    return NameUtil.shortNameFromLongName(getQualifiedName());
  }

  @Override
  public Collection<SReferenceLink> getReferenceLinks() {
    ConceptDescriptor d = getConceptDescriptor();
    if (d == null) {
      return Collections.emptyList();
    }

    ArrayList<SReferenceLink> result = new ArrayList<SReferenceLink>();
    for (ReferenceDescriptor rd : d.getReferenceDescriptors()) {
      result.add(MetaAdapterFactory.getReferenceLink(rd.getId(), rd.getName()));
    }
    return result;
  }

  public boolean hasReference(SReferenceLink r) {
    if (r instanceof InvalidReferenceLink) {
      return false;
    }
    assert r instanceof SReferenceLinkAdapterById : r.getClass().getName();
    ConceptDescriptor d = getConceptDescriptor();
    return d != null && d.getRefDescriptor(((SReferenceLinkAdapterById) r).getId()) != null;
  }

  @Override
  public Collection<SContainmentLink> getContainmentLinks() {
    ConceptDescriptor d = getConceptDescriptor();
    if (d == null) {
      return Collections.emptyList();
    }

    ArrayList<SContainmentLink> result = new ArrayList<SContainmentLink>();
    for (LinkDescriptor ld : d.getLinkDescriptors()) {
      result.add(MetaAdapterFactory.getContainmentLink(ld.getId(), ld.getName()));
    }
    return result;
  }

  public boolean hasLink(SContainmentLink l) {
    if (l instanceof InvalidContainmentLink) {
      return false;
    }
    assert l instanceof SContainmentLinkAdapterById : l.getClass().getName();

    ConceptDescriptor d = getConceptDescriptor();
    return d != null && d.getLinkDescriptor(((SContainmentLinkAdapterById) l).getId()) != null;
  }

  @Override
  @Deprecated
  public SAbstractLink getLink(String role) {
    // INTENTIONALLY DISTURBING
    Logger.getLogger(SAbstractConceptAdapter.class).error("The method is scheduled for removal. There were no uses, do not introduce a new one", new Throwable());
    ConceptDescriptor nodeConcept = getConceptDescriptor();
    if (nodeConcept == null) {
      return null;
    }

    LinkDescriptor d = nodeConcept.getLinkDescriptor(role);
    if (d != null) {
      SContainmentLinkId linkId = d.getId();
      return MetaAdapterFactory.getContainmentLink(linkId, role);
    } else {
      ReferenceDescriptor r = nodeConcept.getRefDescriptor(role);
      if (r == null) {
        return null;
      }

      SReferenceLinkId linkId = r.getId();
      return MetaAdapterFactory.getReferenceLink(linkId, role);
    }
  }

  @Override
  public Iterable<SAbstractLink> getLinks() {
    // INTENTIONALLY DISTURBING
    Logger.getLogger(SAbstractConceptAdapter.class).error("The method is scheduled for removal. There were no uses, do not introduce a new one", new Throwable());
    ArrayList<SAbstractLink> result = new ArrayList<SAbstractLink>();
    ConceptDescriptor cd = getConceptDescriptor();
    if (cd == null) {
      return Collections.emptyList();
    }

    for (LinkDescriptor ld : cd.getLinkDescriptors()) {
      result.add(MetaAdapterFactory.getContainmentLink(ld.getId(), ld.getName()));
    }
    return result;
  }

  @Override
  @Deprecated
  public SProperty getProperty(String name) {
    // INTENTIONALLY DISTURBING
    Logger.getLogger(SAbstractConceptAdapter.class).error("The method is scheduled for removal. There were no uses, do not introduce a new one", new Throwable());
    ConceptDescriptor cd = getConceptDescriptor();
    if (cd == null) {
      return null;
    }

    PropertyDescriptor d = cd.getPropertyDescriptor(name);
    if (d == null) {
      return new InvalidProperty(myFqName, name);
    }

    SPropertyId pid = d.getId();
    return MetaAdapterFactory.getProperty(pid, name);
  }

  public boolean hasProperty(SProperty p) {
    if (p instanceof InvalidProperty) {
      return false;
    }
    assert p instanceof SPropertyAdapterById : p.getClass().getName();


    ConceptDescriptor d = getConceptDescriptor();
    return d != null && d.getPropertyDescriptor(((SPropertyAdapterById) p).getId()) != null;
  }

  @Override
  public Collection<SProperty> getProperties() {
    ConceptDescriptor descriptor = getConceptDescriptor();
    if (descriptor == null) {
      return Collections.emptyList();
    }

    ArrayList<SProperty> result = new ArrayList<SProperty>();
    for (PropertyDescriptor pd : descriptor.getPropertyDescriptors()) {
      result.add(MetaAdapterFactory.getProperty(pd.getId(), pd.getName()));
    }
    return result;
  }

  /**
   * @param anotherConcept -- another SAbstractConcept
   * @return true iff this concept is a subconcept of another concept.
   * if one of the concepts is not valid then false is returned
   */
  @Override
  public boolean isSubConceptOf(SAbstractConcept anotherConcept) {
    // todo: hack, need for working node attributes on nodes of not generated concepts
    // todo: remove
    if (SNodeUtil.concept_BaseConcept.equals(anotherConcept)) { // providing the '* is a subconcept of BaseConcept' contract
      return true;
    }
    if (equals(anotherConcept)) { // providing the 'A is a subconcept of A' contract
      return true;
    }

    ConceptDescriptor descriptor = getConceptDescriptor();
    if (descriptor == null) {
      return false;
    }

    ConceptDescriptor anotherDescriptor = ((SAbstractConceptAdapter) anotherConcept).getConceptDescriptor();
    if (anotherDescriptor == null) {
      return false;
    }
    if (anotherDescriptor.isInterfaceConcept() && anotherConcept instanceof SConceptAdapter) {
      // anotherDescriptor is in fact an interface concept
      // however is created as a SConceptAdapter, not a SInterfaceConceptAdapter (!)
      // currently the editor has to perform hacky operations as this
      return false;
    }

    return isSubConceptOfSpecial(descriptor, anotherDescriptor, anotherConcept);
  }

  protected abstract boolean isSubConceptOfSpecial(@NotNull ConceptDescriptor thisDescriptor, ConceptDescriptor anotherDescriptor,
      SAbstractConcept anotherConcept);

  @Override
  public boolean isAbstract() {
    ConceptDescriptor d = getConceptDescriptor();
    return d == null || d.isAbstract();
  }

  @Nullable
  @Override
  @Deprecated
  @ToRemove(version = 3.4)
  public SNode getDeclarationNode() {
    Language lang = ((Language) getLanguage().getSourceModule());
    if (lang == null) {
      return null;
    }

    SModel structureModel = LanguageAspect.STRUCTURE.get(lang);
    if (structureModel == null) {
      return null;
    }

    return findInModel(structureModel);
  }

  @NotNull
  @Override
  public String getConceptAlias() {
    ConceptDescriptor d = getConceptDescriptor();
    if (d == null) {
      return "";
    }
    return d.getConceptAlias();
  }

  @NotNull
  @Override
  public String getShortDescription() {
    ConceptPresentation pres = ConceptRegistry.getInstance().getConceptProperties(this);
    if (pres != null) {
      return pres.getShortDescription();
    }
    // fallback for legacy code
    ConceptDescriptor d = getConceptDescriptor();
    if (d == null) {
      return "";
    }
    return d.getConceptShortDescription();
  }

  @NotNull
  @Override
  public String getHelpUrl() {
    ConceptPresentation pres = ConceptRegistry.getInstance().getConceptProperties(this);
    if (pres != null) {
      return pres.getHelpUrl();
    }
    ConceptDescriptor d = getConceptDescriptor();
    if (d == null) {
      return "";
    }
    return d.getHelpUrl();
  }

  /**
   * @return true iff the underlying descriptor is present
   */
  @Override
  public final boolean isValid() {
    return getConceptDescriptor() != null;
  }

  @NotNull
  @Override
  public SProperty convertProperty(String propertyName) {
    for (SProperty p : getProperties()) {
      if (p.getName().equals(propertyName)) {
        return p;
      }
    }
    return new InvalidProperty(getQualifiedName(), propertyName);
  }

  @NotNull
  @Override
  public SReferenceLink convertAssociation(String role) {
    for (SReferenceLink r : getReferenceLinks()) {
      if (r.getName().equals(role)) {
        return r;
      }
    }
    return new InvalidReferenceLink(getQualifiedName(), role);
  }

  @NotNull
  @Override
  public SContainmentLink convertAggregation(String role) {
    for (SContainmentLink l : getContainmentLinks()) {
      if (l.getName().equals(role)) {
        return l;
      }
    }
    return new InvalidContainmentLink(getQualifiedName(), role);
  }

  @Override
  public String toString() {
    return myFqName;
  }

  public abstract String serialize();

  public static SAbstractConcept deserialize(String s) {
    if (s.startsWith(SInterfaceConceptAdapterById.INTERFACE_PREFIX)) {
      return SInterfaceConceptAdapterById.deserialize(s);
    } else if (s.startsWith(SConceptAdapterById.CONCEPT_PREFIX)) {
      return SConceptAdapterById.deserialize(s);
    } else if (s.startsWith(InvalidConcept.INVALID_PREFIX)) {
      return InvalidConcept.deserialize(s);
    } else {
      throw new FormatException("Illegal concept type: " + s);
    }
  }
}
