// Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.

package com.intellij.formatting;

import com.intellij.formatting.engine.BlockRangesMap;
import com.intellij.openapi.util.Comparing;
import com.intellij.openapi.util.TextRange;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiFile;
import com.intellij.psi.PsiWhiteSpace;
import com.intellij.psi.codeStyle.CommonCodeStyleSettings;
import com.intellij.psi.formatter.FormattingDocumentModelImpl;
import com.intellij.util.BitUtil;
import com.intellij.util.text.CharArrayUtil;
import org.jetbrains.annotations.NotNull;

import java.util.ArrayList;

/**
 * Object-level representation of continuous white space at document. Either line feed or tabulation or space is considered to be {@code 'white-space'}.
 * I.e. document text fragment like {@code '\t   \t\t\n\t   \t'}  may be considered as a continuous white space and may be represented as a {@link WhiteSpace} object.
 * <p/>
 * Provides number of properties that describe encapsulated continuous white space:
 * <ul>
 *   <li>{@link #getLineFeeds() lineFeeds};</li>
 *   <li>{@link #getSpaces() spaces};</li>
 *   <li>{@link #getIndentSpaces()};</li>
 * </ul>
 * <p/>
 * Provides ability to build string representation of the managed settings taking into consideration user settings for tabulation vs white space usage, tabulation
 * size etc.
 * <p/>
 * Not thread-safe.
 */
public final class WhiteSpace {

  private static final char LINE_FEED = '\n';

  private final int myStart;
  private       int myEnd;

  private int mySpaces;
  private int myIndentSpaces;
  private int myInitialLastLinesSpaces;
  private int myInitialLastLinesTabs;

  private CharSequence myInitial;
  private int          myFlags;
  private boolean      myForceSkipTabulationsUsage;
  private boolean      myIsBeforeCodeBlockEnd;

  private static final byte FIRST_MASK = 1;
  private static final byte SAFE_MASK = 0x2;
  private static final byte KEEP_FIRST_COLUMN_MASK = 0x4;
  private static final byte LINE_FEEDS_ARE_READ_ONLY_MASK = 0x8;
  private static final byte READ_ONLY_MASK = 0x10;
  private static final byte CONTAINS_LF_INITIALLY_MASK = 0x20;
  private static final byte CONTAINS_SPACES_INITIALLY_MASK = 0x40;

  private static final int LF_COUNT_SHIFT = 7;
  private static final int MAX_LF_COUNT = 1 << 24;

  private static final RangesAssert myRangesAssert = new RangesAssert();

  /**
   * Creates new {@code WhiteSpace} object with the given start offset and a flag that shows if current white space is
   * the first white space.
   * <p/>
   * <b>Note:</b> {@link #getEndOffset() end offset} value is the same as the {@link #getStartOffset() start offset} for
   * the newly constructed object. {@link #changeEndOffset(int, FormattingDocumentModel, CommonCodeStyleSettings.IndentOptions)} should be
   * called in order to apply desired end offset.
   *
   * @param startOffset       start offset to use
   * @param isFirst              flag that shows if current white space is the first
   */
  public WhiteSpace(int startOffset, boolean isFirst) {
    myStart = startOffset;
    myEnd = startOffset;
    setIsFirstWhiteSpace(isFirst);
  }

