/*
 * Copyright 2020 The Netty Project
 *
 * The Netty Project licenses this file to you under the Apache License,
 * version 2.0 (the "License"); you may not use this file except in compliance
 * with the License. You may obtain a copy of the License at:
 *
 *   https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 * License for the specific language governing permissions and limitations
 * under the License.
 */
package io.netty.handler.codec.quic;

import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;

import java.net.InetSocketAddress;

import static io.netty.handler.codec.quic.Quiche.allocateNativeOrder;
import static io.netty.util.internal.ObjectUtil.checkPositiveOrZero;

/**
 * Parses the QUIC packet header and notifies a callback once parsing was successful.
 *
 * Once the parser is not needed anymore the user needs to call {@link #close()} to ensure all resources are
 * released. Failed to do so may lead to memory leaks.
 *
 * This class can be used for advanced use-cases. Usually you want to just use {@link QuicClientCodecBuilder} or
 * {@link QuicServerCodecBuilder}.
 */
public final class QuicHeaderParser implements AutoCloseable {
    private final int maxTokenLength;
    private final int localConnectionIdLength;
    private final ByteBuf versionBuffer;
    private final ByteBuf typeBuffer;
    private final ByteBuf scidLenBuffer;
    private final ByteBuf scidBuffer;
    private final ByteBuf dcidLenBuffer;
    private final ByteBuf dcidBuffer;
    private final ByteBuf tokenBuffer;
    private final ByteBuf tokenLenBuffer;
    private boolean closed;

    public QuicHeaderParser(int maxTokenLength, int localConnectionIdLength) {
        Quic.ensureAvailability();
        this.maxTokenLength = checkPositiveOrZero(maxTokenLength, "maxTokenLength");
        this.localConnectionIdLength = checkPositiveOrZero(localConnectionIdLength, "localConnectionIdLength");
        // Allocate the buffer from which we read primative values like integer/long with native order to ensure
        // we read the right value.
        versionBuffer = allocateNativeOrder(Integer.BYTES);
        typeBuffer = allocateNativeOrder(Byte.BYTES);
        scidLenBuffer = allocateNativeOrder(Integer.BYTES);
        dcidLenBuffer = allocateNativeOrder(Integer.BYTES);
        tokenLenBuffer = allocateNativeOrder(Integer.BYTES);

        // Now allocate the buffers that dont need native ordering and so will be cheaper to access when we slice into
        // these or obtain a view into these via internalNioBuffer(...).
        scidBuffer = Unpooled.directBuffer(Quiche.QUICHE_MAX_CONN_ID_LEN);
        dcidBuffer = Unpooled.directBuffer(Quiche.QUICHE_MAX_CONN_ID_LEN);
        tokenBuffer = Unpooled.directBuffer(maxTokenLength);
    }

    @Override
    public void close() {
        if (!closed) {
            closed = true;
            versionBuffer.release();
            typeBuffer.release();
            scidBuffer.release();
            scidLenBuffer.release();
            dcidBuffer.release();
            dcidLenBuffer.release();
            tokenLenBuffer.release();
            tokenBuffer.release();
        }
    }

    /**
     * Parses a QUIC packet and extract the header values out of it. This method takes no ownership of the packet itself
     * which means the caller of this method is expected to call {@link ByteBuf#release()} once the packet is not needed
     * anymore.
     *
     * @param sender        the sender of the packet. This is directly passed to the {@link QuicHeaderProcessor} once
     *                      parsing was successful.
     * @param recipient     the recipient of the packet.This is directly passed to the {@link QuicHeaderProcessor} once
     *                      parsing was successful.
     * @param packet        raw QUIC packet itself. The ownership of the packet is not transferred. This is directly
     *                      passed to the {@link QuicHeaderProcessor} once parsing was successful.
     * @param callback      the {@link QuicHeaderProcessor} that is called once a QUIC packet could be parsed and all
     *                      the header values be extracted.
     * @throws Exception    thrown if we couldn't parse the header or if the {@link QuicHeaderProcessor} throws an
     *                      exception.
     */
    public void parse(InetSocketAddress sender,
                      InetSocketAddress recipient, ByteBuf packet, QuicHeaderProcessor callback) throws Exception {
        if (closed) {
            throw new IllegalStateException(QuicHeaderParser.class.getSimpleName() + " is already closed");
        }

        // Set various len values so quiche_header_info can make use of these.
        scidLenBuffer.setInt(0, Quiche.QUICHE_MAX_CONN_ID_LEN);
        dcidLenBuffer.setInt(0, Quiche.QUICHE_MAX_CONN_ID_LEN);
        tokenLenBuffer.setInt(0, maxTokenLength);

        int res = Quiche.quiche_header_info(
                Quiche.readerMemoryAddress(packet), packet.readableBytes(),
                localConnectionIdLength,
                Quiche.memoryAddress(versionBuffer, 0, versionBuffer.capacity()),
                Quiche.memoryAddress(typeBuffer, 0, typeBuffer.capacity()),
                Quiche.memoryAddress(scidBuffer, 0, scidBuffer.capacity()),
                Quiche.memoryAddress(scidLenBuffer, 0, scidLenBuffer.capacity()),
                Quiche.memoryAddress(dcidBuffer, 0, dcidBuffer.capacity()),
                Quiche.memoryAddress(dcidLenBuffer, 0, dcidLenBuffer.capacity()),
                Quiche.memoryAddress(tokenBuffer, 0, tokenBuffer.capacity()),
                Quiche.writerMemoryAddress(tokenLenBuffer));
        if (res >= 0) {
            int version = versionBuffer.getInt(0);
            byte type = typeBuffer.getByte(0);
            int scidLen = scidLenBuffer.getInt(0);
            int dcidLen = dcidLenBuffer.getInt(0);
            int tokenLen = tokenLenBuffer.getInt(0);

            callback.process(sender, recipient, packet, QuicPacketType.of(type), version,
                    scidBuffer.setIndex(0, scidLen),
                    dcidBuffer.setIndex(0, dcidLen),
                    tokenBuffer.setIndex(0, tokenLen));
        } else {
            throw Quiche.newException(res);
        }
    }

    /**
     * Called when a QUIC packet and its header could be parsed.
     */
    public interface QuicHeaderProcessor {

        /**
         * Called when a QUIC packet header was parsed.
         *
         * @param sender        the sender of the QUIC packet.
         * @param recipient     the recipient of the QUIC packet.
         * @param packet        the raw QUIC packet. The ownership is not transferred, which means you will need to call
         *                      {@link ByteBuf#retain()} on it if you want to keep a reference after this method
         *                      returns.
         * @param type          the type of the packet.
         * @param version       the version of the packet.
         * @param scid          the source connection id. The ownership is not transferred and its generally not allowed
         *                      to hold any references to this buffer outside of the method as it will be re-used.
         * @param dcid          the destination connection id. The ownership is not transferred and its generally not
         *                      allowed to hold any references to this buffer outside of the method as it will be
         *                      re-used.
         * @param token         the token.The ownership is not transferred and its generally not allowed
         *                      to hold any references to this buffer outside of the method as it will be re-used.
         * @throws Exception    throws if an error happens during processing.
         */
        void process(InetSocketAddress sender, InetSocketAddress recipient, ByteBuf packet,
                     QuicPacketType type, int version, ByteBuf scid, ByteBuf dcid, ByteBuf token) throws Exception;
    }
}
