/*
 * Copyright 2004-2022 H2 Group. Multiple-Licensed under the MPL 2.0,
 * and the EPL 1.0 (https://h2database.com/html/license.html).
 * Initial Developer: H2 Group
 */
package org.h2.mvstore;

import java.io.EOFException;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.charset.StandardCharsets;
import java.text.MessageFormat;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;

import org.h2.engine.Constants;
import org.h2.jdbc.JdbcException;
import org.h2.util.StringUtils;

/**
 * Utility methods
 */
public final class DataUtils {

    /**
     * An error occurred while reading from the file.
     */
    public static final int ERROR_READING_FAILED = 1;

    /**
     * An error occurred when trying to write to the file.
     */
    public static final int ERROR_WRITING_FAILED = 2;

    /**
     * An internal error occurred. This could be a bug, or a memory corruption
     * (for example caused by out of memory).
     */
    public static final int ERROR_INTERNAL = 3;

    /**
     * The object is already closed.
     */
    public static final int ERROR_CLOSED = 4;

    /**
     * The file format is not supported.
     */
    public static final int ERROR_UNSUPPORTED_FORMAT = 5;

    /**
     * The file is corrupt or (for encrypted files) the encryption key is wrong.
     */
    public static final int ERROR_FILE_CORRUPT = 6;

    /**
     * The file is locked.
     */
    public static final int ERROR_FILE_LOCKED = 7;

    /**
     * An error occurred when serializing or de-serializing.
     */
    public static final int ERROR_SERIALIZATION = 8;

    /**
     * The application was trying to read data from a chunk that is no longer
     * available.
     */
    public static final int ERROR_CHUNK_NOT_FOUND = 9;

    /**
     * The block in the stream store was not found.
     */
    public static final int ERROR_BLOCK_NOT_FOUND = 50;

    /**
     * The transaction store is corrupt.
     */
    public static final int ERROR_TRANSACTION_CORRUPT = 100;

    /**
     * An entry is still locked by another transaction.
     */
    public static final int ERROR_TRANSACTION_LOCKED = 101;

    /**
     * There are too many open transactions.
     */
    public static final int ERROR_TOO_MANY_OPEN_TRANSACTIONS = 102;

    /**
     * The transaction store is in an illegal state (for example, not yet
     * initialized).
     */
    public static final int ERROR_TRANSACTION_ILLEGAL_STATE = 103;

    /**
     * The transaction contains too many changes.
     */
    public static final int ERROR_TRANSACTION_TOO_BIG = 104;

    /**
     * Deadlock discovered and one of transactions involved chosen as victim and rolled back.
     */
    public static final int ERROR_TRANSACTIONS_DEADLOCK = 105;

    /**
     * The transaction store can not be initialized because data type
     * is not found in type registry.
     */
    public static final int ERROR_UNKNOWN_DATA_TYPE = 106;

    /**
     * The type for leaf page.
     */
    public static final int PAGE_TYPE_LEAF = 0;

    /**
     * The type for node page.
     */
    public static final int PAGE_TYPE_NODE = 1;

    /**
     * The bit mask for compressed pages (compression level fast).
     */
    public static final int PAGE_COMPRESSED = 2;

    /**
     * The bit mask for compressed pages (compression level high).
     */
    public static final int PAGE_COMPRESSED_HIGH = 2 + 4;

    /**
     * The bit mask for pages with page sequential number.
     */
    public static final int PAGE_HAS_PAGE_NO = 8;

    /**
     * The maximum length of a variable size int.
     */
    public static final int MAX_VAR_INT_LEN = 5;

    /**
     * The maximum length of a variable size long.
     */
    public static final int MAX_VAR_LONG_LEN = 10;

    /**
     * The maximum integer that needs less space when using variable size
     * encoding (only 3 bytes instead of 4).
     */
    public static final int COMPRESSED_VAR_INT_MAX = 0x1fffff;

