/* UtilLib - Copyright (c) 2005-2023 Vincent Calame - Exemole
 * Logiciel libre donné sous triple licence :
 * 1) selon les termes de la CeCILL V2
 * 2) selon les termes de l’EUPL V.1.1
 * 3) selon les termes de la GNU GPLv3
 * Voir le fichier licences.txt
 */


package net.mapeadores.util.text;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.StringReader;
import java.io.UnsupportedEncodingException;
import java.math.BigDecimal;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.text.MessageFormat;
import java.text.NumberFormat;
import java.text.ParseException;
import java.util.AbstractList;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
import java.util.RandomAccess;
import java.util.Set;
import net.mapeadores.util.exceptions.NestedIOException;
import net.mapeadores.util.exceptions.ShouldNotOccurException;
import net.mapeadores.util.primitives.Decimal;


/**
 *
 * @author Vincent Calame
 */
public final class StringUtils {

    public static final String[] EMPTY_STRINGARRAY = new String[0];
    public static final List<String> EMPTY_STRINGLIST = Collections.emptyList();
    public static final Set<String> EMPTY_STRINGSET = Collections.emptySet();
    public static final List<RelativePath> EMPTY_RELATIVEPATHLIST = Collections.emptyList();
    public static final short NOTCLEAN = 0;
    public static final short EMPTY_INCLUDE = 1;
    public static final short EMPTY_EXCLUDE = 2;
    private static final SecureRandom SECURE_RANDOM = new SecureRandom();
    private static final String CHAR_SEQUENCE = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";

    private StringUtils() {
    }

    public static boolean isTrue(String text) {
        return toBoolean(text, false);
    }

    public static boolean toBoolean(String text, boolean defaultValue) {
        if (text == null) {
            return defaultValue;
        }
        text = text.trim().toLowerCase();
        switch (text) {
            case "1":
            case "true":
            case "yes":
            case "oui":
            case "si":
                return true;
            case "0":
            case "false":
            case "no":
            case "non":
                return false;
            default:
                return defaultValue;
        }
    }

    public static String getFirstPart(String text) {
        int idx = text.indexOf(';');
        if (idx == -1) {
            return text;
        } else {
            return text.substring(0, idx);
        }
    }

    public static String normalizeRelativePath(String path) {
        return normalizeRelativePath(path, false);
    }

    public static String normalizeRelativePath(String path, boolean allowsParentSegment) {
        try {
            URI uri = new URI(path);
            if (uri.isAbsolute()) {
                return null;
            }
            uri = uri.normalize();
            String result = uri.getPath();
            if (!allowsParentSegment) {
                if (result.startsWith("../")) {
                    return null;
                }
            }
            if (result.startsWith("/")) {
                return null;
            }
            if (result.length() == 0) {
                return null;
            }
            if (result.endsWith("/")) {
                result = result.substring(0, result.length() - 1);
            }
            return result;
        } catch (URISyntaxException use) {
            return null;
        }
    }

    /**
     * Accepte un grand nombre de séparateur : ';',',','\n','\t', ' ','\r'
     */
    public static String[] getTechnicalTokens(String s, boolean includeSpace) {
        return getTechnicalTokens(s, (includeSpace) ? "[ \n\r\t;,]" : "[\n\r\t;,]");
    }

    public static String[] getTechnicalTokens(String s, char separateur, boolean includeSpace) {
        StringBuilder buf = new StringBuilder();
        buf.append("[\n\r\t");
        buf.append(separateur);
        if (includeSpace) {
            buf.append(" ");
        }
        buf.append("]");
        return getTechnicalTokens(s, buf.toString());
    }

    private static String[] getTechnicalTokens(String s, String splitString) {
        String[] tokens = s.split(splitString);
        int p = 0;
        for (String token : tokens) {
            String val = cleanString(token);
            if (val.length() > 0) {
                tokens[p] = val;
                p++;
            }
        }
        if (p < tokens.length) {
            String[] result = new String[p];
            System.arraycopy(tokens, 0, result, 0, p);
            tokens = result;
        }
        return tokens;
    }

    public static String[] getLineTokens(String chaine, short cleanType) {
        if ((chaine == null) || (chaine.length() == 0)) {
            return EMPTY_STRINGARRAY;
        }
        List<String> ps = new ArrayList<String>();
        BufferedReader buflect = new BufferedReader(new StringReader(chaine));
        String ligne;
        try {
            while ((ligne = buflect.readLine()) != null) {
                if (cleanType != NOTCLEAN) {
                    ligne = cleanString(ligne);
                    if ((cleanType == EMPTY_INCLUDE) || (ligne.length() > 0)) {
                        ps.add(ligne);
                    }
                } else {
                    ps.add(ligne);
                }
            }
        } catch (IOException e) {
        }
        int size = ps.size();
        if (size == 0) {
            return EMPTY_STRINGARRAY;
        }
        String[] result = new String[size];
        for (int i = 0; i < size; i++) {
            result[i] = ps.get(i);
        }
        return result;
    }

