package org.jabref.logic.util.strings;

import java.text.Normalizer;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import javafx.util.Pair;

import org.jabref.architecture.AllowedToUseApacheCommonsLang3;
import org.jabref.architecture.AllowedToUseLogic;
import org.jabref.logic.bibtex.FieldWriter;
import org.jabref.logic.os.OS;
import org.jabref.model.strings.UnicodeToReadableCharMap;

import com.google.common.base.CharMatcher;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.Strings;
import org.jspecify.annotations.NonNull;
import org.jspecify.annotations.Nullable;

@SuppressWarnings("checkstyle:NoMultipleClosingBracesAtEndOfLine")
@AllowedToUseApacheCommonsLang3("There is no equivalent in Google's Guava")
@AllowedToUseLogic("OS.NewLine is most basic")
public class StringUtil {

    // Non-letters which are used to denote accents in LaTeX-commands, e.g., in {\"{a}}
    public static final String SPECIAL_COMMAND_CHARS = "\"`^~'=.|";
    // contains all possible line breaks, not omitting any break such as "\\n"
    private static final Pattern LINE_BREAKS = Pattern.compile("\\r\\n|\\r|\\n");
    private static final Pattern BRACED_TITLE_CAPITAL_PATTERN = Pattern.compile("\\{[A-Z]+\\}");

    /// Pattern for normalizing whitespace and punctuation using named capture groups
    private static final Pattern NORMALIZE_PATTERN = Pattern.compile(
            "(?<whitespace>\\s+)|" +                   // multiple whitespace
                    "(?<hyphen>\\s*-+\\s*)|" +         // hyphens with surrounding spaces
                    "(?<comma>\\s*,\\s*)|" +           // commas with surrounding spaces
                    "(?<semicolon>\\s*;\\s*)|" +       // semicolons with surrounding spaces
                    "(?<colon>\\s*:\\s*)"              // colons with surrounding spaces
    );

    private static final UnicodeToReadableCharMap UNICODE_CHAR_MAP = new UnicodeToReadableCharMap();
    private static final String WRAPPED_LINE_PREFIX = ""; // If a line break is added, this prefix will be inserted at the beginning of the next line
    private static final String STRING_TABLE_DELIMITER = " : ";
    // A sentence ends with a .?!;, but not in the case of "Mr.", "Ms.", "Mrs.", "Dr.", "st.", "jr.", "co.", "inc.", and "ltd."
    private static final Pattern SPLIT_TEXT_PATTERN = Pattern.compile("(?<=[\\.!;\\?])(?<![Mm](([Rr]|[Rr][Ss])|[Ss])\\.|[Dd][Rr]\\.|[Ss][Tt]\\.|[Jj][Rr]\\.|[Cc][Oo]\\.|[Ii][Nn][Cc]\\.|[Ll][Tt][Dd]\\.)\\s+");

    public static String booleanToBinaryString(boolean expression) {
        return expression ? "1" : "0";
    }

    /// Quote special characters.
    ///
    /// @param toQuote   The String which may contain special characters.
    /// @param specials  A String containing all special characters except the quoting character itself, which is automatically quoted.
    /// @param quoteChar The quoting character.
    /// @return A String with every special character (including the quoting character itself) quoted.
    public static String quote(String toQuote, String specials, char quoteChar) {
        if (toQuote == null) {
            return "";
        }

        StringBuilder result = new StringBuilder();
        char c;
        boolean isSpecial;
        for (int i = 0; i < toQuote.length(); ++i) {
            c = toQuote.charAt(i);

            isSpecial = c == quoteChar;
            // If non-null specials performs logic-or with specials.indexOf(c) >= 0
            isSpecial |= (specials != null) && (specials.indexOf(c) >= 0);

            if (isSpecial) {
                result.append(quoteChar);
            }
            result.append(c);
        }
        return result.toString();
    }

