/*
 * 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.idea.java.usages;

import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.util.TextRange;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiReference;
import com.intellij.util.IncorrectOperationException;
import jetbrains.mps.ide.project.ProjectHelper;
import jetbrains.mps.idea.core.psi.impl.MPSPsiNode;
import jetbrains.mps.idea.core.psi.impl.MPSPsiProvider;
import jetbrains.mps.idea.core.refactoring.NodePtr;
import jetbrains.mps.idea.java.psiStubs.JavaForeignIdBuilder;
import jetbrains.mps.idea.java.refactoring.MoveRenameBatch;
import jetbrains.mps.smodel.ModelAccessHelper;
import jetbrains.mps.smodel.ModelImports;
import jetbrains.mps.smodel.SNodeId.Foreign;
import jetbrains.mps.smodel.SNodePointer;
import jetbrains.mps.smodel.StaticReference;
import jetbrains.mps.util.Computable;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.mps.openapi.model.SModel;
import org.jetbrains.mps.openapi.model.SModelReference;
import org.jetbrains.mps.openapi.model.SNode;
import org.jetbrains.mps.openapi.model.SNodeId;
import org.jetbrains.mps.openapi.model.SNodeReference;


/**
 * danilla 3/24/13
 */

public class IdPrefixReference implements PsiReference {
  private SNodeReference myTarget;
  private String myRole;
  private PsiElement myParent;

  public IdPrefixReference(SNodeReference target, String role, PsiElement fosterFather) {
    myTarget = target;
    myRole = role;
    myParent = fosterFather;
  }

  @Override
  public PsiElement getElement() {
    return myParent;
  }

  @Override
  public TextRange getRangeInElement() {
    return TextRange.EMPTY_RANGE;
  }

  @Nullable
  @Override
  public PsiElement resolve() {
    ApplicationManager.getApplication().assertReadAccessAllowed();

    return new ModelAccessHelper(ProjectHelper.getModelAccess(myParent.getProject())).runReadAction(new Computable<PsiElement>() {
      @Override
      public PsiElement compute() {
        return MPSPsiProvider.getInstance(myParent.getProject()).getPsi(myTarget);
      }
    });
  }

  @NotNull
  @Override
  public String getCanonicalText() {
    return "MPS id prefix reference";
  }

  @Override
  public PsiElement handleElementRename(String newElementName) throws IncorrectOperationException {
    SNodeId targetNodeId = ((SNodePointer) myTarget).getNodeId();
    String str = targetNodeId.toString();
    String newStr;
    int lastDot = str.lastIndexOf(".");
    if (lastDot < 0) {
      newStr = Foreign.ID_PREFIX + newElementName;
    } else {
      newStr = str.substring(0, lastDot + 1) + newElementName;
    }
    final NodePtr newTarget = new NodePtr(((SNodePointer) myTarget).getModelReference(), new Foreign(newStr));

    SNodeReference source = ((MPSPsiNode) myParent).getSNodeReference();

    myParent.getProject().getComponent(MoveRenameBatch.class).scheduleIdPrefixRefUpdate(source, myRole, new Runnable() {
      @Override
      public void run() {
        handleRename(newTarget);
      }
    });
    return myParent;
  }

  @Override
  public PsiElement bindToElement(@NotNull PsiElement element) throws IncorrectOperationException {
    SNodeReference source = ((MPSPsiNode) myParent).getSNodeReference();
    final NodePtr newTargetPtr = JavaForeignIdBuilder.computeNodePtr(element);
    myParent.getProject().getComponent(MoveRenameBatch.class).scheduleIdPrefixRefUpdate(source, myRole, new Runnable() {
      @Override
      public void run() {
        handleRename(newTargetPtr);
      }
    });
    return myParent;
  }

  private void handleRename(NodePtr newNode) {
    SNodePointer oldNode = (SNodePointer) myTarget;
    SNode source = ((MPSPsiNode) myParent).getSNodeReference().resolve(ProjectHelper.getProjectRepository(myParent.getProject()));
    String oldId = source.getReference(myRole).getTargetNodeId().toString();

    // replacing all proper occurences
    String what = oldNode.getNodeId().toString();
    what = what.startsWith("~") ? what.substring(1) : what;
    String replacement = newNode.getNodeId().toString();
    replacement = replacement.startsWith("~") ? replacement.substring(1) : replacement;

    String newId = carefullyReplace(oldId, what, replacement);

    source.setReference(myRole, StaticReference.create(myRole, source, newNode.getSModelReference(), new Foreign(newId)));

    // add model import if needed
    if (!oldNode.getModelReference().equals(newNode.getSModelReference())) {
      SModel model = ((MPSPsiNode) myParent).getSNodeReference().resolve(ProjectHelper.getProjectRepository(myParent.getProject())).getModel();
      SModelReference newTargetModel = newNode.getSModelReference();

      new ModelImports(model).addModelImport(newTargetModel);
    }
  }

  private String carefullyReplace(String input, String what, String replacement) {
    String result = input;

    int len = what.length();
    int idx = input.indexOf(what);

    while (idx >= 0) {
      if (idx != 0) {
        char c = result.charAt(idx - 1);
        if (c == '.' || Character.isLetterOrDigit(c)) { // java foreign id specific knowledge
          idx = result.indexOf(what, idx + 1);
          continue;
        }
      }
      result = result.substring(0, idx) + replacement + result.substring(idx + len);
      idx = result.indexOf(what, idx + len);
    }

    return result;
  }

  @Override
  public boolean isReferenceTo(PsiElement element) {
    if (!(element instanceof MPSPsiNode)) return false;
    return myTarget.equals(((MPSPsiNode) element).getSNodeReference());
  }

  @NotNull
  @Override
  public Object[] getVariants() {
    return new Object[0];  //To change body of implemented methods use File | Settings | File Templates.
  }

  // Q: should it be true?
  @Override
  public boolean isSoft() {
    return false;
  }
}