    /**
     * The maximum long that needs less space when using variable size
     * encoding (only 7 bytes instead of 8).
     */
    public static final long COMPRESSED_VAR_LONG_MAX = 0x1ffffffffffffL;

    /**
     * The marker size of a very large page.
     */
    public static final int PAGE_LARGE = 2 * 1024 * 1024;

    // The following are key prefixes used in layout map

    /**
     * The prefix for chunks ("chunk."). This, plus the chunk id (hex encoded)
     * is the key, and the serialized chunk metadata is the value.
     */
    public static final String META_CHUNK = "chunk.";

    /**
     * The prefix for root positions of maps ("root."). This, plus the map id
     * (hex encoded) is the key, and the position (hex encoded) is the value.
     */
    public static final String META_ROOT = "root.";

    // The following are key prefixes used in meta map

    /**
     * The prefix for names ("name."). This, plus the name of the map, is the
     * key, and the map id (hex encoded) is the value.
     */
    public static final String META_NAME = "name.";

    /**
     * The prefix for maps ("map."). This, plus the map id (hex encoded) is the
     * key, and the serialized in the map metadata is the value.
     */
    public static final String META_MAP = "map.";

    /**
     * Get the length of the variable size int.
     *
     * @param x the value
     * @return the length in bytes
     */
    public static int getVarIntLen(int x) {
        if ((x & (-1 << 7)) == 0) {
            return 1;
        } else if ((x & (-1 << 14)) == 0) {
            return 2;
        } else if ((x & (-1 << 21)) == 0) {
            return 3;
        } else if ((x & (-1 << 28)) == 0) {
            return 4;
        }
        return 5;
    }

    /**
     * Get the length of the variable size long.
     *
     * @param x the value
     * @return the length in bytes
     */
    public static int getVarLongLen(long x) {
        int i = 1;
        while (true) {
            x >>>= 7;
            if (x == 0) {
                return i;
            }
            i++;
        }
    }

    /**
     * Read a variable size int.
     *
     * @param buff the source buffer
     * @return the value
     */
    public static int readVarInt(ByteBuffer buff) {
        int b = buff.get();
        if (b >= 0) {
            return b;
        }
        // a separate function so that this one can be inlined
        return readVarIntRest(buff, b);
    }

    private static int readVarIntRest(ByteBuffer buff, int b) {
        int x = b & 0x7f;
        b = buff.get();
        if (b >= 0) {
            return x | (b << 7);
        }
        x |= (b & 0x7f) << 7;
        b = buff.get();
        if (b >= 0) {
            return x | (b << 14);
        }
        x |= (b & 0x7f) << 14;
        b = buff.get();
        if (b >= 0) {
            return x | b << 21;
        }
        x |= ((b & 0x7f) << 21) | (buff.get() << 28);
        return x;
    }

    /**
     * Read a variable size long.
     *
     * @param buff the source buffer
     * @return the value
     */
    public static long readVarLong(ByteBuffer buff) {
        long x = buff.get();
        if (x >= 0) {
            return x;
        }
        x &= 0x7f;
        for (int s = 7; s < 64; s += 7) {
            long b = buff.get();
            x |= (b & 0x7f) << s;
            if (b >= 0) {
                break;
            }
        }
        return x;
    }

    /**
     * Write a variable size int.
     *
     * @param out the output stream
     * @param x the value
     * @throws IOException if some data could not be written
     */
    public static void writeVarInt(OutputStream out, int x) throws IOException {
        while ((x & ~0x7f) != 0) {
            out.write((byte) (x | 0x80));
            x >>>= 7;
        }
        out.write((byte) x);
    }

    /**
     * Write a variable size int.
     *
     * @param buff the source buffer
     * @param x the value
     */
    public static void writeVarInt(ByteBuffer buff, int x) {
        while ((x & ~0x7f) != 0) {
            buff.put((byte) (x | 0x80));
            x >>>= 7;
        }
        buff.put((byte) x);
    }

