/*
 * 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 org.jetbrains.mps.openapi.model;

import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.mps.annotations.Immutable;

/**
 * Name of a model is complicated matter, we distinguish qualified/long and simple name, namespace fraction, and optional stereotype fraction of it.
 * <pre>[ {namespace} '.'] {simple name} [ '@' {stereotype} ]</pre>
 * To avoid use of utility methods scattered around the code that extract certain fractions of the model name, this object
 * gives access to all relevant parts of the name.
 *
 * @author Artem Tikhomirov
 * @since 3.4
 */
@Immutable
public final class SModelName {
  private final String myValue;

  public SModelName(@NotNull String qualifiedCompleteName) {
    myValue = checkIllegalChars(qualifiedCompleteName);
  }

  public SModelName(@Nullable CharSequence namespace, @NotNull CharSequence simpleName, @Nullable CharSequence stereotype) {
    StringBuilder sb = new StringBuilder();
    if (namespace != null && namespace.length() > 0) {
      sb.append(namespace);
      sb.append('.');
      assert namespace.toString().indexOf('@') == -1;
    }
    sb.append(simpleName);
    assert simpleName.toString().indexOf('.') == -1;
    assert simpleName.toString().indexOf('@') == -1;
    if (stereotype != null && stereotype.length() > 0) {
      assert simpleName.length() > 0;
      sb.append('@');
      sb.append(stereotype);
    }
    myValue = checkIllegalChars(sb.toString());
  }

  public SModelName(@NotNull CharSequence qualifiedName, @Nullable CharSequence stereotype) {
    if (stereotype != null && stereotype.length() > 0) {
      assert stereotype.toString().indexOf('@') == -1;
      myValue = checkIllegalChars(new StringBuilder(qualifiedName).append('@').append(stereotype).toString());
    } else {
      myValue = checkIllegalChars(qualifiedName.toString());
    }
  }

  /**
   * Covers the case when we constructed a {@link SModelReference} with {@link SModelId} only, unaware of actual model name.
   * @return <code>true</code> iff model name is blank.
   */
  public boolean isEmpty() {
    return myValue.isEmpty();
  }

  /**
   * @return complete name of the model which includes optional namespace part, model name and optional stereotype, such as 'generator' or 'tests',
   * separated by the '@' character e.g. 'jetbrains.mps.sample.generator.main@generator'
   */
  @NotNull
  public String getValue() {
    return myValue;
  }

  /**
   * @return qualified model name (namespace and simple name), without stereotype
   */
  @NotNull
  public String getLongName() {
    int atIndex = myValue.lastIndexOf('@');
    return atIndex != -1 ? myValue.substring(0, atIndex) : myValue;
  }

  /**
   * @return name of the model without namespace nor stereotype, empty string iff model name is blank.
   */
  @NotNull
  public String getSimpleName() {
    String qualifiedName = getLongName();
    int nsSeparator = qualifiedName.lastIndexOf('.');
    return nsSeparator == -1 ? qualifiedName : qualifiedName.substring(nsSeparator + 1);
  }

  @NotNull
  public String getNamespace() {
    String qualifiedName = getLongName();
    int nsSeparator = qualifiedName.lastIndexOf('.');
    return nsSeparator == -1 ? "" : qualifiedName.substring(0, nsSeparator);
  }

  /**
   * @return <code>true</code> iff {@link #getStereotype()} would return non-empty value
   */
  public boolean hasStereotype() {
    return myValue.lastIndexOf('@') > 0;
  }

  @NotNull
  public String getStereotype() {
    int atIndex = myValue.lastIndexOf('@');
    return atIndex != -1 ? myValue.substring(atIndex+1) : "";
  }

  /**
   *
   * @param newStereotype stereotype for the constructed name, or {@code null} to indicate new name
   *        shall not specify stereotype (identical to {@link #withoutStereotype()}
   * @return model name with {@linkplain #getLongName() qualified name} identical to this model name and with a given stereotype.
   *         May return same instance if new stereotype is the same as actual.
   */
  @NotNull
  public SModelName withStereotype(@Nullable CharSequence newStereotype) {
    if (newStereotype == null) {
      return withoutStereotype();
    }
    return new SModelName(getLongName(), newStereotype);
  }

  /**
   * Construct a name with the identical {@linkplain #getLongName() qualified name}, and without any stereotype.
   * May return {@code this} if there's no stereotype in the actual name ({@code SModelName} is immutable).
   * @return model name without a stereotype, never {@code null}
   */
  @NotNull
  public SModelName withoutStereotype() {
    int atIndex = myValue.lastIndexOf('@');
    return atIndex == -1 ? this : new SModelName(myValue.substring(0, atIndex)); // superfluous check for illegal chars, but no private cons.
  }

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

  @Override
  public boolean equals(Object obj) {
    return obj.getClass() == SModelName.class && myValue.equals(((SModelName) obj).myValue);
  }

  @Override
  public int hashCode() {
    return myValue.hashCode();
  }

  private static String checkIllegalChars(String qualifiedCompleteName) throws IllegalArgumentException {
    qualifiedCompleteName = qualifiedCompleteName.trim();
    if (qualifiedCompleteName.isEmpty()) {
      return qualifiedCompleteName;
    }
    int atIndex = qualifiedCompleteName.lastIndexOf('@');
    if (atIndex == 0 || (!qualifiedCompleteName.isEmpty() && atIndex == qualifiedCompleteName.length() - 1)) {
      throw new IllegalArgumentException(String.format("Stereotype separator '@' shall not appear at the position %d in '%s'", atIndex, qualifiedCompleteName));
    }
    int nameLastChar = atIndex > 0 ? atIndex - 1 : qualifiedCompleteName.length() - 1;
    if (qualifiedCompleteName.charAt(nameLastChar) == '.') {
      throw new IllegalArgumentException("Name of the model shall not end with '.'");
    }
    return qualifiedCompleteName;
  }
}