    public static String[] getTokens(String chaine, char separateur, short cleanType) {
        if ((chaine == null) || (chaine.length() == 0)) {
            return EMPTY_STRINGARRAY;
        }
        List<String> ps = new ArrayList<String>();
        int length = chaine.length();
        int lastIndex = 0;
        for (int i = 0; i < length; i++) {
            char carac = chaine.charAt(i);
            if (carac == separateur) {
                if (i == lastIndex) {
                    if (cleanType != EMPTY_EXCLUDE) {
                        ps.add("");
                    }
                } else {
                    String token = chaine.substring(lastIndex, i);
                    if (cleanType != NOTCLEAN) {
                        token = cleanString(token);
                        if ((cleanType == EMPTY_INCLUDE) || (token.length() > 0)) {
                            ps.add(token);
                        }
                    } else {
                        ps.add(token);
                    }

                }
                lastIndex = i + 1;
            }
        }
        if (lastIndex < length) {
            String token = chaine.substring(lastIndex);
            if (cleanType != NOTCLEAN) {
                token = cleanString(token);
                if ((cleanType == EMPTY_INCLUDE) || (token.length() > 0)) {
                    ps.add(token);
                }
            } else {
                ps.add(token);
            }
        }
        int size = ps.size();
        if (size == 0) {
            return EMPTY_STRINGARRAY;
        }
        String[] result = new String[size];
        for (int i = 0; i < size; i++) {
            result[i] = ps.get(i);
        }
        return result;
    }

    public static boolean isAbsoluteUrlString(String s) {
        int length = s.length();
        for (int i = 0; i < length; i++) {
            char carac = s.charAt(i);
            switch (carac) {
                case '.':
                case '+':
                case '-':
                    if (i == 0) {
                        return false;
                    }
                    break;
                case ':':
                    if (i == 0) {
                        return false;
                    } else if (i == (length - 1)) {
                        return false;
                    } else if (s.charAt(i + 1) != '/') {
                        return false;
                    }
                    return true;
                default:
                    if ((carac >= '0') && (carac <= '9')) {
                        if (i == 0) {
                            return false;
                        }
                    } else if ((carac >= 'a') && (carac <= 'z')) {
                    } else if ((carac >= 'A') && (carac <= 'Z')) {
                    } else {
                        return false;
                    }
            }
        }
        return false;
    }

    public static boolean isCleanString(String s) {
        if (s == null) {
            return true;
        }
        String cleanedString = cleanString(s);
        return cleanedString.equals(s);
    }

    public static String cleanString(CharSequence s) {
        return cleanString(s, true);
    }

    /**
     * Nettoie la chaîne en supprimant les espaces blancs en surnombre. Si la
     * chaîne est nulle retourne <em>null</em>.
     */
    public static String cleanString(CharSequence charSequence, boolean trim) {
        if (charSequence == null) {
            return null;
        }
        int length = charSequence.length();
        if (length == 0) {
            return "";
        }
        StringBuilder buf = new StringBuilder(length);
        boolean start = true;
        boolean withBlank = false;
        for (int i = 0; i < length; i++) {
            char carac = charSequence.charAt(i);
            if (carac <= ' ') {
                if (!start) {
                    withBlank = true;
                } else if (!trim) {
                    withBlank = true;
                }
            } else {
                start = false;
                if (withBlank) {
                    buf.append(' ');
                    withBlank = false;
                }
                buf.append(carac);
            }
        }
        if ((!trim) && (withBlank)) {
            buf.append(' ');
        }
        return buf.toString();
    }

    /**
     * Ne garde d'une chaîne que les caractères Lettre ou chiffre. S'il s'agit
     * d'un chiffre, il est ramené à sa valeur décimale.
     */
    public static String reduceToLetterAndDigit(String source) {
        int length = source.length();
        if (length == 0) {
            return "";
        }
        StringBuilder buf = new StringBuilder(length + 10);
        for (int i = 0; i < length; i++) {
            char carac = source.charAt(i);
            if (Character.isLetter(carac)) {
                buf.append(carac);
            } else if (Character.isDigit(carac)) {
                int val = Character.getNumericValue(carac);
                if (val < 0) {
                    buf.append(carac);
                } else {
                    buf.append(val);
                }
            }
        }
        return buf.toString();
    }

    /*
     * Découpage de la chaîne s suivant le nombre de caractères par ligne l.
     */
    public static String[] wrap(String s, final int l) {
        if ((s.length() <= l) || (l < 1)) {
            String[] lineArray = new String[1];
            if (s.length() == 0) {
                lineArray[0] = " ";
            } else {
                lineArray[0] = s;
            }
            return lineArray;
        }
        String line = s;
        int p = 0;
        List<String> lineList = new ArrayList<String>();
        while (line != null) {
            if (line.length() <= l) {
                lineList.add(line);
                break;
            }
            if (Character.isWhitespace(line.charAt(l))) {
                lineList.add(line.substring(0, l));
                if (line.length() == l + 1) {
                    break;
                } else {
                    line = line.substring(l + 1);
                }
            } else {
                boolean coupure = false;
                for (int q = l - 1; q > 0; q--) {
                    char car = line.charAt(q);
                    if (car == '-') {
                        lineList.add(line.substring(0, q + 1));
                        line = line.substring(q + 1);
                        coupure = true;
                        break;
                    } else if (Character.isWhitespace(car)) {
                        lineList.add(line.substring(0, q));
                        line = line.substring(q + 1);
                        coupure = true;
                        break;
                    }
                }
                if (!coupure) {
                    lineList.add(line.substring(0, l));
                    line = line.substring(l);
                }
            }
        }
        return lineList.toArray(new String[lineList.size()]);
    }