  /**
   * Applies new end offset to the current object.
   * <p/>
   * Namely, performs the following:
   * <ol>
   *   <li>Checks if new end offset can be applied, return in case of negative answer;</li>
   *   <li>
   *          Processes all new symbols introduced by the new end offset value, calculates number of line feeds,
   *          white spaces and tabulations between them and updates {@link #getLineFeeds() lineFeeds}, {@link #getSpaces() spaces},
   *          {@link #getIndentSpaces() indentSpaces} and {@link #getTotalSpaces() totalSpaces} properties accordingly;
   *    </li>
   * </ol>
   *
   * @param newEndOffset      new end offset value
   * @param model                 formatting model that is used to access to the underlying document text
   * @param options               indent formatting options
   */
  public void changeEndOffset(int newEndOffset, FormattingDocumentModel model, CommonCodeStyleSettings.IndentOptions options) {
    final int oldEndOffset = myEnd;
    if (newEndOffset == oldEndOffset) return;
    if (myStart >= newEndOffset) {
      myRangesAssert.assertInvalidRanges(myStart, newEndOffset, model, "some block intersects with whitespace");
      return;
    }

    myEnd = newEndOffset;
    TextRange range = new TextRange(myStart, myEnd);
    CharSequence oldText = myInitial;
    myInitial = model.getText(range);

    if (!coveredByBlock(model)) {
      myRangesAssert.assertInvalidRanges(myStart, myEnd, model, "nonempty text is not covered by block");
      return;
    }

    if (newEndOffset > oldEndOffset) {
      refreshStateOnEndOffsetIncrease(newEndOffset, oldEndOffset, options.TAB_SIZE);
    } else {
      refreshStateOnEndOffsetDecrease(oldText, newEndOffset, oldEndOffset, options.TAB_SIZE);
    }
    IndentInside indent = IndentInside.getLastLineIndent(myInitial);
    myInitialLastLinesSpaces = indent.whiteSpaces;
    myInitialLastLinesTabs = indent.tabs;

    setFlag(CONTAINS_LF_INITIALLY_MASK, getLineFeeds() > 0);

    final int totalSpaces = getTotalSpaces();
    setFlag(CONTAINS_SPACES_INITIALLY_MASK, totalSpaces > 0);
  }

  /**
   * Allows to check if {@code 'myInitial'} property value stands for continuous white space text.
   * <p/>
   * The text is considered to be continuous {@code 'white space'} at following cases:
   * <ul>
   *   <li>{@code 'myInitial'} is empty string or string that contains white spaces only;</li>
   *   <li>{@code 'myInitial'} is a {@code CDATA} string which content is empty or consists from white spaces only;</li>
   *   <li>{@code 'myInitial'} string belongs to the same {@link PsiWhiteSpace} element;</li>
   * </ul>
   *
   * @param model     formatting model that is used to provide access to the {@code PSI API} if necessary
   * @return          {@code true} if {@code 'myInitial'} property value stands for white space;
   *                  {@code false} otherwise
   */
  private boolean coveredByBlock(final FormattingDocumentModel model) {
    if (myInitial == null) return true;
    if (model.containsWhiteSpaceSymbolsOnly(myStart, myEnd)) return true;

    if (!(model instanceof FormattingDocumentModelImpl)) return false;
    PsiFile psiFile = ((FormattingDocumentModelImpl)model).getFile();
    PsiElement start = psiFile.findElementAt(myStart);
    PsiElement end = psiFile.findElementAt(myEnd - 1);
    return start == end && start instanceof PsiWhiteSpace; // there maybe non-white text inside CDATA-encoded injected elements
  }

  private void refreshStateOnEndOffsetIncrease(int newEndOffset, int oldEndOffset, int tabSize) {
    assert newEndOffset > oldEndOffset;
    WhiteSpaceInfo info = parse(myInitial, oldEndOffset - myStart, newEndOffset - myStart, mySpaces + myIndentSpaces, tabSize);
    if (info.lineFeeds > 0) {
      setLineFeeds(getLineFeeds() + info.lineFeeds);
      mySpaces = 0;
      myIndentSpaces = 0;
    }
    mySpaces += info.spaces;
    myIndentSpaces += info.indentSpaces;
  }

