StringHolder.java

/*
 * stringhold
 *
 * Copyright 2022-2024 Christian Kohlschütter
 *
 * 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.kohlschutter.stringhold;

import java.io.IOException;
import java.io.Reader;
import java.io.Writer;
import java.util.PrimitiveIterator;
import java.util.concurrent.Executor;
import java.util.function.Predicate;
import java.util.function.Supplier;

/**
 * A {@link StringHolder} holds something that can <em>eventually</em> turn into a string.
 *
 * {@link StringHolder}s may reduce string allocation in cases where the final string sequence is,
 * for example sent to a Writer (or other Appendable), discarded after a certain length, ignored
 * upon an exception thrown along the way, etc.
 *
 * Apart from reducing string concatenation-related allocations, {@link StringHolder}s may reduce
 * the end-to-end string life-cycle by allowing concurrency between construction and transmission:
 * The string can be transmitted while it's being constructed.
 *
 * Unlike regular stream-based approaches, a pre-rendered structure is available before transmission
 * starts. This means a transmission that is known to exceed certain limits can be stopped before a
 * single character is transmitted.
 *
 * @author Christian Kohlschütter
 */
@SuppressWarnings("PMD.ExcessivePublicCount")
public interface StringHolder extends CharSequence, HasLength, Comparable<Object>, Cloneable {
  /**
   * Constructs a new {@link StringHolder} with content from the given supplier, assuming a minimum
   * length of 0.
   *
   * @param supplier The supplier.
   * @return The {@link StringHolder} instance.
   * @throws IllegalStateException if minLength is negative.
   * @throws NullPointerException if supplier was {@code null}.
   */
  static StringHolder withSupplier(Supplier<?> supplier) {
    return withSupplierMinimumLength(0, supplier);
  }

  /**
   * Constructs a new {@link StringHolder} with content from the given supplier, assuming a minimum
   * length of 0.
   *
   * @param supplier The supplier (may throw an {@link IOException} upon {@link Supplier#get()},
   *          which is handled by the given exception handler).
   * @param onError The exception handler.
   * @return The {@link StringHolder} instance.
   * @throws IllegalStateException if minLength is negative.
   * @throws NullPointerException if supplier was {@code null}.
   */
  static StringHolder withSupplier(IOSupplier<?> supplier, IOExceptionHandler onError) {
    return withSupplierMinimumLength(0, supplier, onError);
  }

  /**
   * Constructs a new {@link StringHolder} with content from the given supplier, specifying a
   * minimum of the estimated length.
   *
   * @param minLength The minimum length, must not be larger than the actual length.
   * @param supplier The supplier.
   * @return The {@link StringHolder} instance.
   * @throws IllegalStateException if minLength is negative.
   * @throws NullPointerException if supplier was {@code null}.
   */
  static StringHolder withSupplierMinimumLength(int minLength, Supplier<?> supplier) {
    return new SuppliedStringHolder(minLength, minLength, supplier);
  }

  /**
   * Constructs a new {@link StringHolder} with content from the given supplier, specifying a
   * minimum of the estimated length.
   *
   * @param minLength The minimum length, must not be larger than the actual length.
   * @param supplier The supplier (may throw an {@link IOException} upon {@link Supplier#get()},
   *          which is handled by the given exception handler).
   * @param onError The exception handler.
   * @return The {@link StringHolder} instance.
   * @throws IllegalStateException if minLength is negative.
   * @throws NullPointerException if supplier was {@code null}.
   */
  static StringHolder withSupplierMinimumLength(int minLength, IOSupplier<?> supplier,
      IOExceptionHandler onError) {
    return new SuppliedStringHolder(minLength, minLength, supplier, onError);
  }

  /**
   * Constructs a new {@link StringHolder} with content from the given supplier, specifying a
   * minimum of the estimated length.
   *
   * @param expectedLength The expected length, may be larger than the actual length.
   * @param supplier The supplier.
   * @return The {@link StringHolder} instance.
   * @throws IllegalStateException if minLength is negative.
   * @throws NullPointerException if supplier was {@code null}.
   */
  static StringHolder withSupplierExpectedLength(int expectedLength, Supplier<?> supplier) {
    return new SuppliedStringHolder(0, expectedLength, supplier);
  }