    public static String[] wrap(String s, final int l, int lineMax) {
        if ((s.length() <= l) || (l < 1)) {
            String[] lineArray = new String[1];
            if (s.length() == 0) {
                lineArray[0] = " ";
            } else {
                lineArray[0] = s;
            }
            return lineArray;
        }
        String line = s;
        int lineNumber = 0;
        ArrayList<String> lineList = new ArrayList<String>();
        while (line != null) {
            if (line.length() <= l) {
                lineList.add(line);
                break;
            }
            if (Character.isWhitespace(line.charAt(l))) {
                lineList.add(line.substring(0, l));
                if (line.length() == l + 1) {
                    break;
                } else {
                    line = line.substring(l + 1);
                }
            } else {
                boolean coupure = false;
                for (int q = l - 1; q > 0; q--) {
                    char car = line.charAt(q);
                    if (car == '-') {
                        lineList.add(line.substring(0, q + 1));
                        line = line.substring(q + 1);
                        coupure = true;
                        break;
                    } else if (Character.isWhitespace(car)) {
                        lineList.add(line.substring(0, q));
                        line = line.substring(q + 1);
                        coupure = true;
                        break;
                    }
                }
                if (!coupure) {
                    lineList.add(line.substring(0, l));
                    line = line.substring(l);
                }

            }
            if (lineMax > 0) {
                lineNumber++;
                if (lineNumber == lineMax) {
                    String lastLine = (String) lineList.get(lineList.size() - 1);
                    lastLine = lastLine + "...";
                    lineList.set(lineList.size() - 1, lastLine);
                    break;
                }
            }
        }
        return lineList.toArray(new String[lineList.size()]);
    }

    public static String getRandomToken(int length) {
        StringBuilder buf = new StringBuilder(length);
        for (int i = 0; i < length; i++) {
            buf.append(CHAR_SEQUENCE.charAt(SECURE_RANDOM.nextInt(CHAR_SEQUENCE.length())));
        }
        return buf.toString();
    }

    /**
     * Teste si la chaîne en argument correspond bien à un UUID. Elle doit avoir
     * la forme 6ba7b812-9dad-11d1-80b4-00c04fd430c8. En particulier, 36
     * caractères, les tirets au bon emplacement, ne contenir que des chiffres
     * ou les lettres en minuscules : a,b,c,d,e,f
     */
    public static boolean isUUID(String uuid) {
        if (uuid == null) {
            return false;
        }
        if (uuid.length() != 36) {
            return false;
        }
        for (int i = 0; i < 36; i++) {
            char carac = uuid.charAt(i);
            if ((i == 8) || (i == 13) || (i == 18) || (i == 23)) {
                if (carac != '-') {
                    return false;
                }
            } else if (!(((carac >= 'a') && (carac <= 'f')) || ((carac >= '0') && (carac <= '9')))) {
                return false;
            }
        }
        return true;
    }