  private void refreshStateOnEndOffsetDecrease(CharSequence oldText, int newEndOffset, int oldEndOffset, int tabSize) {
    assert newEndOffset < oldEndOffset;
    int lineFeedsNumberAtRemovedText = 0;
    int spacesNumberAtRemovedText = 0;
    int indentSpacesNumberAtRemovedText = 0;
    int column = mySpaces + myIndentSpaces;
    for (int i = oldEndOffset - 1; i >= newEndOffset; i--) {
      switch (oldText.charAt(i)) {
        case LINE_FEED -> {
          ++lineFeedsNumberAtRemovedText;
          column = 0;
        }
        case ' ' -> {
          ++spacesNumberAtRemovedText;
          column--;
        }
        case '\t' -> {
          int change = column % tabSize;
          if (change == 0) {
            change = tabSize;
          }
          indentSpacesNumberAtRemovedText += change;
          column -= change;
        }
      }
    }

    // There are no line feeds at remained text, hence, we can just subtract number of spaces and indent spaces
    // from removed text and finish.
    if (getLineFeeds() - lineFeedsNumberAtRemovedText <= 0) {
      setLineFeeds(0);
      mySpaces -= spacesNumberAtRemovedText;
      myIndentSpaces -= indentSpacesNumberAtRemovedText;
      return;
    }

    // There are white spaces at remained text, hence, we need to calculate number of spaces and indent spaces between
    // last line feed and new end offset.
    int newLineFeedsNumber = getLineFeeds() - lineFeedsNumberAtRemovedText;
    assert newLineFeedsNumber >= 0;
    newLineFeedsNumber = Math.max(newLineFeedsNumber, 0); // Never expect the defense to be triggered.
    setLineFeeds(newLineFeedsNumber);
    int startOffset = CharArrayUtil.shiftForwardUntil(oldText, newEndOffset - 1, "\n") + 1;
    WhiteSpaceInfo info = parse(oldText, startOffset, newEndOffset, 0, tabSize);
    mySpaces = info.spaces;
    myIndentSpaces = info.indentSpaces;
  }

  /**
   * Parses information about white space symbols at the target region of the given text.
   *
   * @param text         target text
   * @param startOffset  target text range's start offset (inclusive)
   * @param endOffset    target text range's end offset (exclusive)
   * @param startColumn  given start offset's column. It affects how tab width is calculated, say, a tab symbol which
   *                     occupies four columns, will occupy only three if located at the first column
   * @param tabSize      tab width in columns
   * @return             information about white space symbols at the target region of the given text
   */
  private static @NotNull WhiteSpaceInfo parse(@NotNull CharSequence text, int startOffset, int endOffset, int startColumn, int tabSize) {
    assert startOffset <= endOffset;

    int spaces = 0;
    int indentSpaces = 0;
    int lineFeeds = 0;
    int column = startColumn;

    for (int i = startOffset; i < endOffset; i++) {
      switch (text.charAt(i)) {
        case LINE_FEED -> {
          lineFeeds++;
          spaces = 0;
          indentSpaces = 0;
          column = 0;
        }
        case '\t' -> {
          int change = tabSize - (column % tabSize);
          indentSpaces += change;
          column += change;
        }
        default -> {
          spaces++;
          column++;
        }
      }
    }

    return new WhiteSpaceInfo(lineFeeds, indentSpaces, spaces);
  }

  /**
   * Builds string that contains line feeds, white spaces and tabulation symbols known to the current {@link WhiteSpace} object.
   *
   * @param options     indentation formatting options
   * @return            string that contains line feeds, white spaces and tabulation symbols known to the current
   *                    {@link WhiteSpace} object
   */
  public String generateWhiteSpace(CommonCodeStyleSettings.IndentOptions options) {
    return
      new IndentInfo(getLineFeeds(), myIndentSpaces, mySpaces, myForceSkipTabulationsUsage)
        .setIndentEmptyLines(myIsBeforeCodeBlockEnd)
        .generateNewWhiteSpace(options);
  }

  /**
   * Tries to apply given values to {@link #getSpaces() spaces} and {@link #getIndentSpaces() indentSpaces} properties accordingly.
   * <p/>
   * The action is not guaranteed to be executed (i.e. the it's not guaranteed that target properties return given values after
   * this method call - see {@link #performModification(Runnable)} for more details).
   * <p/>
   * Moreover, the action is guaranteed to be <b>not</b> executed  if {@link #isKeepFirstColumn() keepFirstColumn} property
   * is unset and target document string doesn't contain spaces.
   *
   * @param spaces      new value for the {@link #getSpaces() spaces} property
   * @param indent      new value for the {@link #getIndentSpaces()}  indentSpaces} property
   */
  public void setSpaces(final int spaces, final int indent) {
    if (spaces < 0 || indent < 0) {
      throw new IllegalStateException("Neither spaces: " + spaces + " or indent " + indent + " should be null");
    }

    performModification(() -> {
      if (!isKeepFirstColumn() || getFlag(CONTAINS_SPACES_INITIALLY_MASK)) {
        mySpaces = spaces;
        myIndentSpaces = indent;
      }
    });
  }

