/*
 * Copyright 2000-2015 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.editor.impl.softwrap.mapping;

import com.intellij.diagnostic.Dumpable;
import com.intellij.openapi.diagnostic.Attachment;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.editor.*;
import com.intellij.openapi.editor.ex.EditorEx;
import com.intellij.openapi.editor.impl.TextChangeImpl;
import com.intellij.openapi.editor.impl.softwrap.SoftWrapImpl;
import com.intellij.openapi.editor.impl.softwrap.SoftWrapsStorage;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

public class CachingSoftWrapDataMapper implements SoftWrapAwareDocumentParsingListener, Dumpable {
  
  private static final Logger LOG = Logger.getInstance("#" + CachingSoftWrapDataMapper.class.getName());
  
  private final List<SoftWrapImpl>             myAffectedByUpdateSoftWraps           = new ArrayList<>();

  private final EditorEx                           myEditor;
  private final SoftWrapsStorage                   myStorage;

  public CachingSoftWrapDataMapper(@NotNull EditorEx editor, @NotNull SoftWrapsStorage storage)
  {
    myEditor = editor;
    myStorage = storage;
  }

  boolean matchesOldSoftWrap(SoftWrap newSoftWrap, int lengthDiff) {
    return Collections.binarySearch(myAffectedByUpdateSoftWraps, new SoftWrapImpl(new TextChangeImpl(newSoftWrap.getText(),
                                                                                                     newSoftWrap.getStart() - lengthDiff,
                                                                                                     newSoftWrap.getEnd() - lengthDiff),
                                                                                  newSoftWrap.getIndentInColumns(),
                                                                                  newSoftWrap.getIndentInPixels()),
                                    (o1, o2) -> {
                                      int offsetDiff = o1.getStart() - o2.getStart();
                                      if (offsetDiff != 0) {
                                        return offsetDiff;
                                      }
                                      int textDiff = o1.getText().toString().compareTo(o2.getText().toString());
                                      if (textDiff != 0) {
                                        return textDiff;
                                      }
                                      int colIndentDiff = o1.getIndentInColumns() - o2.getIndentInColumns();
                                      if (colIndentDiff != 0) {
                                        return colIndentDiff;
                                      }
                                      return o1.getIndentInPixels() - o2.getIndentInPixels();
                                    }) >= 0;
  }

  @Override
  public void recalculationEnds() {
  }

  @Override
  public void onCacheUpdateStart(@NotNull IncrementalCacheUpdateEvent event) {
    int startOffset = event.getStartOffset();

    myAffectedByUpdateSoftWraps.clear();
    myAffectedByUpdateSoftWraps.addAll(myStorage.removeStartingFrom(startOffset + 1));
  }

  @Override
  public void onRecalculationEnd(@NotNull IncrementalCacheUpdateEvent event) {
    advanceSoftWrapOffsets(event);
  }

  @Override
  public void reset() {
    myAffectedByUpdateSoftWraps.clear();
  }

  /**
   * Determines which soft wraps were not affected by recalculation, and shifts them to their new offsets.
   *
   * @return Change in soft wraps count after recalculation
   */
  private void advanceSoftWrapOffsets(@NotNull IncrementalCacheUpdateEvent event) {
    int lengthDiff = event.getLengthDiff();
    int recalcEndOffsetTranslated = event.getActualEndOffset() - lengthDiff;

    int firstIndex = -1;
    int softWrappedLinesDiff = myStorage.getNumberOfSoftWrapsInRange(event.getStartOffset() + 1, myEditor.getDocument().getTextLength());
    boolean softWrapsChanged = softWrappedLinesDiff > 0;
    for (int i = 0; i < myAffectedByUpdateSoftWraps.size(); i++) {
      SoftWrap softWrap = myAffectedByUpdateSoftWraps.get(i);
      if (firstIndex < 0) {
        if (softWrap.getStart() > recalcEndOffsetTranslated) {
          firstIndex = i;
          if (lengthDiff == 0) {
            break;
          }
        } else {
          softWrappedLinesDiff--;
          softWrapsChanged = true;
        }
      } 
      if (firstIndex >= 0 && i >= firstIndex) {
        ((SoftWrapImpl)softWrap).advance(lengthDiff);
      }
    }
    if (firstIndex >= 0) {
      List<SoftWrapImpl> updated = myAffectedByUpdateSoftWraps.subList(firstIndex, myAffectedByUpdateSoftWraps.size());
      SoftWrapImpl lastSoftWrap = getLastSoftWrap();
      if (lastSoftWrap != null && lastSoftWrap.getStart() >= updated.get(0).getStart()) {
        LOG.error("Invalid soft wrap recalculation", new Attachment("state.txt", myEditor.getSoftWrapModel().toString()));
      }
      myStorage.addAll(updated);
    }
    myAffectedByUpdateSoftWraps.clear();
    if (softWrapsChanged) {
      myStorage.notifyListenersAboutChange();
    }
  }

  @Nullable
  SoftWrapImpl getLastSoftWrap() {
    List<SoftWrapImpl> softWraps = myStorage.getSoftWraps();
    return softWraps.isEmpty() ? null : softWraps.get(softWraps.size() - 1);
  }
  
  @NotNull
  @Override
  public String dumpState() {
    return "Soft wraps affected by current update: " + myAffectedByUpdateSoftWraps;
  }

  @Override
  public String toString() {
    return dumpState();
  }
}