    /**
     * Teste si la chaine en argument peut être utilisé comme un nom technique.
     * Un nom technique est un nom qui sert par exemple d'identifiant d'un
     * ensemble (par exemple, un thésaurus) et qui peut être utilisé à beaucoup
     * d'endroits différents (nom de fichier, adresse url, nom de table d'une
     * base, paramètre d'un formulaire, etc.). Pour que ce nom technique soit le
     * plus compatible possible avec d'autres systèmes, un nom technique est
     * limité à 16 caractères, ne doit comporter que des minuscules, que des
     * caractères alphanumériques ascii (« _ » est exclu, ne pas commencer par
     * un chiffre. La méthode retourne 1 si le nom technique est valide, un
     * nombre négatif si elle ne l'est pas. Les valeurs des négatifs sont les
     * suivantes. <ul><li>-100 : la chaine est nulle ou de longueur nulle.
     * <li>-101 : la chaine fait plus de 16 caractères. <li>-102 : la chaîne
     * commence par un nombre. <li> -i : le caractère à la position -(i-1) est
     * incorrect (i.e. : non compris dans les intervalles 0-9 et a-z.
     */
    /*public static int checkTechnicalName(String chaine) {
     if (chaine == null) return -100;
     int longueur = chaine.length();
     if (longueur == 0) return -100;
     if (longueur > 16) return -101;
     for (int i = 0; i < longueur;i++) {
     char carac = chaine.charAt(i);
     if (i == 0) {
     if ((carac >= '0') && (carac <= '9')) return -102;
     }
     if (!( ((carac >= 'a') && (carac <= 'z')) || ((carac >= '0') && (carac <= '9')) )) {
     return - (i + 1);
     }
     }
     return 1;
     }*/
    /**
     * Vérifie si la chaîne en argument correspond à un nom technique. Un « nom
     * technique » est un nom d'abord destiné au traitement informatique tout en
     * étant parlant à l'utilisateur averti qui voudrait faire des manipulations
     * particulières.
     * <p>
     * Afin d'assurer l'utilisation sans problème de ce nom technique dans des
     * applications, un nom technique ne doit être constituée que des minuscules
     * ASCII de 'a' à 'z' ainsi que des chiffres de 0 à 9, ne pas commencer par
     * un chiffre et pas moins de deux caractères.
     * <p>
     * Un nom technique peut être facilement inséré dans un URL, utilisé sans
     * guillemets dans une application (par exemple, comme nom de table dans une
     * base SQL, etc).
     * <p>
     * Il peut autoriser ou non le tiret bas « _ ». Cependant, s'il est
     * autorisé, la chaine ne peut pas commencer par ce caractère.</p>
     * <p>
     * Cela correspond aux expressions rationnelles suivantes : <ul> <li>Sans
     * tiret bas : [a-z]([a-z0-9]*)</li> <li>Avec tiret bas :
     * [a-z]([_a-z0-9]*)</li> </ul>
     *
     * @throws ParseException si s est nul, de longueur inférieure à deux
     * commence par un chiffre ou possède des caractères interdits.
     */
    public static void checkTechnicalName(String s, boolean underscoreAllowed) throws ParseException {
        if (s == null) {
            throw new ParseException("string argument is null", 0);
        }
        int longueur = s.length();
        if (longueur < 2) {
            throw new ParseException("string length < 2", 1);
        }
        for (int i = 0; i < longueur; i++) {
            char carac = s.charAt(i);
            if (i == 0) {
                if ((carac >= '0') && (carac <= '9')) {
                    throw new ParseException("starts with a number", 0);
                }
            }
            if (carac == '_') {
                if (underscoreAllowed) {
                    if (i == 0) {
                        throw new ParseException("starts with an underscore", 0);
                    } else {
                        continue;
                    }
                } else {
                    throw new ParseException("underscore _ is not allowed", i);
                }
            }
            if (!(((carac >= 'a') && (carac <= 'z')) || ((carac >= '0') && (carac <= '9')))) {
                throw new ParseException("bar character : " + String.valueOf(carac), i);
            }
        }
    }

    public static boolean isTechnicalName(String s, boolean underscoreAllowed) {
        try {
            checkTechnicalName(s, underscoreAllowed);
            return true;
        } catch (ParseException pe) {
            return false;
        }
    }

    public static boolean isAuthority(String s) {
        try {
            checkAuthority(s);
            return true;
        } catch (ParseException pe) {
            return false;
        }
    }

    public static String nullTrim(String s) {
        if (s == null) {
            return "";
        }
        return s.trim();
    }

    /**
     * Un nom d'autorité est composé de minuscules ou de majuscule ASCCI, de
     * chiffres et des caractères « .-_ » uniquement
     */
    public static void checkAuthority(String s) throws ParseException {
        if (s == null) {
            throw new ParseException("string argument is null", 0);
        }
        int length = s.length();
        if (length == 0) {
            throw new ParseException("string argument is empty", 0);
        }
        for (int i = 0; i < length; i++) {
            char carac = s.charAt(i);
            switch (carac) {
                case '.':
                case '_':
                case '-':
                    continue;
                default:
                    if (!(((carac >= 'a') && (carac <= 'z')) || ((carac >= 'A') && (carac <= 'Z')) || ((carac >= '0') && (carac <= '9')))) {
                        throw new ParseException("bar character : " + String.valueOf(carac), i);
                    }
            }
        }
    }

    /**
     * Un nom de script est composé de minuscules, de chiffres et des caractères
     * « .-_ » uniquement
     */
    public static void checkScriptName(String s) throws ParseException {
        if (s == null) {
            throw new ParseException("string argument is null", 0);
        }
        int length = s.length();
        if (length == 0) {
            throw new ParseException("string argument is empty", 0);
        }
        for (int i = 0; i < length; i++) {
            char carac = s.charAt(i);
            if (i == 0) {
                if ((carac < 'a') || (carac > 'z')) {
                    throw new ParseException("starts with : " + carac, 0);
                }
            } else {
                switch (carac) {
                    case '.':
                    case '_':
                    case '-':
                        continue;
                    default:
                        if (!(((carac >= 'a') && (carac <= 'z')) || ((carac >= '0') && (carac <= '9')))) {
                            throw new ParseException("bar character : " + String.valueOf(carac), i);
                        }
                }
            }
        }
    }