  /**
   * Constructs a new {@link StringHolder} with content from the given supplier, specifying a
   * minimum of the estimated length.
   *
   * @param expectedLength The expected length, may be larger than the actual length.
   * @param supplier The supplier (may throw an {@link IOException} upon {@link Supplier#get()},
   *          which is handled by the given exception handler).
   * @param onError The exception handler.
   * @return The {@link StringHolder} instance.
   * @throws IllegalStateException if minLength is negative.
   * @throws NullPointerException if supplier was {@code null}.
   */
  static StringHolder withSupplierExpectedLength(int expectedLength, IOSupplier<?> supplier,
      IOExceptionHandler onError) {
    return new SuppliedStringHolder(0, expectedLength, supplier, onError);
  }

  /**
   * Constructs a new {@link StringHolder} with content from the given supplier, specifying a
   * minimum of the estimated length.
   *
   * @param minLength The minimum length, must not be larger than the actual length.
   * @param expectedLength The expected length, may be larger than the actual length.
   * @param supplier The supplier.
   * @return The {@link StringHolder} instance.
   * @throws IllegalStateException if minLength is negative.
   * @throws NullPointerException if supplier was {@code null}.
   */
  static StringHolder withSupplierMinimumAndExpectedLength(int minLength, int expectedLength,
      Supplier<?> supplier) {
    return new SuppliedStringHolder(minLength, expectedLength, supplier);
  }

  /**
   * Constructs a new {@link StringHolder} with content from the given supplier, specifying a
   * minimum of the estimated length.
   *
   * @param minLength The minimum length, must not be larger than the actual length.
   * @param expectedLength The expected length, may be larger than the actual length.
   * @param supplier The supplier (may throw an {@link IOException} upon {@link Supplier#get()},
   *          which is handled by the given exception handler).
   * @param onError The exception handler.
   * @return The {@link StringHolder} instance.
   * @throws IllegalStateException if minLength is negative.
   * @throws NullPointerException if supplier was {@code null}.
   */
  static StringHolder withSupplierMinimumAndExpectedLength(int minLength, int expectedLength,
      IOSupplier<?> supplier, IOExceptionHandler onError) {
    return new SuppliedStringHolder(minLength, expectedLength, supplier, onError);
  }

  /**
   * Constructs a new {@link StringHolder} with content from the given supplier, specifying the
   * length the supplied string is going to have. An {@link IllegalStateException} will be thrown
   * once a string is supplied that does not match this length.
   *
   * @param fixedLength The exact length of the string.
   * @param supplier The supplier.
   * @return The {@link StringHolder} instance.
   * @throws IllegalStateException if fixedLength is negative.
   * @throws NullPointerException if supplier was {@code null}.
   */
  static StringHolder withSupplierFixedLength(int fixedLength, Supplier<?> supplier) {
    if (fixedLength == 0) {
      return CommonStrings.EMPTY_STRINGHOLDER;
    }
    return new FixedLengthSuppliedStringHolder(fixedLength, supplier);
  }

  /**
   * Constructs a new {@link StringHolder} with content from the given supplier, specifying the
   * length the supplied string is going to have. An {@link IllegalStateException} will be thrown
   * once a string is supplied that does not match this length.
   *
   * @param fixedLength The exact length of the string.
   * @param supplier The supplier (may throw an {@link IOException} upon {@link Supplier#get()},
   *          which is handled by the given exception handler).
   * @param onError The exception handler.
   * @return The {@link StringHolder} instance.
   * @throws IllegalStateException if fixedLength is negative.
   * @throws NullPointerException if supplier was {@code null}.
   */
  static StringHolder withSupplierFixedLength(int fixedLength, IOSupplier<?> supplier,
      IOExceptionHandler onError) {
    if (fixedLength == 0) {
      return CommonStrings.EMPTY_STRINGHOLDER;
    }
    return new FixedLengthSuppliedStringHolder(fixedLength, supplier, onError);
  }

  /**
   * Constructs a {@link ReaderStringHolder} with the given Reader source.
   *
   * @param readerSupply The supply of {@link Reader} instances for the content.
   * @param onError The exception handler.
   * @return The {@link ReaderStringHolder}.
   * @throws NullPointerException if supplier was {@code null}.
   */
  static StringHolder withReaderSupplier(IOSupplier<Reader> readerSupply,
      IOExceptionHandler onError) {
    return new ReaderStringHolder(0, 0, readerSupply, onError);
  }

