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

import jetbrains.mps.util.InternUtil;
import jetbrains.mps.util.annotation.ToRemove;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.mps.annotations.Immutable;

import java.util.UUID;

public abstract class SModelId implements org.jetbrains.mps.openapi.model.SModelId {
  private static final String REGULAR_PREFIX = "r:";
  private static final String FOREIGN_PREFIX = "f:";

  public static SModelId generate() {
    return new RegularSModelId(UUID.randomUUID());
  }

  public static SModelId regular(UUID uid) {
    return new RegularSModelId(uid);
  }

  public static SModelId regular(String suffix) {
    try {
      UUID uuid = UUID.fromString(suffix);
      return regular(uuid);
    } catch (IllegalArgumentException e) {
      long lower = Long.parseLong(suffix);
      UUID uuid = new UUID(0x0000000000004000, lower);
      return regular(uuid);
    }
  }

  public static SModelId foreign(@NotNull String id) {
    return new ForeignSModelId(id);
  }

  /**
   * See {@link jetbrains.mps.smodel.SModelId.ForeignSModelId} for details
   */
  public static SModelId foreign(@Nullable String kind, @NotNull String id) {
    if (kind != null && !kind.trim().isEmpty()) {
      return new ForeignSModelId(kind.trim() + "#" + id);
    } else {
      return new ForeignSModelId(id);
    }
  }

  /**
   * @deprecated moduleId shall not be part of model id. Although it's possible that intention was to make it 'globally unique' (as it superclass suggests),
   * usage pattern tells us they were not deemed global (there's always module id when a model reference with foreign id is created). And even global id shall
   * not use set of strings concatenated with '#' to describe complex data structure.
   */
  @Deprecated
  @ToRemove(version = 3.4)
  public static SModelId foreign(String kind, String moduleId, String id) {
    if (moduleId == null || moduleId.length() == 0) {
      return new ForeignSModelId(kind + "#" + id);
    }
    return new ForeignSModelId(kind + "#" + moduleId + "#" + id);
  }

  /**
   * @deprecated this method doesn't support {@link org.jetbrains.mps.openapi.persistence.SModelIdFactory},
   * use {@link org.jetbrains.mps.openapi.persistence.PersistenceFacade#createModelId(String)} instead.
   */
  @Deprecated
  @ToRemove(version = 3.3)
  public static SModelId fromString(String id) {
    if (id.startsWith(REGULAR_PREFIX)) {
      String suffix = id.substring(REGULAR_PREFIX.length());
      return regular(suffix);
    }
    if (id.startsWith(FOREIGN_PREFIX)) {
      String suffix = id.substring(FOREIGN_PREFIX.length());
      return foreign(suffix);
    }
    if (id.startsWith(RelativePathSModelId.TYPE + ":")) {
      return new RelativePathSModelId(id.substring(1 + RelativePathSModelId.TYPE.length()));
    }
    throw new IllegalArgumentException("wrong id " + id);
  }

  private SModelId() {
  }

  /**
   * @deprecated model id is immutable, what's the point to make a copy?
   */
  @Deprecated
  @ToRemove(version = 3.4)
  public SModelId getCopy() {
    return fromString(toString());
  }

  @Override
  public boolean isGloballyUnique() {
    return true;
  }

  @Override
  public String getModelName() {
    return null;
  }

  @Immutable
  public final static class RegularSModelId extends SModelId {
    public static final String TYPE = "r";

    private final UUID myId;

    /*package*/ RegularSModelId(UUID id) {
      myId = id;
    }

    public UUID getId() {
      return myId;
    }

    public boolean equals(Object obj) {
      if (!(obj instanceof RegularSModelId)) return false;
      return ((RegularSModelId) obj).myId.equals(myId);
    }

    public int hashCode() {
      return myId.hashCode();
    }

    @Override
    public String getType() {
      return TYPE;
    }

    public String toString() {
      return REGULAR_PREFIX + myId;
    }
  }