    /// Creates a substring from a text
    public static String getPart(String text, int startIndex, boolean terminateOnEndBraceOnly) {
        char c;
        int count = 0;

        StringBuilder part = new StringBuilder();

        // advance to first char and skip whitespace
        int index = startIndex + 1;
        while ((index < text.length()) && Character.isWhitespace(text.charAt(index))) {
            index++;
        }

        // then grab whatever is the first token (counting braces)
        while (index < text.length()) {
            c = text.charAt(index);
            if (!terminateOnEndBraceOnly && (count == 0) && Character.isWhitespace(c)) {
                // end argument and leave whitespace for further processing
                break;
            }
            if ((c == '}') && (--count < 0)) {
                break;
            } else if (c == '{') {
                count++;
            }
            part.append(c);
            index++;
        }
        return part.toString();
    }

    /// Returns the string, after shaving off whitespace at the beginning and end,
    /// and removing (at most) one pair of braces or " surrounding it.
    public static String shaveString(String toShave) {
        if ((toShave == null) || (toShave.isEmpty())) {
            return "";
        }
        String shaved = toShave.trim();
        if (isInCurlyBrackets(shaved) || isInCitationMarks(shaved)) {
            return shaved.substring(1, shaved.length() - 1);
        }
        return shaved;
    }

    /// Concatenate all strings in the array from index 'from' to 'to' (excluding
    /// to) with the given separator.
    ///
    /// Example:
    ///
    /// String[] s = "ab/cd/ed".split("/"); join(s, "\\", 0, s.length) ->
    /// "ab\\cd\\ed"
    ///
    /// @param to Excluding strings[to]
    public static String join(String[] strings, String separator, int from, int to) {
        if ((strings.length == 0) || (from >= to)) {
            return "";
        }

        int updatedFrom = Math.max(0, from);
        int updatedTo = Math.min(strings.length, to);

        StringBuilder stringBuilder = new StringBuilder();
        for (int i = updatedFrom; i < (updatedTo - 1); i++) {
            stringBuilder.append(strings[i]).append(separator);
        }
        return stringBuilder.append(strings[updatedTo - 1]).toString();
    }

    /// Removes optional square brackets from the string s
    public static String stripBrackets(String toStrip) {
        if (isInSquareBrackets(toStrip)) {
            return toStrip.substring(1, toStrip.length() - 1);
        }
        return toStrip;
    }

    /// extends the filename with a default Extension, if no Extension '.x' could
    /// be found
    public static String getCorrectFileName(String orgName, String defaultExtension) {
        if (orgName == null) {
            return "";
        }

        if (orgName.toLowerCase(Locale.ROOT).endsWith("." + defaultExtension.toLowerCase(Locale.ROOT))) {
            return orgName;
        }

        int hiddenChar = orgName.indexOf('.', 1); // hidden files Linux/Unix (?)
        if (hiddenChar < 1) {
            return orgName + "." + defaultExtension;
        }

        return orgName;
    }

    /// Formats field contents for output. Must be "symmetric" with the parse method above, so stored and reloaded fields
    /// are not mangled.
    ///
    /// @param in         the string to wrap
    /// @param wrapAmount the number of characters belonging to a line of text
    /// @param newline    the newline character(s)
    /// @return the wrapped string
    public static String wrap(String in, int wrapAmount, String newline) {
        String[] lines = in.split("\n");
        StringBuilder result = new StringBuilder();
        // remove all whitespace at the end of the string, this especially includes \r created when the field content has \r\n as line separator
        addWrappedLine(result, CharMatcher.whitespace().trimTrailingFrom(lines[0]), wrapAmount, newline);
        for (int i = 1; i < lines.length; i++) {
            if (lines[i].isBlank()) {
                result.append(newline);
                result.append('\t');
            } else {
                result.append(newline);
                result.append('\t');
                result.append(newline);
                result.append('\t');
                // remove all whitespace at the end of the string, this especially includes \r created when the field content has \r\n as line separator
                String line = CharMatcher.whitespace().trimTrailingFrom(lines[i]);
                addWrappedLine(result, line, wrapAmount, newline);
            }
        }
        return result.toString();
    }

