/*
 * 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.smodel.persistence.def.v9;

import jetbrains.mps.persistence.registry.IdInfoRegistry;
import jetbrains.mps.smodel.JavaFriendlyBase64;
import jetbrains.mps.smodel.SNodeId.Foreign;
import jetbrains.mps.smodel.SNodeId.Regular;
import jetbrains.mps.smodel.StaticReference;
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.util.Pair;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.mps.openapi.model.SModelReference;
import org.jetbrains.mps.openapi.model.SNodeId;
import org.jetbrains.mps.openapi.model.SReference;
import org.jetbrains.mps.openapi.module.SModuleReference;
import org.jetbrains.mps.openapi.persistence.PersistenceFacade;

/**
 * Intention is to keep all serialize/de-serialize code in a single place.
 * <p/>
 * FIXME this class is public merely for the sake of GoToNodeById action. Once this encoder is part of persistence API, action shall use API, not this class
 * <p/>
 * This class is not thread-safe, uses internal buffers to save memory on (de-)serialize, do not share it between thread (although unlikely to happen as
 * persistence demands single thread access).
 */
public final class IdEncoder implements IdInfoRegistry.IndexEncoder {
  // separator for import in serialized reference target
  private static final char REF_TARGET_IMPORT_SEPARATOR = ':';
  private static final String DYNAMIC_REFERENCE_ID = "^";
  private final JavaFriendlyBase64 myBase64 = new JavaFriendlyBase64();

  public IdEncoder() {
  }

  public String toText(SLanguageId langId) {
    return langId.serialize();
  }

  public SLanguageId parseLanguageId(String text) {
    return SLanguageId.deserialize(text);
  }

  public String toText(SConceptId conceptId) {
    return Long.toString(conceptId.getIdValue());
  }

  public SConceptId parseConceptId(SLanguageId lang, String text) {
    return new SConceptId(lang, Long.parseLong(text));
  }

  public String toText(SPropertyId propertyId) {
    return Long.toString(propertyId.getIdValue());
  }

  public SPropertyId parsePropertyId(SConceptId concept, String text) {
    return new SPropertyId(concept, Long.parseLong(text));
  }

  public String toText(SReferenceLinkId linkId) {
    return Long.toString(linkId.getIdValue());
  }

  public SReferenceLinkId parseAssociation(SConceptId concept, String text) {
    return new SReferenceLinkId(concept, Long.parseLong(text));
  }

  public String toText(SContainmentLinkId linkId) {
    return Long.toString(linkId.getIdValue());
  }

  public SContainmentLinkId parseAggregation(SConceptId concept, String text) {
    return new SContainmentLinkId(concept, Long.parseLong(text));
  }

  public String toText(SNodeId nodeId) {
    if (nodeId instanceof Regular) {
      final long v = ((Regular) nodeId).getId();
      return myBase64.toString(v);
    }
    // fall-through
    return nodeId.toString();
  }

  public SNodeId parseNodeId(String text) throws EncodingException {
    try {
      if (!text.startsWith(Foreign.ID_PREFIX)) {
        long v = myBase64.parseLong(text);
        return new Regular(v);
      }
      // fall-through
      return jetbrains.mps.smodel.SNodeId.fromString(text);
    } catch (IllegalArgumentException ex) {
      throw new EncodingException(ex.getMessage());
    }
  }

  public String toText(SModelReference mr) {
    return PersistenceFacade.getInstance().asString(mr);
  }

  public SModelReference parseModelReference(String text) {
    return PersistenceFacade.getInstance().createModelReference(text);
  }

  public String toText(SModuleReference ref) {
//    return PersistenceFacade.getInstance().asString(ref); FIXME add counterpart for createModuleReference
    return ref.toString();
  }

  public SModuleReference parseModuleReference(String text) {
    return PersistenceFacade.getInstance().createModuleReference(text);
  }

  public String toTextLocal(SReference ref) {
    String target;
    if (ref instanceof StaticReference) {
      final SNodeId targetNodeId = ref.getTargetNodeId();
      if (targetNodeId == null) {
        target = DYNAMIC_REFERENCE_ID;
      } else {
        target = toText(targetNodeId);
      }
    } else {
      target = DYNAMIC_REFERENCE_ID;
    }
    return target;
  }

  /**
   * Local references are saved in a form of serialized node id, or '^' for dynamic references.
   * External references are prefixed with import index and ':'.
   * <p/>
   * NOTE, the way import index and nodeId value are serialized is expected to never include ':' separator char
   */
  public String toTextExternal(@NotNull ImportsHelper imports, @NotNull SReference ref) {
    String target = toTextLocal(ref);
    SModelReference targetModel = ref.getTargetSModelReference();

    if (targetModel == null) {
      return REF_TARGET_IMPORT_SEPARATOR + target;
    }

    String index = imports.getIndex(targetModel);
    assert index != null : "model " + targetModel + " not found in index";
    return index + REF_TARGET_IMPORT_SEPARATOR + target;
  }

  @Nullable
  public SNodeId parseLocalNodeReference(String text) {
    if (DYNAMIC_REFERENCE_ID.equals(text)) {
      return null;
    }
    try {
      return parseNodeId(text);
    } catch (EncodingException e) {
      throw new IllegalArgumentException(e);
    }
  }

  public Pair<SModelReference, SNodeId> parseExternalNodeReference(ImportsHelper imports, String referenceTarget) {
    int separatorIndex = referenceTarget.indexOf(REF_TARGET_IMPORT_SEPARATOR);
    assert separatorIndex >= 0;
    final SModelReference modelRef = separatorIndex == 0 ? null : imports.getModelReference(referenceTarget.substring(0, separatorIndex));
    SNodeId nodeId = parseLocalNodeReference(referenceTarget.substring(separatorIndex + 1, referenceTarget.length()));
    return new Pair<SModelReference, SNodeId>(modelRef, nodeId);
  }

  /**
   * Dedicated alternative of the {@link #parseExternalNodeReference(String)} that cares about target node id only, for indexing purposes,
   * see {@link jetbrains.mps.smodel.persistence.def.v9.Indexer9}
   */
  @Nullable
  SNodeId parseExternalNodeReference(String referenceTarget) {
    int separatorIndex = referenceTarget.indexOf(REF_TARGET_IMPORT_SEPARATOR);
    assert separatorIndex >= 0;
    return parseLocalNodeReference(referenceTarget.substring(separatorIndex + 1, referenceTarget.length()));
  }


  @Override
  public String index(int key) {
    return myBase64.indexValue(key);
  }

  public static class EncodingException extends Exception {
    public EncodingException(String message) {
      super(message);
    }
  }
}
