/*
 * Copyright (c) 2015 Twitter, Inc. All rights reserved.
 * Licensed under the Apache License v2.0
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * This file is substantially based on work from the Netty project, also
 * released under the above license.
 */

package com.twitter.whiskey.net;

import com.twitter.whiskey.util.Platform;

import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.Set;

import static com.twitter.whiskey.net.SpdyCodecUtil.*;

/**
 * Encodes a SPDY Frame into a {@link ByteBuffer}.
 */
class SpdyFrameEncoder {

    private final SpdyHeaderBlockEncoder headerBlockEncoder;
    private final int version;

    /**
     * Creates a new instance with the specified {@code spdyVersion}.
     */
    public SpdyFrameEncoder(SpdyVersion spdyVersion) {
        if (spdyVersion == null) {
            throw new NullPointerException("spdyVersion");
        }
        headerBlockEncoder = new SpdyHeaderBlockZlibEncoder(spdyVersion, 9);
        version = spdyVersion.getVersion();
    }

    private void writeControlFrameHeader(ByteBuffer buffer, int type, byte flags, int length) {
        buffer.putShort((short) (version | 0x8000));
        buffer.putShort((short) type);
        buffer.put(flags);
        writeMedium(buffer, length);
    }

    public void writeMedium(ByteBuffer buffer, int medium) {
        buffer.put((byte) (medium >>> 16));
        buffer.put((byte) (medium >>> 8));
        buffer.put((byte) medium);
    }

    public ByteBuffer[] encodeDataFrame(int streamId, boolean last, ByteBuffer data) {
        byte flags = last ? SPDY_DATA_FLAG_FIN : 0;
        int length = data.limit();
        ByteBuffer frame = ByteBuffer.allocateDirect(SPDY_HEADER_SIZE).order(ByteOrder.BIG_ENDIAN);
        frame.putInt(streamId & 0x7FFFFFFF);
        frame.put(flags);
        writeMedium(frame, length);
        frame.flip();
        return new ByteBuffer[]{ frame, data };
    }

    public ByteBuffer[] encodeSynStreamFrame(int streamId, int associatedToStreamId,
            byte priority, boolean last, boolean unidirectional, Headers headers) {
        ByteBuffer headerBlock;
        try {
            headerBlock = headerBlockEncoder.encode(headers);
        } catch (Exception e) {
            headerBlock = ByteBuffer.allocate(0);
            Platform.LOGGER.debug(e.toString());
        }
        int headerBlockLength = headerBlock.limit();
        byte flags = last ? SPDY_FLAG_FIN : 0;
        if (unidirectional) {
            flags |= SPDY_FLAG_UNIDIRECTIONAL;
        }
        int length = 10 + headerBlockLength;
        ByteBuffer frame = ByteBuffer.allocateDirect(SPDY_HEADER_SIZE + 10).order(ByteOrder.BIG_ENDIAN);
        writeControlFrameHeader(frame, SPDY_SYN_STREAM_FRAME, flags, length);
        frame.putInt(streamId);
        frame.putInt(associatedToStreamId);
        frame.putShort((short) ((priority & 0xFF) << 13));
        frame.flip();
        return new ByteBuffer[]{ frame, headerBlock };
    }

    public ByteBuffer[] encodeSynReplyFrame(int streamId, boolean last, Headers headers) {
        ByteBuffer headerBlock;
        try {
            headerBlock = headerBlockEncoder.encode(headers);
        } catch (Exception e) {
            headerBlock = ByteBuffer.allocate(0);
            Platform.LOGGER.debug(e.toString());
        }
        int headerBlockLength = headerBlock.limit();
        byte flags = last ? SPDY_FLAG_FIN : 0;
        int length = 4 + headerBlockLength;
        ByteBuffer frame = ByteBuffer.allocateDirect(SPDY_HEADER_SIZE + 4).order(ByteOrder.BIG_ENDIAN);
        writeControlFrameHeader(frame, SPDY_SYN_REPLY_FRAME, flags, length);
        frame.putInt(streamId);
        frame.flip();
        return new ByteBuffer[]{ frame, headerBlock };
    }