    /**
     * Transforme un flux de caractères en chaîne. Les éventuelles exceptions de
     * type
     */
    public static String toString(InputStream inputStream) throws IOException {
        StringBuilder buf = new StringBuilder(1024);
        char[] bufArray = new char[1024];
        InputStreamReader isr = new InputStreamReader(inputStream, "UTF-8");
        while (true) {
            int i = isr.read(bufArray);
            if (i == -1) {
                break;
            }
            buf.append(bufArray, 0, i);
            if (i < 1024) {
                break;
            }
        }
        return buf.toString();
    }

    /**
     * Retourne un entier sous forme de chaîne en respectant les paramètres
     * locaux.
     */
    public static String toString(int integer, Locale locale) {
        NumberFormat numberFormat = NumberFormat.getInstance(locale);
        return numberFormat.format((long) integer);
    }

    public static String formatMessage(String messageString, Object value, Locale formatLocale) throws FormatException {
        if (value == null) {
            return messageString;
        }
        Object[] objs;
        if (value.getClass().isArray()) {
            objs = (Object[]) value;
            if (objs.length == 0) {
                return messageString;
            }
        } else {
            objs = new Object[1];
            objs[0] = value;
        }
        try {
            MessageFormat messageFormat = new MessageFormat(messageString, formatLocale);
            return messageFormat.format(objs);
        } catch (IllegalArgumentException iae) {
            throw new FormatException(iae.getLocalizedMessage());
        }
    }

    public static SubstringPosition getSubstringPosition(String source, String part) {
        int idx = source.indexOf(part);
        if (idx == -1) {
            return null;
        }
        return new SubstringPosition(idx, part.length());
    }

    public static SubstringPosition getSubstringPosition(String source, String part, int type) {
        int partLength = part.length();
        switch (type) {
            case TextConstants.CONTAINS:
                int idx = source.indexOf(part);
                if (idx != -1) {
                    return new SubstringPosition(idx, partLength);
                } else {
                    return null;
                }
            case TextConstants.MATCHES:
                if (source.equals(part)) {
                    return new SubstringPosition(0, partLength);
                } else {
                    return null;
                }
            case TextConstants.ENDSWITH:
                if (source.endsWith(part)) {
                    return new SubstringPosition(source.length() - partLength, partLength);
                } else {
                    return null;
                }
            case TextConstants.STARTSWITH:
                if (source.startsWith(part)) {
                    return new SubstringPosition(0, partLength);
                } else {
                    return null;
                }
            default:
                throw new IllegalArgumentException("wrong type");
        }
    }

    public static boolean contains(String source, String part, int type) {
        switch (type) {
            case TextConstants.CONTAINS:
                int idx = source.indexOf(part);
                if (idx == -1) {
                    return false;
                } else {
                    return true;
                }
            case TextConstants.MATCHES:
                return source.equals(part);
            case TextConstants.ENDSWITH:
                return source.endsWith(part);
            case TextConstants.STARTSWITH:
                return source.startsWith(part);
            default:
                throw new IllegalArgumentException("wrong type");
        }
    }

    public static int getIndent(String text) {
        int indent = 0;
        int textLength = text.length();
        boolean previous = false;
        for (int j = 0; j < textLength; j++) {
            char carac = text.charAt(j);
            if (carac == ' ') {
                if (previous) {
                    previous = false;
                    indent++;
                } else {
                    previous = true;
                }
            } else if (carac == '\t') {
                indent++;
                previous = false;
            } else {
                break;
            }
        }
        return indent;
    }

    public short getEMPTY_EXCLUDE() {
        return EMPTY_EXCLUDE;
    }

    public static String escapeDoubleQuote(String s) {
        if (s == null) {
            return s;
        }
        StringBuffer buf = new StringBuffer(s.length() + 20);
        escapeDoubleQuote(s, buf);
        return buf.toString();
    }

    public static void escapeDoubleQuote(String s, Appendable buf) {
        if (s == null) {
            return;
        }
        int length = s.length();
        try {
            for (int i = 0; i < length; i++) {
                char carac = s.charAt(i);
                if (carac == '\"') {
                    buf.append("\\\"");
                } else if (carac == '\\') {
                    buf.append("\\\\");
                } else {
                    buf.append(carac);
                }
            }
        } catch (IOException ioe) {
            throw new NestedIOException(ioe);
        }
    }

    public static String charToString(char carac) {
        switch (carac) {
            case '\t':
                return "{TAB}";
            case '\n':
                return "{NL}";
            case '\r':
                return "{CR}";
            case ' ':
                return "{SPACE}";
            case '\u00A0':
                return "{NBSP}";
            default:
                StringBuilder buf = new StringBuilder();
                if (Character.isISOControl(carac)) {
                    buf.append("{x");
                    buf.append(Integer.toHexString((char) carac).toUpperCase());
                    buf.append("}");
                } else {
                    buf.append(carac);
                }
                return buf.toString();
        }
    }