  /**
   * Constructs a {@link ReaderStringHolder} with the given Reader source.
   *
   * @param minLen The minimum length of the content, must not be larger than the actual length.
   * @param readerSupply The supply of {@link Reader} instances for the content.
   * @param onError The exception handler.
   * @return The {@link ReaderStringHolder}.
   * @throws NullPointerException if supplier was {@code null}.
   */
  static StringHolder withReaderSupplierMinimumLength(int minLen, IOSupplier<Reader> readerSupply,
      IOExceptionHandler onError) {
    return new ReaderStringHolder(minLen, minLen, readerSupply, onError);
  }

  /**
   * Constructs a {@link ReaderStringHolder} with the given Reader source.
   *
   * @param expectedLen The expected length of the content, which is only an estimate.
   * @param readerSupply The supply of {@link Reader} instances for the content.
   * @param onError The exception handler.
   * @return The {@link ReaderStringHolder}.
   * @throws NullPointerException if supplier was {@code null}.
   */
  static StringHolder withReaderSupplierExpectedLength(int expectedLen,
      IOSupplier<Reader> readerSupply, IOExceptionHandler onError) {
    return new ReaderStringHolder(0, expectedLen, readerSupply, onError);
  }

  /**
   * Constructs a {@link ReaderStringHolder} with the given Reader source.
   *
   * @param minLen The minimum length of the content, must not be larger than the actual length.
   * @param expectedLen The expected length of the content, which is only an estimate.
   * @param readerSupply The supply of {@link Reader} instances for the content.
   * @param onError The exception handler.
   * @return The {@link ReaderStringHolder}.
   * @throws NullPointerException if supplier was {@code null}.
   */
  static StringHolder withReaderSupplierMinimumAndExpectedLength(int minLen, int expectedLen,
      IOSupplier<Reader> readerSupply, IOExceptionHandler onError) {
    return new ReaderStringHolder(minLen, expectedLen, readerSupply, onError);
  }

  /**
   * Constructs a new, empty {@link StringHolderSequence}.
   *
   * @return The new, empty sequence.
   */
  static StringHolderSequence newSequence() {
    return new StringHolderSequence();
  }

  /**
   * Constructs a new, empty {@link StringHolderSequence}.
   *
   * @param estimatedNumberOfAppends Estimated number of calls to
   *          {@link StringHolderSequence#append(Object)}, etc.
   *
   * @return The new, empty sequence.
   */
  static StringHolderSequence newSequence(int estimatedNumberOfAppends) {
    return new StringHolderSequence(estimatedNumberOfAppends);
  }

  /**
   * Constructs a new, empty async {@link StringHolderSequence}.
   *
   * {@link StringHolder}s are automatically converted upon append, with appends being run
   * asynchronously, using temporary intermediate storage, if possible/necessary.
   *
   * @return The new, empty async sequence.
   */
  static StringHolderSequence newAsyncSequence() {
    return new AsyncStringHolderSequence();
  }

  /**
   * Constructs a new, empty async {@link StringHolderSequence}.
   *
   * {@link StringHolder}s are automatically converted upon append, with appends being run
   * asynchronously, using temporary intermediate storage, if possible/necessary.
   *
   * @param executor The executor to use.
   *
   * @return The new, empty async sequence.
   */
  static StringHolderSequence newAsyncSequence(Executor executor) {
    return new AsyncStringHolderSequence(executor);
  }

  /**
   * Constructs a new, empty async {@link StringHolderSequence}.
   *
   * {@link StringHolder}s are automatically converted upon append, with appends being run
   * asynchronously, using temporary intermediate storage, if possible/necessary.
   *
   * @param estimatedNumberOfAppends Estimated number of calls to
   *          {@link StringHolderSequence#append(Object)}, etc.
   *
   * @return The new, empty async sequence.
   */
  static StringHolderSequence newAsyncSequence(int estimatedNumberOfAppends) {
    return new AsyncStringHolderSequence(estimatedNumberOfAppends);
  }

