package com.taobao.tddl.dbsync.binlog;

import static com.taobao.tddl.dbsync.binlog.event.RowsLogBuffer.*;

import java.nio.charset.Charset;

/**
 * 处理下MySQL json二进制转化为可读的字符串
 * 
 * @author agapple 2016年6月30日 上午11:26:17
 * @since 1.0.22
 */
public class JsonConversion {

    // JSON TYPE
    public static final int  JSONB_TYPE_SMALL_OBJECT = 0x0;
    public static final int  JSONB_TYPE_LARGE_OBJECT = 0x1;
    public static final int  JSONB_TYPE_SMALL_ARRAY  = 0x2;
    public static final int  JSONB_TYPE_LARGE_ARRAY  = 0x3;
    public static final int  JSONB_TYPE_LITERAL      = 0x4;
    public static final int  JSONB_TYPE_INT16        = 0x5;
    public static final int  JSONB_TYPE_UINT16       = 0x6;
    public static final int  JSONB_TYPE_INT32        = 0x7;
    public static final int  JSONB_TYPE_UINT32       = 0x8;
    public static final int  JSONB_TYPE_INT64        = 0x9;
    public static final int  JSONB_TYPE_UINT64       = 0xA;
    public static final int  JSONB_TYPE_DOUBLE       = 0xB;
    public static final int  JSONB_TYPE_STRING       = 0xC;
    public static final int  JSONB_TYPE_OPAQUE       = 0xF;
    public static final char JSONB_NULL_LITERAL      = '\0';
    public static final char JSONB_TRUE_LITERAL      = '\1';
    public static final char JSONB_FALSE_LITERAL     = '\2';

    /*
     * The size of offset or size fields in the small and the large storage format
     * for JSON objects and JSON arrays.
     */
    public static final int  SMALL_OFFSET_SIZE       = 2;
    public static final int  LARGE_OFFSET_SIZE       = 4;

    /*
     * The size of key entries for objects when using the small storage format or
     * the large storage format. In the small format it is 4 bytes (2 bytes for key
     * length and 2 bytes for key offset). In the large format it is 6 (2 bytes for
     * length, 4 bytes for offset).
     */
    public static final int  KEY_ENTRY_SIZE_SMALL    = (2 + SMALL_OFFSET_SIZE);
    public static final int  KEY_ENTRY_SIZE_LARGE    = (2 + LARGE_OFFSET_SIZE);

    /*
     * The size of value entries for objects or arrays. When using the small storage
     * format, the entry size is 3 (1 byte for type, 2 bytes for offset). When using
     * the large storage format, it is 5 (1 byte for type, 4 bytes for offset).
     */
    public static final int  VALUE_ENTRY_SIZE_SMALL  = (1 + SMALL_OFFSET_SIZE);
    public static final int  VALUE_ENTRY_SIZE_LARGE  = (1 + LARGE_OFFSET_SIZE);

    public static Json_Value parse_value(int type, LogBuffer buffer, long len, String charsetName) {
        return parse_value(type, buffer, len, Charset.forName(charsetName));
    }

    public static Json_Value parse_value(int type, LogBuffer buffer, long len, Charset charset) {
        buffer = buffer.duplicate(buffer.position(), (int) len);
        switch (type) {
            case JSONB_TYPE_SMALL_OBJECT:
                return parse_array_or_object(Json_enum_type.OBJECT, buffer, len, false, charset);
            case JSONB_TYPE_LARGE_OBJECT:
                return parse_array_or_object(Json_enum_type.OBJECT, buffer, len, true, charset);
            case JSONB_TYPE_SMALL_ARRAY:
                return parse_array_or_object(Json_enum_type.ARRAY, buffer, len, false, charset);
            case JSONB_TYPE_LARGE_ARRAY:
                return parse_array_or_object(Json_enum_type.ARRAY, buffer, len, true, charset);
            default:
                return parse_scalar(type, buffer, len, charset);
        }
    }

    private static Json_Value parse_array_or_object(Json_enum_type type, LogBuffer buffer, long len, boolean large,
                                                    Charset charset) {
        long offset_size = large ? LARGE_OFFSET_SIZE : SMALL_OFFSET_SIZE;
        if (len < 2 * offset_size) {
            throw new IllegalArgumentException("illegal json data");
        }
        long element_count = read_offset_or_size(buffer, large);
        long bytes = read_offset_or_size(buffer, large);

        if (bytes > len) {
            throw new IllegalArgumentException("illegal json data");
        }
        long header_size = 2 * offset_size;
        if (type == Json_enum_type.OBJECT) {
            header_size += element_count * (large ? KEY_ENTRY_SIZE_LARGE : KEY_ENTRY_SIZE_SMALL);
        }

        header_size += element_count * (large ? VALUE_ENTRY_SIZE_LARGE : VALUE_ENTRY_SIZE_SMALL);
        if (header_size > bytes) {
            throw new IllegalArgumentException("illegal json data");
        }
        return new Json_Value(type, buffer.rewind(), element_count, bytes, large);
    }

