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

import jetbrains.mps.smodel.adapter.ids.MetaIdFactory;
import jetbrains.mps.smodel.adapter.ids.SConceptId;
import jetbrains.mps.util.EqualUtil;
import jetbrains.mps.util.io.ModelInputStream;
import jetbrains.mps.util.io.ModelOutputStream;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.mps.annotations.Immutable;
import org.jetbrains.mps.openapi.model.SModelId;
import org.jetbrains.mps.openapi.model.SModelReference;
import org.jetbrains.mps.openapi.model.SNodeId;
import org.jetbrains.mps.openapi.module.SModuleId;

import java.io.IOException;

/**
 * Entry in the usage index. We keep distinct instances to tell concept uses from node or model uses.
 *
 * Though concept instances ({@link ConceptInstance}) are now different from node usages, they come from the same indexing process,
 * and thus it would be ineffective to index twice to keep them separate.
 *
 * @author Artem Tikhomirov
 * @since 3.4
 */
abstract class UsageEntry {
  static final byte TOKEN_CONCEPT_INSTANCE = 1;
  static final byte TOKEN_NODE_USE = 2;
  static final byte TOKEN_MODEL_USE = 4;


  // XXX SHALL BE ModelDataOutput instead (once I extract relevant part of MOS API into MDO)
  /*package*/ abstract void save(ModelOutputStream mos) throws IOException;

  /*package*/ static UsageEntry load(ModelInputStream mis) throws IOException {
    byte token = mis.readByte();
    if (token != TOKEN_CONCEPT_INSTANCE && token != TOKEN_MODEL_USE && token != TOKEN_NODE_USE) {
      throw new IOException(String.format("Bad UsageEntry identifier: %x", token));
    }
    switch(token) {
      case TOKEN_CONCEPT_INSTANCE : return new ConceptInstance(MetaIdFactory.conceptId(mis.readUUID(), mis.readLong()));
      case TOKEN_NODE_USE : return new NodeUse(mis.readNodeId());
      case TOKEN_MODEL_USE : return new ModelUse(mis.readModuleID(), mis.readModelID());
    }
    throw new IllegalStateException();
  }

  @Immutable
  final static class ConceptInstance extends UsageEntry {
    private final SConceptId myConcept;

    public ConceptInstance(SConceptId concept) {
      myConcept = concept;
    }

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

    @Override
    public boolean equals(Object obj) {
      if (obj instanceof ConceptInstance) {
        return myConcept.equals(((ConceptInstance) obj).myConcept);
      }
      return false;
    }

    @Override
    /*package*/ void save(ModelOutputStream mos) throws IOException {
      mos.writeByte(TOKEN_CONCEPT_INSTANCE);
      mos.writeUUID(myConcept.getLanguageId().getIdValue());
      mos.writeLong(myConcept.getIdValue());
    }
  }

  @Immutable
  final static class NodeUse extends UsageEntry {
    private final SNodeId myNode;

    public NodeUse(SNodeId node) {
      myNode = node;
    }

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

    @Override
    public boolean equals(Object obj) {
      if (obj instanceof NodeUse) {
        return myNode.equals(((NodeUse) obj).myNode);
      }
      return false;
    }

    @Override
    void save(ModelOutputStream mos) throws IOException {
      mos.writeByte(TOKEN_NODE_USE);
      mos.writeNodeId(myNode);
    }
  }

  @Immutable
  final static class ModelUse extends UsageEntry {
    private final SModuleId myModule;
    private final SModelId myModel;

    public ModelUse(@NotNull SModelReference modelRef) {
      myModel = modelRef.getModelId();
      myModule = myModel.isGloballyUnique() ? null : modelRef.getModuleReference().getModuleId();
    }

    /*package*/ModelUse(@Nullable SModuleId module, @NotNull SModelId model) {
      myModule = module;
      myModel = model;
    }

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

    @Override
    public boolean equals(Object obj) {
      if (obj instanceof ModelUse) {
        ModelUse o = (ModelUse) obj;
        return myModel.equals(o.myModel) && EqualUtil.equals(myModule, o.myModule);
      }
      return false;
    }

    @Override
    void save(ModelOutputStream mos) throws IOException {
      mos.writeByte(TOKEN_MODEL_USE);
      mos.writeModuleID(myModule);
      mos.writeModelID(myModel);
    }
  }
}