  /**
   * Constructs a new, empty async {@link StringHolderSequence}.
   *
   * {@link StringHolder}s are automatically converted upon append, with appends being run
   * asynchronously, using temporary intermediate storage, if possible/necessary.
   *
   * @param estimatedNumberOfAppends Estimated number of calls to
   *          {@link StringHolderSequence#append(Object)}, etc.
   * @param executor The executor to use.
   *
   * @return The new, empty sequence.
   */
  static StringHolderSequence newAsyncSequence(int estimatedNumberOfAppends, Executor executor) {
    return new AsyncStringHolderSequence(estimatedNumberOfAppends, executor);
  }

  /**
   * Constructs a {@link StringHolder} with the given content.
   *
   * Unless the object already is a {@link StringHolder}, or is known to be empty, its contents are
   * converted to String. {@code null} objects are converted to {@code "null"}, in accordance with
   * {@link String#valueOf(Object)}.
   *
   * @param obj The object.
   * @return The {@link StringHolder} instance.
   */
  @SuppressWarnings("PMD.CognitiveComplexity")
  static StringHolder withContent(Object obj) {
    if (obj == null) {
      return CommonStrings.NULL_STRINGHOLDER;
    } else if (obj instanceof String) {
      String s = (String) obj;
      StringHolder sh = CommonStrings.lookup(s);
      if (sh != null) {
        return sh;
      }
      return new SimpleStringHolder(s);
    } else if (obj instanceof StringHolder) {
      return (StringHolder) obj;
    } else {
      if (obj instanceof CharSequence) {
        if (CharSequenceReleaseShim.isEmpty((CharSequence) obj)) {
          return CommonStrings.EMPTY_STRINGHOLDER;
        }
      } else {
        StringHolder sh = CommonStrings.lookup(obj);
        if (sh != null) {
          return sh;
        }
      }
      String s = String.valueOf(obj);
      StringHolder sh = CommonStrings.lookup(s);
      if (sh != null) {
        return sh;
      }
      return new SimpleStringHolder(s);
    }
  }

  /**
   * Constructs a {@link StringHolder} with the given content sequences, as if they had been
   * appended to a {@link StringHolderSequence}. Empty sequences are optimized.
   *
   * @param objects The objects to represent as a sequence; if {@code null}, a StringHolder of the
   *          string {@code "null"} is returned.
   * @return The {@link StringHolder} instance.
   * @see #withContent(Object)
   */
  static StringHolder withContent(Object... objects) {
    if (objects == null) {
      return CommonStrings.NULL_STRINGHOLDER;
    }
    switch (objects.length) {
      case 0:
        return CommonStrings.EMPTY_STRINGHOLDER;
      case 1:
        return withContent(objects[0]);
      default:
        // see below
    }
    StringHolderSequence seq = new StringHolderSequence(objects.length);
    for (Object obj : objects) {
      if (obj instanceof StringHolder) {
        if (((StringHolder) obj).isKnownEmpty()) {
          continue;
        }
      } else if (obj instanceof CharSequence && CharSequenceReleaseShim.isEmpty(
          (CharSequence) obj)) {
        continue;
      }
      seq.append(obj);
    }
    if (seq.numberOfAppends() == 0) {
      return CommonStrings.EMPTY_STRINGHOLDER;
    } else {
      return seq;
    }
  }

  /**
   * Constructs a conditional wrapper around the given {@link StringHolder}; the given
   * {@code include} supplier controls whether the {@link StringHolder} is included, or effectively
   * empty.
   *
   * The conditional supplier is called at most once; the check is delayed as much as possible.
   *
   * @param wrapped The wrapped {@link StringHolder}.
   * @param includePredicate Controls the inclusion of that {@link StringHolder}; {@code false}
   *          means "excluded".
   * @return the conditional {@link StringHolder}.
   */
  static StringHolder withConditionalStringHolder(StringHolder wrapped,
      Predicate<StringHolder> includePredicate) {
    return new ConditionalStringHolder(wrapped, includePredicate);
  }

