/*
 * Copyright 2000-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 com.intellij.openapi.diff.impl.incrementalMerge;

import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.command.CommandProcessor;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.diff.DiffBundle;
import com.intellij.openapi.diff.impl.highlighting.FragmentSide;
import com.intellij.openapi.diff.impl.util.DocumentUtil;
import com.intellij.openapi.editor.Document;
import com.intellij.openapi.editor.Editor;
import com.intellij.openapi.editor.RangeMarker;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.TextRange;
import com.intellij.openapi.vfs.ReadonlyStatusHandler;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import java.util.Comparator;

/**
 * Represents a change in diff or merge view.
 * A change has two {@link com.intellij.openapi.diff.impl.incrementalMerge.Change.SimpleChangeSide sides} (left and right), each of them representing the text which has been changed and the original text
 * shown in the diff/merge.
 * Change can be applied, then its sides would be equal.
 */
public abstract class Change {
  private static final Logger LOG = Logger.getInstance(Change.class);

  public abstract ChangeSide getChangeSide(FragmentSide side);

  public abstract ChangeType getType();

  public abstract ChangeList getChangeList();

  protected abstract void removeFromList();

  /**
   * Called when a change has been applied.
   */
  public abstract void onApplied();

  /**
   * Called when a change has been removed from the list.
   */
  public abstract void onRemovedFromList();

  public abstract boolean isValid();

  /**
   * Apply the change, i.e. change the "Merge result" document and update range markers, highlighting, gutters, etc.
   * @param original The source side of the change, which is being applied.
   */
  private void apply(@NotNull FragmentSide original) {
    FragmentSide targetSide = original.otherSide();
    RangeMarker originalRangeMarker = getRangeMarker(original);
    RangeMarker rangeMarker = getRangeMarker(targetSide);

    TextRange textRange = modifyDocument(getProject(), originalRangeMarker, rangeMarker);
    if (textRange != null && isValid()) {
      updateTargetRangeMarker(targetSide, textRange);
    }
    onApplied();
  }

  /**
   * Updates the target marker of a change after the change has been applied
   * to allow highlighting of the document modification which has been performed.
   * @param targetFragmentSide The side to be changed.
   * @param updatedTextRange   New text range to be applied to the side.
   */
  protected final void updateTargetRangeMarker(@NotNull FragmentSide targetFragmentSide, @NotNull TextRange updatedTextRange) {
    ChangeSide targetSide = getChangeSide(targetFragmentSide);
    DiffRangeMarker originalRange = targetSide.getRange();
    DiffRangeMarker updatedRange = new DiffRangeMarker(originalRange.getDocument(), updatedTextRange, null);
    changeSide(targetSide, updatedRange);
  }

  /**
   * Substitutes the specified side of this change to a new side which contains the given range.
   * @param sideToChange The side to be changed.
   * @param newRange     New text range of the new side.
   */
  protected abstract void changeSide(ChangeSide sideToChange, DiffRangeMarker newRange);

  /**
   * Applies the text from the original marker to the target marker.
   * @return the resulting TextRange from the target document, or null if the document if not writable.
   */
  @Nullable
  private static TextRange modifyDocument(@Nullable Project project, @NotNull RangeMarker original, @NotNull RangeMarker target) {
    Document document = target.getDocument();
    if (project != null && !ReadonlyStatusHandler.ensureDocumentWritable(project, document)) {
      return null;
    }
    if (DocumentUtil.isEmpty(original)) {
      int offset = target.getStartOffset();
      document.deleteString(offset, target.getEndOffset());
    }
    CharSequence text = original.getDocument().getImmutableCharSequence().subSequence(original.getStartOffset(), original.getEndOffset());
    int startOffset = target.getStartOffset();
    if (DocumentUtil.isEmpty(target)) {
      document.insertString(startOffset, text);
    } else {
      document.replaceString(startOffset, target.getEndOffset(), text);
    }
    return new TextRange(startOffset, startOffset + text.length());
  }

  public void addMarkup(Editor[] editors) {
    LOG.assertTrue(editors.length == 2);
    highlight(editors, FragmentSide.SIDE1);
    highlight(editors, FragmentSide.SIDE2);
  }

  private void highlight(Editor[] editors, FragmentSide side) {
    getHighlighterHolder(side).highlight(getChangeSide(side), editors[side.getIndex()], getType());
  }

  private void updateHighlighter(FragmentSide side) {
    getHighlighterHolder(side).updateHighlighter(getChangeSide(side), getType());
  }

  @Nullable
  private Project getProject() {
    return getChangeList().getProject();
  }

  @NotNull
  private ChangeHighlighterHolder getHighlighterHolder(FragmentSide side) {
    return getChangeSide(side).getHighlighterHolder();
  }

  @NotNull
  private RangeMarker getRangeMarker(FragmentSide side) {
    ChangeSide changeSide = getChangeSide(side);
    LOG.assertTrue(changeSide != null);
    return changeSide.getRange();
  }

  public static void apply(final Change change, final FragmentSide fromSide) {
    ApplicationManager.getApplication().runWriteAction(() -> CommandProcessor.getInstance().executeCommand(change.getProject(), () -> doApply(change, fromSide), null, DiffBundle.message("save.merge.result.command.name")));
  }

  public static void doApply(final Change change, final FragmentSide fromSide) {
    change.apply(fromSide);
  }

  public void updateMarkup() {
    updateHighlighter(FragmentSide.SIDE1);
    updateHighlighter(FragmentSide.SIDE2);
  }

  public boolean canHasActions(FragmentSide fromSide) {
    FragmentSide targetSide = fromSide.otherSide();
    Document targetDocument = getChangeList().getDocument(targetSide);
    if (!targetDocument.isWritable()) return false;
    Editor targetEditor = getHighlighterHolder(targetSide).getEditor();
    return !targetEditor.isViewer();
  }

  public static class ChangeOrder implements Comparator<Change> {
    private final FragmentSide myMainSide;

    public ChangeOrder(FragmentSide mainSide) {
      myMainSide = mainSide;
    }

    @Override
    public int compare(@NotNull Change change, @NotNull Change change1) {
      int result1 = compareSide(change, change1, myMainSide);
      if (result1 != 0) return result1;
      return compareSide(change, change1, myMainSide.otherSide());
    }

    private static int compareSide(Change change, Change change1, FragmentSide side) {
      return RangeMarker.BY_START_OFFSET.compare(change.getRangeMarker(side), change1.getRangeMarker(side));
    }
  }

  protected static class SimpleChangeSide extends ChangeSide {
    private final FragmentSide mySide;
    private final DiffRangeMarker myRange;
    private final ChangeHighlighterHolder myHighlighterHolder;

    public SimpleChangeSide(FragmentSide side, DiffRangeMarker rangeMarker) {
      mySide = side;
      myRange = rangeMarker;
      myHighlighterHolder = new ChangeHighlighterHolder();
    }

    public SimpleChangeSide(@NotNull ChangeSide originalSide, @NotNull DiffRangeMarker newRange) {
      mySide = ((SimpleChangeSide)originalSide).getFragmentSide();
      myRange = newRange;
      myHighlighterHolder = originalSide.getHighlighterHolder();
    }

    @NotNull
    public FragmentSide getFragmentSide() {
      return mySide;
    }

    @Override
    @NotNull
    public DiffRangeMarker getRange() {
      return myRange;
    }

    @NotNull
    @Override
    public ChangeHighlighterHolder getHighlighterHolder() {
      return myHighlighterHolder;
    }
  }
}