    /**
     * Write characters from a string (without the length).
     *
     * @param buff the target buffer (must be large enough)
     * @param s the string
     * @param len the number of characters
     */
    public static void writeStringData(ByteBuffer buff,
            String s, int len) {
        for (int i = 0; i < len; i++) {
            int c = s.charAt(i);
            if (c < 0x80) {
                buff.put((byte) c);
            } else if (c >= 0x800) {
                buff.put((byte) (0xe0 | (c >> 12)));
                buff.put((byte) ((c >> 6) & 0x3f));
                buff.put((byte) (c & 0x3f));
            } else {
                buff.put((byte) (0xc0 | (c >> 6)));
                buff.put((byte) (c & 0x3f));
            }
        }
    }

    /**
     * Read a string.
     *
     * @param buff the source buffer
     * @return the value
     */
    public static String readString(ByteBuffer buff) {
        return readString(buff, readVarInt(buff));
    }

    /**
     * Read a string.
     *
     * @param buff the source buffer
     * @param len the number of characters
     * @return the value
     */
    public static String readString(ByteBuffer buff, int len) {
        char[] chars = new char[len];
        for (int i = 0; i < len; i++) {
            int x = buff.get() & 0xff;
            if (x < 0x80) {
                chars[i] = (char) x;
            } else if (x >= 0xe0) {
                chars[i] = (char) (((x & 0xf) << 12)
                        + ((buff.get() & 0x3f) << 6) + (buff.get() & 0x3f));
            } else {
                chars[i] = (char) (((x & 0x1f) << 6) + (buff.get() & 0x3f));
            }
        }
        return new String(chars);
    }

    /**
     * Write a variable size long.
     *
     * @param buff the target buffer
     * @param x the value
     */
    public static void writeVarLong(ByteBuffer buff, long x) {
        while ((x & ~0x7f) != 0) {
            buff.put((byte) (x | 0x80));
            x >>>= 7;
        }
        buff.put((byte) x);
    }

    /**
     * Write a variable size long.
     *
     * @param out the output stream
     * @param x the value
     * @throws IOException if some data could not be written
     */
    public static void writeVarLong(OutputStream out, long x)
            throws IOException {
        while ((x & ~0x7f) != 0) {
            out.write((byte) (x | 0x80));
            x >>>= 7;
        }
        out.write((byte) x);
    }

    /**
     * Copy the elements of an array, with a gap.
     *
     * @param src the source array
     * @param dst the target array
     * @param oldSize the size of the old array
     * @param gapIndex the index of the gap
     */
    public static void copyWithGap(Object src, Object dst, int oldSize,
            int gapIndex) {
        if (gapIndex > 0) {
            System.arraycopy(src, 0, dst, 0, gapIndex);
        }
        if (gapIndex < oldSize) {
            System.arraycopy(src, gapIndex, dst, gapIndex + 1, oldSize
                    - gapIndex);
        }
    }

    /**
     * Copy the elements of an array, and remove one element.
     *
     * @param src the source array
     * @param dst the target array
     * @param oldSize the size of the old array
     * @param removeIndex the index of the entry to remove
     */
    public static void copyExcept(Object src, Object dst, int oldSize,
            int removeIndex) {
        if (removeIndex > 0 && oldSize > 0) {
            System.arraycopy(src, 0, dst, 0, removeIndex);
        }
        if (removeIndex < oldSize) {
            System.arraycopy(src, removeIndex + 1, dst, removeIndex, oldSize
                    - removeIndex - 1);
        }
    }

    /**
     * Read from a file channel until the buffer is full.
     * The buffer is rewind after reading.
     *
     * @param file the file channel
     * @param pos the absolute position within the file
     * @param dst the byte buffer
     * @throws MVStoreException if some data could not be read
     */
    public static void readFully(FileChannel file, long pos, ByteBuffer dst) {
        try {
            do {
                int len = file.read(dst, pos);
                if (len < 0) {
                    throw new EOFException();
                }
                pos += len;
            } while (dst.remaining() > 0);
            dst.rewind();
        } catch (IOException e) {
            long size;
            try {
                size = file.size();
            } catch (IOException e2) {
                size = -1;
            }
            throw newMVStoreException(
                    ERROR_READING_FAILED,
                    "Reading from file {0} failed at {1} (length {2}), " +
                    "read {3}, remaining {4}",
                    file, pos, size, dst.position(), dst.remaining(), e);
        }
    }

