/*
 *     Created by Angel Leon (@gubatron), Alden Torres (aldenml)
 *     Copyright (c) 2011-2026, FrostWire(R). All rights reserved.
 * 
 *     This program is free software: you can redistribute it and/or modify
 *     it under the terms of the GNU General Public License as published by
 *     the Free Software Foundation, either version 3 of the License, or
 *     (at your option) any later version.
 * 
 *     This program is distributed in the hope that it will be useful,
 *     but WITHOUT ANY WARRANTY; without even the implied warranty of
 *     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *     GNU General Public License for more details.
 * 
 *     You should have received a copy of the GNU General Public License
 *     along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */

package org.team_hermes.wireshare.util;

import java.io.UnsupportedEncodingException;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * Provides static methods to split, check for substrings, change case and
 * compare strings, along with additional string utility methods.
 */
public class StringUtils {
    /**
     * Returns true if input contains the given pattern, which may contain the
     * wildcard character '*'.  TODO: need more formal definition.  Examples:
     *
     * <pre>
     *  StringUtils.contains("", "") ==> true
     *  StringUtils.contains("abc", "") ==> true
     *  StringUtils.contains("abc", "b") ==> true
     *  StringUtils.contains("abc", "d") ==> false
     *  StringUtils.contains("abcd", "a*d") ==> true
     *  StringUtils.contains("abcd", "*a**d*") ==> true
     *  StringUtils.contains("abcd", "d*a") ==> false
     *  </pre>
     */
    public static boolean contains(String input, String pattern) {
        return contains(input, pattern, false);
    }

    /**
     * Exactly like contains(input, pattern), but case is ignored if
     * ignoreCase==true.
     */
    public static boolean contains(String input, String pattern, boolean ignoreCase) {
        //More efficient algorithms are possible, e.g. a modified version of the
        //Rabin-Karp algorithm, but they are unlikely to be faster with such
        //short strings.  Also, some contant time factors could be shaved by
        //combining the second FOR loop below with the subset(..) call, but that
        //just isn't important.  The important thing is to avoid needless
        //allocations.
        final int n = pattern.length();
        //Where to resume searching after last wildcard, e.g., just past
        //the last match in input.
        int last = 0;
        //For each token in pattern starting at i...
        for (int i = 0; i < n; ) {
            //1. Find the smallest j>i s.t. pattern[j] is space, *, or +.
            char c = ' ';
            int j = i;
            for (; j < n; j++) {
                char c2 = pattern.charAt(j);
                if (c2 == ' ' || c2 == '+' || c2 == '*') {
                    c = c2;
                    break;
                }
            }
            //2. Match pattern[i..j-1] against input[last...].
            int k = subset(pattern, i, j, input, last, ignoreCase);
            if (k < 0)
                return false;
            //3. Reset the starting search index if got ' ' or '+'.
            //Otherwise increment past the match in input.
            if (c == ' ' || c == '+')
                last = 0;
            else if (c == '*')
                last = k + j - i;
            i = j + 1;
        }
        return true;
    }

    /**
     * @requires TODO3: fill this in
     * @effects returns the the smallest i>=bigStart
     * s.t. little[littleStart...littleStop-1] is a prefix of big[i...]
     * or -1 if no such i exists.  If ignoreCase==false, case doesn't matter
     * when comparing characters.
     */
    private static int subset(String little, int littleStart, int littleStop, String big, int bigStart, boolean ignoreCase) {
        //Equivalent to
        // return big.indexOf(little.substring(littleStart, littleStop), bigStart);
        //but without an allocation.
        //Note special case for ignoreCase below.
        if (ignoreCase) {
            final int n = big.length() - (littleStop - littleStart) + 1;
            outerLoop:
            for (int i = bigStart; i < n; i++) {
                //Check if little[littleStart...littleStop-1] matches with shift i
                final int n2 = littleStop - littleStart;
                for (int j = 0; j < n2; j++) {
                    char c1 = big.charAt(i + j);
                    char c2 = little.charAt(littleStart + j);
                    if (c1 != c2 && c1 != toOtherCase(c2)) //Ignore case. See below.
                        continue outerLoop;
                }
                return i;
            }
            return -1;
        } else {
            final int n = big.length() - (littleStop - littleStart) + 1;
            outerLoop:
            for (int i = bigStart; i < n; i++) {
                final int n2 = littleStop - littleStart;
                for (int j = 0; j < n2; j++) {
                    char c1 = big.charAt(i + j);
                    char c2 = little.charAt(littleStart + j);
                    if (c1 != c2) //Consider case.  See above.
                        continue outerLoop;
                }
                return i;
            }
            return -1;
        }
    }