    /// Appends a text to a string builder. Wraps the text so that each line is approx wrapAmount characters long.
    /// Wrapping is done using newline and tab character.
    ///
    /// @param line          the line of text to be wrapped and appended
    /// @param wrapAmount    the number of characters belonging to a line of text
    /// @param newlineString a string containing the newline character(s)
    private static void addWrappedLine(StringBuilder result, String line, int wrapAmount, String newlineString) {
        // Set our pointer to the beginning of the new line in the StringBuffer:
        int length = result.length();
        // Add the line, unmodified:
        result.append(line);

        // insert newlines and one tab character at each position, where wrapping is necessary
        while (length < result.length()) {
            int current = result.indexOf(" ", length + wrapAmount);
            if ((current < 0) || (current >= result.length())) {
                break;
            }

            result.deleteCharAt(current);
            result.insert(current, newlineString + "\t");
            length = current + newlineString.length();
        }
    }

    /// Quotes each and every character, e.g. '!' as &#33;. Used for verbatim
    /// display of arbitrary strings that may contain HTML entities.
    public static String quoteForHTML(String toQuote) {
        StringBuilder result = new StringBuilder();
        for (int i = 0; i < toQuote.length(); ++i) {
            result.append("&#").append((int) toQuote.charAt(i)).append(';');
        }
        return result.toString();
    }

    /// Decodes an encoded double String array back into array form. The array
    /// is assumed to be square, and delimited by the characters ';' (first dim) and
    /// ':' (second dim).
    ///
    /// @param value The encoded String to be decoded.
    /// @return The decoded String array.
    public static String[][] decodeStringDoubleArray(String value) {
        List<List<String>> newList = new ArrayList<>();
        StringBuilder sb = new StringBuilder();
        List<String> thisEntry = new ArrayList<>();
        boolean escaped = false;
        for (int i = 0; i < value.length(); i++) {
            char c = value.charAt(i);
            if (!escaped && (c == '\\')) {
                escaped = true;
                continue;
            } else if (!escaped && (c == ':')) {
                thisEntry.add(sb.toString());
                sb = new StringBuilder();
            } else if (!escaped && (c == ';')) {
                thisEntry.add(sb.toString());
                sb = new StringBuilder();
                newList.add(thisEntry);
                thisEntry = new ArrayList<>();
            } else {
                sb.append(c);
            }
            escaped = false;
        }
        if (!sb.isEmpty()) {
            thisEntry.add(sb.toString());
        }
        if (!thisEntry.isEmpty()) {
            newList.add(thisEntry);
        }

        // Convert to String[][]:
        String[][] res = new String[newList.size()][];
        for (int i = 0; i < res.length; i++) {
            res[i] = new String[newList.get(i).size()];
            for (int j = 0; j < res[i].length; j++) {
                res[i][j] = newList.get(i).get(j);
            }
        }
        return res;
    }

    /// Wrap all uppercase letters, or sequences of uppercase letters, in curly
    /// braces. Ignore letters within a pair of # character, as these are part of
    /// a string label that should not be modified.
    ///
    /// @param s The string to modify.
    /// @return The resulting string after wrapping capitals.
    public static String putBracesAroundCapitals(String s) {
        boolean inString = false;
        boolean isBracing = false;
        boolean escaped = false;
        int inBrace = 0;
        StringBuilder buf = new StringBuilder();
        for (int i = 0; i < s.length(); i++) {
            // Update variables based on special characters:
            int c = s.charAt(i);
            if (c == '{') {
                inBrace++;
            } else if (c == '}') {
                inBrace--;
            } else if (!escaped && (c == FieldWriter.BIBTEX_STRING_START_END_SYMBOL)) {
                inString = !inString;
            }

            // See if we should start bracing:
            if ((inBrace == 0) && !isBracing && !inString && Character.isLetter((char) c)
                    && Character.isUpperCase((char) c)) {
                buf.append('{');
                isBracing = true;
            }

            // See if we should close a brace set:
            if (isBracing && !(Character.isLetter((char) c) && Character.isUpperCase((char) c))) {
                buf.append('}');
                isBracing = false;
            }

            // Add the current character:
            buf.append((char) c);

            // Check if we are entering an escape sequence:
            escaped = (c == '\\') && !escaped;
        }
        // Check if we have an unclosed brace:
        if (isBracing) {
            buf.append('}');
        }

        return buf.toString();
    }