    private static long read_offset_or_size(LogBuffer buffer, boolean large) {
        return large ? buffer.getUint32() : buffer.getUint16();
    }

    private static Json_Value parse_scalar(int type, LogBuffer buffer, long len, Charset charset) {
        switch (type) {
            case JSONB_TYPE_LITERAL:
                /* purecov: inspected */
                int data = buffer.getUint8();
                switch (data) {
                    case JSONB_NULL_LITERAL:
                        return new Json_Value(Json_enum_type.LITERAL_NULL);
                    case JSONB_TRUE_LITERAL:
                        return new Json_Value(Json_enum_type.LITERAL_TRUE);
                    case JSONB_FALSE_LITERAL:
                        return new Json_Value(Json_enum_type.LITERAL_FALSE);
                    default:
                        throw new IllegalArgumentException("illegal json data");
                }
            case JSONB_TYPE_INT16:
                return new Json_Value(Json_enum_type.INT, buffer.getInt16());
            case JSONB_TYPE_INT32:
                return new Json_Value(Json_enum_type.INT, buffer.getInt32());
            case JSONB_TYPE_INT64:
                return new Json_Value(Json_enum_type.INT, buffer.getLong64());
            case JSONB_TYPE_UINT16:
                return new Json_Value(Json_enum_type.UINT, buffer.getUint16());
            case JSONB_TYPE_UINT32:
                return new Json_Value(Json_enum_type.UINT, buffer.getUint32());
            case JSONB_TYPE_UINT64:
                return new Json_Value(Json_enum_type.UINT, buffer.getUlong64());
            case JSONB_TYPE_DOUBLE:
                return new Json_Value(Json_enum_type.DOUBLE, Double.valueOf(buffer.getDouble64()));
            case JSONB_TYPE_STRING:
                int max_bytes = (int) Math.min(len, 5);
                long tlen = 0;
                long str_len = 0;
                long n = 0;
                byte[] datas = buffer.getData(max_bytes);
                for (int i = 0; i < max_bytes; i++) {
                    // Get the next 7 bits of the length.
                    tlen |= (datas[i] & 0x7f) << (7 * i);
                    if ((datas[i] & 0x80) == 0) {
                        // The length shouldn't exceed 32 bits.
                        if (tlen > 4294967296L) {
                            throw new IllegalArgumentException("illegal json data");
                        }

                        // This was the last byte. Return successfully.
                        n = i + 1;
                        str_len = tlen;
                        break;
                    }
                }

                if (len < n + str_len) {
                    throw new IllegalArgumentException("illegal json data");
                }
                return new Json_Value(Json_enum_type.STRING,
                    buffer.rewind().forward((int) n).getFixString((int) str_len, charset));
            case JSONB_TYPE_OPAQUE:
                /*
                 * There should always be at least one byte, which tells the field type of the
                 * opaque value.
                 */
                // The type is encoded as a uint8 that maps to an
                // enum_field_types.
                int type_byte = buffer.getUint8();
                int position = buffer.position();
                // Then there's the length of the value.
                int q_max_bytes = (int) Math.min(len - 1, 5);
                long q_tlen = 0;
                long q_str_len = 0;
                long q_n = 0;
                byte[] q_datas = buffer.getData(q_max_bytes);
                for (int i = 0; i < q_max_bytes; i++) {
                    // Get the next 7 bits of the length.
                    q_tlen |= (q_datas[i] & 0x7f) << (7 * i);
                    if ((q_datas[i] & 0x80) == 0) {
                        // The length shouldn't exceed 32 bits.
                        if (q_tlen > 4294967296L) {
                            throw new IllegalArgumentException("illegal json data");
                        }

                        // This was the last byte. Return successfully.
                        q_n = i + 1;
                        q_str_len = q_tlen;
                        break;
                    }
                }

                if (q_str_len == 0 || len < q_n + q_str_len) {
                    throw new IllegalArgumentException("illegal json data");
                }
                return new Json_Value(type_byte, buffer.position(position).forward((int) q_n), q_str_len);
            default:
                throw new IllegalArgumentException("illegal json data");
        }
    }

    public static class Json_Value {

        Json_enum_type m_type;
        int            m_field_type;
        LogBuffer      m_data;
        long           m_element_count;
        long           m_length;
        String         m_string_value;
        Number         m_int_value;
        double         m_double_value;
        boolean        m_large;