    /**
     * If c is a lower case ASCII character, returns Character.toUpperCase(c).
     * Else if c is an upper case ASCII character, returns Character.toLowerCase(c),
     * Else returns c.
     * Note that this is <b>not internationalized</b>; but it is fast.
     */
    public static char toOtherCase(char c) {
        int i = c;
        final int A = 'A'; //65
        final int Z = 'Z'; //90
        final int a = 'a'; //97
        final int z = 'z'; //122
        final int SHIFT = a - A;
        if (i < A) //non alphabetic
            return c;
        else if (i <= Z) //upper-case
            return (char) (i + SHIFT);
        else if (i < a) //non alphabetic
            return c;
        else if (i <= z) //lower-case
            return (char) (i - SHIFT);
        else
            //non alphabetic
            return c;
    }

    /**
     * Exactly like split(s, Character.toString(delimiter))
     */
    public static String[] split(String s, char delimiter) {
        return split(s, Character.toString(delimiter));
    }

    /**
     * Returns the tokens of s delimited by the given delimiter, without
     * returning the delimiter.  Repeated sequences of delimiters are treated
     * as one. Examples:
     * <pre>
     *    split("a//b/ c /","/")=={"a","b"," c "}
     *    split("a b", "/")=={"a b"}.
     *    split("///", "/")=={}.
     *  </pre>
     *
     * <b>Note that whitespace is preserved if it is not part of the delimiter.</b>
     * An older version of this trim()'ed each token of whitespace.
     */
    public static String[] split(String s, String delimiters) {
        //Tokenize s based on delimiters, adding to buffer.
        StringTokenizer tokenizer = new StringTokenizer(s, delimiters);
        // Pre-size: typical splits have 5-10 tokens; avoids 2-3 array copies
        List<String> tokens = new ArrayList<>(8);
        while (tokenizer.hasMoreTokens())
            tokens.add(tokenizer.nextToken());
        return tokens.toArray(new String[0]);
    }

    /**
     * Exactly like splitNoCoalesce(s, Character.toString(delimiter))
     */
    public static String[] splitNoCoalesce(String s, char delimiter) {
        return splitNoCoalesce(s, Character.toString(delimiter));
    }