  /**
   * Model identity based on plain string, with optional kind part up to first hash ('#') sign (i.e. "[kind#]identity").
   * Almost any string could be used for identity, provided it's unique within a repository.
   * Although there's no known restrictions about string except uniqueness at the moment, it's advised not to stretch this freedom too much.
   * Primary difference with {@link jetbrains.mps.smodel.SModelId.ModelNameSModelId} is that identity is not treated as model name
   *
   * IMPORTANT: it's advised not to use this kind of model id and leave it for legacy code (e.g. VCS that reads model in old persistence format).
   *            This one has misguiding name (foreign to what?), mandates globally uniqueness while doesn't help to achieve one.
   *            Consider use of {@link jetbrains.mps.smodel.SModelId.IntegerSModelId} if you need simple model id.
   */
  @Immutable
  public final static class ForeignSModelId extends SModelId {
    public static final String TYPE = "f";
    private final String myId;

    /*package*/ ForeignSModelId(String id) {
      myId = InternUtil.intern(id);
    }

    /**
     * @return never <code>null</code>
     */
    public String getId() {
      return myId;
    }

    /**
     * @return optional part of the identity string, up to first '#', excluding; empty string if no kind part found
     */
    public String getKind() {
      // What's the point of this ForeignSModelId then if we don't expose 'kind', provided we've specified one at creation time?
      // It could be plain AnyStringModelId then.
      int x = myId.indexOf('#');
      return x == -1 ? "" : myId.substring(0, x);
    }

    public boolean equals(Object obj) {
      if (!(obj instanceof ForeignSModelId)) return false;
      return ((ForeignSModelId) obj).myId.equals(myId);
    }

    public int hashCode() {
      return myId.hashCode();
    }

    @Override
    public String getType() {
      return TYPE;
    }

    public String toString() {
      return FOREIGN_PREFIX + myId;
    }
  }

  @Immutable
  public final static class ModelNameSModelId extends SModelId {
    public static final String TYPE = "m";
    private final String myModelName;

    public ModelNameSModelId(String modelName) {
      super();
      myModelName = modelName;
    }

    @Override
    public String getModelName() {
      return myModelName;
    }

    @Override
    public boolean equals(Object o) {
      if (this == o) return true;
      if (o == null || getClass() != o.getClass()) return false;
      return myModelName.equals(((ModelNameSModelId) o).myModelName);
    }

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

    @Override
    public String getType() {
      return TYPE;
    }

    public String toString() {
      return TYPE + ":" + myModelName;
    }
  }

  @Immutable
  public final static class RelativePathSModelId extends SModelId {
    public static final String TYPE = "path";
    private final String myPath;

    public RelativePathSModelId(String path) {
      super();
      myPath = path;
    }

    @Override
    public String getModelName() {
      return myPath;
    }

    public String getFileName() {
      int i = myPath.lastIndexOf('/');
      if (i >= 0 && i + 1 < myPath.length()) {
        return myPath.substring(i + 1);
      }
      return myPath;
    }

    @Override
    public boolean equals(Object o) {
      if (this == o) return true;
      if (o == null || getClass() != o.getClass()) return false;
      return myPath.equals(((RelativePathSModelId) o).myPath);
    }

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

    @Override
    public String getType() {
      return TYPE;
    }

    public String toString() {
      return TYPE + ":" + myPath;
    }
  }

  /**
   * Integer-backed, module-private model identity.
   * MPS reserves values in range [0x0F000000..0xFFFFFFFF] for own uses, you're free to use lower range.
   */
  @Immutable
  public static final class IntegerSModelId extends SModelId {
    public static final String TYPE = "i";
    private final int myValue;

    public IntegerSModelId(int value) {
      myValue = value;
    }

    public int getValue() {
      return myValue;
    }

    @Override
    public boolean isGloballyUnique() {
      return false;
    }

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

    @Override
    public boolean equals(Object obj) {
      return obj instanceof IntegerSModelId && ((IntegerSModelId) obj).myValue == myValue;
    }

    @Override
    public String getType() {
      return TYPE;
    }

    @Override
    public String toString() {
      return String.format("%s:%04x", TYPE, myValue);
    }

    public static IntegerSModelId parse(String cs) throws IllegalArgumentException {
      return new IntegerSModelId(Integer.parseInt(cs, 16));
    }
  }
}