    public static char stringToChar(String s) {
        int length = s.length();
        if (length == 0) {
            throw new IllegalArgumentException("s is empty");
        }
        if (s.equals("{TAB}")) {
            return '\t';
        }
        if (s.equals("{NL}")) {
            return '\n';
        }
        if (length != 1) {
            throw new IllegalArgumentException("s is too long");
        }
        return s.charAt(0);
    }

    public static String[] toStringArray(char[] caracs) {
        int length = caracs.length;
        String[] result = new String[length];
        for (int i = 0; i < length; i++) {
            result[i] = charToString(caracs[i]);
        }
        return result;
    }

    public static String[] toStringArray(CharSequence charSequence) {
        int length = charSequence.length();
        String[] result = new String[length];
        for (int i = 0; i < length; i++) {
            result[i] = charToString(charSequence.charAt(i));
        }
        return result;
    }

    public static String implode(String[] stringArray, char delim) {
        int length = stringArray.length;
        StringBuilder buf = new StringBuilder();
        for (int i = 0; i < length; i++) {
            if (i > 0) {
                buf.append(delim);
            }
            buf.append(stringArray[i]);
        }
        return buf.toString();
    }

    public static String implode(Collection<String> values, char delim) {
        if (values.isEmpty()) {
            return "";
        }
        boolean next = false;
        StringBuilder buf = new StringBuilder();
        for (String value : values) {
            if (next) {
                buf.append(delim);
            } else {
                next = true;
            }
            buf.append(value);
        }
        return buf.toString();
    }

    /*
     * Retourne de 0 à 9 s'il s'agit d'un chiffre, - 1 sinon.
     */
    public static int testNumberChar(char carac) {
        if ((carac < '0') || (carac > '9')) {
            return -1;
        }
        return (int) carac - 48;
    }

    public static boolean isValidAsciiChar(char carac) {
        if (carac > '~') {
            return false;
        }
        if ((carac >= 'a') && (carac <= 'z')) {
            return true;
        }
        if ((carac >= 'A') && (carac <= 'Z')) {
            return true;
        }
        if ((carac >= '0') && (carac <= '9')) {
            return true;
        }
        switch (carac) {
            case '!':
            case '#':
            case '$':
            case '%':
            case '&':
            case '*':
            case '+':
            case '-':
            case '.':
            case '/':
            case '?':
            case ':':
            case '@':
            case '_':
            case '|':
            case '~':
                return true;
            default:
                return false;
        }
    }

    public static boolean isValidFirstAsciiChar(char carac) {
        if (carac > '~') {
            return false;
        }
        if ((carac >= 'a') && (carac <= 'z')) {
            return true;
        }
        if ((carac >= 'A') && (carac <= 'Z')) {
            return true;
        }
        switch (carac) {
            case '_':
                return true;
            default:
                return false;
        }
    }

    public static boolean isValidAsciiString(String s) {
        int length = s.length();
        for (int i = 0; i < length; i++) {
            if (!isValidAsciiChar(s.charAt(i))) {
                return false;
            }
        }
        return true;
    }

    public static double parseDouble(String s) throws NumberFormatException {
        if (s == null) {
            throw new NumberFormatException("null");
        }
        s = s.trim();
        int length = s.length();
        if (length == 0) {
            throw new NumberFormatException("empty");
        }
        DoubleParser doubleParser = new DoubleParser();
        return doubleParser.parse(s);
    }

    public static Decimal parseDecimal(String s) throws NumberFormatException {
        if (s == null) {
            throw new NumberFormatException("null");
        }
        s = s.trim();
        int length = s.length();
        if (length == 0) {
            throw new NumberFormatException("empty");
        }
        DecimalParser decimalParser = new DecimalParser();
        return decimalParser.parse(s);
    }

    public static BigDecimal parseBigDecimal(String s) throws NumberFormatException {
        Decimal decimal = parseDecimal(s);
        return decimal.toBigDecimal();
    }

    public static Decimal parseStrictDecimal(String decimalString) throws NumberFormatException {
        String partieEntiereString = decimalString;
        String partieDecimaleString = "";
        int idx = decimalString.indexOf('.');
        if (idx != -1) {
            partieEntiereString = decimalString.substring(0, idx);
            partieDecimaleString = decimalString.substring(idx + 1);
        }
        return parseDecimal(partieEntiereString, partieDecimaleString);
    }

    public static Decimal parseDecimal(String partieEntiereString, String partieDecimaleString) throws NumberFormatException {
        long partieEntiere = 0;
        boolean negatif = false;
        if (partieEntiereString.length() > 0) {
            partieEntiere = Long.parseLong(partieEntiereString);
            if (partieEntiere < 0) {
                negatif = true;
            } else if (partieEntiere == 0) {
                if (partieEntiereString.charAt(0) == '-') {
                    negatif = true;
                }
            }
        }
        int partieDecimale = 0;
        int zeroLength = 0;
        int length = partieDecimaleString.length();
        if (length > 0) {
            if (length > 9) {
                partieDecimaleString = partieDecimaleString.substring(0, 9);
            }
            for (int i = 0; i < length; i++) {
                if (partieDecimaleString.charAt(i) == '0') {
                    zeroLength++;
                } else {
                    break;
                }
            }
            partieDecimale = Integer.parseInt(partieDecimaleString);
        }
        if (negatif) {
            partieDecimale = -partieDecimale;
        }
        Decimal decimal = new Decimal(partieEntiere, (byte) zeroLength, partieDecimale);
        return decimal;
    }