    /**
     * Write to a file channel.
     *
     * @param file the file channel
     * @param pos the absolute position within the file
     * @param src the source buffer
     */
    public static void writeFully(FileChannel file, long pos, ByteBuffer src) {
        try {
            int off = 0;
            do {
                int len = file.write(src, pos + off);
                off += len;
            } while (src.remaining() > 0);
        } catch (IOException e) {
            throw newMVStoreException(
                    ERROR_WRITING_FAILED,
                    "Writing to {0} failed; length {1} at {2}",
                    file, src.remaining(), pos, e);
        }
    }

    /**
     * Convert the length to a length code 0..31. 31 means more than 1 MB.
     *
     * @param len the length
     * @return the length code
     */
    public static int encodeLength(int len) {
        if (len <= 32) {
            return 0;
        }
        int code = Integer.numberOfLeadingZeros(len);
        int remaining = len << (code + 1);
        code += code;
        if ((remaining & (1 << 31)) != 0) {
            code--;
        }
        if ((remaining << 1) != 0) {
            code--;
        }
        code = Math.min(31, 52 - code);
        // alternative code (slower):
        // int x = len;
        // int shift = 0;
        // while (x > 3) {
        //    shift++;
        //    x = (x >>> 1) + (x & 1);
        // }
        // shift = Math.max(0,  shift - 4);
        // int code = (shift << 1) + (x & 1);
        // code = Math.min(31, code);
        return code;
    }

    /**
     * Get the chunk id from the position.
     *
     * @param pos the position
     * @return the chunk id
     */
    public static int getPageChunkId(long pos) {
        return (int) (pos >>> 38);
    }

    /**
     * Get the map id from the chunk's table of content element.
     *
     * @param tocElement packed table of content element
     * @return the map id
     */
    public static int getPageMapId(long tocElement) {
        return (int) (tocElement >>> 38);
    }

    /**
     * Get the maximum length for the given page position.
     *
     * @param pos the position
     * @return the maximum length
     */
    public static int getPageMaxLength(long pos) {
        int code = (int) ((pos >> 1) & 31);
        return decodePageLength(code);
    }

    /**
     * Get the maximum length for the given code.
     * For the code 31, PAGE_LARGE is returned.
     *
     * @param code encoded page length
     * @return the maximum length
     */
    public static int decodePageLength(int code) {
        if (code == 31) {
            return PAGE_LARGE;
        }
        return (2 + (code & 1)) << ((code >> 1) + 4);
    }

    /**
     * Get the offset from the position.
     *
     * @param tocElement packed table of content element
     * @return the offset
     */
    public static int getPageOffset(long tocElement) {
        return (int) (tocElement >> 6);
    }

    /**
     * Get the page type from the position.
     *
     * @param pos the position
     * @return the page type (PAGE_TYPE_NODE or PAGE_TYPE_LEAF)
     */
    public static int getPageType(long pos) {
        return ((int) pos) & 1;
    }

    /**
     * Determines whether specified file position corresponds to a leaf page
     * @param pos the position
     * @return true if it is a leaf, false otherwise
     */
    public static boolean isLeafPosition(long pos) {
        return getPageType(pos) == PAGE_TYPE_LEAF;
    }

    /**
     * Find out if page was saved.
     *
     * @param pos the position
     * @return true if page has been saved
     */
    public static boolean isPageSaved(long pos) {
        return (pos & ~1L) != 0;
    }

    /**
     * Find out if page was removed.
     *
     * @param pos the position
     * @return true if page has been removed (no longer accessible from the
     *         current root of the tree)
     */
    static boolean isPageRemoved(long pos) {
        return pos == 1L;
    }