    /// This method looks for occurrences of capital letters enclosed in an
    /// arbitrary number of pairs of braces, e.g. "{AB}" or "{{T}}". All of these
    /// pairs of braces are removed.
    ///
    /// @param s The String to analyze.
    /// @return A new String with braces removed.
    public static String removeBracesAroundCapitals(String s) {
        String current = s;
        String previous = s;
        while ((current = removeSingleBracesAroundCapitals(current)).length() < previous.length()) {
            previous = current;
        }
        return current;
    }

    /// This method looks for occurrences of capital letters enclosed in one pair
    /// of braces, e.g. "{AB}". All these are replaced by only the capitals in
    /// between the braces.
    ///
    /// @param s The String to analyze.
    /// @return A new String with braces removed.
    private static String removeSingleBracesAroundCapitals(String s) {
        Matcher mcr = BRACED_TITLE_CAPITAL_PATTERN.matcher(s);
        StringBuilder buf = new StringBuilder();
        while (mcr.find()) {
            String replaceStr = mcr.group();
            mcr.appendReplacement(buf, replaceStr.substring(1, replaceStr.length() - 1));
        }
        mcr.appendTail(buf);
        return buf.toString();
    }

    /// Replaces all platform-dependent line breaks by OS.NEWLINE line breaks.
    /// AKA normalize newlines
    ///
    /// We do NOT use UNIX line breaks as the user explicitly configures its linebreaks and this method is used in bibtex field writing
    ///
    /// <h4>Example</h4>
    /// <pre>{@code
    /// Legacy Macintosh \r -> OS.NEWLINE
    /// Windows \r\n -> OS.NEWLINE
    /// }</pre>
    ///
    /// @return a String with only OS.NEWLINE as line breaks
    public static String unifyLineBreaks(String s, String newline) {
        return LINE_BREAKS.matcher(s).replaceAll(newline);
    }

    /// Checks if the given String has exactly one pair of surrounding curly braces <br>
    /// Strings with escaped characters in curly braces at the beginning and end are respected, too
    ///
    /// @param toCheck The string to check
    /// @return True, if the check was successful. False otherwise.
    public static boolean isInCurlyBrackets(String toCheck) {
        int count = 0;
        int brackets = 0;
        if ((toCheck == null) || toCheck.isEmpty()) {
            return false;
        } else {
            if ((toCheck.charAt(0) == '{') && (toCheck.charAt(toCheck.length() - 1) == '}')) {
                for (char c : toCheck.toCharArray()) {
                    if (c == '{') {
                        if (brackets == 0) {
                            count++;
                        }
                        brackets++;
                    } else if (c == '}') {
                        brackets--;
                    }
                }

                return count == 1;
            }
            return false;
        }
    }

    public static boolean isInSquareBrackets(String toCheck) {
        if ((toCheck == null) || toCheck.isEmpty()) {
            return false; // In case of null or empty string
        } else {
            return (toCheck.charAt(0) == '[') && (toCheck.charAt(toCheck.length() - 1) == ']');
        }
    }

    public static boolean isInCitationMarks(String toCheck) {
        if ((toCheck == null) || (toCheck.length() <= 1)) {
            return false; // In case of null, empty string, or a single citation mark
        } else {
            return (toCheck.charAt(0) == '"') && (toCheck.charAt(toCheck.length() - 1) == '"');
        }
    }

    /// Optimized method for converting a String into an Integer
    ///
    /// From http://stackoverflow.com/questions/1030479/most-efficient-way-of-converting-string-to-integer-in-java
    ///
    /// @param str the String holding an Integer value
    /// @return the int value of str
    /// @throws NumberFormatException if str cannot be parsed to an int
    public static int intValueOf(String str) {
        int idx = 0;
        int end;
        boolean sign = false;
        char ch;

        if ((str == null) || ((end = str.length()) == 0) || ((((ch = str.charAt(0)) < '0') || (ch > '9')) && (!(sign = ch == '-') || (++idx == end) || ((ch = str.charAt(idx)) < '0') || (ch > '9')))) {
            throw new NumberFormatException(str);
        }

        int ival = 0;
        for (; ; ival *= 10) {
            ival += '0' - ch;
            if (++idx == end) {
                return sign ? ival : -ival;
            }
            if (((ch = str.charAt(idx)) < '0') || (ch > '9')) {
                throw new NumberFormatException(str);
            }
        }
    }

