package org.yzh.protocol.codec;

import io.github.yezhihao.protostar.Schema;
import io.github.yezhihao.protostar.SchemaManager;
import io.github.yezhihao.protostar.schema.RuntimeSchema;
import io.github.yezhihao.protostar.util.ArrayMap;
import io.github.yezhihao.protostar.util.Explain;
import io.netty.buffer.*;
import io.netty.util.ByteProcessor;
import org.yzh.protocol.basics.JTMessage;
import org.yzh.protocol.commons.JTUtils;

import java.util.LinkedList;

/**
 * JT协议编码器
 * @author yezhihao
 * https://gitee.com/yezhihao/jt808-server
 */
public class JTMessageEncoder {

    private static final ByteBufAllocator ALLOC = PooledByteBufAllocator.DEFAULT;

    private final SchemaManager schemaManager;

    private final ArrayMap<RuntimeSchema> headerSchemaMap;

    public JTMessageEncoder(String... basePackages) {
        this.schemaManager = new SchemaManager(basePackages);
        this.headerSchemaMap = schemaManager.getRuntimeSchema(JTMessage.class);
    }

    public JTMessageEncoder(SchemaManager schemaManager) {
        this.schemaManager = schemaManager;
        this.headerSchemaMap = schemaManager.getRuntimeSchema(JTMessage.class);
    }

    public ByteBuf encode(JTMessage message) {
        return encode(message, null);
    }

    public ByteBuf encode(JTMessage message, Explain explain) {
        int version = message.getProtocolVersion();
        int headLength = JTUtils.headerLength(version, false);
        int bodyLength = 0;

        Schema headSchema = headerSchemaMap.get(version);
        Schema bodySchema = schemaManager.getRuntimeSchema(message.getMessageId(), version);

        ByteBuf output;
        if (bodySchema != null) {
            output = ALLOC.buffer(headLength + bodySchema.length());
            output.writerIndex(headLength);
            bodySchema.writeTo(output, message, explain);
            bodyLength = output.writerIndex() - headLength;
        } else {
            output = ALLOC.buffer(headLength, 21);
        }

        if (bodyLength <= 1023) {
            message.setBodyLength(bodyLength);

            int writerIndex = output.writerIndex();
            if (writerIndex > 0) {
                output.writerIndex(0);
                headSchema.writeTo(output, message, explain);
                output.writerIndex(writerIndex);
            } else {
                headSchema.writeTo(output, message, explain);
            }

            output = sign(output);
            output = escape(output);

        } else {

            ByteBuf[] slices = slices(output, headLength, 1023);
            int total = slices.length;

            CompositeByteBuf _allBuf = new CompositeByteBuf(ALLOC, false, total);
            output = _allBuf;

            message.setSubpackage(true);
            message.setPackageTotal(total);

            headLength = JTUtils.headerLength(version, true);
            for (int i = 0; i < total; i++) {
                ByteBuf slice = slices[i];

                message.setPackageNo(i + 1);
                message.setBodyLength(slice.readableBytes());

                ByteBuf headBuf = ALLOC.buffer(headLength, headLength);
                headSchema.writeTo(headBuf, message, explain);
                ByteBuf msgBuf = new CompositeByteBuf(ALLOC, false, 2)
                        .addComponent(true, 0, headBuf)
                        .addComponent(true, 1, slice);
                msgBuf = sign(msgBuf);
                msgBuf = escape(msgBuf);
                _allBuf.addComponent(true, i, msgBuf);
            }
        }
        return output;
    }

    public static ByteBuf[] slices(ByteBuf output, int start, int unitSize) {
        int totalSize = output.writerIndex() - start;
        int tailIndex = (totalSize - 1) / unitSize;

        ByteBuf[] slices = new ByteBuf[tailIndex + 1];
        output.skipBytes(start);
        for (int i = 0; i < tailIndex; i++) {
            slices[i] = output.readSlice(unitSize);
        }
        slices[tailIndex] = output.readSlice(output.readableBytes());
        output.retain(tailIndex);
        return slices;
    }

    /** 签名 */
    public static ByteBuf sign(ByteBuf buf) {
        byte checkCode = JTUtils.bcc(buf, 0);
        buf.writeByte(checkCode);
        return buf;
    }

    private static final ByteProcessor searcher = value -> !(value == 0x7d || value == 0x7e);

    /** 转义处理 */
    public static ByteBuf escape(ByteBuf source) {
        int low = source.readerIndex();
        int high = source.writerIndex();

        LinkedList<ByteBuf> bufList = new LinkedList();
        int mark, len;
        while ((mark = source.forEachByte(low, high - low, searcher)) > 0) {

            len = mark + 1 - low;
            ByteBuf[] slice = slice(source, low, len);
            bufList.add(slice[0]);
            bufList.add(slice[1]);
            low += len;
        }

        if (bufList.size() > 0) {
            bufList.add(source.slice(low, high - low));
        } else {
            bufList.add(source);
        }

        ByteBuf delimiter = Unpooled.buffer(1, 1).writeByte(0x7e).retain();
        bufList.addFirst(delimiter);
        bufList.addLast(delimiter);

        CompositeByteBuf byteBufs = new CompositeByteBuf(ALLOC, false, bufList.size());
        byteBufs.addComponents(true, bufList);
        return byteBufs;
    }

    /** 截断转义前报文，并转义 */
    protected static ByteBuf[] slice(ByteBuf byteBuf, int index, int length) {
        byte first = byteBuf.getByte(index + length - 1);

        ByteBuf[] bufs = new ByteBuf[2];
        bufs[0] = byteBuf.retainedSlice(index, length);

        if (first == 0x7d) {
            bufs[1] = Unpooled.buffer(1, 1).writeByte(0x01);
        } else {
            byteBuf.setByte(index + length - 1, 0x7d);
            bufs[1] = Unpooled.buffer(1, 1).writeByte(0x02);
        }
        return bufs;
    }
}