  private boolean doesNotContainAnySpaces() {
    return getTotalSpaces() == 0 && getLineFeeds() == 0;
  }

  public int getStartOffset() {
    return myStart;
  }

  public int getEndOffset() {
    return myEnd;
  }

  /**
   * Execute given action in a safe manner.
   * <p/>
   * {@code 'Safe manner'} here means the following:
   * <ul>
   *   <li>don't execute the action if {@link #isIsReadOnly() isReadOnly} property value is set to {@code true};</li>
   *   <li>
   *        ensure that number of line feeds after action execution is preserved if line feeds are
   *        {@link #isLineFeedsAreReadOnly() read only};
   *   </li>
   *   <li>
   *        ensure the following if {@link #isIsSafe() isSafe} property is set to {@code true}:
   *        <ul>
   *          <li>
   *            cut all white spaces and line feeds appeared after given action execution to single white space if there
   *            were no line feeds and white spaces before;
   *          </li>
   *        </ul>
   *    </li>
   * </ul>
   *
   * @param action      action that should be performed in a safe manner
   */
  private void performModification(Runnable action) {
    if (isIsReadOnly()) return;
    final boolean before = doesNotContainAnySpaces();
    final int lineFeedsBefore = getLineFeeds();
    action.run();
    if (isLineFeedsAreReadOnly()) {
      setLineFeeds(lineFeedsBefore);
    }
    if (isIsSafe()) {
      final boolean after = doesNotContainAnySpaces();
      if (before && !after) {
        // Actions below seem to be useless if 'after' value is 'false'. Are kept as historical heritage.
        mySpaces = 0;
        myIndentSpaces = 0;
        setLineFeeds(0);
      }
      else if (!before && after) {
        mySpaces = 1;
        myIndentSpaces = 0;
      }
    }
  }

  /**
   * Tries to arrange {@link #getSpaces() spaces} property value to belong to
   * [{@link SpacingImpl#getMinSpaces() min}; {@link SpacingImpl#getMaxSpaces()}] bounds defined by the given spacing object
   * if {@link #getTotalSpaces() totalSpaces} property value lays out of the same bounds.
   * <p/>
   * The action is <b>not</b> performed if there are line feeds configured for the
   * current {@link WhiteSpace} object ({@link #getSpaces()} != 0).
   *
   * @param spaceProperty     spacing settings holder
   */
  public void arrangeSpaces(final SpacingImpl spaceProperty) {
    performModification(() -> {
      if (spaceProperty != null) {
        if (getLineFeeds() == 0) {
          if (spaceProperty.getMinSpaces() >= 0 && getTotalSpaces() < spaceProperty.getMinSpaces()) {
            setSpaces(spaceProperty.getMinSpaces(), 0);
          }
          if (spaceProperty.getMaxSpaces() >= 0 && getTotalSpaces() > spaceProperty.getMaxSpaces()) {
            setSpaces(spaceProperty.getMaxSpaces(), 0);
          }
        }
      }
    });
  }