    /**
     * Get the position of this page. The following information is encoded in
     * the position: the chunk id, the page sequential number, the maximum length, and the type
     * (node or leaf).
     *
     * @param chunkId the chunk id
     * @param offset the offset
     * @param length the length
     * @param type the page type (1 for node, 0 for leaf)
     * @return the position
     */
    public static long getPagePos(int chunkId, int offset, int length, int type) {
        long pos = (long) chunkId << 38;
        pos |= (long) offset << 6;
        pos |= encodeLength(length) << 1;
        pos |= type;
        return pos;
    }

    /**
     * Convert tocElement into pagePos by replacing mapId with chunkId.
     *
     * @param chunkId the chunk id
     * @param tocElement the element
     * @return the page position
     */
    public static long getPagePos(int chunkId, long tocElement) {
        return (tocElement & 0x3FFFFFFFFFL) | ((long) chunkId << 38);
    }

    /**
     * Create table of content element. The following information is encoded in it:
     * the map id, the page offset, the maximum length, and the type
     * (node or leaf).
     *
     * @param mapId the chunk id
     * @param offset the offset
     * @param length the length
     * @param type the page type (1 for node, 0 for leaf)
     * @return the position
     */
    public static long getTocElement(int mapId, int offset, int length, int type) {
        long pos = (long) mapId << 38;
        pos |= (long) offset << 6;
        pos |= encodeLength(length) << 1;
        pos |= type;
        return pos;
    }

    /**
     * Calculate a check value for the given integer. A check value is mean to
     * verify the data is consistent with a high probability, but not meant to
     * protect against media failure or deliberate changes.
     *
     * @param x the value
     * @return the check value
     */
    public static short getCheckValue(int x) {
        return (short) ((x >> 16) ^ x);
    }

    /**
     * Append a map to the string builder, sorted by key.
     *
     * @param buff the target buffer
     * @param map the map
     * @return the string builder
     */
    public static StringBuilder appendMap(StringBuilder buff, HashMap<String, ?> map) {
        Object[] keys = map.keySet().toArray();
        Arrays.sort(keys);
        for (Object k : keys) {
            String key = (String) k;
            Object value = map.get(key);
            if (value instanceof Long) {
                appendMap(buff, key, (long) value);
            } else if (value instanceof Integer) {
                appendMap(buff, key, (int) value);
            } else {
                appendMap(buff, key, value.toString());
            }
        }
        return buff;
    }

    private static StringBuilder appendMapKey(StringBuilder buff, String key) {
        if (buff.length() > 0) {
            buff.append(',');
        }
        return buff.append(key).append(':');
    }

    /**
     * Append a key-value pair to the string builder. Keys may not contain a
     * colon. Values that contain a comma or a double quote are enclosed in
     * double quotes, with special characters escaped using a backslash.
     *
     * @param buff the target buffer
     * @param key the key
     * @param value the value
     */
    public static void appendMap(StringBuilder buff, String key, String value) {
        appendMapKey(buff, key);
        if (value.indexOf(',') < 0 && value.indexOf('\"') < 0) {
            buff.append(value);
        } else {
            buff.append('\"');
            for (int i = 0, size = value.length(); i < size; i++) {
                char c = value.charAt(i);
                if (c == '\"') {
                    buff.append('\\');
                }
                buff.append(c);
            }
            buff.append('\"');
        }
    }

    /**
     * Append a key-value pair to the string builder. Keys may not contain a
     * colon.
     *
     * @param buff the target buffer
     * @param key the key
     * @param value the value
     */
    public static void appendMap(StringBuilder buff, String key, long value) {
        appendMapKey(buff, key).append(Long.toHexString(value));
    }

    /**
     * Append a key-value pair to the string builder. Keys may not contain a
     * colon.
     *
     * @param buff the target buffer
     * @param key the key
     * @param value the value
     */
    public static void appendMap(StringBuilder buff, String key, int value) {
        appendMapKey(buff, key).append(Integer.toHexString(value));
    }