    private static class DoubleParser {

        boolean negatif;

        DoubleParser() {
        }

        double parse(String s) {
            int length = s.length();
            int[] chiffres = new int[length];
            int currentIndex = 0;
            int decimalIndex = -1;
            char decimalCar = '?';
            boolean forceDecimal = false;
            int startIndex = testNegatif(s.charAt(0), length);
            for (int i = startIndex; i < length; i++) {
                char carac = s.charAt(i);
                switch (carac) {
                    case ' ':
                        continue;
                    case '.':
                    case ',':
                        if (forceDecimal) {
                            throw new NumberFormatException("For input :  " + s);
                        }
                        if (decimalIndex == -1) {
                            decimalIndex = currentIndex;
                            decimalCar = carac;
                        } else if (carac == decimalCar) {
                            decimalIndex = -9;
                        } else if (carac != decimalCar) {
                            forceDecimal = true;
                            decimalIndex = currentIndex;
                        }
                        break;
                    case '0':
                    case '1':
                    case '2':
                    case '3':
                    case '4':
                    case '5':
                    case '6':
                    case '7':
                    case '8':
                    case '9':
                        chiffres[currentIndex] = ((int) carac) - 48;
                        currentIndex++;
                        break;
                    default:
                        throw new NumberFormatException("For input :  " + s);

                }
            }
            int chiffreLength = currentIndex;
            if (decimalIndex < 0) {
                decimalIndex = chiffreLength;
            }
            double resultat = 0;
            for (int i = 0; i < decimalIndex; i++) {
                resultat = resultat * 10 + chiffres[i];
            }
            if (decimalIndex < chiffreLength) {
                double dizaines = 10d;
                for (int i = decimalIndex; i < chiffreLength; i++) {
                    resultat = resultat + (((double) chiffres[i]) / dizaines);
                    dizaines = 10 * dizaines;
                }
            }
            if (negatif) {
                return -resultat;
            } else {
                return resultat;
            }
        }

        int testNegatif(char first, int length) {
            if (first == '-') {
                negatif = true;
                if (length == 1) {
                    throw new NumberFormatException("only - ");
                }
                return 1;
            } else {
                return 0;
            }
        }

    }


    private static class DecimalParser {

        boolean negatif;

        DecimalParser() {
        }

        Decimal parse(String s) {
            int length = s.length();
            int[] chiffres = new int[length];
            int currentIndex = 0;
            int decimalIndex = -1;
            char decimalCar = '?';
            boolean forceDecimal = false;
            int startIndex = testNegatif(s.charAt(0), length);
            for (int i = startIndex; i < length; i++) {
                char carac = s.charAt(i);
                if (Character.isSpaceChar(carac)) {
                    continue;
                }
                switch (carac) {
                    case '\'':
                        continue;
                    case '.':
                    case ',':
                        if (forceDecimal) {
                            throw new NumberFormatException("For input :  " + s);
                        }
                        if (decimalIndex == -1) {
                            decimalIndex = currentIndex;
                            decimalCar = carac;
                        } else if (carac == decimalCar) {
                            decimalIndex = -9;
                        } else if (carac != decimalCar) {
                            forceDecimal = true;
                            decimalIndex = currentIndex;
                        }
                        break;
                    case '0':
                    case '1':
                    case '2':
                    case '3':
                    case '4':
                    case '5':
                    case '6':
                    case '7':
                    case '8':
                    case '9':
                        chiffres[currentIndex] = ((int) carac) - 48;
                        currentIndex++;
                        break;
                    default:
                        throw new NumberFormatException("For input :  " + s);

                }
            }
            int chiffreLength = currentIndex;
            if (decimalIndex < 0) {
                decimalIndex = chiffreLength;
            }
            long partieEntiere = 0;
            for (int i = 0; i < decimalIndex; i++) {
                partieEntiere = partieEntiere * 10 + chiffres[i];
            }
            int partieDecimale = 0;
            int zeroLength = 0;
            if (decimalIndex < chiffreLength) {
                double dizaines = 10d;
                boolean debut = true;
                int premierschiffres = Math.min(chiffreLength, decimalIndex + 9);
                for (int i = decimalIndex; i < premierschiffres; i++) {
                    int chiffre = chiffres[i];
                    if ((debut) && (chiffre == 0)) {
                        zeroLength++;
                    } else {
                        debut = false;
                        partieDecimale = partieDecimale * 10 + chiffre;
                    }
                }
            }
            if (negatif) {
                partieEntiere = -partieEntiere;
                partieDecimale = -partieDecimale;
            }
            Decimal decimal = new Decimal(partieEntiere, (byte) zeroLength, partieDecimale);
            return decimal;
        }