        public Json_Value(Json_enum_type t){
            this.m_type = t;
        }

        public Json_Value(Json_enum_type t, Number val){
            this.m_type = t;
            if (t == Json_enum_type.DOUBLE) {
                this.m_double_value = val.doubleValue();
            } else {
                this.m_int_value = val;
            }
        }

        public Json_Value(Json_enum_type t, String value){
            this.m_type = t;
            this.m_string_value = value;
        }

        public Json_Value(int field_type, LogBuffer data, long bytes){
            this.m_type = Json_enum_type.OPAQUE; // 不确定类型
            this.m_field_type = field_type;
            this.m_data = data;
            this.m_length = bytes;
        }

        public Json_Value(Json_enum_type t, LogBuffer data, long element_count, long bytes, boolean large){
            this.m_type = t;
            this.m_data = data;
            this.m_element_count = element_count;
            this.m_length = bytes;
            this.m_large = large;
        }

        public String key(int i, Charset charset) {
            m_data.rewind();
            int offset_size = m_large ? LARGE_OFFSET_SIZE : SMALL_OFFSET_SIZE;
            int key_entry_size = m_large ? KEY_ENTRY_SIZE_LARGE : KEY_ENTRY_SIZE_SMALL;
            int entry_offset = 2 * offset_size + key_entry_size * i;
            // The offset of the key is the first part of the key
            // entry.
            m_data.forward(entry_offset);
            long key_offset = read_offset_or_size(m_data, m_large);
            // The length of the key is the second part of the
            // entry, always two
            // bytes.
            long key_length = m_data.getUint16();
            return m_data.rewind().forward((int) key_offset).getFixString((int) key_length, charset);
        }

        public Json_Value element(int i, Charset charset) {
            m_data.rewind();
            int offset_size = m_large ? LARGE_OFFSET_SIZE : SMALL_OFFSET_SIZE;
            int key_entry_size = m_large ? KEY_ENTRY_SIZE_LARGE : KEY_ENTRY_SIZE_SMALL;
            int value_entry_size = m_large ? VALUE_ENTRY_SIZE_LARGE : VALUE_ENTRY_SIZE_SMALL;
            int first_entry_offset = 2 * offset_size;
            if (m_type == Json_enum_type.OBJECT) {
                first_entry_offset += m_element_count * key_entry_size;
            }
            int entry_offset = first_entry_offset + value_entry_size * i;
            int type = m_data.forward(entry_offset).getUint8();
            if (type == JSONB_TYPE_INT16 || type == JSONB_TYPE_UINT16 || type == JSONB_TYPE_LITERAL
                || (m_large && (type == JSONB_TYPE_INT32 || type == JSONB_TYPE_UINT32))) {
                return parse_scalar(type, m_data, value_entry_size - 1, charset);
            }
            int value_offset = (int) read_offset_or_size(m_data, m_large);
            return parse_value(type, m_data.rewind().forward(value_offset), (int) m_length - value_offset, charset);
        }