  /**
   * Constructs a wrapper around the given {@link StringHolder}, marking it as "uncacheable" (i.e.,
   * {@link #isCacheable()} returning {@code false}, unless {@link #isString()} is {@code true}).
   *
   * @param wrapped The wrapped {@link StringHolder}.
   * @return the uncacheable {@link StringHolder}.
   */
  static StringHolder withUncacheableStringHolder(StringHolder wrapped) {
    return wrapped.isCacheable() ? withConditionalStringHolder(wrapped, (k) -> true) : wrapped;
  }

  @Override
  default boolean isEmpty() {
    return length() == 0;
  }

  /**
   * Checks if this holder is currently backed by a plain {@link String}.
   *
   * @return {@code true} if currently backed by a plain {@link String}.
   */
  boolean isString();

  /**
   * Provides the contents of this {@link StringHolder} as a {@link Reader}.
   *
   * @return The reader.
   * @throws IOException on error.
   */
  Reader toReader() throws IOException;

  /**
   * Append the contents of this {@link StringHolder} to the given {@link Appendable}; this may or
   * may not turn the contents of this instance into a String.
   *
   * @param out The target.
   * @throws IOException on error.
   */
  void appendTo(Appendable out) throws IOException;

  /**
   * Append the contents of this {@link StringHolder} to the given {@link StringBuilder}; this may
   * or may not turn the contents of this instance into a String.
   *
   * @param out The target.
   */
  void appendTo(StringBuilder out);

  /**
   * Append the contents of this {@link StringHolder} to the given {@link StringBuffer}; this may or
   * may not turn the contents of this instance into a String.
   *
   * @param out The target.
   */
  void appendTo(StringBuffer out);

  /**
   * Append the contents of this {@link StringHolder} to the given {@link Writer}; this may or may
   * not turn the contents of this instance into a String.
   *
   * @param out The target.
   * @throws IOException on error.
   */
  void appendTo(Writer out) throws IOException;

  /**
   * Append the contents of this {@link StringHolder} to the given {@link Appendable}, and returns
   * the number of characters appended. This call may or may not turn the contents of this instance
   * into a String.
   *
   * @param out The target.
   * @return The number of characters appended.
   * @throws IOException on error.
   */
  int appendToAndReturnLength(Appendable out) throws IOException;

  /**
   * Append the contents of this {@link StringHolder} to the given {@link StringBuilder}, and
   * returns the number of characters appended. This call may or may not turn the contents of this
   * instance into a String.
   *
   * @param out The target.
   * @return The number of characters appended.
   */
  int appendToAndReturnLength(StringBuilder out);

  /**
   * Append the contents of this {@link StringHolder} to the given {@link StringBuffer}, and returns
   * the number of characters appended. This call may or may not turn the contents of this instance
   * into a String.
   *
   * @param out The target.
   * @return The number of characters appended.
   */
  int appendToAndReturnLength(StringBuffer out);

  /**
   * Append the contents of this {@link StringHolder} to the given {@link Writer}, and returns the
   * number of characters appended. This call may or may not turn the contents of this instance into
   * a String.
   *
   * @param out The target.
   * @return The number of characters appended.
   * @throws IOException on error.
   */
  int appendToAndReturnLength(Writer out) throws IOException;

  /**
   * Returns the current minimum length of the expected string length in this {@link StringHolder}.
   *
   * This is equivalent to {@link #length()} if {@link #isString()} is {@code true}.
   *
   * NOTE: When using this parameter for optimizations (e.g., to speed-up equality checks), make
   * sure to also check {@link #checkError()}. When that method returns {@code true}, the minimum
   * length can actually not be guaranteed.
   *
   * @return The minimum length (but be sure to see {@link #checkError()}).
   */
  @Override
  int getMinimumLength();

  /**
   * Returns the current estimate of the length of the string in this {@link StringHolder}, which is
   * at least the {@link #getMinimumLength()} but could be substantially larger.
   *
   * This is equivalent to {@link #length()} if {@link #isString()} is {@code true}.
   *
   * @return The currently expected length.
   */
  @Override
  int getExpectedLength();

  /**
   * Updates the current estimate of the length of the string in this {@link StringHolder}.
   *
   * The value will be rounded to {@link #getMinimumLength()} if necessary.
   *
   * @param len The new expected length
   */
  void setExpectedLength(int len);

  /**
   * Returns the actual length of this instance's contents. This may trigger a conversion to
   * {@link String}.
   *
   * @return The actual length.
   */
  @Override
  int length();

