/*
 * Copyright (C) 2016-2022 ActionTech.
 * based on code by MyCATCopyrightHolder Copyright (c) 2013, OpenCloudDB/MyCAT.
 * License: http://www.gnu.org/licenses/gpl.html GPL version 2 or higher.
 */
package com.actiontech.dble.util;

import com.actiontech.dble.backend.mysql.CharsetUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.ByteArrayOutputStream;
import java.io.UnsupportedEncodingException;
import java.nio.ByteBuffer;
import java.nio.charset.Charset;
import java.nio.charset.CharsetDecoder;
import java.nio.charset.CodingErrorAction;
import java.util.List;
import java.util.Objects;
import java.util.Random;
import java.util.Set;
import java.util.regex.Pattern;

/**
 * @author mycat
 */
public final class StringUtil {
    private StringUtil() {
    }

    public static final String TABLE_COLUMN_SEPARATOR = ".";
    public static final String ISO_8859_1 = "ISO-8859-1";

    private static final Logger LOGGER = LoggerFactory.getLogger(StringUtil.class);
    private static final byte[] EMPTY_BYTE_ARRAY = new byte[0];
    private static final Random RANDOM = new Random();
    private static final char[] CHARS = {'1', '2', '3', '4', '5', '6', '7',
            '8', '9', '0', 'q', 'w', 'e', 'r', 't', 'y', 'u', 'i', 'o', 'p',
            'a', 's', 'd', 'f', 'g', 'h', 'j', 'k', 'l', 'z', 'x', 'c', 'v',
            'b', 'n', 'm', 'Q', 'W', 'E', 'R', 'T', 'Y', 'U', 'I', 'O', 'P',
            'A', 'S', 'D', 'F', 'G', 'H', 'J', 'K', 'L', 'Z', 'X', 'C', 'V',
            'B', 'N', 'M'};

    /**
     * String hash:s[0]*31^(n-1) + s[1]*31^(n-2) + ... + s[n-1] <br>
     * h = 31*h + s.charAt(i); => h = (h << 5) - h + s.charAt(i); <br>
     *
     * @param start hash for s.substring(start, end)
     * @param end   hash for s.substring(start, end)
     */
    public static long hash(String s, int start, int end) {
        if (start < 0) {
            start = 0;
        }
        if (end > s.length()) {
            end = s.length();
        }
        long h = 0;
        for (int i = start; i < end; ++i) {
            h = (h << 5) - h + s.charAt(i);
        }
        return h;
    }

    public static byte[] encode(String src, String charset) {
        if (src == null) {
            return null;
        }
        if (charset == null) {
            return src.getBytes();
        }
        try {
            return src.getBytes(CharsetUtil.getJavaCharset(charset));
        } catch (UnsupportedEncodingException e) {
            return src.getBytes();
        }
    }

    public static String decode(byte[] src, String charset) {
        return decode(src, 0, src.length, charset);
    }

    public static String decode(byte[] src, int offset, int length,
                                String charset) {
        try {
            return new String(src, offset, length, CharsetUtil.getJavaCharset(charset));
        } catch (UnsupportedEncodingException e) {
            return new String(src, offset, length);
        }
    }

    public static String getRandomString(int size) {
        StringBuilder s = new StringBuilder(size);
        int len = CHARS.length;
        for (int i = 0; i < size; i++) {
            int x = RANDOM.nextInt();
            s.append(CHARS[(x < 0 ? -x : x) % len]);
        }
        return s.toString();
    }


    public static boolean isEmpty(String str) {
        return ((str == null) || (str.length() == 0));
    }

    public static boolean isBlank(String str) {
        return ((str == null) || (str.length() == 0)) || (str.trim().length() == 0);
    }

