/*
 * 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;

import com.intellij.diagnostic.Dumpable;
import com.intellij.openapi.editor.SoftWrap;
import com.intellij.openapi.editor.TextChange;
import com.intellij.openapi.editor.ex.SoftWrapChangeListener;
import com.intellij.util.containers.ContainerUtil;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

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

/**
 * Holds registered soft wraps and provides monitoring and management facilities for them.
 * <p/>
 * Not thread-safe.
 *
 * @author Denis Zhdanov
 * @since Jun 29, 2010 3:04:20 PM
 */
public class SoftWrapsStorage implements Dumpable {

  private final List<SoftWrapImpl>        myWraps     = new ArrayList<>();
  private final List<SoftWrapImpl>        myWrapsView = Collections.unmodifiableList(myWraps);
  private final List<SoftWrapChangeListener> myListeners = ContainerUtil.createLockFreeCopyOnWriteList();

  /**
   * @return    <code>true</code> if there is at least one soft wrap registered at the current storage; <code>false</code> otherwise
   */
  public boolean isEmpty() {
    return myWraps.isEmpty();
  }

  @Nullable
  public SoftWrap getSoftWrap(int offset) {
    int i = getSoftWrapIndex(offset);
    return i >= 0 ? myWraps.get(i) : null;
  }

  /**
   * @return    view for registered soft wraps sorted by offset in ascending order if any; empty collection otherwise
   */
  @NotNull
  public List<SoftWrapImpl> getSoftWraps() {
    return myWrapsView;
  }

  /**
   * Tries to find index of the target soft wrap stored at {@link #myWraps} collection. <code>'Target'</code> soft wrap is the one
   * that starts at the given offset.
   *
   * @param offset    target offset
   * @return          index that conforms to {@link Collections#binarySearch(List, Object)} contract, i.e. non-negative returned
   *                  index points to soft wrap that starts at the given offset; <code>'-(negative value) - 1'</code> points
   *                  to position at {@link #myWraps} collection where soft wrap for the given index should be inserted
   */
  public int getSoftWrapIndex(int offset) {
    int start = 0;
    int end = myWraps.size() - 1;

    // We use custom inline implementation of binary search here because profiling shows that standard Collections.binarySearch()
    // is a bottleneck. The most probable reason is a big number of interface calls.
    while (start <= end) {
      int i = (start + end) >>> 1;
      SoftWrap softWrap = myWraps.get(i);
      int softWrapOffset = softWrap.getStart();
      if (softWrapOffset > offset) {
        end = i - 1;
      }
      else if (softWrapOffset < offset) {
        start = i + 1;
      }
      else {
        return i;
      }
    }
    return -(start + 1);
  }

  /**
   * Allows to answer how many soft wraps which {@link TextChange#getStart() start offsets} belong to given
   * <code>[start; end]</code> interval are registered withing the current storage.
   * 
   * @param startOffset   target start offset (inclusive)
   * @param endOffset     target end offset (inclusive)
   * @return              number of soft wraps which {@link TextChange#getStart() start offsets} belong to the target range
   */
  public int getNumberOfSoftWrapsInRange(int startOffset, int endOffset) {
    int startIndex = getSoftWrapIndex(startOffset);
    if (startIndex < 0) {
      startIndex = -startIndex - 1;
    }

    if (startIndex >= myWraps.size()) {
      return 0;
    }
    int result = 0;
    int endIndex = startIndex;
    for (; endIndex < myWraps.size(); endIndex++) {
      SoftWrap softWrap = myWraps.get(endIndex);
      if (softWrap.getStart() > endOffset) {
        break;
      }
      result++;
    }
    return result;
  }
  
  /**
   * Inserts given soft wrap to {@link #myWraps} collection at the given index.
   *
   * @param softWrap          soft wrap to store
   * @return                  previous soft wrap object stored for the same offset if any; <code>null</code> otherwise
   */
  public void storeOrReplace(SoftWrapImpl softWrap) {
    int i = getSoftWrapIndex(softWrap.getStart());
    if (i >= 0) {
      myWraps.set(i, softWrap);
      return;
    }

    i = -i - 1;
    myWraps.add(i, softWrap);
  }

  public void remove(SoftWrapImpl softWrap) {
    if (myWraps.isEmpty()) return;
    int i = myWraps.size() - 1; // expected use case is removing of last soft wrap, so we have a fast path here for that case
    if (myWraps.get(i).getStart() != softWrap.getStart()) {
      i = getSoftWrapIndex(softWrap.getStart());
    }
    if (i >= 0) {
      myWraps.remove(i);
    }
  }

  /**
   * Removes soft wraps with offsets equal or larger than a given offset from storage.
   * 
   * @return soft wraps that were removed, ordered by offset
   */
  public List<SoftWrapImpl> removeStartingFrom(int offset) {
    int startIndex = getSoftWrapIndex(offset);
    if (startIndex < 0) {
      startIndex = -startIndex - 1;
    }

    if (startIndex >= myWraps.size()) {
      return Collections.emptyList();
    }

    List<SoftWrapImpl> tail = myWraps.subList(startIndex, myWraps.size());
    List<SoftWrapImpl> result = new ArrayList<>(tail);
    tail.clear();
    return result;
  }

  /**
   * Adds soft wraps to storage. They are supposed to be sorted by their offsets, and have offsets larger than offsets for soft wraps 
   * existing in storage at the moment.
   */
  public void addAll(List<SoftWrapImpl> softWraps) {
    myWraps.addAll(softWraps);
  }

  /**
   * Removes all soft wraps registered at the current storage.
   */
  public void removeAll() {
    myWraps.clear();
    notifyListenersAboutChange();
  }

  /**
   * Registers given listener within the current model
   *
   * @param listener    listener to register
   * @return            <code>true</code> if given listener was not registered before; <code>false</code> otherwise
   */
  public boolean addSoftWrapChangeListener(@NotNull SoftWrapChangeListener listener) {
    return myListeners.add(listener);
  }

  public void notifyListenersAboutChange() {
    for (SoftWrapChangeListener listener : myListeners) {
      listener.softWrapsChanged();
    }
  }

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