  /**
   * Tries to ensure that number of line feeds managed by the current {@link WhiteSpace} is consistent to the settings
   * defined at the given spacing property.
   */
  public void arrangeLineFeeds(final SpacingImpl spaceProperty, final BlockRangesMap helper) {
    performModification(() -> {
      if (spaceProperty != null) {
        spaceProperty.refresh(helper);

        if (spaceProperty.getMinLineFeeds() >= 0 && getLineFeeds() < spaceProperty.getMinLineFeeds()) {
          setLineFeeds(spaceProperty.getMinLineFeeds());
        }
        if (getLineFeeds() > 0) {
          if (spaceProperty.getKeepBlankLines() > 0) {
            if (getLineFeeds() >= spaceProperty.getKeepBlankLines() + 1) {
              setLineFeeds(spaceProperty.getKeepBlankLines() + 1);
            }
          }
          else {
            if (getLineFeeds() > spaceProperty.getMinLineFeeds()) {
              if (spaceProperty.shouldKeepLineFeeds()) {
                setLineFeeds(Math.max(spaceProperty.getMinLineFeeds(), 1));
              }
              else {
                setLineFeeds(spaceProperty.getMinLineFeeds());
                if (getLineFeeds() == 0) mySpaces = 0;
              }
            }
          }
          if (getLineFeeds() == 1 && spaceProperty.getMinLineFeeds() == 0) {
            if (spaceProperty.shouldKeepLineFeeds()) {
              KeptLineFeedsCollector.registerLineFeed(this);
            }
            else {
              setLineFeeds(0);
              mySpaces = 0;
            }
          }

          if (getLineFeeds() > 0 && getLineFeeds() < spaceProperty.getPrefLineFeeds()) {
            setLineFeeds(spaceProperty.getPrefLineFeeds());
          }
        }
      } else if (isFirst()) {
        setLineFeeds(0);
        mySpaces = 0;
      }
    });
  }

  /**
   * There is a possible case that particular indent info is applied to the code block that is not the first block on a line.
   * E.g. we may want to align field name during {@code 'align fields in columns'} processing:
   *     <pre>
   *         public class Test {
   *             private Object o;
   *             private int {@code <white space to align>}i;
   *         }
   *     </pre>
   * We may not want to use tabulation characters then even if user configured
   * {@link CommonCodeStyleSettings.IndentOptions#USE_TAB_CHARACTER their usage} because we can't be sure how many visual columns
   * will be used for tab representation if there are non-white space symbols before it (IJ editor may use different number of columns
   * for single tabulation symbol representation).
   * <p/>
   * Hence, we can ask current white space object to avoid using tabulation symbols.
   *
   * @param skip   indicates if tabulations symbols usage should be suppressed
   */
  public void setForceSkipTabulationsUsage(boolean skip) {
    myForceSkipTabulationsUsage = skip;
  }

  /**
   * Allows to get information if target text document continuous 'white space' represented by the current object contained line feed
   * symbol(s) initially or contains line feed(s) now.
   *
   * @return    {@code true} if this object contained line feeds initially or contains them now; {@code false} otherwise
   * @see #containsLineFeedsInitially()
   */
  public boolean containsLineFeeds() {
    return isIsFirstWhiteSpace() || getLineFeeds() > 0;
  }

  public int getTotalSpaces() {
    return mySpaces + myIndentSpaces;
  }

  /**
   * Tries to ensure that current {@link WhiteSpace} object contains at least one line feed.
   */
  public void ensureLineFeed() {
    performModification(() -> {
      if (!containsLineFeeds()) {
        setLineFeeds(1);
        mySpaces = 0;
      }
    });
  }

  public boolean isReadOnly() {
    return isIsReadOnly() || (isIsSafe() && doesNotContainAnySpaces());
  }

  /**
   * @param ws      char sequence to check
   * @return        {@code true} if given char sequence is equal to the target document text identified by
   *                start/end offsets managed by the current {@link WhiteSpace} object; {@code false} otherwise
   */
  public boolean equalsToString(CharSequence ws) {
    if (myInitial == null) return ws.length() == 0;
    return Comparing.equal(ws, myInitial, true);
  }

  public void setIsSafe(final boolean value) {
    setFlag(SAFE_MASK, value);
  }

  private void setFlag(final int mask, final boolean value) {
    myFlags = BitUtil.set(myFlags, mask, value);
  }

  private boolean getFlag(final int mask) {
    return BitUtil.isSet(myFlags, mask);
  }

  private boolean isFirst() {
    return isIsFirstWhiteSpace();
  }

  /**
   * Allows to get information if current object contained line feed(s) initially. It's considered to contain them if
   * line feeds were found at the target text document fragment after
   * new {@link #changeEndOffset(int, FormattingDocumentModel, CommonCodeStyleSettings.IndentOptions)}  end offset appliance}.
   *
   * @return    {@code true} if current object contained line feeds initially; {@code false} otherwise
   */
  public boolean containsLineFeedsInitially() {
    if (myInitial == null) return false;
    return getFlag(CONTAINS_LF_INITIALLY_MASK);
  }