    public static byte[] hexString2Bytes(char[] hexString, int offset,
                                         int length) {
        if (hexString == null) {
            return null;
        }
        if (length == 0) {
            return EMPTY_BYTE_ARRAY;
        }
        boolean odd = length << 31 == Integer.MIN_VALUE;
        byte[] bs = new byte[odd ? (length + 1) >> 1 : length >> 1];
        for (int i = offset, limit = offset + length; i < limit; ++i) {
            char high, low;
            if (i == offset && odd) {
                high = '0';
                low = hexString[i];
            } else {
                high = hexString[i];
                low = hexString[++i];
            }
            int b;
            switch (high) {
                case '0':
                    b = 0;
                    break;
                case '1':
                    b = 0x10;
                    break;
                case '2':
                    b = 0x20;
                    break;
                case '3':
                    b = 0x30;
                    break;
                case '4':
                    b = 0x40;
                    break;
                case '5':
                    b = 0x50;
                    break;
                case '6':
                    b = 0x60;
                    break;
                case '7':
                    b = 0x70;
                    break;
                case '8':
                    b = 0x80;
                    break;
                case '9':
                    b = 0x90;
                    break;
                case 'a':
                case 'A':
                    b = 0xa0;
                    break;
                case 'b':
                case 'B':
                    b = 0xb0;
                    break;
                case 'c':
                case 'C':
                    b = 0xc0;
                    break;
                case 'd':
                case 'D':
                    b = 0xd0;
                    break;
                case 'e':
                case 'E':
                    b = 0xe0;
                    break;
                case 'f':
                case 'F':
                    b = 0xf0;
                    break;
                default:
                    throw new IllegalArgumentException("illegal hex-string: " + new String(hexString, offset, length));
            }
            switch (low) {
                case '0':
                    break;
                case '1':
                    b += 1;
                    break;
                case '2':
                    b += 2;
                    break;
                case '3':
                    b += 3;
                    break;
                case '4':
                    b += 4;
                    break;
                case '5':
                    b += 5;
                    break;
                case '6':
                    b += 6;
                    break;
                case '7':
                    b += 7;
                    break;
                case '8':
                    b += 8;
                    break;
                case '9':
                    b += 9;
                    break;
                case 'a':
                case 'A':
                    b += 10;
                    break;
                case 'b':
                case 'B':
                    b += 11;
                    break;
                case 'c':
                case 'C':
                    b += 12;
                    break;
                case 'd':
                case 'D':
                    b += 13;
                    break;
                case 'e':
                case 'E':
                    b += 14;
                    break;
                case 'f':
                case 'F':
                    b += 15;
                    break;
                default:
                    throw new IllegalArgumentException("illegal hex-string: " + new String(hexString, offset, length));
            }
            bs[(i - offset) >> 1] = (byte) b;
        }
        return bs;
    }

    public static String dumpAsHex(byte[] src, int length) {
        StringBuilder out = new StringBuilder(length * 4);
        int p = 0;
        int rows = length / 8;
        for (int i = 0; (i < rows) && (p < length); i++) {
            int ptemp = p;
            for (int j = 0; j < 8; j++) {
                String hexVal = Integer.toHexString(src[ptemp] & 0xff);
                if (hexVal.length() == 1) {
                    out.append('0');
                }
                out.append(hexVal).append(' ');
                ptemp++;
            }
            out.append("    ");
            for (int j = 0; j < 8; j++) {
                int b = 0xff & src[p];
                if (b > 32 && b < 127) {
                    out.append((char) b).append(' ');
                } else {
                    out.append(". ");
                }
                p++;
            }
            out.append('\n');
        }
        int n = 0;
        for (int i = p; i < length; i++) {
            String hexVal = Integer.toHexString(src[i] & 0xff);
            if (hexVal.length() == 1) {
                out.append('0');
            }
            out.append(hexVal).append(' ');
            n++;
        }
        for (int i = n; i < 8; i++) {
            out.append("   ");
        }
        out.append("    ");
        for (int i = p; i < length; i++) {
            int b = 0xff & src[i];
            if (b > 32 && b < 127) {
                out.append((char) b).append(' ');
            } else {
                out.append(". ");
            }
        }
        out.append('\n');
        return out.toString();
    }

    public static byte[] escapeEasternUnicodeByteStream(byte[] src,
                                                        String srcString, int offset, int length) {
        if ((src == null) || (src.length == 0)) {
            return src;
        }
        int bytesLen = src.length;
        int bufIndex = 0;
        int strIndex = 0;
        ByteArrayOutputStream out = new ByteArrayOutputStream(bytesLen);
        while (true) {
            if (srcString.charAt(strIndex) == '\\') { // writeDirectly it out as-is
                out.write(src[bufIndex++]);
            } else { // Grab the first byte
                int loByte = src[bufIndex];
                if (loByte < 0) {
                    loByte += 256; // adjust for signedness/wrap-around
                }
                out.write(loByte); // We always writeDirectly the first byte
                if (loByte >= 0x80) {
                    if (bufIndex < (bytesLen - 1)) {
                        int hiByte = src[bufIndex + 1];
                        if (hiByte < 0) {
                            hiByte += 256; // adjust for signedness/wrap-around
                        }
                        out.write(hiByte); // writeDirectly the high byte here, and
                        // increment the index for the high
                        // byte
                        bufIndex++;
                        if (hiByte == 0x5C) {
                            out.write(hiByte); // escape 0x5c if necessary
                        }
                    }
                } else if (loByte == 0x5c && bufIndex < (bytesLen - 1)) {
                    int hiByte = src[bufIndex + 1];
                    if (hiByte < 0) {
                        hiByte += 256; // adjust for signedness/wrap-around
                    }
                    if (hiByte == 0x62) { // we need to escape the 0x5c
                        out.write(0x5c);
                        out.write(0x62);
                        bufIndex++;
                    }
                }
                bufIndex++;
            }
            if (bufIndex >= bytesLen) {
                break; // we're done
            }
            strIndex++;
        }
        return out.toByteArray();
    }