    /**
     * @param buff output buffer, should be empty
     * @param s parsed string
     * @param i offset to parse from
     * @param size stop offset (exclusive)
     * @return new offset
     */
    private static int parseMapValue(StringBuilder buff, String s, int i, int size) {
        while (i < size) {
            char c = s.charAt(i++);
            if (c == ',') {
                break;
            } else if (c == '\"') {
                while (i < size) {
                    c = s.charAt(i++);
                    if (c == '\\') {
                        if (i == size) {
                            throw newMVStoreException(ERROR_FILE_CORRUPT, "Not a map: {0}", s);
                        }
                        c = s.charAt(i++);
                    } else if (c == '\"') {
                        break;
                    }
                    buff.append(c);
                }
            } else {
                buff.append(c);
            }
        }
        return i;
    }

    /**
     * Parse a key-value pair list.
     *
     * @param s the list
     * @return the map
     * @throws MVStoreException if parsing failed
     */
    public static HashMap<String, String> parseMap(String s) {
        HashMap<String, String> map = new HashMap<>();
        StringBuilder buff = new StringBuilder();
        for (int i = 0, size = s.length(); i < size;) {
            int startKey = i;
            i = s.indexOf(':', i);
            if (i < 0) {
                throw newMVStoreException(ERROR_FILE_CORRUPT, "Not a map: {0}", s);
            }
            String key = s.substring(startKey, i++);
            i = parseMapValue(buff, s, i, size);
            map.put(key, buff.toString());
            buff.setLength(0);
        }
        return map;
    }

    /**
     * Parse a key-value pair list and checks its checksum.
     *
     * @param bytes encoded map
     * @return the map without mapping for {@code "fletcher"}, or {@code null} if checksum is wrong
     *              or parameter do not represent a properly formatted map serialization
     */
    static HashMap<String, String> parseChecksummedMap(byte[] bytes) {
        int start = 0, end = bytes.length;
        while (start < end && bytes[start] <= ' ') {
            start++;
        }
        while (start < end && bytes[end - 1] <= ' ') {
            end--;
        }
        String s = new String(bytes, start, end - start, StandardCharsets.ISO_8859_1);
        HashMap<String, String> map = new HashMap<>();
        StringBuilder buff = new StringBuilder();
        for (int i = 0, size = s.length(); i < size;) {
            int startKey = i;
            i = s.indexOf(':', i);
            if (i < 0) {
                // Corrupted map
                return null;
            }
            if (i - startKey == 8 && s.regionMatches(startKey, "fletcher", 0, 8)) {
                parseMapValue(buff, s, i + 1, size);
                int check = (int) Long.parseLong(buff.toString(), 16);
                if (check == getFletcher32(bytes, start, startKey - 1)) {
                    return map;
                }
                // Corrupted map
                return null;
            }
            String key = s.substring(startKey, i++);
            i = parseMapValue(buff, s, i, size);
            map.put(key, buff.toString());
            buff.setLength(0);
        }
        // Corrupted map
        return null;
    }

    /**
     * Parse a name from key-value pair list.
     *
     * @param s the list
     * @return value of name item, or {@code null}
     * @throws MVStoreException if parsing failed
     */
    public static String getMapName(String s) {
        return getFromMap(s, "name");
    }

    /**
     * Parse a specified pair from key-value pair list.
     *
     * @param s the list
     * @param key the name of the key
     * @return value of the specified item, or {@code null}
     * @throws MVStoreException if parsing failed
     */
    public static String getFromMap(String s, String key) {
        int keyLength = key.length();
        for (int i = 0, size = s.length(); i < size;) {
            int startKey = i;
            i = s.indexOf(':', i);
            if (i < 0) {
                throw newMVStoreException(ERROR_FILE_CORRUPT, "Not a map: {0}", s);
            }
            if (i++ - startKey == keyLength && s.regionMatches(startKey, key, 0, keyLength)) {
                StringBuilder buff = new StringBuilder();
                parseMapValue(buff, s, i, size);
                return buff.toString();
            } else {
                while (i < size) {
                    char c = s.charAt(i++);
                    if (c == ',') {
                        break;
                    } else if (c == '\"') {
                        while (i < size) {
                            c = s.charAt(i++);
                            if (c == '\\') {
                                if (i++ == size) {
                                    throw newMVStoreException(ERROR_FILE_CORRUPT, "Not a map: {0}", s);
                                }
                            } else if (c == '\"') {
                                break;
                            }
                        }
                    }
                }
            }
        }
        return null;
    }