    /**
     * Similar to split(s, delimiters) except that subsequent delimiters are not
     * coalesced, so the returned array may contain empty strings.  If s starts
     * (ends) with a delimiter, the returned array starts (ends) with an empty
     * strings.  If s contains N delimiters, N+1 strings are always returned.
     * Examples:
     *
     * <pre>
     *    split("a//b/ c /","/")=={"a","","b"," c ", ""}
     *    split("a b", "/")=={"a b"}.
     *    split("///", "/")=={"","","",""}.
     *  </pre>
     *
     * @return an array A s.t. s.equals(A[0]+d0+A[1]+d1+...+A[N]), where
     * for all dI, dI.size()==1 &amp;&amp; delimiters.indexOf(dI)&gt;=0; and for
     * all c in A[i], delimiters.indexOf(c)&lt;0
     */
    public static String[] splitNoCoalesce(String s, String delimiters) {
        //Tokenize s based on delimiters, adding to buffer.
        StringTokenizer tokenizer = new StringTokenizer(s, delimiters, true);
        // Pre-size: typical splits have 5-10 tokens; avoids 2-3 array copies
        List<String> tokens = new ArrayList<>(8);
        //True if last token was a delimiter.  Initialized to true to force
        //an empty string if s starts with a delimiter.
        boolean gotDelimiter = true;
        while (tokenizer.hasMoreTokens()) {
            String token = tokenizer.nextToken();
            //Is token a delimiter?
            if (token.length() == 1 && delimiters.contains(token)) {
                //If so, add blank only if last token was a delimiter.
                if (gotDelimiter)
                    tokens.add("");
                gotDelimiter = true;
            } else {
                //If not, add "real" token.
                tokens.add(token);
                gotDelimiter = false;
            }
        }
        //Add trailing empty string UNLESS s is the empty string.
        if (gotDelimiter && !tokens.isEmpty())
            tokens.add("");
        return tokens.toArray(new String[0]);
    }

    /**
     * Returns true iff s starts with prefix, ignoring case.
     *
     * @return true iff s.toUpperCase().startsWith(prefix.toUpperCase())
     */
    public static boolean startsWithIgnoreCase(String s, String prefix) {
        final int pl = prefix.length();
        if (s.length() < pl)
            return false;
        for (int i = 0; i < pl; i++) {
            char sc = s.charAt(i);
            char pc = prefix.charAt(i);
            if (sc != pc) {
                sc = Character.toUpperCase(sc);
                pc = Character.toUpperCase(pc);
                if (sc != pc) {
                    sc = Character.toLowerCase(sc);
                    pc = Character.toLowerCase(pc);
                    if (sc != pc)
                        return false;
                }
            }
        }
        return true;
    }

    /**
     * Replaces all occurrences of old_str in str with new_str
     *
     * @param str     the String to modify
     * @param old_str the String to be replaced
     * @param new_str the String to replace old_str with
     * @return the modified str.
     */
    public static String replace(String str, String old_str, String new_str) {
        int o = 0;
        StringBuilder buf = new StringBuilder();
        for (int i = str.indexOf(old_str); i > -1; i = str.indexOf(old_str, i + 1)) {
            if (i > o) {
                buf.append(str.substring(o, i));
            }
            buf.append(new_str);
            o = i + old_str.length();
        }
        buf.append(str.substring(o, str.length()));
        return buf.toString();
    }

    /**
     * Returns a truncated string, up to the maximum number of characters
     */
    public static String truncate(final String string, final int maxLen) {
        if (string.length() <= maxLen)
            return string;
        else
            return string.substring(0, maxLen);
    }

    /**
     * Helper method to obtain the starting index of a substring within another
     * string, ignoring their case.  This method is expensive because it has
     * to set each character of each string to lower case before doing the
     * comparison.  Uses the default <code>Locale</code> for case conversion.
     *
     * @param str       the string in which to search for the `substring`
     *                  argument
     * @param substring the substring to search for in `str`
     * @return if the `substring` argument occurs as a substring within
     * `str`, then the index of the first character of the first such
     * substring is returned; if it does not occur as a substring, -1 is
     * returned
     */
    public static int indexOfIgnoreCase(String str, String substring) {
        return indexOfIgnoreCase(str, substring, Locale.getDefault());
    }

    /**
     * Helper method to obtain the starting index of a substring within another
     * string, ignoring their case.  This method is expensive because it has
     * to set each character of each string to lower case before doing the
     * comparison.
     *
     * @param str       the string in which to search for the `substring`
     *                  argument
     * @param substring the substring to search for in `str`
     * @param locale    the <code>Locale</code> to use when converting the
     *                  case of <code>str</code> and <code>substring</code>.  This is necessary because
     *                  case conversion is <code>Locale</code> specific.
     * @return if the `substring` argument occurs as a substring within
     * `str`, then the index of the first character of the first such
     * substring is returned; if it does not occur as a substring, -1 is
     * returned
     */
    public static int indexOfIgnoreCase(String str, String substring, Locale locale) {
        // Look for the index after the expensive conversion to lower case.
        return str.toLowerCase(locale).indexOf(substring.toLowerCase(locale));
    }