    /// Optimized method for converting a String into an Integer
    ///
    /// From http://stackoverflow.com/questions/1030479/most-efficient-way-of-converting-string-to-integer-in-java
    ///
    /// @param str the String holding an Integer value
    /// @return the int value of str or Optional.empty() if not possible
    public static Optional<Integer> intValueOfOptional(String str) {
        int idx = 0;
        int end;
        boolean sign = false;
        char ch;

        if ((str == null) || ((end = str.length()) == 0) || ((((ch = str.charAt(0)) < '0') || (ch > '9')) && (!(sign = ch == '-') || (++idx == end) || ((ch = str.charAt(idx)) < '0') || (ch > '9')))) {
            return Optional.empty();
        }

        int ival = 0;
        for (; ; ival *= 10) {
            ival += '0' - ch;
            if (++idx == end) {
                return Optional.of(sign ? ival : -ival);
            }
            if (((ch = str.charAt(idx)) < '0') || (ch > '9')) {
                return Optional.empty();
            }
        }
    }

    /// This method ensures that the output String has only
    /// valid XML unicode characters as specified by the
    /// XML 1.0 standard. For reference, please see
    /// <a href="http://www.w3.org/TR/2000/REC-xml-20001006#NT-Char">the
    /// standard</a>. This method will return an empty
    /// String if the input is null or empty.
    ///
    /// URL: http://cse-mjmcl.cse.bris.ac.uk/blog/2007/02/14/1171465494443.html
    ///
    /// @param in The String whose non-valid characters we want to remove.
    /// @return The in String, stripped of non-valid characters.
    public static String stripNonValidXMLCharacters(String in) {
        if ((in == null) || in.isEmpty()) {
            return ""; // vacancy test.
        }
        StringBuilder out = new StringBuilder(); // Used to hold the output.
        char current; // Used to reference the current character.

        for (int i = 0; i < in.length(); i++) {
            current = in.charAt(i); // NOTE: No IndexOutOfBoundsException caught here; it should not happen.
            if ((current == 0x9) || (current == 0xA) || (current == 0xD) || ((current >= 0x20) && (current <= 0xD7FF))
                    || ((current >= 0xE000) && (current <= 0xFFFD))) {
                out.append(current);
            }
        }
        return out.toString();
    }

    /// @param buffer    String to be tokenized
    /// @param delimiter Delimiter string
    /// @return list      {@link java.util.List} of <tt>String</tt>
    public static List<String> tokenizeToList(String buffer, String delimiter) {
        // delimiter is a set of characters, so it is turned into a character class.
        // \Q...\E keeps any regex metacharacter inside the class literal.
        return Pattern.compile("[\\Q" + delimiter + "\\E]")
                      .splitAsStream(buffer + '\n')
                      .filter(token -> !token.isEmpty())
                      .toList();
    }

    /// Limits the length of a string to a maximum length.
    ///
    /// Note the implementation is different from [StringUtils.substring](https://commons.apache.org/proper/commons-lang/javadocs/api-2.6/org/apache/commons/lang/StringUtils.html#substring%28java.lang.String,%20int,%20int%29), because it accepts parameters smaller than 4.
    ///
    /// @param maxLength the maximum length of the string - <= 0 means no limit
    public static String limitStringLength(String s, int maxLength) {
        if (s == null) {
            return "";
        }

        if (maxLength <= 0 || s.length() <= maxLength) {
            return s;
        }

        return s.substring(0, Math.max(0, maxLength - 3)) + "...";
    }