    public static String toString(byte[] bytes) {
        if (bytes == null || bytes.length == 0) {
            return "";
        }
        StringBuilder buffer = new StringBuilder();
        for (byte byt : bytes) {
            buffer.append((char) byt);
        }
        return buffer.toString();
    }

    public static boolean equalsIgnoreCase(String str1, String str2) {
        if (str1 == null) {
            return str2 == null;
        }
        return str1.equalsIgnoreCase(str2);
    }

    public static boolean equals(String str1, String str2) {
        if (str1 == null) {
            return str2 == null;
        }
        return str1.equals(str2);
    }

    /**
     * option to ignore case depending on the condition
     *
     * @param str1
     * @param str2
     * @param ignoreCase
     * @return
     */
    public static boolean equalsConditionIgnoreCase(String str1, String str2, boolean ignoreCase) {
        if (ignoreCase) {
            return equalsIgnoreCase(str1, str2);
        }
        return str1.equals(str2);
    }

    public static boolean equalsWithEmpty(String str1, String str2) {
        if (isEmpty(str1)) {
            return isEmpty(str2);
        }
        return str1.equals(str2);
    }

    public static int countChar(String str, char c) {
        if (str == null || str.isEmpty()) {
            return 0;
        }
        final int len = str.length();
        int cnt = 0;
        for (int i = 0; i < len; ++i) {
            if (c == str.charAt(i)) {
                ++cnt;
            }
        }
        return cnt;
    }

    public static String replaceOnce(String text, String repl, String with) {
        return replace(text, repl, with, 1);
    }

    public static String replace(String text, String repl, String with) {
        return replace(text, repl, with, -1);
    }

    public static String replace(String text, String repl, String with, int max) {
        if ((text == null) || (repl == null) || (with == null) || (repl.length() == 0) || (max == 0)) {
            return text;
        }
        StringBuilder buf = new StringBuilder(text.length());
        int start = 0;
        int end = 0;
        while ((end = text.indexOf(repl, start)) != -1) {
            buf.append(text.substring(start, end)).append(with);
            start = end + repl.length();
            if (--max == 0) {
                break;
            }
        }
        buf.append(text.substring(start));
        return buf.toString();
    }

    public static String replaceChars(String str, char searchChar,
                                      char replaceChar) {
        if (str == null) {
            return null;
        }
        return str.replace(searchChar, replaceChar);
    }

    public static String replaceChars(String str, String searchChars,
                                      String replaceChars) {
        if ((str == null) || (str.length() == 0) || (searchChars == null) || (searchChars.length() == 0)) {
            return str;
        }
        char[] chars = str.toCharArray();
        int len = chars.length;
        boolean modified = false;
        for (int i = 0, isize = searchChars.length(); i < isize; i++) {
            char searchChar = searchChars.charAt(i);
            if ((replaceChars == null) || (i >= replaceChars.length())) { // DELETE
                int pos = 0;
                for (int j = 0; j < len; j++) {
                    if (chars[j] != searchChar) {
                        chars[pos++] = chars[j];
                    } else {
                        modified = true;
                    }
                }
                len = pos;
            } else { // REPLACE
                for (int j = 0; j < len; j++) {
                    if (chars[j] == searchChar) {
                        chars[j] = replaceChars.charAt(i);
                        modified = true;
                    }
                }
            }
        }
        if (!modified) {
            return str;
        }
        return new String(chars, 0, len);
    }

    /**
     * remove ` in `tablename`
     * FIXME```tablename` may not correct
     *
     * @param str
     * @return
     */
    public static String removeBackQuote(String str) {
        if (str.length() > 1) {
            char firstValue = str.charAt(0);
            if ((firstValue == '`') && (firstValue == str.charAt(str.length() - 1))) {
                return str.substring(1, str.length() - 1);
            } else {
                return str;
            }
        }
        return str;
    }

    public static String removeBackAndDoubleQuote(String str) {
        if (str.length() > 1) {
            char firstValue = str.charAt(0);
            if ((firstValue == '`') && (firstValue == str.charAt(str.length() - 1))) {
                return str.substring(1, str.length() - 1);
            } else if ((firstValue == '"') && (firstValue == str.charAt(str.length() - 1))) {
                return str.substring(1, str.length() - 1);
            } else {
                return str;
            }
        }
        return str;
    }


    /**
     * remove ' from 'value'
     *
     * @param str
     * @return
     */
    public static String removeApostrophe(String str) {
        if (str.length() > 1) {
            char firstValue = str.charAt(0);
            if ((firstValue == '\'') && (firstValue == str.charAt(str.length() - 1))) {
                return str.substring(1, str.length() - 1);
            } else {
                return str;
            }
        }
        return str;
    }