    /**
     * Calculate the Fletcher32 checksum.
     *
     * @param bytes the bytes
     * @param offset initial offset
     * @param length the message length (if odd, 0 is appended)
     * @return the checksum
     */
    public static int getFletcher32(byte[] bytes, int offset, int length) {
        int s1 = 0xffff, s2 = 0xffff;
        int i = offset, len = offset + (length & ~1);
        while (i < len) {
            // reduce after 360 words (each word is two bytes)
            for (int end = Math.min(i + 720, len); i < end;) {
                int x = ((bytes[i++] & 0xff) << 8) | (bytes[i++] & 0xff);
                s2 += s1 += x;
            }
            s1 = (s1 & 0xffff) + (s1 >>> 16);
            s2 = (s2 & 0xffff) + (s2 >>> 16);
        }
        if ((length & 1) != 0) {
            // odd length: append 0
            int x = (bytes[i] & 0xff) << 8;
            s2 += s1 += x;
        }
        s1 = (s1 & 0xffff) + (s1 >>> 16);
        s2 = (s2 & 0xffff) + (s2 >>> 16);
        return (s2 << 16) | s1;
    }

    /**
     * Throw an IllegalArgumentException if the argument is invalid.
     *
     * @param test true if the argument is valid
     * @param message the message
     * @param arguments the arguments
     * @throws IllegalArgumentException if the argument is invalid
     */
    public static void checkArgument(boolean test, String message,
            Object... arguments) {
        if (!test) {
            throw newIllegalArgumentException(message, arguments);
        }
    }

    /**
     * Create a new IllegalArgumentException.
     *
     * @param message the message
     * @param arguments the arguments
     * @return the exception
     */
    public static IllegalArgumentException newIllegalArgumentException(
            String message, Object... arguments) {
        return initCause(new IllegalArgumentException(
                formatMessage(0, message, arguments)),
                arguments);
    }

    /**
     * Create a new UnsupportedOperationException.
     *
     * @param message the message
     * @return the exception
     */
    public static UnsupportedOperationException
            newUnsupportedOperationException(String message) {
        return new UnsupportedOperationException(formatMessage(0, message));
    }

    /**
     * Create a new MVStoreException.
     *
     * @param errorCode the error code
     * @param message the message
     * @param arguments the arguments
     * @return the exception
     */
    public static MVStoreException newMVStoreException(
            int errorCode, String message, Object... arguments) {
        return initCause(new MVStoreException(errorCode,
                formatMessage(errorCode, message, arguments)),
                arguments);
    }

    private static <T extends Exception> T initCause(T e, Object... arguments) {
        int size = arguments.length;
        if (size > 0) {
            Object o = arguments[size - 1];
            if (o instanceof Throwable) {
                e.initCause((Throwable) o);
            }
        }
        return e;
    }

    /**
     * Format an error message.
     *
     * @param errorCode the error code
     * @param message the message
     * @param arguments the arguments
     * @return the formatted message
     */
    public static String formatMessage(int errorCode, String message,
            Object... arguments) {
        // convert arguments to strings, to avoid locale specific formatting
        arguments = arguments.clone();
        for (int i = 0; i < arguments.length; i++) {
            Object a = arguments[i];
            if (!(a instanceof Exception)) {
                String s = a == null ? "null" : a.toString();
                if (s.length() > 1000) {
                    s = s.substring(0, 1000) + "...";
                }
                arguments[i] = s;
            }
        }
        return MessageFormat.format(message, arguments) +
                " [" + Constants.VERSION_MAJOR + "." +
                Constants.VERSION_MINOR + "." + Constants.BUILD_ID +
                "/" + errorCode + "]";
    }