    public ByteBuffer encodeRstStreamFrame(int streamId, int statusCode) {
        byte flags = 0;
        int length = 8;
        ByteBuffer frame = ByteBuffer.allocateDirect(SPDY_HEADER_SIZE + length).order(ByteOrder.BIG_ENDIAN);
        writeControlFrameHeader(frame, SPDY_RST_STREAM_FRAME, flags, length);
        frame.putInt(streamId);
        frame.putInt(statusCode);
        frame.flip();
        return frame;
    }

    public ByteBuffer encodeSettingsFrame(SpdySettings spdySettings) {
        Set<Integer> ids = spdySettings.ids();
        int numSettings = ids.size();

        byte flags = spdySettings.clearPreviouslyPersistedSettings() ?
                SPDY_SETTINGS_CLEAR : 0;
        int length = 4 + 8 * numSettings;
        ByteBuffer frame = ByteBuffer.allocateDirect(SPDY_HEADER_SIZE + length).order(ByteOrder.BIG_ENDIAN);
        writeControlFrameHeader(frame, SPDY_SETTINGS_FRAME, flags, length);
        frame.putInt(numSettings);
        for (Integer id : ids) {
            flags = 0;
            if (spdySettings.isPersistValue(id)) {
                flags |= SPDY_SETTINGS_PERSIST_VALUE;
            }
            if (spdySettings.isPersisted(id)) {
                flags |= SPDY_SETTINGS_PERSISTED;
            }
            frame.put(flags);
            writeMedium(frame, id);
            frame.putInt(spdySettings.getValue(id));
        }
        frame.flip();
        return frame;
    }

    public ByteBuffer encodePingFrame(int id) {
        byte flags = 0;
        int length = 4;
        ByteBuffer frame = ByteBuffer.allocateDirect(SPDY_HEADER_SIZE + length).order(ByteOrder.BIG_ENDIAN);
        writeControlFrameHeader(frame, SPDY_PING_FRAME, flags, length);
        frame.putInt(id);
        frame.flip();
        return frame;
    }

    public ByteBuffer encodeGoAwayFrame(int lastGoodStreamId, int statusCode) {
        byte flags = 0;
        int length = 8;
        ByteBuffer frame = ByteBuffer.allocateDirect(SPDY_HEADER_SIZE + length).order(ByteOrder.BIG_ENDIAN);
        writeControlFrameHeader(frame, SPDY_GOAWAY_FRAME, flags, length);
        frame.putInt(lastGoodStreamId);
        frame.putInt(statusCode);
        frame.flip();
        return frame;
    }

    public ByteBuffer[] encodeHeadersFrame(int streamId, boolean last, Headers headers) {
        ByteBuffer headerBlock;
        try {
            headerBlock = headerBlockEncoder.encode(headers);
        } catch (Exception e) {
            headerBlock = ByteBuffer.allocate(0);
            Platform.LOGGER.debug(e.toString());
        }
        int headerBlockLength = headerBlock.limit();
        byte flags = last ? SPDY_FLAG_FIN : 0;
        int length = 4 + headerBlockLength;
        ByteBuffer frame = ByteBuffer.allocateDirect(SPDY_HEADER_SIZE + 4).order(ByteOrder.BIG_ENDIAN);
        writeControlFrameHeader(frame, SPDY_HEADERS_FRAME, flags, length);
        frame.putInt(streamId);
        frame.flip();
        return new ByteBuffer[]{ frame, headerBlock };
    }

    public ByteBuffer encodeWindowUpdateFrame(int streamId, int deltaWindowSize) {
        byte flags = 0;
        int length = 8;
        ByteBuffer frame = ByteBuffer.allocateDirect(SPDY_HEADER_SIZE + length).order(ByteOrder.BIG_ENDIAN);
        writeControlFrameHeader(frame, SPDY_WINDOW_UPDATE_FRAME, flags, length);
        frame.putInt(streamId);
        frame.putInt(deltaWindowSize);
        frame.flip();
        return frame;
    }
}