    public static boolean containsApostrophe(String tableName) {
        char firstValue = tableName.charAt(0);
        return (firstValue == '`') && (firstValue == tableName.charAt(tableName.length() - 1));
    }

    public static String removeAllApostrophe(String str) {
        if (str.length() > 1) {
            char firstValue = str.charAt(0);
            if ((firstValue == '\'' || firstValue == '"') && (firstValue == str.charAt(str.length() - 1))) {
                return str.substring(1, str.length() - 1);
            } else {
                return str;
            }
        }
        return str;
    }

    /**
     * remove ' from 'value'
     *
     * @param str
     * @return
     */
    public static String removeApostropheOrBackQuote(String str) {
        if (str.length() > 1) {
            char firstValue = str.charAt(0);
            if (((firstValue == '\'') || (firstValue == '`')) && (firstValue == str.charAt(str.length() - 1))) {
                return str.substring(1, str.length() - 1);
            } else {
                return str;
            }
        }
        return str;
    }

    public static String getFullName(String schema, String tableName) {
        return String.format("`%s`.`%s`", schema, tableName);
    }


    public static String getUFullName(String schema, String tableName) {
        return String.format("%s.%s", schema, tableName);
    }

    public static String join(List<String> list, String flag) {
        if (list.size() < 1) {
            return "";
        }

        String[] arr = list.toArray(new String[list.size()]);
        return join(arr, flag);
    }

    public static String join(Set<String> set, String flag) {
        if (set.size() < 1) {
            return "";
        }

        String[] arr = set.toArray(new String[set.size()]);
        return join(arr, flag);
    }

    public static String join(String[] list, String flag) {
        if (list.length < 1) {
            return "";
        }
        StringBuffer sb = new StringBuffer();
        for (int i = 0; i < list.length - 1; i++) {
            sb.append(list[i]).append(flag);
        }
        sb.append(list[list.length - 1]);
        return sb.toString();
    }


    public static String trim(String orgStr, char c) {
        if (orgStr != null && orgStr.length() > 1) {
            if (orgStr.charAt(0) == c && orgStr.charAt(orgStr.length() - 1) == c) {
                return orgStr.substring(1, orgStr.length() - 1);
            }
        }
        return orgStr;
    }


    /**
     * Refer to String source code handling code，Whether the encoding can parse String properly
     *
     * @param charsetEncode
     * @param values
     * @return
     */
    public static boolean charsetParseString(String charsetEncode, byte[] values) {
        try {
            Charset charset = Charset.forName(charsetEncode);
            CharsetDecoder decoder = charset.newDecoder().onMalformedInput(CodingErrorAction.REPORT).onUnmappableCharacter(CodingErrorAction.REPORT);
            decoder.decode(ByteBuffer.wrap(values));
        } catch (Exception e) {
            return false;
        }
        return true;
    }

    /**
     * iso Charset may need to be replaced with the correct Charset
     *
     * @param clientCharset
     * @param value
     * @return
     */
    public static String isoCharsetReplace(String clientCharset, String value, boolean charsetReplace) {
        if (!charsetReplace) {
            return value;
        }
        try {
            byte[] isoBytes = value.getBytes(ISO_8859_1);
            String isoValues = new String(isoBytes, ISO_8859_1);
            String clientValues = new String(value.getBytes(clientCharset), clientCharset);
            if (!equals(isoValues, clientValues) && equals(value, clientValues)) {
                return value;
            }
            if (charsetParseString(clientCharset, value.getBytes(clientCharset))) {
                value = new String(isoBytes, clientCharset);
            }
        } catch (Exception e) {
            return value;
        }
        return value;
    }

    public static boolean byteEqual(byte[] a, byte[] b, int i) {
        if (Objects.isNull(a) || Objects.isNull(b)) {
            return false;
        }
        int length = a.length - i;
        if (length != b.length || length <= 0) {
            return false;
        }
        for (int len = 0; len < length; len++) {
            if (a[len + i] != b[len]) {
                return false;
            }
        }
        return true;
    }

    public static boolean isDoubleOrFloat(String str) {
        Pattern pattern = Pattern.compile("^[-\\+]?[.\\d]*$");
        return pattern.matcher(str).matches();
    }
    /**
     * <p>
     * src: https://stackoverflow.com/questions/26357938/detect-chinese-character-in-java/26357985
     * Now Character.isIdeographic(int codepoint) would tell wether the codepoint is a C (Chinese) ideograph.
     * Nearer is using Character.UnicodeScript.HAN.
     *
     * @param val
     * @return
     */
    public static boolean isChinese(String val) {
        return val.codePoints().allMatch(codepoint -> Character.UnicodeScript.of(codepoint) == Character.UnicodeScript.HAN);
    }

}