    /**
     * Read a hex long value from a map.
     *
     * @param map the map
     * @param key the key
     * @param defaultValue if the value is null
     * @return the parsed value
     * @throws MVStoreException if parsing fails
     */
    public static long readHexLong(Map<String, ?> map, String key, long defaultValue) {
        Object v = map.get(key);
        if (v == null) {
            return defaultValue;
        } else if (v instanceof Long) {
            return (Long) v;
        }
        try {
            return parseHexLong((String) v);
        } catch (NumberFormatException e) {
            throw newMVStoreException(ERROR_FILE_CORRUPT,
                    "Error parsing the value {0}", v, e);
        }
    }

    /**
     * Parse an unsigned, hex long.
     *
     * @param x the string
     * @return the parsed value
     * @throws MVStoreException if parsing fails
     */
    public static long parseHexLong(String x) {
        try {
            if (x.length() == 16) {
                // avoid problems with overflow
                // in Java 8, this special case is not needed
                return (Long.parseLong(x.substring(0, 8), 16) << 32) |
                        Long.parseLong(x.substring(8, 16), 16);
            }
            return Long.parseLong(x, 16);
        } catch (NumberFormatException e) {
            throw newMVStoreException(ERROR_FILE_CORRUPT,
                    "Error parsing the value {0}", x, e);
        }
    }

    /**
     * Parse an unsigned, hex long.
     *
     * @param x the string
     * @return the parsed value
     * @throws MVStoreException if parsing fails
     */
    public static int parseHexInt(String x) {
        try {
            // avoid problems with overflow
            // in Java 8, we can use Integer.parseLong(x, 16);
            return (int) Long.parseLong(x, 16);
        } catch (NumberFormatException e) {
            throw newMVStoreException(ERROR_FILE_CORRUPT,
                    "Error parsing the value {0}", x, e);
        }
    }

    /**
     * Read a hex int value from a map.
     *
     * @param map the map
     * @param key the key
     * @param defaultValue if the value is null
     * @return the parsed value
     * @throws MVStoreException if parsing fails
     */
    static int readHexInt(Map<String, ?> map, String key, int defaultValue) {
        Object v = map.get(key);
        if (v == null) {
            return defaultValue;
        } else if (v instanceof Integer) {
            return (Integer) v;
        }
        try {
            // support unsigned hex value
            return (int) Long.parseLong((String) v, 16);
        } catch (NumberFormatException e) {
            throw newMVStoreException(ERROR_FILE_CORRUPT,
                    "Error parsing the value {0}", v, e);
        }
    }

    /**
     * Parse the hex-encoded bytes of an entry in the map.
     *
     * @param map the map
     * @param key the key
     * @return the byte array, or null if not in the map
     */
    static byte[] parseHexBytes(Map<String, ?> map, String key) {
        Object v = map.get(key);
        if (v == null) {
            return null;
        }
        return StringUtils.convertHexToBytes((String)v);
    }

    /**
     * Get the configuration parameter value, or default.
     *
     * @param config the configuration
     * @param key the key
     * @param defaultValue the default
     * @return the configured value or default
     */
    static int getConfigParam(Map<String, ?> config, String key, int defaultValue) {
        Object o = config.get(key);
        if (o instanceof Number) {
            return ((Number) o).intValue();
        } else if (o != null) {
            try {
                return Integer.decode(o.toString());
            } catch (NumberFormatException e) {
                // ignore
            }
        }
        return defaultValue;
    }

    /**
     * Convert an exception to an IO exception.
     *
     * @param e the root cause
     * @return the IO exception
     */
    public static IOException convertToIOException(Throwable e) {
        if (e instanceof IOException) {
            return (IOException) e;
        }
        if (e instanceof JdbcException) {
            if (e.getCause() != null) {
                e = e.getCause();
            }
        }
        return new IOException(e.toString(), e);
    }
}