        int testNegatif(char first, int length) {
            if (first == '-') {
                negatif = true;
                if (length == 1) {
                    throw new NumberFormatException("only - ");
                }
                return 1;
            } else {
                return 0;
            }
        }

    }

    public static boolean isBlank(String s) {
        if (s == null) {
            return true;
        }
        int length = s.length();
        if (length == 0) {
            return true;
        }
        for (int i = 0; i < length; i++) {
            char carac = s.charAt(i);
            if (carac > '\u0020') {
                return false;
            }
        }
        return true;
    }

    public static String toMD5(String s) {
        try {
            return digest("MD5", s);
        } catch (NoSuchAlgorithmException e) {
            throw new ShouldNotOccurException(e);
        }

    }

    public static String digest(String algorithm, String s) throws NoSuchAlgorithmException {
        try {
            MessageDigest msgDigest = MessageDigest.getInstance(algorithm);
            msgDigest.update(s.getBytes("UTF-8"));
            byte[] digest = msgDigest.digest();
            StringBuilder result = new StringBuilder();
            for (int i = 0; i < digest.length; ++i) {
                String hex = Integer.toHexString(0xFF & digest[i]);
                if (hex.length() == 1) {
                    result.append('0');
                }
                result.append(hex);
            }
            return result.toString();
        } catch (UnsupportedEncodingException e) {
            throw new ShouldNotOccurException(e);
        }

    }

    public static String getMD5Checksum(File file) throws IOException {
        try {
            return getFileChecksum(MessageDigest.getInstance("MD5"), file);
        } catch (NoSuchAlgorithmException e) {
            throw new ShouldNotOccurException(e);
        }
    }

    /*
    * From https://howtodoinjava.com/java/io/how-to-generate-sha-or-md5-file-checksum-hash-in-java/
     */
    private static String getFileChecksum(MessageDigest digest, File file) throws IOException {
        //Get file input stream for reading the file content
        FileInputStream fis = new FileInputStream(file);
        //Create byte array to read data in chunks
        byte[] byteArray = new byte[1024];
        int bytesCount = 0;
        //Read file data and update in message digest
        while ((bytesCount = fis.read(byteArray)) != -1) {
            digest.update(byteArray, 0, bytesCount);
        };
        //close the stream; We don't need it now.
        fis.close();
        //Get the hash's bytes
        byte[] bytes = digest.digest();
        //This bytes[] has bytes in decimal format;
        //Convert it to hexadecimal format
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < bytes.length; i++) {
            sb.append(Integer.toString((bytes[i] & 0xff) + 0x100, 16).substring(1));
        }
        //return complete hash
        return sb.toString();
    }

    public static String getReversePath(String path) {
        int length = path.length();
        StringBuilder buf = new StringBuilder();
        for (int i = 0; i < length; i++) {
            if (path.charAt(i) == '/') {
                buf.append("../");
            }
        }
        return buf.toString();
    }

    public static List<String> wrap(String[] array) {
        return new StringList(array);
    }

    public static List<String> toList(Collection<String> collection) {
        int size = collection.size();
        if (size == 0) {
            return EMPTY_STRINGLIST;
        }
        return new StringList(collection.toArray(new String[size]));
    }

    public static MultiStringable toMultiStringable(Collection<String> collection) {
        String[] array = collection.toArray(new String[collection.size()]);
        return new StringMultiStringable(array);
    }


    private static class StringList extends AbstractList<String> implements RandomAccess {

        private final String[] array;

        private StringList(String[] array) {
            this.array = array;
        }

        @Override
        public int size() {
            return array.length;
        }

        @Override
        public String get(int index) {
            return array[index];
        }

    }

    public static List<URL> wrap(URL[] array) {
        return new URLList(array);
    }


    private static class URLList extends AbstractList<URL> implements RandomAccess {

        private final URL[] array;

        private URLList(URL[] array) {
            this.array = array;
        }

        @Override
        public int size() {
            return array.length;
        }

        @Override
        public URL get(int index) {
            return array[index];
        }

    }

    public static List<RelativePath> wrap(RelativePath[] array) {
        return new RelativePathList(array);
    }


    private static class RelativePathList extends AbstractList<RelativePath> implements RandomAccess {

        private final RelativePath[] array;

        private RelativePathList(RelativePath[] array) {
            this.array = array;
        }

        @Override
        public int size() {
            return array.length;
        }

        @Override
        public RelativePath get(int index) {
            return array[index];
        }

    }


    private static class StringMultiStringable implements MultiStringable {

        private final String[] array;

        private StringMultiStringable(String[] array) {
            this.array = array;
        }

        @Override
        public String[] toStringArray() {
            int size = array.length;
            String[] result = new String[size];
            System.arraycopy(array, 0, result, 0, size);
            return result;
        }

        @Override
        public int getStringSize() {
            return array.length;
        }

        @Override
        public String getStringValue(int index) {
            return array[index];
        }

    }


    public static class FormatException extends Exception {

        private FormatException(String message) {
            super(message);
        }

    }


}