        public StringBuilder toJsonString(StringBuilder buf, Charset charset) {
            switch (m_type) {
                case OBJECT:
                    buf.append("{");
                    for (int i = 0; i < m_element_count; ++i) {
                        if (i > 0) {
                            buf.append(", ");
                        }
                        buf.append('"').append(escapse(key(i, charset))).append('"');
                        buf.append(": ");
                        element(i, charset).toJsonString(buf, charset);
                    }
                    buf.append("}");
                    break;
                case ARRAY:
                    buf.append("[");
                    for (int i = 0; i < m_element_count; ++i) {
                        if (i > 0) {
                            buf.append(", ");
                        }
                        element(i, charset).toJsonString(buf, charset);
                    }
                    buf.append("]");
                    break;
                case DOUBLE:
                    buf.append(Double.valueOf(m_double_value).toString());
                    break;
                case INT:
                    buf.append(m_int_value.toString());
                    break;
                case UINT:
                    buf.append(m_int_value.toString());
                    break;
                case LITERAL_FALSE:
                    buf.append("false");
                    break;
                case LITERAL_TRUE:
                    buf.append("true");
                    break;
                case LITERAL_NULL:
                    buf.append("null");
                    break;
                case OPAQUE:
                    String text = null;
                    if (m_field_type == LogEvent.MYSQL_TYPE_NEWDECIMAL) {
                        int precision = m_data.getInt8();
                        int scale = m_data.getInt8();
                        text = m_data.getDecimal(precision, scale).toPlainString();
                        buf.append(text);
                    } else if (m_field_type == LogEvent.MYSQL_TYPE_TIME) {
                        long packed_value = m_data.getLong64();
                        if (packed_value == 0) {
                            text = "00:00:00";
                        } else {
                            long ultime = Math.abs(packed_value);
                            long intpart = ultime >> 24;
                            int frac = (int) (ultime % (1L << 24));
                            // text = String.format("%s%02d:%02d:%02d",
                            // packed_value >= 0 ? "" : "-",
                            // (int) ((intpart >> 12) % (1 << 10)),
                            // (int) ((intpart >> 6) % (1 << 6)),
                            // (int) (intpart % (1 << 6)));
                            // text = text + "." + usecondsToStr(frac, 6);
                            StringBuilder builder = new StringBuilder(17);
                            if (packed_value < 0) {
                                builder.append('-');
                            }

                            int d = (int) ((intpart >> 12) % (1 << 10));
                            if (d > 100) {
                                builder.append(String.valueOf(d));
                            } else {
                                appendNumber2(builder, d);
                            }
                            builder.append(':');
                            appendNumber2(builder, (int) ((intpart >> 6) % (1 << 6)));
                            builder.append(':');
                            appendNumber2(builder, (int) (intpart % (1 << 6)));

                            builder.append('.').append(usecondsToStr(frac, 6));
                            text = builder.toString();
                        }
                        buf.append('"').append(text).append('"');
                    } else if (m_field_type == LogEvent.MYSQL_TYPE_DATE || m_field_type == LogEvent.MYSQL_TYPE_DATETIME
                               || m_field_type == LogEvent.MYSQL_TYPE_TIMESTAMP) {
                                   long packed_value = m_data.getLong64();
                                   if (packed_value == 0) {
                                       text = "0000-00-00 00:00:00";
                                   } else {
                                       // 构造TimeStamp只处理到秒
                                       long ultime = Math.abs(packed_value);
                                       long intpart = ultime >> 24;
                                       int frac = (int) (ultime % (1L << 24));
                                       long ymd = intpart >> 17;
                                       long ym = ymd >> 5;
                                       long hms = intpart % (1 << 17);
                                       // text =
                                       // String.format("%04d-%02d-%02d %02d:%02d:%02d",
                                       // (int) (ym / 13),
                                       // (int) (ym % 13),
                                       // (int) (ymd % (1 << 5)),
                                       // (int) (hms >> 12),
                                       // (int) ((hms >> 6) % (1 << 6)),
                                       // (int) (hms % (1 << 6)));
                                       StringBuilder builder = new StringBuilder(26);
                                       appendNumber4(builder, (int) (ym / 13));
                                       builder.append('-');
                                       appendNumber2(builder, (int) (ym % 13));
                                       builder.append('-');
                                       appendNumber2(builder, (int) (ymd % (1 << 5)));
                                       builder.append(' ');
                                       appendNumber2(builder, (int) (hms >> 12));
                                       builder.append(':');
                                       appendNumber2(builder, (int) ((hms >> 6) % (1 << 6)));
                                       builder.append(':');
                                       appendNumber2(builder, (int) (hms % (1 << 6)));
                                       builder.append('.').append(usecondsToStr(frac, 6));
                                       text = builder.toString();
                                   }
                                   buf.append('"').append(text).append('"');
                               } else {
                                   text = m_data.getFixString((int) m_length, charset);
                                   buf.append('"').append(escapse(text)).append('"');
                               }

                    break;
                case STRING:
                    buf.append('"').append(escapse(m_string_value)).append('"');
                    break;
                case ERROR:
                    throw new IllegalArgumentException("illegal json data");
            }

            return buf;
        }
    }

    private static StringBuilder escapse(String data) {
        StringBuilder sb = new StringBuilder(data.length());
        int endIndex = data.length();
        for (int i = 0; i < endIndex; ++i) {
            char c = data.charAt(i);
            if (c == '"') {
                sb.append("\\\"");
            } else if (c == '\n') {
                sb.append("\\n");
            } else if (c == '\r') {
                sb.append("\\r");
            } else if (c == '\\') {
                sb.append("\\\\");
            } else if (c == '\t') {
                sb.append("\\t");
            } else if (c < 16) {
                sb.append("\\u000");
                sb.append(Integer.toHexString(c));
            } else if (c < 32) {
                sb.append("\\u00");
                sb.append(Integer.toHexString(c));
            } else if (c >= 0x7f && c <= 0xA0) {
                sb.append("\\u00");
                sb.append(Integer.toHexString(c));
            } else {
                sb.append(c);
            }
        }
        return sb;
    }

    public static enum Json_enum_type {
                                       OBJECT, ARRAY, STRING, INT, UINT, DOUBLE, LITERAL_NULL, LITERAL_TRUE,
                                       LITERAL_FALSE, OPAQUE, ERROR
    }

}
