/*
 * 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.diff.comparison;

import com.intellij.diff.comparison.iterables.DiffIterable;
import com.intellij.diff.comparison.iterables.FairDiffIterable;
import com.intellij.diff.fragments.*;
import com.intellij.diff.util.DiffUtil;
import com.intellij.diff.util.IntPair;
import com.intellij.diff.util.MergeRange;
import com.intellij.diff.util.Range;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.progress.ProgressIndicator;
import com.intellij.openapi.util.TextRange;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.util.Consumer;
import com.intellij.util.containers.ContainerUtil;
import com.intellij.util.diff.FilesTooBigForDiffException;
import com.intellij.util.text.CharSequenceSubSequence;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

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

import static java.util.Collections.singletonList;

public class ComparisonManagerImpl extends ComparisonManager {
  private static final Logger LOG = Logger.getInstance(ComparisonManagerImpl.class);

  @NotNull
  public static ComparisonManagerImpl getInstanceImpl() {
    return (ComparisonManagerImpl)getInstance();
  }

  @NotNull
  @Override
  public List<LineFragment> compareLines(@NotNull CharSequence text1,
                                         @NotNull CharSequence text2,
                                         @NotNull ComparisonPolicy policy,
                                         @NotNull ProgressIndicator indicator) throws DiffTooBigException {
    List<Line> lines1 = getLines(text1);
    List<Line> lines2 = getLines(text2);

    List<CharSequence> lineTexts1 = ContainerUtil.map(lines1, Line::getContent);
    List<CharSequence> lineTexts2 = ContainerUtil.map(lines2, Line::getContent);

    FairDiffIterable iterable = ByLine.compare(lineTexts1, lineTexts2, policy, indicator);
    return convertIntoLineFragments(lines1, lines2, iterable);
  }

  @NotNull
  @Override
  public List<MergeLineFragment> compareLines(@NotNull CharSequence text1,
                                              @NotNull CharSequence text2,
                                              @NotNull CharSequence text3,
                                              @NotNull ComparisonPolicy policy,
                                              @NotNull ProgressIndicator indicator) throws DiffTooBigException {
    List<Line> lines1 = getLines(text1);
    List<Line> lines2 = getLines(text2);
    List<Line> lines3 = getLines(text3);

    List<CharSequence> lineTexts1 = ContainerUtil.map(lines1, Line::getContent);
    List<CharSequence> lineTexts2 = ContainerUtil.map(lines2, Line::getContent);
    List<CharSequence> lineTexts3 = ContainerUtil.map(lines3, Line::getContent);

    List<MergeRange> ranges = ByLine.compare(lineTexts1, lineTexts2, lineTexts3, policy, indicator);
    return convertIntoMergeLineFragments(ranges);
  }

  @NotNull
  @Override
  public List<LineFragment> compareLinesInner(@NotNull CharSequence text1,
                                              @NotNull CharSequence text2,
                                              @NotNull ComparisonPolicy policy,
                                              @NotNull ProgressIndicator indicator) throws DiffTooBigException {
    List<LineFragment> lineFragments = compareLines(text1, text2, policy, indicator);
    return createInnerFragments(lineFragments, text1, text2, policy, indicator);
  }

  private static List<LineFragment> createInnerFragments(@NotNull List<LineFragment> lineFragments,
                                                         @NotNull CharSequence text1,
                                                         @NotNull CharSequence text2,
                                                         @NotNull ComparisonPolicy policy,
                                                         @NotNull ProgressIndicator indicator) {
    List<LineFragment> result = new ArrayList<>(lineFragments.size());

    int tooBigChunksCount = 0;
    for (LineFragment fragment : lineFragments) {
      assert fragment.getInnerFragments() == null;

      try {
        // Do not try to build fine blocks after few fails
        boolean tryComputeDifferences = tooBigChunksCount < FilesTooBigForDiffException.MAX_BAD_LINES;
        result.addAll(createInnerFragments(fragment, text1, text2, policy, indicator, tryComputeDifferences));
      }
      catch (DiffTooBigException e) {
        result.add(fragment);
        tooBigChunksCount++;
      }
    }

    return result;
  }

  @NotNull
  private static List<LineFragment> createInnerFragments(@NotNull LineFragment fragment,
                                                         @NotNull CharSequence text1,
                                                         @NotNull CharSequence text2,
                                                         @NotNull ComparisonPolicy policy,
                                                         @NotNull ProgressIndicator indicator,
                                                         boolean tryComputeDifferences) throws DiffTooBigException {
    CharSequence subSequence1 = text1.subSequence(fragment.getStartOffset1(), fragment.getEndOffset1());
    CharSequence subSequence2 = text2.subSequence(fragment.getStartOffset2(), fragment.getEndOffset2());

    if (fragment.getStartLine1() == fragment.getEndLine1() ||
        fragment.getStartLine2() == fragment.getEndLine2()) { // Insertion / Deletion
      if (ComparisonUtil.isEquals(subSequence1, subSequence2, policy)) {
        return singletonList(new LineFragmentImpl(fragment, Collections.emptyList()));
      }
      else {
        return singletonList(fragment);
      }
    }

    if (!tryComputeDifferences) return singletonList(fragment);

    List<ByWord.LineBlock> lineBlocks = ByWord.compareAndSplit(subSequence1, subSequence2, policy, indicator);
    assert lineBlocks.size() != 0;

    int startOffset1 = fragment.getStartOffset1();
    int startOffset2 = fragment.getStartOffset2();

    int currentStartLine1 = fragment.getStartLine1();
    int currentStartLine2 = fragment.getStartLine2();

    List<LineFragment> chunks = new ArrayList<>();
    for (int i = 0; i < lineBlocks.size(); i++) {
      ByWord.LineBlock block = lineBlocks.get(i);
      Range offsets = block.offsets;

      // special case for last line to void problem with empty last line
      int currentEndLine1 = i != lineBlocks.size() - 1 ? currentStartLine1 + block.newlines1 : fragment.getEndLine1();
      int currentEndLine2 = i != lineBlocks.size() - 1 ? currentStartLine2 + block.newlines2 : fragment.getEndLine2();

      chunks.add(new LineFragmentImpl(currentStartLine1, currentEndLine1, currentStartLine2, currentEndLine2,
                                      offsets.start1 + startOffset1, offsets.end1 + startOffset1,
                                      offsets.start2 + startOffset2, offsets.end2 + startOffset2,
                                      block.fragments));

      currentStartLine1 = currentEndLine1;
      currentStartLine2 = currentEndLine2;
    }
    return chunks;
  }

  @NotNull
  @Override
  @Deprecated
  public List<LineFragment> compareLinesInner(@NotNull CharSequence text1,
                                              @NotNull CharSequence text2,
                                              @NotNull List<LineFragment> lineFragments,
                                              @NotNull ComparisonPolicy policy,
                                              @NotNull ProgressIndicator indicator) throws DiffTooBigException {
    return compareLinesInner(text1, text2, policy, indicator);
  }

  @NotNull
  @Override
  public List<DiffFragment> compareWords(@NotNull CharSequence text1,
                                         @NotNull CharSequence text2,
                                         @NotNull ComparisonPolicy policy,
                                         @NotNull ProgressIndicator indicator) throws DiffTooBigException {
    return ByWord.compare(text1, text2, policy, indicator);
  }

  @NotNull
  @Override
  public List<DiffFragment> compareChars(@NotNull CharSequence text1,
                                         @NotNull CharSequence text2,
                                         @NotNull ComparisonPolicy policy,
                                         @NotNull ProgressIndicator indicator) throws DiffTooBigException {
    if (policy == ComparisonPolicy.IGNORE_WHITESPACES) {
      return convertIntoDiffFragments(ByChar.compareIgnoreWhitespaces(text1, text2, indicator));
    }
    if (policy == ComparisonPolicy.DEFAULT) {
      return convertIntoDiffFragments(ByChar.compareTwoStep(text1, text2, indicator));
    }
    LOG.warn(policy.toString() + " is not supported by ByChar comparison");
    return convertIntoDiffFragments(ByChar.compareTwoStep(text1, text2, indicator));
  }

  @NotNull
  public List<Range> compareLines(@NotNull List<? extends CharSequence> lines1,
                                  @NotNull List<? extends CharSequence> lines2,
                                  @NotNull ComparisonPolicy policy,
                                  @NotNull ProgressIndicator indicator) throws DiffTooBigException {
    FairDiffIterable iterable = ByLine.compare(lines1, lines2, policy, indicator);
    return ContainerUtil.newArrayList(iterable.iterateChanges());
  }

  @Override
  public boolean isEquals(@NotNull CharSequence text1, @NotNull CharSequence text2, @NotNull ComparisonPolicy policy) {
    return ComparisonUtil.isEquals(text1, text2, policy);
  }

  //
  // Fragments
  //

  @NotNull
  public static List<DiffFragment> convertIntoDiffFragments(@NotNull DiffIterable changes) {
    final List<DiffFragment> fragments = new ArrayList<>();
    for (Range ch : changes.iterateChanges()) {
      fragments.add(new DiffFragmentImpl(ch.start1, ch.end1, ch.start2, ch.end2));
    }
    return fragments;
  }

  @NotNull
  public static List<LineFragment> convertIntoLineFragments(@NotNull List<Line> lines1,
                                                            @NotNull List<Line> lines2,
                                                            @NotNull FairDiffIterable changes) {
    List<LineFragment> fragments = new ArrayList<>();
    for (Range ch : changes.iterateChanges()) {
      IntPair offsets1 = getOffsets(lines1, ch.start1, ch.end1);
      IntPair offsets2 = getOffsets(lines2, ch.start2, ch.end2);

      fragments.add(new LineFragmentImpl(ch.start1, ch.end1, ch.start2, ch.end2,
                                         offsets1.val1, offsets1.val2, offsets2.val1, offsets2.val2));
    }
    return fragments;
  }

  @NotNull
  private static IntPair getOffsets(@NotNull List<Line> lines, int startIndex, int endIndex) {
    if (startIndex == endIndex) {
      int offset;
      if (startIndex < lines.size()) {
        offset = lines.get(startIndex).getOffset1();
      }
      else {
        offset = lines.get(lines.size() - 1).getOffset2();
      }
      return new IntPair(offset, offset);
    }
    else {
      int offset1 = lines.get(startIndex).getOffset1();
      int offset2 = lines.get(endIndex - 1).getOffset2();
      return new IntPair(offset1, offset2);
    }
  }

  @NotNull
  public static List<MergeLineFragment> convertIntoMergeLineFragments(@NotNull List<MergeRange> conflicts) {
    return ContainerUtil.map(conflicts, ch -> new MergeLineFragmentImpl(ch.start1, ch.end1, ch.start2, ch.end2, ch.start3, ch.end3));
  }

  @NotNull
  public static List<MergeWordFragment> convertIntoMergeWordFragments(@NotNull List<MergeRange> conflicts) {
    return ContainerUtil.map(conflicts, ch -> new MergeWordFragmentImpl(ch.start1, ch.end1, ch.start2, ch.end2, ch.start3, ch.end3));
  }

  //
  // Post process line fragments
  //

  @NotNull
  @Override
  public List<LineFragment> squash(@NotNull List<LineFragment> oldFragments) {
    if (oldFragments.isEmpty()) return oldFragments;

    final List<LineFragment> newFragments = new ArrayList<>();
    processAdjoining(oldFragments, fragments -> newFragments.add(doSquash(fragments)));
    return newFragments;
  }

  @NotNull
  @Override
  public List<LineFragment> processBlocks(@NotNull List<LineFragment> oldFragments,
                                          @NotNull final CharSequence text1, @NotNull final CharSequence text2,
                                          @NotNull final ComparisonPolicy policy,
                                          final boolean squash, final boolean trim) {
    if (!squash && !trim) return oldFragments;
    if (oldFragments.isEmpty()) return oldFragments;

    final List<LineFragment> newFragments = new ArrayList<>();
    processAdjoining(oldFragments, fragments -> newFragments.addAll(processAdjoining(fragments, text1, text2, policy, squash, trim)));
    return newFragments;
  }

  private static void processAdjoining(@NotNull List<LineFragment> oldFragments,
                                       @NotNull Consumer<List<LineFragment>> consumer) {
    int startIndex = 0;
    for (int i = 1; i < oldFragments.size(); i++) {
      if (!isAdjoining(oldFragments.get(i - 1), oldFragments.get(i))) {
        consumer.consume(oldFragments.subList(startIndex, i));
        startIndex = i;
      }
    }
    if (startIndex < oldFragments.size()) {
      consumer.consume(oldFragments.subList(startIndex, oldFragments.size()));
    }
  }

  @NotNull
  private static List<LineFragment> processAdjoining(@NotNull List<LineFragment> fragments,
                                                     @NotNull CharSequence text1, @NotNull CharSequence text2,
                                                     @NotNull ComparisonPolicy policy, boolean squash, boolean trim) {
    int start = 0;
    int end = fragments.size();

    // TODO: trim empty leading/trailing lines
    if (trim && policy == ComparisonPolicy.IGNORE_WHITESPACES) {
      while (start < end) {
        LineFragment fragment = fragments.get(start);
        CharSequenceSubSequence sequence1 = new CharSequenceSubSequence(text1, fragment.getStartOffset1(), fragment.getEndOffset1());
        CharSequenceSubSequence sequence2 = new CharSequenceSubSequence(text2, fragment.getStartOffset2(), fragment.getEndOffset2());

        if ((fragment.getInnerFragments() == null || !fragment.getInnerFragments().isEmpty()) &&
            !StringUtil.equalsIgnoreWhitespaces(sequence1, sequence2)) {
          break;
        }
        start++;
      }
      while (start < end) {
        LineFragment fragment = fragments.get(end - 1);
        CharSequenceSubSequence sequence1 = new CharSequenceSubSequence(text1, fragment.getStartOffset1(), fragment.getEndOffset1());
        CharSequenceSubSequence sequence2 = new CharSequenceSubSequence(text2, fragment.getStartOffset2(), fragment.getEndOffset2());

        if ((fragment.getInnerFragments() == null || !fragment.getInnerFragments().isEmpty()) &&
            !StringUtil.equalsIgnoreWhitespaces(sequence1, sequence2)) {
          break;
        }
        end--;
      }
    }

    if (start == end) return Collections.emptyList();
    if (squash) {
      return singletonList(doSquash(fragments.subList(start, end)));
    }
    return fragments.subList(start, end);
  }

  @NotNull
  private static LineFragment doSquash(@NotNull List<LineFragment> oldFragments) {
    assert !oldFragments.isEmpty();
    if (oldFragments.size() == 1) return oldFragments.get(0);

    LineFragment firstFragment = oldFragments.get(0);
    LineFragment lastFragment = oldFragments.get(oldFragments.size() - 1);

    List<DiffFragment> newInnerFragments = new ArrayList<>();
    for (LineFragment fragment : oldFragments) {
      for (DiffFragment innerFragment : extractInnerFragments(fragment)) {
        int shift1 = fragment.getStartOffset1() - firstFragment.getStartOffset1();
        int shift2 = fragment.getStartOffset2() - firstFragment.getStartOffset2();

        DiffFragment previousFragment = ContainerUtil.getLastItem(newInnerFragments);
        if (previousFragment == null || !isAdjoiningInner(previousFragment, innerFragment, shift1, shift2)) {
          newInnerFragments.add(new DiffFragmentImpl(innerFragment.getStartOffset1() + shift1, innerFragment.getEndOffset1() + shift1,
                                                     innerFragment.getStartOffset2() + shift2, innerFragment.getEndOffset2() + shift2));
        }
        else {
          newInnerFragments.remove(newInnerFragments.size() - 1);
          newInnerFragments.add(new DiffFragmentImpl(previousFragment.getStartOffset1(), innerFragment.getEndOffset1() + shift1,
                                                     previousFragment.getStartOffset2(), innerFragment.getEndOffset2() + shift2));
        }
      }
    }

    return new LineFragmentImpl(firstFragment.getStartLine1(), lastFragment.getEndLine1(),
                                firstFragment.getStartLine2(), lastFragment.getEndLine2(),
                                firstFragment.getStartOffset1(), lastFragment.getEndOffset1(),
                                firstFragment.getStartOffset2(), lastFragment.getEndOffset2(),
                                newInnerFragments);
  }

  private static boolean isAdjoining(@NotNull LineFragment beforeFragment, @NotNull LineFragment afterFragment) {
    if (beforeFragment.getEndLine1() != afterFragment.getStartLine1() ||
        beforeFragment.getEndLine2() != afterFragment.getStartLine2() ||
        beforeFragment.getEndOffset1() != afterFragment.getStartOffset1() ||
        beforeFragment.getEndOffset2() != afterFragment.getStartOffset2()) {
      return false;
    }

    return true;
  }

  private static boolean isAdjoiningInner(@NotNull DiffFragment beforeFragment, @NotNull DiffFragment afterFragment,
                                          int shift1, int shift2) {
    if (beforeFragment.getEndOffset1() != afterFragment.getStartOffset1() + shift1 ||
        beforeFragment.getEndOffset2() != afterFragment.getStartOffset2() + shift2) {
      return false;
    }

    return true;
  }

  @NotNull
  private static List<DiffFragment> extractInnerFragments(@NotNull LineFragment lineFragment) {
    if (lineFragment.getInnerFragments() != null) return lineFragment.getInnerFragments();

    int length1 = lineFragment.getEndOffset1() - lineFragment.getStartOffset1();
    int length2 = lineFragment.getEndOffset2() - lineFragment.getStartOffset2();
    return singletonList(new DiffFragmentImpl(0, length1, 0, length2));
  }

  @NotNull
  private static List<Line> getLines(@NotNull CharSequence text) {
    List<Line> lines = new ArrayList<>();

    int offset = 0;
    while (true) {
      int lineEnd = StringUtil.indexOf(text, '\n', offset);
      if (lineEnd != -1) {
        lines.add(new Line(text, offset, lineEnd, true));
        offset = lineEnd + 1;
      }
      else {
        lines.add(new Line(text, offset, text.length(), false));
        break;
      }
    }

    return lines;
  }


  /**
   * Compare two texts by-line and then compare changed fragments by-word
   */
  @NotNull
  public List<LineFragment> compareLinesWithIgnoredRanges(@NotNull CharSequence text1,
                                                          @NotNull CharSequence text2,
                                                          @NotNull List<TextRange> ignoredRanges1,
                                                          @NotNull List<TextRange> ignoredRanges2,
                                                          boolean innerFragments,
                                                          @NotNull ProgressIndicator indicator) throws DiffTooBigException {
    BitSet ignored1 = collectIgnoredRanges(ignoredRanges1);
    BitSet ignored2 = collectIgnoredRanges(ignoredRanges2);

    List<Line> lines1 = getLines(text1);
    List<Line> lines2 = getLines(text2);

    List<CharSequence> lineTexts1 = ContainerUtil.map(lines1, line -> line.getNotIgnoredContent(ignored1));
    List<CharSequence> lineTexts2 = ContainerUtil.map(lines2, line -> line.getNotIgnoredContent(ignored2));

    FairDiffIterable iterable = ByLine.compare(lineTexts1, lineTexts2, ComparisonPolicy.DEFAULT, indicator);
    List<LineFragment> lineFragments = convertIntoLineFragments(lines1, lines2, iterable);

    if (innerFragments) {
      lineFragments = createInnerFragments(lineFragments, text1, text2, ComparisonPolicy.DEFAULT, indicator);
    }

    return ContainerUtil.mapNotNull(lineFragments, fragment -> {
      return trimIgnoredChanges(fragment, lines1, lines2, ignored1, ignored2);
    });
  }

  @NotNull
  private static BitSet collectIgnoredRanges(@NotNull List<TextRange> ignoredRanges) {
    BitSet set = new BitSet();
    for (TextRange range : ignoredRanges) {
      set.set(range.getStartOffset(), range.getEndOffset());
    }
    return set;
  }

  @Nullable
  private static LineFragment trimIgnoredChanges(@NotNull LineFragment fragment,
                                                 @NotNull List<Line> lines1,
                                                 @NotNull List<Line> lines2,
                                                 @NotNull BitSet ignored1,
                                                 @NotNull BitSet ignored2) {
    // trim ignored lines
    Range range = TrimUtil.trimExpandList(lines1, lines2,
                                          fragment.getStartLine1(), fragment.getStartLine2(),
                                          fragment.getEndLine1(), fragment.getEndLine2(),
                                          (line1, line2) -> areIgnoredEqualLines(line1, line2, ignored1, ignored2),
                                          line -> isIgnoredLine(line, ignored1),
                                          line -> isIgnoredLine(line, ignored2));

    int startLine1 = range.start1;
    int startLine2 = range.start2;
    int endLine1 = range.end1;
    int endLine2 = range.end2;

    if (startLine1 == endLine1 && startLine2 == endLine2) return null;

    IntPair offsets1 = getOffsets(lines1, startLine1, endLine1);
    IntPair offsets2 = getOffsets(lines2, startLine2, endLine2);
    int startOffset1 = offsets1.val1;
    int endOffset1 = offsets1.val2;
    int startOffset2 = offsets2.val1;
    int endOffset2 = offsets2.val2;

    List<DiffFragment> newInner = null;
    if (fragment.getInnerFragments() != null) {
      int shift1 = startOffset1 - fragment.getStartOffset1();
      int shift2 = startOffset2 - fragment.getStartOffset2();
      int newCount1 = endOffset1 - startOffset1;
      int newCount2 = endOffset2 - startOffset2;

      newInner = ContainerUtil.mapNotNull(fragment.getInnerFragments(), it -> {
        // update offsets, as some lines might have been ignored completely
        int start1 = DiffUtil.bound(it.getStartOffset1() - shift1, 0, newCount1);
        int start2 = DiffUtil.bound(it.getStartOffset2() - shift2, 0, newCount2);
        int end1 = DiffUtil.bound(it.getEndOffset1() - shift1, 0, newCount1);
        int end2 = DiffUtil.bound(it.getEndOffset2() - shift2, 0, newCount2);

        // trim inner fragments
        TextRange range1 = trimIgnoredRange(start1, end1, ignored1, startOffset1);
        TextRange range2 = trimIgnoredRange(start2, end2, ignored2, startOffset2);

        if (range1.isEmpty() && range2.isEmpty()) return null;
        return new DiffFragmentImpl(range1.getStartOffset(), range1.getEndOffset(),
                                    range2.getStartOffset(), range2.getEndOffset());
      });
      if (newInner.isEmpty()) return null;
    }

    return new LineFragmentImpl(startLine1, endLine1, startLine2, endLine2,
                                startOffset1, endOffset1, startOffset2, endOffset2,
                                newInner);
  }

  private static boolean isIgnoredLine(@NotNull Line line, @NotNull BitSet ignored) {
    return trimIgnoredRange(line.getOffset1(), line.getOffset2(), ignored, 0).isEmpty();
  }

  private static boolean areIgnoredEqualLines(@NotNull Line line1, @NotNull Line line2,
                                              @NotNull BitSet ignored1, @NotNull BitSet ignored2) {
    int start1 = line1.getOffset1();
    int end1 = line1.getOffset2();
    int start2 = line2.getOffset1();
    int end2 = line2.getOffset2();

    Range range = TrimUtil.trimExpandText(line1.getOriginalText(), line2.getOriginalText(),
                                          start1, start2, end1, end2,
                                          ignored1, ignored2);
    return range.isEmpty();
  }


  @NotNull
  private static TextRange trimIgnoredRange(int start, int end, @NotNull BitSet ignored, int offset) {
    IntPair intPair = TrimUtil.trim(offset + start, offset + end, ignored);
    return new TextRange(intPair.val1 - offset, intPair.val2 - offset);
  }


  private static class Line {
    @NotNull private final CharSequence myChars;
    private final int myOffset1;
    private final int myOffset2;
    private final boolean myNewline;

    public Line(@NotNull CharSequence chars, int offset1, int offset2, boolean newline) {
      myChars = chars;
      myOffset1 = offset1;
      myOffset2 = offset2;
      myNewline = newline;
    }

    public int getOffset1() {
      return myOffset1;
    }

    public int getOffset2() {
      return myOffset2 + (myNewline ? 1 : 0);
    }

    @NotNull
    public CharSequence getContent() {
      return new CharSequenceSubSequence(myChars, myOffset1, myOffset2);
    }

    @NotNull
    public CharSequence getNotIgnoredContent(@NotNull BitSet ignored) {
      StringBuilder sb = new StringBuilder();
      for (int i = myOffset1; i < myOffset2; i++) {
        if (ignored.get(i)) continue;
        sb.append(myChars.charAt(i));
      }
      return sb.toString();
    }

    @NotNull
    public CharSequence getOriginalText() {
      return myChars;
    }
  }
}
