/*
 * Copyright 1999-2018 Alibaba Group Holding Ltd.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.alibaba.druid.util;

import com.alibaba.druid.support.logging.Log;
import com.alibaba.druid.support.logging.LogFactory;

import java.io.*;
import java.lang.management.ManagementFactory;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.text.SimpleDateFormat;
import java.util.*;
import java.util.function.Consumer;

public class Utils {
    public static final int DEFAULT_BUFFER_SIZE = 1024 * 4;
    private static final Log LOG = LogFactory.getLog(DruidDataSourceUtils.class);

    public static String read(InputStream in) {
        if (in == null) {
            return null;
        }

        InputStreamReader reader = new InputStreamReader(in, StandardCharsets.UTF_8);
        return read(reader);
    }

    public static Properties loadProperties(String resource) {
        InputStream in = Utils.class.getClassLoader().getResourceAsStream(resource);
        Properties properties = new Properties();
        if (in != null) {
            try {
                properties.load(in);
             } catch (IOException ignore) {
                // ignored
            }
        }
        return properties;
    }

    public static List<String> readLines(String resource) {
        List<String> lines = new ArrayList<>();
        InputStream in = Utils.class.getClassLoader().getResourceAsStream(resource);
        if (in != null) {
            try (BufferedReader reader = new BufferedReader(new InputStreamReader(in))) {
                for (;;) {
                    String line = reader.readLine();
                    if (line == null) {
                        break;
                    }
                    lines.add(line);
                }
            } catch (IOException ignore) {
                // ignored
            }
        }
        return lines;
    }

    public static String readFromResource(String resource) throws IOException {
        if (resource == null
                || resource.isEmpty()
                || resource.contains("..")
                || resource.contains("?")
                || resource.contains(":")) {
            return null;
        }

        InputStream in = null;
        try {
            in = Thread.currentThread().getContextClassLoader().getResourceAsStream(resource);
            if (in == null) {
                in = Utils.class.getResourceAsStream(resource);
            }

            if (in == null) {
                return null;
            }

            String text = Utils.read(in);
            return text;
        } finally {
            JdbcUtils.close(in);
        }
    }

    public static byte[] readByteArrayFromResource(String resource) throws IOException {
        if (resource == null
                || resource.isEmpty()
                || resource.contains("..")
                || resource.contains("?")
                || resource.contains(":")) {
            return null;
        }

        InputStream in = null;
        try {
            in = Thread.currentThread().getContextClassLoader().getResourceAsStream(resource);
            if (in == null) {
                return null;
            }

            return readByteArray(in);
        } finally {
            JdbcUtils.close(in);
        }
    }

    public static byte[] readByteArray(InputStream input) throws IOException {
        if (input == null) {
            return null;
        }
        ByteArrayOutputStream output = new ByteArrayOutputStream();
        copy(input, output);
        byte[] bytes = output.toByteArray();
        output.close();
        return bytes;
    }

    public static long copy(InputStream input, OutputStream output) throws IOException {
        final int EOF = -1;

        byte[] buffer = new byte[DEFAULT_BUFFER_SIZE];

        long count = 0;
        int n = 0;
        while (EOF != (n = input.read(buffer))) {
            output.write(buffer, 0, n);
            count += n;
        }
        return count;
    }

    public static String read(Reader reader) {
        if (reader == null) {
            return null;
        }

        try {
            StringWriter writer = new StringWriter();

            char[] buffer = new char[DEFAULT_BUFFER_SIZE];
            int n = 0;
            while (-1 != (n = reader.read(buffer))) {
                writer.write(buffer, 0, n);
            }

            return writer.toString();
        } catch (IOException ex) {
            throw new IllegalStateException("read error", ex);
        }
    }

    public static String read(Reader reader, int length) {
        if (reader == null) {
            return null;
        }

        try {
            char[] buffer = new char[length];

            int offset = 0;
            int rest = length;
            int len;
            while ((len = reader.read(buffer, offset, rest)) != -1) {
                rest -= len;
                offset += len;

                if (rest == 0) {
                    break;
                }
            }

            return new String(buffer, 0, length - rest);
        } catch (IOException ex) {
            throw new IllegalStateException("read error", ex);
        }
    }

    public static String toString(java.util.Date date) {
        if (date == null) {
            return null;
        }
        return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss")
                .format(date);
    }

    public static String getStackTrace(Throwable ex) {
        StringWriter buf = new StringWriter();
        ex.printStackTrace(new PrintWriter(buf));
        return buf.toString();
    }

    public static String toString(StackTraceElement[] stackTrace) {
        StringBuilder buf = new StringBuilder();
        for (StackTraceElement item : stackTrace) {
            buf.append(item.toString());
            buf.append("\n");
        }
        return buf.toString();
    }

    static void trySetIntProperty(Properties properties, String key, Consumer<Integer> setter) {
        if (properties == null) {
            LOG.error("propertiesisnull");
            return;
        }
        if (key == null || setter == null) {
            LOG.error("keyisnullOrSetterisnull,|key=" + key + ",setter=" + setter);
            return;
        }
        String property = properties.getProperty(key);
        if (property != null && property.length() > 0) {
            try {
                int value = Integer.parseInt(property);
                setter.accept(value);
            } catch (NumberFormatException e) {
                LOG.error("illegal property '" + key + "' = " + property, e);
            }
        }
    }

    static void trySetLongProperty(Properties properties, String key, Consumer<Long> setter) {
        if (properties == null) {
            LOG.error("propertiesisnull");
            return;
        }
        if (key == null || setter == null) {
            LOG.error("keyisnullOrSetterisnull,|key=" + key + ",setter=" + setter);
            return;
        }
        String property = properties.getProperty(key);
        if (property != null && property.length() > 0) {
            try {
                long value = Long.parseLong(property);
                setter.accept(value);
            } catch (NumberFormatException e) {
                LOG.error("illegal property '" + key + "' = " + property, e);
            }
        }
    }
    static void trySetStringProperty(Properties properties, String key, Consumer<String> setter) {
        if (properties == null) {
            LOG.error("propertiesisnull");
            return;
        }
        if (key == null || setter == null) {
            LOG.error("keyisnullOrSetterisnull,|key=" + key + ",setter=" + setter);
            return;
        }
        String value = properties.getProperty(key);
        if (value != null) {
            setter.accept(value);
        }
    }
    static void trySetBooleanProperty(Properties properties, String key, Consumer<Boolean> setter) {
        if (properties == null) {
            LOG.error("propertiesisnull");
            return;
        }
        if (key == null || setter == null) {
            LOG.error("keyisnullOrSetterisnull,|key=" + key + ",setter=" + setter);
            return;
        }
        Boolean value = getBoolean(properties, key);
        if (value != null) {
            setter.accept(value);
        }
    }
    public static Boolean getBoolean(Properties properties, String key) {
        String property = properties.getProperty(key);
        if ("true".equals(property)) {
            return Boolean.TRUE;
        } else if ("false".equals(property)) {
            return Boolean.FALSE;
        }
        return null;
    }

    public static Integer getInteger(Properties properties, String key) {
        String property = properties.getProperty(key);

        if (property == null) {
            return null;
        }
        try {
            return Integer.parseInt(property);
        } catch (NumberFormatException ex) {
            // skip
        }
        return null;
    }

    public static Long getLong(Properties properties, String key) {
        String property = properties.getProperty(key);

        if (property == null) {
            return null;
        }
        try {
            return Long.parseLong(property);
        } catch (NumberFormatException ex) {
            // skip
        }
        return null;
    }

    public static Class<?> loadClass(String className) {
        Class<?> clazz = null;

        if (className == null) {
            return null;
        }

        try {
            return Class.forName(className);
        } catch (ClassNotFoundException e) {
            // skip
        }

        ClassLoader ctxClassLoader = Thread.currentThread().getContextClassLoader();
        if (ctxClassLoader != null) {
            try {
                clazz = ctxClassLoader.loadClass(className);
            } catch (ClassNotFoundException e) {
                // skip
            }
        }

        return clazz;
    }

    private static Date startTime;

    public static final Date getStartTime() {
        if (startTime == null) {
            startTime = new Date(ManagementFactory.getRuntimeMXBean().getStartTime());
        }
        return startTime;
    }

    public static long murmurhash2_64(String text) {
        final byte[] bytes = text.getBytes();
        return murmurhash2_64(bytes, bytes.length, 0xe17a1465);
    }

    /**
     * murmur hash 2.0, The murmur hash is a relatively fast hash function from http://murmurhash.googlepages.com/ for
     * platforms with efficient multiplication.
     *
     * @author Viliam Holub
     */
    public static long murmurhash2_64(final byte[] data, int length, int seed) {
        final long m = 0xc6a4a7935bd1e995L;
        final int r = 47;

        long h = (seed & 0xffffffffL) ^ (length * m);

        int length8 = length / 8;

        for (int i = 0; i < length8; i++) {
            final int i8 = i * 8;
            long k = ((long) data[i8 + 0] & 0xff) //
                    + (((long) data[i8 + 1] & 0xff) << 8) //
                    + (((long) data[i8 + 2] & 0xff) << 16)//
                    + (((long) data[i8 + 3] & 0xff) << 24) //
                    + (((long) data[i8 + 4] & 0xff) << 32)//
                    + (((long) data[i8 + 5] & 0xff) << 40)//
                    + (((long) data[i8 + 6] & 0xff) << 48) //
                    + (((long) data[i8 + 7] & 0xff) << 56);

            k *= m;
            k ^= k >>> r;
            k *= m;

            h ^= k;
            h *= m;
        }

        switch (length % 8) {
            case 7:
                h ^= (long) (data[(length & ~7) + 6] & 0xff) << 48;
            case 6:
                h ^= (long) (data[(length & ~7) + 5] & 0xff) << 40;
            case 5:
                h ^= (long) (data[(length & ~7) + 4] & 0xff) << 32;
            case 4:
                h ^= (long) (data[(length & ~7) + 3] & 0xff) << 24;
            case 3:
                h ^= (long) (data[(length & ~7) + 2] & 0xff) << 16;
            case 2:
                h ^= (long) (data[(length & ~7) + 1] & 0xff) << 8;
            case 1:
                h ^= (long) (data[length & ~7] & 0xff);
                h *= m;
        }

        h ^= h >>> r;
        h *= m;
        h ^= h >>> r;

        return h;
    }

    public static byte[] md5Bytes(String text) {
        MessageDigest msgDigest = null;

        try {
            msgDigest = MessageDigest.getInstance("MD5");
        } catch (NoSuchAlgorithmException e) {
            throw new IllegalStateException("System doesn't support MD5 algorithm.");
        }

        msgDigest.update(text.getBytes());

        byte[] bytes = msgDigest.digest();

        return bytes;
    }

    public static String md5(String text) {
        byte[] bytes = md5Bytes(text);
        return HexBin.encode(bytes, false);
    }

    public static void putLong(byte[] b, int off, long val) {
        b[off + 7] = (byte) (val >>> 0);
        b[off + 6] = (byte) (val >>> 8);
        b[off + 5] = (byte) (val >>> 16);
        b[off + 4] = (byte) (val >>> 24);
        b[off + 3] = (byte) (val >>> 32);
        b[off + 2] = (byte) (val >>> 40);
        b[off + 1] = (byte) (val >>> 48);
        b[off + 0] = (byte) (val >>> 56);
    }

    public static boolean equals(Object a, Object b) {
        return (a == b) || (a != null && a.equals(b));
    }

    public static String hex(int hash) {
        byte[] bytes = new byte[4];

        bytes[3] = (byte) (hash);
        bytes[2] = (byte) (hash >>> 8);
        bytes[1] = (byte) (hash >>> 16);
        bytes[0] = (byte) (hash >>> 24);

        char[] chars = new char[8];
        for (int i = 0; i < 4; ++i) {
            byte b = bytes[i];

            int a = b & 0xFF;
            int b0 = a >> 4;
            int b1 = a & 0xf;

            chars[i * 2] = (char) (b0 + (b0 < 10 ? 48 : 55));
            chars[i * 2 + 1] = (char) (b1 + (b1 < 10 ? 48 : 55));
        }

        return new String(chars);
    }

    public static String hex(long hash) {
        byte[] bytes = new byte[8];

        bytes[7] = (byte) (hash);
        bytes[6] = (byte) (hash >>> 8);
        bytes[5] = (byte) (hash >>> 16);
        bytes[4] = (byte) (hash >>> 24);
        bytes[3] = (byte) (hash >>> 32);
        bytes[2] = (byte) (hash >>> 40);
        bytes[1] = (byte) (hash >>> 48);
        bytes[0] = (byte) (hash >>> 56);

        char[] chars = new char[16];
        for (int i = 0; i < 8; ++i) {
            byte b = bytes[i];

            int a = b & 0xFF;
            int b0 = a >> 4;
            int b1 = a & 0xf;

            chars[i * 2] = (char) (b0 + (b0 < 10 ? 48 : 55));
            chars[i * 2 + 1] = (char) (b1 + (b1 < 10 ? 48 : 55));
        }

        return new String(chars);
    }

    public static String hex_t(long hash) {
        byte[] bytes = new byte[8];

        bytes[7] = (byte) (hash);
        bytes[6] = (byte) (hash >>> 8);
        bytes[5] = (byte) (hash >>> 16);
        bytes[4] = (byte) (hash >>> 24);
        bytes[3] = (byte) (hash >>> 32);
        bytes[2] = (byte) (hash >>> 40);
        bytes[1] = (byte) (hash >>> 48);
        bytes[0] = (byte) (hash >>> 56);

        char[] chars = new char[18];
        chars[0] = 'T';
        chars[1] = '_';
        for (int i = 0; i < 8; ++i) {
            byte b = bytes[i];

            int a = b & 0xFF;
            int b0 = a >> 4;
            int b1 = a & 0xf;

            chars[i * 2 + 2] = (char) (b0 + (b0 < 10 ? 48 : 55));
            chars[i * 2 + 3] = (char) (b1 + (b1 < 10 ? 48 : 55));
        }

        return new String(chars);
    }

    public static long fnv_64(String input) {
        return FnvHash.fnv1a_64(input);
    }

    public static long fnv_64_lower(String key) {
        return FnvHash.fnv1a_64_lower(key);
    }

    public static long fnv_32_lower(String key) {
        return FnvHash.fnv_32_lower(key);
    }

    public static void loadFromFile(String path, Set<String> set) {
        InputStream is = null;
        BufferedReader reader = null;
        try {
            is = Thread.currentThread().getContextClassLoader().getResourceAsStream(path);
            reader = new BufferedReader(new InputStreamReader(is));
            for (; ; ) {
                String line = reader.readLine();
                if (line == null) {
                    break;
                }

                line = line.trim().toLowerCase();

                if (line.length() == 0) {
                    continue;
                }
                set.add(line);
            }
        } catch (Exception ex) {
            // skip
        } finally {
            JdbcUtils.close(is);
            JdbcUtils.close(reader);
        }
    }

    /**
     * Returns a default value if the object passed is {@code null}.
     */
    public static <T> T getIfNull(final T object, final T defaultValue) {
        return object != null ? object : defaultValue;
    }

}