    /**
     * Utility wrapper for getting a String object out of
     * byte [] using the ascii encoding.
     */
    public static String getASCIIString(byte[] bytes) {
        return getEncodedString(bytes, "ISO-8859-1");
    }

    /**
     * Utility wrapper for getting a String object out of
     * byte [] using the UTF-8 encoding.
     */
    public static String getUTF8String(byte[] bytes) {
        return getEncodedString(bytes, "UTF-8");
    }

    /**
     * @return a string with an encoding we know we support.
     */
    private static String getEncodedString(byte[] bytes, String encoding) {
        try {
            return new String(bytes, encoding);
        } catch (UnsupportedEncodingException impossible) {
            throw new RuntimeException(impossible);
        }
    }

    /**
     * Returns the tokens of array concanated to a delimited by the given
     * delimiter, without Examples:
     *
     * <pre>
     *     explode({ "a", "b" }, " ") == "a b"
     *     explode({ "a", "b" }, "") == "ab"
     * </pre>
     */
    public static String explode(String[] array, String delimeter) {
        StringBuilder sb = new StringBuilder();
        if (array.length > 0) {
            sb.append(array[0]);
            for (int i = 1; i < array.length; i++) {
                sb.append(delimeter);
                sb.append(array[i]);
            }
        }
        return sb.toString();
    }

    /**
     * Returns the tokens of a collection concanated to a delimited by the given
     * delimiter.
     */
    public static String explode(Collection<String> collection, String delimiter) {
        StringBuilder sb = new StringBuilder();
        if (!collection.isEmpty()) {
            Iterator<String> i = collection.iterator();
            sb.append(i.next());
            while (i.hasNext()) {
                sb.append(delimiter);
                sb.append(i.next());
            }
        }
        return sb.toString();
    }

    /**
     * Check if a String is null or empty (the length is null).
     *
     * @param s the string to check
     * @return true if it is null or empty
     */
    public static boolean isNullOrEmpty(String s, boolean trim) {
        return s == null || (trim ? s.trim().length() == 0 : s.length() == 0);
    }

    public static boolean isNullOrEmpty(String s) {
        return isNullOrEmpty(s, false);
    }

    /**
     * Hot-path method: Removes consecutive whitespace characters, replacing them with a single space.
     * Optimized to reuse pre-compiled pattern from HtmlPatterns.MULTI_SPACE.
     * 
     * @param s the string to process, may be null
     * @return the string with consecutive whitespace collapsed to single spaces, or null if input is null
     */
    public static String removeDoubleSpaces(String s) {
        return s != null ? HtmlPatterns.MULTI_SPACE.matcher(s).replaceAll(" ") : null;
    }

    /**
     * Hot-path method: Removes characters outside allowed Unicode categories (letters, numbers, punctuation, separators).
     * Optimized to use StringBuilder loop instead of regex for better performance.
     * Allowed categories: \p{L} (letters), \p{N} (numbers), \p{P} (punctuation), \p{Z} (separators).
     * 
     * @param s the string to filter, must not be null
     * @return the filtered string containing only allowed characters
     */
    public static String removeUnicodeCharacters(String s) {
        if (s == null || s.isEmpty()) {
            return s;
        }
        
        StringBuilder sb = new StringBuilder(s.length());
        for (int i = 0; i < s.length(); i++) {
            char c = s.charAt(i);
            int type = Character.getType(c);
            
            // Check if character belongs to allowed Unicode categories:
            // \p{L} = Letters (types 1-5)
            // \p{N} = Numbers (types 9-11)  
            // \p{P} = Punctuation (types 20-24)
            // \p{Z} = Separators (types 12-14)
            boolean isLetter = (type >= Character.UPPERCASE_LETTER && type <= Character.OTHER_LETTER);
            boolean isNumber = (type >= Character.DECIMAL_DIGIT_NUMBER && type <= Character.OTHER_NUMBER);
            boolean isPunctuation = (type >= Character.DASH_PUNCTUATION && type <= Character.OTHER_PUNCTUATION);
            boolean isSeparator = (type >= Character.SPACE_SEPARATOR && type <= Character.PARAGRAPH_SEPARATOR);
            
            if (isLetter || isNumber || isPunctuation || isSeparator) {
                sb.append(c);
            }
        }
        
        return sb.toString();
    }