  /**
   * Checks if this {@link StringHolder} is known to yield an empty {@link String}.
   *
   * @return {@code true} if known non-empty.
   */
  @Override
  boolean isKnownEmpty();

  /**
   * Returns {@code true} if the actual length is known, i.e. {@link #getMinimumLength()} {@code ==}
   * {@link #getExpectedLength()} {@code == } {@link #length()}.
   *
   * By default, this is only true if {@link #isString()} {@code == true}, but subclasses may
   * override this check. When they do, they must include a check for
   * {@code || super.isLengthKnown()}.
   *
   * Note that once a length is <em>known</em>, it cannot change (i.e., don't override this for
   * mutable objects like {@link StringHolderSequence}).
   *
   * @return {@code true} if the length in this holder is known.
   * @see #isKnownEmpty()
   */
  @Override
  boolean isLengthKnown();

  /**
   * Checks if this {@link StringHolder} had some kind of unexpected condition.
   *
   * If so, {@link #getMinimumLength()} may be adjusted to a value smaller than its previous state.
   *
   * @return {@code true} if trouble was detected.
   */
  boolean checkError();

  /**
   * Returns the {@link StringHolderScope} associated with this {@link StringHolder}, or
   * {@code null} if no scope was associated.
   *
   * @return The scope, or {@code null}.
   */
  StringHolderScope getScope();

  /**
   * Sets the {@link StringHolderScope} associated with this {@link StringHolder}. Any previously
   * associated scope is removed from this instance and returned.
   *
   * @param newScope The new scope, or {@code null}/{@link StringHolderScope#NONE} to set "no
   *          scope".
   * @return The old scope, or {@code null} if none was set before.
   */
  StringHolderScope updateScope(StringHolderScope newScope);

  /**
   * Returns something that can be used in {@link StringHolder#withContent(Object)} which then
   * yields the same output when calling {@link #toString()} on either instance.
   *
   * The returned object usually is this instance itself. However, this method may return a
   * simplified version of the content stored in this instance. For example, if the content already
   * is a string, the string is returned.
   *
   * @return The "content" of this instance, which could be the instance itself, or something else.
   * @see #withContent(Object)
   */
  Object asContent();

  @Override
  default int compareTo(Object o) {
    if (o instanceof StringHolder) {
      return compareTo((StringHolder) o);
    } else if (o instanceof CharSequence) {
      return compareTo((CharSequence) o);
    } else {
      throw new ClassCastException("Cannot compare " + o.getClass());
    }
  }

  /**
   * Narrower implementation of {@link #compareTo(Object)} for {@link String}s.
   *
   * @param o The other object.
   * @return The comparison result, as defined by {@link #compareTo(Object)}.
   */
  int compareTo(CharSequence o);

  /**
   * Narrower implementation of {@link #compareTo(Object)} for {@link StringHolder}s.
   *
   * @param o The other object.
   * @return The comparison result, as defined by {@link #compareTo(Object)}.
   */
  int compareTo(StringHolder o);

  /**
   * Checks if this {@link StringHolder} is <em>effectively immutable</em>.
   *
   * @return {@code true} if the contents aren't going to change.
   */
  boolean isEffectivelyImmutable();

  /**
   * Marks this instance as <em>effectively immutable</em>.
   */
  void markEffectivelyImmutable();

  /**
   * Checks if this {@link StringHolder} is cacheable. A cacheable {@link StringHolder} may be
   * probed early for its contents (length, hashCode, etc.). This is usually the case, except when a
   * {@link StringHolder} is involved that was supplied via
   * {@link StringHolder#withConditionalStringHolder(StringHolder, Predicate)} or via
   * {@link #withUncacheableStringHolder(StringHolder)}, and that StringHolder is not already a
   * string or otherwise supplied, for example. By convention, it should be cacheable whenever
   * {@link #isString()} is {@code true}.
   *
   * @return {@code true} if cacheable.
   */
  default boolean isCacheable() {
    return true;
  }

  /**
   * Deep-clones this {@link StringHolder}.
   *
   * @return The cloned instance.
   */
  StringHolder clone();

