package biz.paluch.logging.gelf.netty;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.*;
import java.util.zip.GZIPInputStream;

import com.fasterxml.jackson.databind.ObjectMapper;

import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.channel.socket.DatagramPacket;
import io.netty.util.ReferenceCountUtil;

/**
 * @author Mark Paluch
 * @since 10.11.13 10:35
 */
public class GelfInboundHandler extends ChannelInboundHandlerAdapter {
    private static final byte[] GELF_CHUNKED_ID = new byte[] { 0x1e, 0x0f };
    private static final byte[] GZIP_ID = new byte[] { 0x1f, 0xffffff8b };

    private final Map<ChunkId, List<Chunk>> chunks = new HashMap<>();
    private final List<Object> values = new ArrayList<>();
    private ByteArrayOutputStream intermediate;
    private final ObjectMapper objectMapper = new ObjectMapper();

    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {

        try {

            boolean requireNullEnd = false;
            ByteBuf buffer = null;

            if (msg instanceof DatagramPacket) {
                DatagramPacket packet = (DatagramPacket) msg;
                buffer = packet.content();
            } else if (msg instanceof ByteBuf) {
                buffer = (ByteBuf) msg;
                requireNullEnd = true;
            }

            ByteArrayOutputStream temporaryBuffer = new ByteArrayOutputStream();
            while (buffer.readableBytes() != 0) {
                buffer.readBytes(temporaryBuffer, buffer.readableBytes());
            }

            byte bytes[] = temporaryBuffer.toByteArray();
            if (bytes.length > 2) {
                // if chunked 0x1e, 0x0f ,8 bytes id, number, size
                // if gzip 0x1f 0x8b

                if (startsWith(bytes, GELF_CHUNKED_ID)) {

                    byte id[] = new byte[8];
                    System.arraycopy(bytes, 2, id, 0, 8);
                    byte number = bytes[10];
                    byte count = bytes[11];
                    ChunkId chunkId = new ChunkId(id, count);

                    byte message[] = new byte[bytes.length - 12];
                    System.arraycopy(bytes, 12, message, 0, message.length);

                    synchronized (chunks) {

                        List<Chunk> messageChunks;
                        if (chunks.containsKey(chunkId)) {
                            messageChunks = chunks.get(chunkId);
                        } else {
                            messageChunks = new ArrayList<>();
                            chunks.put(chunkId, messageChunks);
                        }
                        messageChunks.add(new Chunk(message, number));
                        if (messageChunks.size() == count) {
                            Collections.sort(messageChunks);
                            bytes = getBytes(messageChunks);
                            chunks.remove(chunkId);
                        } else {
                            return;
                        }
                    }
                }

                if (requireNullEnd) {
                    if (bytes[bytes.length - 1] != 0) {
                        intermediate = new ByteArrayOutputStream();
                        intermediate.write(bytes, 0, bytes.length);
                        return;
                    } else if (intermediate != null) {
                        intermediate.write(bytes, 0, bytes.length);
                        bytes = intermediate.toByteArray();
                        intermediate = null;
                    }
                }

                InputStream is = null;

                if (startsWith(bytes, GZIP_ID)) {
                    is = new GZIPInputStream(new ByteArrayInputStream(bytes));
                } else {
                    if (bytes[bytes.length - 1] == 0) {
                        is = new ByteArrayInputStream(bytes, 0, bytes.length - 1);
                    } else {
                        is = new ByteArrayInputStream(bytes);
                    }
                }

                Object parse = objectMapper.readValue(is, Map.class);
                synchronized (values) {
                    values.add(parse);
                }

            }

        } finally {
            ReferenceCountUtil.release(msg);
        }
    }

    private byte[] getBytes(List<Chunk> messageChunks) throws IOException {
        ByteArrayOutputStream buffer = new ByteArrayOutputStream();

        for (Chunk messageChunk : messageChunks) {
            buffer.write(messageChunk.bytes);
        }
        return buffer.toByteArray();
    }

    private boolean startsWith(byte[] base, byte[] compareWith) {
        if (base.length >= compareWith.length) {
            for (int i = 0; i < compareWith.length; i++) {
                if (base[i] != compareWith[i]) {
                    return false;
                }
            }

            return true;
        }
        return false;
    }

    public List<Object> getJsonValues() {
        synchronized (values) {
            return new ArrayList<>(values);
        }
    }

    public void clear() {
        synchronized (values) {
            values.clear();
            chunks.clear();
        }
    }

    private class Chunk implements Comparable<Chunk> {
        private byte[] bytes;
        private int seq;

        private Chunk(byte[] bytes, int seq) {
            this.bytes = bytes;
            this.seq = seq;
        }

        @Override
        public int compareTo(Chunk o) {
            return seq - o.seq;
        }
    }

    private class ChunkId {

        private byte[] id;
        private byte count;

        private ChunkId(byte[] id, byte count) {
            this.id = id;
            this.count = count;
        }

        @Override
        public boolean equals(Object o) {
            if (this == o) {
                return true;
            }
            if (!(o instanceof ChunkId)) {
                return false;
            }

            ChunkId chunkId = (ChunkId) o;

            if (count != chunkId.count) {
                return false;
            }
            if (!Arrays.equals(id, chunkId.id)) {
                return false;
            }

            return true;
        }

        @Override
        public int hashCode() {
            int result = id != null ? Arrays.hashCode(id) : 0;
            result = 31 * result + (int) count;
            return result;
        }
    }
}