    public static String getLocaleString(Map<String, String> strMap, String defaultStr) {
        String localeLanguageCode = Locale.getDefault().getLanguage();
        if (StringUtils.isNullOrEmpty(localeLanguageCode, true)) {
            localeLanguageCode = "en";
        }
        String str = strMap.get(localeLanguageCode);
        if (StringUtils.isNullOrEmpty(str, true)) {
            str = defaultStr;
        }
        return str;
    }

    public static int quickHash(String s) {
        int hash = 1;
        for (int i = 0; i < s.length(); i++) {
            hash = (hash * (int) s.charAt(i)) % 0x7fffffff; // modded to int max val
        }
        return hash;
    }

    private static final class HtmlPatterns {
        private static final Pattern HTML_COMMENT  =
                Pattern.compile("(?s)<!--.*?-->");
        private static final Pattern INLINE_STYLE =
                Pattern.compile("(?i)<(\\w+)\\s+style=\"[^\"]*\">");
        private static final Pattern SCRIPT       =
                Pattern.compile("(?i)<script.*?</script>", Pattern.DOTALL);
        private static final Pattern BR_TAG       =
                Pattern.compile("(?i)<br\\s*/?>");
        private static final Pattern EMPHASIS     =
                Pattern.compile("(?i)<(em|i)>(.*?)</\\1>");
        private static final Pattern STRONG       =
                Pattern.compile("(?i)<(strong|b)>(.*?)</\\1>");
        private static final Pattern ANY_TAG      =
                Pattern.compile("<.*?>", Pattern.DOTALL);
        private static final Pattern MULTI_SPACE  =
                Pattern.compile("\\s+");
    }

    public static String fromHtml(String html) {
        if (html == null || html.indexOf('<') < 0) {
            // fast‑path when the string clearly contains no markup
            return html;
        }

        // --- strip blocks we don’t want -------------------------------------------------
        html = HtmlPatterns.HTML_COMMENT.matcher(html).replaceAll("");
        html = HtmlPatterns.INLINE_STYLE.matcher(html).replaceAll("<$1>");
        html = HtmlPatterns.SCRIPT.matcher(html).replaceAll("");

        // --- cheap, literal replacements (no regex) ------------------------------------
        html = html.replace("&nbsp;",  " ")
                .replace("&amp;",   "&")
                .replace("&lt;",    "<")
                .replace("&gt;",    ">")
                .replace("&quot;",  "\"")
                .replace("&apos;",  "'");

        // --- small regexes that need capturing groups ----------------------------------
        html = HtmlPatterns.BR_TAG.matcher(html)
                .replaceAll(System.lineSeparator());
        html = HtmlPatterns.EMPHASIS.matcher(html).replaceAll("_$2_");
        html = HtmlPatterns.STRONG.matcher(html).replaceAll("*$2*");

        // --- final cleanup --------------------------------------------------------------
        html = HtmlPatterns.ANY_TAG.matcher(html).replaceAll("");
        html = HtmlPatterns.MULTI_SPACE.matcher(html).replaceAll(" ");

        return html.trim();
    }
}