  /**
   * Tries to ensure that number of line feeds and white spaces managed by the given {@link WhiteSpace} object is the
   * same as the one defined by the given {@code 'spacing'} object.
   * <p/>
   * This method may be considered a shortcut for calling {@link #arrangeLineFeeds(SpacingImpl, BlockRangesMap)} and
   * {@link #arrangeSpaces(SpacingImpl)}.
   */
  public void removeLineFeeds(final SpacingImpl spacing, final BlockRangesMap helper) {
    performModification(() -> {
      setLineFeeds(0);
      mySpaces = 0;
      myIndentSpaces = 0;
    });
    arrangeLineFeeds(spacing, helper);
    arrangeSpaces(spacing);
  }

  public int getIndentOffset() {
    return myIndentSpaces;
  }

  /**
   * Allows to get information about number of 'pure' white space symbols at the last line of continuous white space document
   * text fragment represented by the current {@link WhiteSpace} object.
   * <p/>
   * <b>Note:</b> pay special attention to {@code 'last line'} qualification here. Consider the following target continuous
   * white space document fragment:
   * <pre>
   *        ' ws<sub>11</sub>ws<sub>12</sub>
   *        'ws<sub>21</sub>'
   * </pre>
   * <p/>
   * Here <code>'ws<sub>nm</sub>'</code> is a m-th white space symbol at the n-th line. {@code 'Spaces'} property of
   * {@link WhiteSpace} object for such white-space text has a value not <b>2</b> or <b>3</b> but <b>1</b>.
   *
   * @return number of white spaces at the last line of target continuous white space text document fragment
   */
  public int getSpaces() {
    return mySpaces;
  }

  public void setKeepFirstColumn(final boolean b) {
    setFlag(KEEP_FIRST_COLUMN_MASK, b);
  }

  public void setReadOnly(final boolean isReadOnly) {
    setIsReadOnly(isReadOnly);
  }

  public boolean isIsFirstWhiteSpace() {
    return getFlag(FIRST_MASK);
  }

  public boolean isIsSafe() {
    return getFlag(SAFE_MASK);
  }

  public boolean isKeepFirstColumn() {
    return getFlag(KEEP_FIRST_COLUMN_MASK);
  }

  public boolean isLineFeedsAreReadOnly() {
    return getFlag(LINE_FEEDS_ARE_READ_ONLY_MASK);
  }

  public void setLineFeedsAreReadOnly(final boolean lineFeedsAreReadOnly) {
    setFlag(LINE_FEEDS_ARE_READ_ONLY_MASK, lineFeedsAreReadOnly);
  }

  public boolean isIsReadOnly() {
    return getFlag(READ_ONLY_MASK);
  }

  public void setIsReadOnly(final boolean isReadOnly) {
    setFlag(READ_ONLY_MASK, isReadOnly);
  }

  public void setIsFirstWhiteSpace(final boolean isFirstWhiteSpace) {
    setFlag(FIRST_MASK, isFirstWhiteSpace);
  }

  public StringBuilder generateWhiteSpace(final CommonCodeStyleSettings.IndentOptions indentOptions,
                                          final int offset,
                                          final IndentInfo indent) {
    final StringBuilder result = new StringBuilder();
    int currentOffset = getStartOffset();
    CharSequence[] lines = getInitialLines();
    int currentLine = 0;
    for (int i = 0; i < lines.length - 1 && currentOffset + lines[i].length() <= offset; i++) {
      result.append(lines[i]);
      currentOffset += lines[i].length();
      result.append(LINE_FEED);
      currentOffset++;
      currentLine++;
      if (currentOffset == offset) {
        break;
      }
    }
    final String newIndentSpaces = indent.generateNewWhiteSpace(indentOptions);
    result.append(newIndentSpaces);
    appendNonWhitespaces(result, lines, currentLine);
    if (currentLine + 1 < lines.length) {
      result.append(LINE_FEED);
      for (int i = currentLine + 1; i < lines.length - 1; i++) {
        result.append(lines[i]);
        result.append(LINE_FEED);
      }
      appendNonWhitespaces(result, lines, lines.length - 1);
      result.append(lines[lines.length - 1]);
    }
    return result;
  }