  /**
   * Returns the index within this string of the first occurrence of the specified
   * character/codepoint, or {@code -1} if not found.
   *
   * @param c The character/codepoint to look for.
   * @return The position, or {@code -1} if not found.
   */
  default int indexOf(int c) {
    return indexOf(c, 0);
  }

  /**
   * Returns the index within this string of the first occurrence of the specified
   * character/codepoint, starting with the given character offset, or {@code -1} if not found.
   *
   * @param c The character/codepoint to look for.
   * @param start The character offset.
   * @return The position, or {@code -1} if not found.
   */
  default int indexOf(int c, int start) {
    if (isString()) {
      return toString().indexOf(c, start);
    } else if (isKnownEmpty()) {
      return -1;
    }

    boolean isSurrogatePair = c > 0xFFFF;

    int i = 0;
    int next = -1;
    for (PrimitiveIterator.OfInt it = chars().skip(start).iterator(); next != -1 || it
        .hasNext(); i++) {
      int ch;
      if (next != -1) {
        ch = next;
        next = -1;
      } else {
        ch = it.nextInt();
      }
      if (isSurrogatePair && Character.isHighSurrogate((char) ch) && it.hasNext()) {
        char ch2 = (char) it.nextInt();
        if (Character.isLowSurrogate(ch2)) {
          ch = Character.toCodePoint((char) ch, ch2);
        } else {
          // We detected an invalid UTF-8 surrogate, restart search from here
          next = ch2;
        }
      }
      if (ch == c) {
        return i;
      }
    }
    return -1;
  }

  /**
   * Returns the index within this StringHolder of the first occurrence of the specified
   * CharSequence, or {@code -1} if not found.
   *
   * @param str The char sequence to look for.
   * @return The position, or {@code -1} if not found.
   */
  @SuppressWarnings("PMD.CognitiveComplexity")
  default int indexOf(CharSequence str) {
    return indexOf(str, 0);
  }

  /**
   * Returns the index within this StringHolder of the first occurrence of the specified
   * CharSequence, starting with the given character offset, or {@code -1} if not found.
   *
   * @param str The char sequence to look for.
   * @param start The character offset.
   * @return The position, or {@code -1} if not found.
   */
  @SuppressWarnings({"PMD.CognitiveComplexity", "PMD.CyclomaticComplexity", "PMD.NPathComplexity"})
  default int indexOf(CharSequence str, int start) {
    if (str == this) { // NOPMD.CompareObjectsWithEquals
      return start == 0 || isEmpty() ? 0 : -1;
    } else if (CharSequenceReleaseShim.isEmpty(str)) {
      return Math.min(start, length());
    } else if (isKnownEmpty()) {
      return -1;
    }

    if (isString()) {
      if (str instanceof String || //
          (str instanceof StringHolder && ((StringHolder) str).isString())) {
        return toString().indexOf(str.toString());
      }
    }

    int strLen = str.length();
    if (isLengthKnown() && length() < (strLen + start)) {
      return -1;
    }

    char firstChar = str.charAt(start);
    switch (strLen) {
      case 1:
        return indexOf(firstChar);
      case 2:
        char secondChar = str.charAt(start + 1);
        if (Character.isSurrogatePair(firstChar, secondChar)) {
          return indexOf(Character.toCodePoint(firstChar, secondChar), start);
        }
        break;
      default:
        // continue below
        break;
    }

    int max = length() - strLen - start;

    boolean found = false;
    loop : for (int i = start; i <= max; i++) {
      char myChar = charAt(i);
      if (myChar != firstChar) {
        // seek ahead
        while (++i <= max) { // NOPMD.AvoidReassigningLoopVariables
          if (charAt(i) == firstChar) {
            found = true;
            break;
          }
        }
      } else {
        found = true;
      }
      if (found) {
        int myPos = i + 1;
        int end = myPos + strLen - 1;
        for (int strPos = 1; myPos < end; myPos++, strPos++) {
          if (charAt(myPos) != str.charAt(strPos)) {
            continue loop;
          }
        }
        return i;
      }
    }
    return -1;
  }

  /**
   * Checks if this {@link StringHolder} contains the given {@link CharSequence}.
   *
   * @param s The char sequence to look for.
   * @return {@code true} if found (also if the sequence is empty).
   */
  default boolean contains(CharSequence s) {
    return indexOf(s) >= 0;
  }
}