    /// Replace non-English characters like umlauts etc. with a sensible letter or letter combination that bibtex can
    /// accept. The basis for replacement is the HashMap UnicodeToReadableCharMap.
    public static String replaceSpecialCharacters(String s) {
        /* Some unicode characters can be encoded in multiple ways. This uses <a href="https://docs.oracle.com/en/java/javase/14/docs/api/java.base/java/text/Normalizer.Form.html#NFC">NFC</a>
         * to re-encode the characters so that these characters can be found.
         * Most people expect Unicode to work similar to NFC, i.e., if characters looks the same, it is likely that they are equivalent.
         * Hence, if someone debugs issues in the `UNICODE_CHAR_MAP`, they will expect NFC.
         * A more holistic approach should likely start with the <a href="http://unicode.org/reports/tr15/#Compatibility_Equivalence_Figure">compatibility equivalence</a>. */
        String result = Normalizer.normalize(s, Normalizer.Form.NFC);
        for (Map.Entry<String, String> chrAndReplace : UNICODE_CHAR_MAP.entrySet()) {
            result = result.replace(chrAndReplace.getKey(), chrAndReplace.getValue());
        }
        return result;
    }

    public static boolean isNullOrEmpty(@Nullable String toTest) {
        return (toTest == null) || toTest.isEmpty();
    }

    public static boolean isBlank(@Nullable String string) {
        return (string == null) || string.isBlank();
    }

    public static boolean isBlank(@NonNull Optional<String> string) {
        return string.isEmpty() || isBlank(string.get());
    }

    /// Checks if a CharSequence is not empty (""), not null and not whitespace only.
    public static boolean isNotBlank(@Nullable String string) {
        return !isBlank(string);
    }

    public static boolean isNotBlank(Optional<String> string) {
        return string.isPresent() && isNotBlank(string.get());
    }

    /// Return string enclosed in HTML bold tags
    public static String boldHTML(String input) {
        return "<b>" + input + "</b>";
    }

    /// Return string enclosed in HTML bold tags  if not null, otherwise return alternative text in HTML bold tags
    public static String boldHTML(String input, String alternative) {
        if (input == null) {
            return "<b>" + alternative + "</b>";
        }
        return "<b>" + input + "</b>";
    }

    /// Unquote special characters.
    ///
    /// @param toUnquote The String which may contain quoted special characters.
    /// @param quoteChar The quoting character.
    /// @return A String with all quoted characters unquoted.
    public static String unquote(String toUnquote, char quoteChar) {
        StringBuilder result = new StringBuilder();
        char c;
        boolean quoted = false;
        for (int i = 0; i < toUnquote.length(); ++i) {
            c = toUnquote.charAt(i);
            if (quoted) { // append literally...
                if (c != '\n') {
                    result.append(c);
                }
                quoted = false;
            } else if (c == quoteChar) {
                // quote char
                quoted = true;
            } else {
                result.append(c);
            }
        }
        return result.toString();
    }

    @AllowedToUseApacheCommonsLang3("No Guava equivalent existing - see https://stackoverflow.com/q/3322152/873282 for a list of other implementations")
    public static String stripAccents(String searchQuery) {
        return StringUtils.stripAccents(searchQuery);
    }

    /// Make first character of String uppercase, and the rest lowercase.
    public static String capitalizeFirst(String toCapitalize) {
        if (toCapitalize.length() > 1) {
            return toCapitalize.substring(0, 1).toUpperCase(Locale.ROOT)
                    + toCapitalize.substring(1).toLowerCase(Locale.ROOT);
        } else {
            return toCapitalize.toUpperCase(Locale.ROOT);
        }
    }

    /// Returns a list of words contained in the given text.
    /// Whitespace, comma and semicolon are considered as separator between words.
    ///
    /// @param text the input
    /// @return a list of words
    public static List<String> getStringAsWords(String text) {
        return Arrays.asList(text.split("[\\s,;]+"));
    }

    /// Returns a list of sentences contained in the given text.
    public static List<String> getStringAsSentences(String text) {
        return Arrays.asList(SPLIT_TEXT_PATTERN.split(text));
    }

    @AllowedToUseApacheCommonsLang3("No direct Guava equivalent existing - see https://stackoverflow.com/q/16560635/873282")
    public static boolean containsIgnoreCase(String text, String searchString) {
        return Strings.CI.contains(text, searchString);
    }