  public @NotNull IndentInside getInitialLastLineIndent() {
    return new IndentInside(myInitialLastLinesSpaces, myInitialLastLinesTabs);
  }

  private static void appendNonWhitespaces(StringBuilder result, CharSequence[] lines, int currentLine) {
    // It looks like regexp usage is too heavy here.
    if (currentLine != lines.length && !lines[currentLine].toString().matches("\\s*")) {
      result.append(lines[currentLine]);
    }
  }

  /**
   * Shows target document text fragment identified by start/end offsets as a sequence of strings.
   * <p/>
   * <b>Note:</b> arrays usage here is considered to be a historical heritage.
   *
   * @return target document text fragment as a sequence of strings
   */
  private CharSequence[] getInitialLines() {
    if (myInitial == null) return new CharSequence[]{""};
    final ArrayList<CharSequence> result = new ArrayList<>();
    StringBuilder currentLine = new StringBuilder();
    for (int i = 0; i < myInitial.length(); i++) {
      final char c = myInitial.charAt(i);
      if (c == LINE_FEED) {
        result.add(currentLine);
        currentLine = new StringBuilder();
      }
      else {
        currentLine.append(c);
      }
    }
    result.add(currentLine);
    return result.toArray(new CharSequence[0]);
  }

  /**
   * Provides access to the information about indent white spaces at last line of continuous white space text document
   * fragment represented by the current {@link WhiteSpace} object.
   * <p/>
   * {@code 'Indent white space'} here is a white space representation of tabulation symbol. User may define that
   * he or she wants to use particular number of white spaces instead of tabulation
   * ({@link CommonCodeStyleSettings.IndentOptions#TAB_SIZE}). So, {@link WhiteSpace} object uses corresponding
   * number of 'indent white spaces' for each tabulation symbol encountered at target continuous white space text document fragment.
   * <p/>
   * <b>Note:</b> pay special attention to {@code 'last line'} qualification here. Consider the following target
   * continuous white space document fragment:
   * <pre>
   *        ' \t\t
   *        '\t'
   * </pre>
   * <p/>
   * Let's consider that {@code 'tab size'} is defined as four white spaces (default setting).
   * {@code 'IndentSpaces'} property of {@link WhiteSpace} object for such white-space text has a value not
   * <b>8</b> or <b>12</b> but <b>4</b>, i.e. tabulation symbols from last line
   * only are counted.
   *
   * @return number of indent spaces at the last line of target continuous white space text document fragment
   */
  public int getIndentSpaces() {
    return myIndentSpaces;
  }

  public int getLength() {
    return myEnd - myStart;
  }

  /**
   * Provides access to the line feed symbols number at continuous white space text document fragment represented
   * by the current {@link WhiteSpace} object.
   *
   * @return line feed symbols number
   */
  public final int getLineFeeds() {
    return myFlags >>> LF_COUNT_SHIFT;
  }

  public void setLineFeeds(final int lineFeeds) {
    assert lineFeeds < MAX_LF_COUNT;
    final int flags = myFlags;
    myFlags &= ~0xFFFFFF80; // keep only seven lower bits, i.e. drop all line feeds registered before if any
    myFlags |= (lineFeeds << LF_COUNT_SHIFT);

    assert getLineFeeds() == lineFeeds;
    assert (flags & 0x7F) == (myFlags & 0x7F);
  }

  @SuppressWarnings("SameParameterValue")
  void setBeforeCodeBlockEnd(boolean isBeforeCodeBlockEnd) {
    myIsBeforeCodeBlockEnd = isBeforeCodeBlockEnd;
  }

  public TextRange getTextRange() {
    return new TextRange(myStart, myEnd);
  }

  @Override
  public String toString() {
    return "WhiteSpace(" + myStart + "-" + myEnd + " spaces=" + mySpaces + " LFs=" + getLineFeeds() + ")";
  }

  private record WhiteSpaceInfo(int lineFeeds, int indentSpaces, int spaces) {
  }
}

