package com.zzsn.common.cache;

import io.protostuff.*;
import io.protostuff.runtime.RuntimeSchema;
import org.springframework.objenesis.Objenesis;
import org.springframework.objenesis.ObjenesisStd;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

/**
 * @Author Sugar
 * @Version 2018/3/16 13:32
 */
public class ProtostuffUtil {
    public static final Objenesis objenesis = new ObjenesisStd(true);

    private static final String ERR_TRUNCATED_MESSAGE =
            "While parsing a protocol message, the input ended unexpectedly " +
                    "in the middle of a field.  This could mean either than the " +
                    "input has been truncated or that an embedded message " +
                    "misreported its own length.";

    /**
     * 序列化
     *
     * @param obj
     * @return
     */
    public static <T> byte[] serializer(T obj) {
        LinkedBuffer buffer = LinkedBuffer.allocate(LinkedBuffer.DEFAULT_BUFFER_SIZE);
        try {
            Schema<T> schema = RuntimeSchema.getSchema((Class<T>) obj.getClass());
            return ProtostuffIOUtil.toByteArray(obj, schema, buffer);
        } catch (Exception e) {
            throw new IllegalStateException("序列化对象失败：" + obj, e);
        } finally {
            buffer.clear();
        }
    }

    /**
     * 反序列化
     *
     * @param data
     * @param clazz
     * @return
     */
    public static <T> T deserializer(byte[] data, Class<T> clazz) {
        T obj = null;
        try {
            Schema<T> schema = RuntimeSchema.getSchema(clazz);
//            obj = schema.newMessage();
            obj = objenesis.newInstance(clazz);
            ProtostuffIOUtil.mergeFrom(data, obj, schema);
        } catch (Exception e) {
            throw new IllegalStateException("反序列化对象失败：class=" + clazz + ", data=" + new String(data), e);
        }
        return obj;
    }

    public static <T> byte[] serializeList(List<T> list) {
        @SuppressWarnings("unchecked")
        Schema<T> schema = (Schema<T>) RuntimeSchema.getSchema(list.get(0).getClass());
        LinkedBuffer buffer = LinkedBuffer.allocate(1024 * 1024);
        ByteArrayOutputStream bos = null;
        try {
            bos = new ByteArrayOutputStream();
            ProtostuffIOUtil.writeListTo(bos, list, schema, buffer);
            return bos.toByteArray();
        } catch (Exception e) {
            throw new IllegalStateException("序列化对象列表失败：" + list, e);
        } finally {
            buffer.clear();
            try {
                if (bos != null) {
                    bos.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    /**
     * 反序列化对象列表
     *
     * @param data
     * @param clazz
     * @param <T>
     * @return
     */
    public static <T> List<T> deserializeList(byte[] data, Class<T> clazz) {
        Schema<T> schema = RuntimeSchema.getSchema(clazz);
        List<T> result = null;
        try {
            result = parseListFrom(new ByteArrayInputStream(data), schema, clazz);
        } catch (IOException e) {
            throw new IllegalStateException("反序列化对象列表失败：class=" + clazz + ", data=" + new String(data), e);
        }
        return result;
    }

    private static <T> List<T> parseListFrom(final InputStream in, final Schema<T> schema, Class<T> clazz)
            throws IOException {
        int size = in.read();
        if (size == -1) {
            return Collections.emptyList();
        }

        if (size > 0x7f) {
            size = readRawVarint32(in, size);
        }

        final ArrayList<T> list = new ArrayList<T>(size);
        final CodedInput input = new CodedInput(in, true);
        for (int i = 0; i < size; i++) {
//            final T message = schema.newMessage();
            final T message = objenesis.newInstance(clazz);//使用objensis代替newInstance()
            list.add(message);
            schema.mergeFrom(input, message);
            input.checkLastTagWas(0);
        }
        assert in.read() == -1;
        return list;
    }

    /**
     * Reads a varint from the input one byte at a time, so that it does not read any bytes after the end of the varint.
     * If you simply wrapped the stream in a CodedInput and used readRawVarint32(InputStream) then you would
     * probably end up reading past the end of the varint since CodedInput buffers its input.
     */
    private static int readRawVarint32(final InputStream input, final int firstByte) throws IOException {
        int result = firstByte & 0x7f;
        int offset = 7;
        for (; offset < 32; offset += 7) {
            final int b = input.read();
            if (b == -1) {
                throw new ProtobufException(ERR_TRUNCATED_MESSAGE);
            }
            result |= (b & 0x7f) << offset;
            if ((b & 0x80) == 0) {
                return result;
            }
        }
        // Keep reading up to 64 bits.
        for (; offset < 64; offset += 7) {
            final int b = input.read();
            if (b == -1) {
                throw new ProtobufException(ERR_TRUNCATED_MESSAGE);
            }
            if ((b & 0x80) == 0) {
                return result;
            }
        }
        throw new ProtobufException(
                "CodedInput encountered a malformed varint.");
    }
}