    public static boolean equalsUnifiedLineBreak(Optional<String> stringOne, Optional<String> stringTwo) {
        if (stringOne.isEmpty() && stringTwo.isEmpty()) {
            return true;
        }
        if (stringOne.isEmpty() || stringTwo.isEmpty()) {
            return false;
        }
        return StringUtil.unifyLineBreaks(stringOne.get(), OS.NEWLINE).equals(
                StringUtil.unifyLineBreaks(stringTwo.get(), OS.NEWLINE));
    }

    public static String substringBetween(String str, String open, String close) {
        return StringUtils.substringBetween(str, open, close);
    }

    public static String ignoreCurlyBracket(String title) {
        return isNotBlank(title) ? title.replace("{", "").replace("}", "") : title;
    }

    /// Encloses the given string with " if there is a space contained
    ///
    /// @return Returns a string
    public static String quoteStringIfSpaceIsContained(String string) {
        if (string.contains(" ")) {
            return "\"" + string + "\"";
        } else {
            return string;
        }
    }

    /// Checks if the given string contains any whitespace characters. The supported whitespace characters
    /// are the set of characters matched by `\s` in regular expressions, which are `[ \t\n\x0B\f\r]`.
    ///
    /// @param s The string to check
    /// @return `True` if the given string does contain at least one whitespace character, `False` otherwise
    public static boolean containsWhitespace(String s) {
        return s.chars().anyMatch(Character::isWhitespace);
    }

    @AllowedToUseApacheCommonsLang3("No Guava equivalent existing - see https://stackoverflow.com/a/23825984")
    public static String removeStringAtTheEnd(String string, String stringToBeRemoved) {
        return StringUtils.removeEndIgnoreCase(string, stringToBeRemoved);
    }

    @AllowedToUseApacheCommonsLang3("No Guava equivalent existing")
    public static boolean endsWithIgnoreCase(String string, String suffix) {
        return StringUtils.endsWithIgnoreCase(string, suffix);
    }

    public static String alignStringTable(List<Pair<String, String>> table) {
        StringBuilder sb = new StringBuilder();

        int maxLength = table.stream()
                             .mapToInt(pair -> Objects.requireNonNullElse(pair.getKey(), "").length())
                             .max().orElse(0);

        for (Pair<String, String> pair : table) {
            int padding = Math.max(0, maxLength - pair.getKey().length());
            sb.append(WRAPPED_LINE_PREFIX);
            sb.append(pair.getKey());

            sb.repeat(" ", padding);

            sb.append(STRING_TABLE_DELIMITER);
            sb.append(pair.getValue());
            sb.append(OS.NEWLINE);
        }

        return sb.toString();
    }

    /// Splits a string by whitespace, treating backslash-escaped spaces as part of the same token.
    ///
    /// Example: {@code ""C:\Current Python\python.exe" -m ocrmypdf"} -> {@code [""C:\Current Python\python.exe"", "-m", "ocrmypdf"]}
    ///
    /// @param path the string to split
    /// @return a list of tokens
    public static ArrayList<String> splitRespectingEscapedWhitespace(String path) {
        ArrayList<String> result = new ArrayList<>();
        // first check if the path contains a slash or backslash, if so it gets the first space after the last slash
        // then it splits the rest of the string by spaces, otherwise it just splits the whole string by spaces
        if (path.contains("/") || path.contains("\\")) {
            int lastSlash = Math.max(path.lastIndexOf("/"), path.lastIndexOf("\\"));
            lastSlash = path.indexOf(" ", lastSlash);
            if (lastSlash == -1) {
                return new ArrayList<>(List.of(path));
            }
            result.add(path.substring(0, lastSlash));
            for (int i = lastSlash + 1; i < path.length(); i++) {
                int spaceIndex = path.indexOf(' ', i);
                if (spaceIndex == -1) {
                    result.add(path.substring(i));
                    break;
                } else {
                    result.add(path.substring(i, spaceIndex));
                    i = spaceIndex;
                }
            }
        } else {
            return new ArrayList<>(Arrays.asList(path.split(" ")));
        }
        return result;
    }

    public static String makeSafe(@Nullable String string) {
        return Optional.ofNullable(string).orElse("");
    }
}
