/*
 * JBoss, Home of Professional Open Source.
 * Copyright 2014 Red Hat, Inc., and individual contributors
 * as indicated by the @author tags.
 *
 * Licensed 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
 *
 *     http://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.undertow.websockets.core;

import io.undertow.connector.PooledByteBuffer;
import io.undertow.util.ImmediatePooledByteBuffer;
import io.undertow.util.WorkerUtils;
import org.xnio.Buffers;
import org.xnio.ChannelExceptionHandler;
import org.xnio.ChannelListener;
import org.xnio.IoUtils;
import org.xnio.XnioExecutor;

import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.TimeUnit;

import static org.xnio.ChannelListeners.flushingChannelListener;

/**
 * @author Stuart Douglas
 */
public class WebSockets {

    /**
     * Sends a complete text message, invoking the callback when complete
     *
     * @param message The text to send
     * @param wsChannel The web socket channel
     * @param callback The callback to invoke on completion
     */
    public static void sendText(final String message, final WebSocketChannel wsChannel, final WebSocketCallback<Void> callback) {
        sendText(message, wsChannel, callback, null);
    }

    /**
     * Sends a complete text message, invoking the callback when complete
     *
     * @param message The text to send
     * @param wsChannel The web socket channel
     * @param callback The callback to invoke on completion
     * @param context The context object that will be passed to the callback on completion
     */
    public static <T> void sendText(final String message, final WebSocketChannel wsChannel, final WebSocketCallback<T> callback, T context) {
        final ByteBuffer data = ByteBuffer.wrap(message.getBytes(StandardCharsets.UTF_8));
        sendInternal(data, WebSocketFrameType.TEXT, wsChannel, callback, context, -1);
    }

    /**
     * Sends a complete text message, invoking the callback when complete
     *
     * @param message The text to send
     * @param wsChannel The web socket channel
     * @param callback The callback to invoke on completion
     * @param timeoutmillis the timeout in milliseconds
     */
    public static void sendText(final String message, final WebSocketChannel wsChannel, final WebSocketCallback<Void> callback, long timeoutmillis) {
        sendText(message, wsChannel, callback, null, timeoutmillis);
    }

    /**
     * Sends a complete text message, invoking the callback when complete
     *
     * @param message The text to send
     * @param wsChannel The web socket channel
     * @param callback The callback to invoke on completion
     * @param context The context object that will be passed to the callback on completion
     * @param timeoutmillis the timeout in milliseconds
     */
    public static <T> void sendText(final String message, final WebSocketChannel wsChannel, final WebSocketCallback<T> callback, T context, long timeoutmillis) {
        final ByteBuffer data = ByteBuffer.wrap(message.getBytes(StandardCharsets.UTF_8));
        sendInternal(data, WebSocketFrameType.TEXT, wsChannel, callback, context, timeoutmillis);
    }

    /**
     * Sends a complete text message, invoking the callback when complete
     *
     * @param message The text to send
     * @param wsChannel The web socket channel
     * @param callback The callback to invoke on completion
     */
    public static void sendText(final ByteBuffer message, final WebSocketChannel wsChannel, final WebSocketCallback<Void> callback) {
        sendInternal(message, WebSocketFrameType.TEXT, wsChannel, callback, null, -1);
    }

    /**
     * Sends a complete text message, invoking the callback when complete
     *
     * @param message The text to send
     * @param wsChannel The web socket channel
     * @param callback The callback to invoke on completion
     * @param context The context object that will be passed to the callback on completion
     */
    public static <T> void sendText(final ByteBuffer message, final WebSocketChannel wsChannel, final WebSocketCallback<T> callback, T context) {
        sendInternal(message, WebSocketFrameType.TEXT, wsChannel, callback, context, -1);
    }

    /**
     * Sends a complete text message, invoking the callback when complete
     *
     * @param message The text to send
     * @param wsChannel The web socket channel
     * @param callback The callback to invoke on completion
     * @param timeoutmillis the timeout in milliseconds
     */
    public static void sendText(final ByteBuffer message, final WebSocketChannel wsChannel, final WebSocketCallback<Void> callback, long timeoutmillis) {
        sendInternal(message, WebSocketFrameType.TEXT, wsChannel, callback, null, timeoutmillis);
    }

    /**
     * Sends a complete text message, invoking the callback when complete
     *
     * @param message The text to send
     * @param wsChannel The web socket channel
     * @param callback The callback to invoke on completion
     * @param context The context object that will be passed to the callback on completion
     * @param timeoutmillis the timeout in milliseconds
     */
    public static <T> void sendText(final ByteBuffer message, final WebSocketChannel wsChannel, final WebSocketCallback<T> callback, T context, long timeoutmillis) {
        sendInternal(message, WebSocketFrameType.TEXT, wsChannel, callback, context, timeoutmillis);
    }

    /**
     * Sends a complete text message, invoking the callback when complete
     * Automatically frees the pooled byte buffer when done.
     *
     * @param pooledData The data to send, it will be freed when done
     * @param wsChannel The web socket channel
     * @param callback The callback to invoke on completion
     */
    public static void sendText(final PooledByteBuffer pooledData, final WebSocketChannel wsChannel, final WebSocketCallback<Void> callback) {
        sendInternal(pooledData, WebSocketFrameType.TEXT, wsChannel, callback, null, -1);
    }

    /**
     * Sends a complete text message, invoking the callback when complete
     * Automatically frees the pooled byte buffer when done.
     *
     * @param pooledData The data to send, it will be freed when done
     * @param wsChannel The web socket channel
     * @param callback The callback to invoke on completion
     * @param context The context object that will be passed to the callback on completion
     */
    public static <T> void sendText(final PooledByteBuffer pooledData, final WebSocketChannel wsChannel, final WebSocketCallback<T> callback, T context) {
        sendInternal(pooledData, WebSocketFrameType.TEXT, wsChannel, callback, context, -1);
    }

    /**
     * Sends a complete text message, invoking the callback when complete
     * Automatically frees the pooled byte buffer when done.
     *
     * @param pooledData The data to send, it will be freed when done
     * @param wsChannel The web socket channel
     * @param callback The callback to invoke on completion
     * @param timeoutmillis the timeout in milliseconds
     */
    public static void sendText(final PooledByteBuffer pooledData, final WebSocketChannel wsChannel, final WebSocketCallback<Void> callback, long timeoutmillis) {
        sendInternal(pooledData, WebSocketFrameType.TEXT, wsChannel, callback, null, timeoutmillis);
    }

    /**
     * Sends a complete text message, invoking the callback when complete
     * Automatically frees the pooled byte buffer when done.
     *
     * @param pooledData The data to send, it will be freed when done
     * @param wsChannel The web socket channel
     * @param callback The callback to invoke on completion
     * @param context The context object that will be passed to the callback on completion
     * @param timeoutmillis the timeout in milliseconds
     */
    public static <T> void sendText(final PooledByteBuffer pooledData, final WebSocketChannel wsChannel, final WebSocketCallback<T> callback, T context, long timeoutmillis) {
        sendInternal(pooledData, WebSocketFrameType.TEXT, wsChannel, callback, context, timeoutmillis);
    }

    /**
     * Sends a complete text message, invoking the callback when complete
     *
     * @param message The text to send
     * @param wsChannel The web socket channel
     */
    public static void sendTextBlocking(final String message, final WebSocketChannel wsChannel) throws IOException {
        final ByteBuffer data = ByteBuffer.wrap(message.getBytes(StandardCharsets.UTF_8));
        sendBlockingInternal(data, WebSocketFrameType.TEXT, wsChannel);
    }

    /**
     * Sends a complete text message, invoking the callback when complete
     *
     * @param message The text to send
     * @param wsChannel The web socket channel
     */
    public static void sendTextBlocking(final ByteBuffer message, final WebSocketChannel wsChannel) throws IOException {
        sendBlockingInternal(message, WebSocketFrameType.TEXT, wsChannel);
    }

    /**
     * Sends a complete text message, invoking the callback when complete
     * Automatically frees the pooled byte buffer when done.
     *
     * @param pooledData The data to send, it will be freed when done
     * @param wsChannel The web socket channel
     */
    public static void sendTextBlocking(final PooledByteBuffer pooledData, final WebSocketChannel wsChannel) throws IOException {
        sendBlockingInternal(pooledData, WebSocketFrameType.TEXT, wsChannel);
    }

    /**
     * Sends a complete ping message, invoking the callback when complete
     *
     * @param data The data to send
     * @param wsChannel The web socket channel
     * @param callback The callback to invoke on completion
     */
    public static void sendPing(final ByteBuffer data, final WebSocketChannel wsChannel, final WebSocketCallback<Void> callback) {
        sendInternal(data, WebSocketFrameType.PING, wsChannel, callback, null, -1);
    }

    /**
     * Sends a complete ping message, invoking the callback when complete
     *
     * @param data The data to send
     * @param wsChannel The web socket channel
     * @param callback The callback to invoke on completion
     * @param context The context object that will be passed to the callback on completion
     */
    public static <T> void sendPing(final ByteBuffer data, final WebSocketChannel wsChannel, final WebSocketCallback<T> callback, T context) {
        sendInternal(data, WebSocketFrameType.PING, wsChannel, callback, context, -1);
    }

    /**
     * Sends a complete ping message, invoking the callback when complete
     *
     * @param data The data to send
     * @param wsChannel The web socket channel
     * @param callback The callback to invoke on completion
     * @param timeoutmillis the timeout in milliseconds
     */
    public static void sendPing(final ByteBuffer data, final WebSocketChannel wsChannel, final WebSocketCallback<Void> callback, long timeoutmillis) {
        sendInternal(data, WebSocketFrameType.PING, wsChannel, callback, null, timeoutmillis);
    }

    /**
     * Sends a complete ping message, invoking the callback when complete
     *
     * @param data The data to send
     * @param wsChannel The web socket channel
     * @param callback The callback to invoke on completion
     * @param context The context object that will be passed to the callback on completion
     * @param timeoutmillis the timeout in milliseconds
     */
    public static <T> void sendPing(final ByteBuffer data, final WebSocketChannel wsChannel, final WebSocketCallback<T> callback, T context, long timeoutmillis) {
        sendInternal(data, WebSocketFrameType.PING, wsChannel, callback, context, timeoutmillis);
    }

    /**
     * Sends a complete ping message, invoking the callback when complete
     *
     * @param data The data to send
     * @param wsChannel The web socket channel
     * @param callback The callback to invoke on completion
     */
    public static void sendPing(final ByteBuffer[] data, final WebSocketChannel wsChannel, final WebSocketCallback<Void> callback) {
        sendInternal(mergeBuffers(data), WebSocketFrameType.PING, wsChannel, callback, null, -1);
    }

    /**
     * Sends a complete ping message, invoking the callback when complete
     *
     * @param data The data to send
     * @param wsChannel The web socket channel
     * @param callback The callback to invoke on completion
     * @param context The context object that will be passed to the callback on completion
     */
    public static <T> void sendPing(final ByteBuffer[] data, final WebSocketChannel wsChannel, final WebSocketCallback<T> callback, T context) {
        sendInternal(mergeBuffers(data), WebSocketFrameType.PING, wsChannel, callback, context, -1);
    }

    /**
     * Sends a complete ping message, invoking the callback when complete
     *
     * @param data The data to send
     * @param wsChannel The web socket channel
     * @param callback The callback to invoke on completion
     * @param timeoutmillis the timeout in milliseconds
     */
    public static void sendPing(final ByteBuffer[] data, final WebSocketChannel wsChannel, final WebSocketCallback<Void> callback, long timeoutmillis) {
        sendInternal(mergeBuffers(data), WebSocketFrameType.PING, wsChannel, callback, null, timeoutmillis);
    }

    /**
     * Sends a complete ping message, invoking the callback when complete
     *
     * @param data The data to send
     * @param wsChannel The web socket channel
     * @param callback The callback to invoke on completion
     * @param context The context object that will be passed to the callback on completion
     * @param timeoutmillis the timeout in milliseconds
     */
    public static <T> void sendPing(final ByteBuffer[] data, final WebSocketChannel wsChannel, final WebSocketCallback<T> callback, T context, long timeoutmillis) {
        sendInternal(mergeBuffers(data), WebSocketFrameType.PING, wsChannel, callback, context, timeoutmillis);
    }

    /**
     * Sends a complete ping message, invoking the callback when complete
     * Automatically frees the pooled byte buffer when done.
     *
     * @param pooledData The data to send, it will be freed when done
     * @param wsChannel The web socket channel
     * @param callback The callback to invoke on completion
     */
    public static void sendPing(final PooledByteBuffer pooledData, final WebSocketChannel wsChannel, final WebSocketCallback<Void> callback) {
        sendInternal(pooledData, WebSocketFrameType.PING, wsChannel, callback, null, -1);
    }

    /**
     * Sends a complete ping message, invoking the callback when complete
     * Automatically frees the pooled byte buffer when done.
     *
     * @param pooledData The data to send, it will be freed when done
     * @param wsChannel The web socket channel
     * @param callback The callback to invoke on completion
     * @param context The context object that will be passed to the callback on completion
     */
    public static <T> void sendPing(final PooledByteBuffer pooledData, final WebSocketChannel wsChannel, final WebSocketCallback<T> callback, T context) {
        sendInternal(pooledData, WebSocketFrameType.PING, wsChannel, callback, context, -1);
    }

    /**
     * Sends a complete ping message, invoking the callback when complete
     * Automatically frees the pooled byte buffer when done.
     *
     * @param pooledData The data to send, it will be freed when done
     * @param wsChannel The web socket channel
     * @param callback The callback to invoke on completion
     * @param timeoutmillis the timeout in milliseconds
     */
    public static void sendPing(final PooledByteBuffer pooledData, final WebSocketChannel wsChannel, final WebSocketCallback<Void> callback, long timeoutmillis) {
        sendInternal(pooledData, WebSocketFrameType.PING, wsChannel, callback, null, timeoutmillis);
    }

    /**
     * Sends a complete ping message, invoking the callback when complete
     * Automatically frees the pooled byte buffer when done.
     *
     * @param pooledData The data to send, it will be freed when done
     * @param wsChannel The web socket channel
     * @param callback The callback to invoke on completion
     * @param context The context object that will be passed to the callback on completion
     * @param timeoutmillis the timeout in milliseconds
     */
    public static <T> void sendPing(final PooledByteBuffer pooledData, final WebSocketChannel wsChannel, final WebSocketCallback<T> callback, T context, long timeoutmillis) {
        sendInternal(pooledData, WebSocketFrameType.PING, wsChannel, callback, context, timeoutmillis);
    }

    /**
     * Sends a complete ping message using blocking IO
     *
     * @param data The data to send
     * @param wsChannel The web socket channel
     */
    public static void sendPingBlocking(final ByteBuffer data, final WebSocketChannel wsChannel) throws IOException {
        sendBlockingInternal(data, WebSocketFrameType.PING, wsChannel);
    }

    /**
     * Sends a complete ping message using blocking IO
     *
     * @param data The data to send
     * @param wsChannel The web socket channel
     */
    public static void sendPingBlocking(final ByteBuffer[] data, final WebSocketChannel wsChannel) throws IOException {
        sendBlockingInternal(mergeBuffers(data), WebSocketFrameType.PING, wsChannel);
    }

    /**
     * Sends a complete ping message using blocking IO
     * Automatically frees the pooled byte buffer when done.
     *
     * @param pooledData The data to send, it will be freed when done
     * @param wsChannel The web socket channel
     */
    public static void sendPingBlocking(final PooledByteBuffer pooledData, final WebSocketChannel wsChannel) throws IOException {
        sendBlockingInternal(pooledData, WebSocketFrameType.PING, wsChannel);
    }

    /**
     * Sends a complete pong message, invoking the callback when complete
     *
     * @param data The data to send
     * @param wsChannel The web socket channel
     * @param callback The callback to invoke on completion
     */
    public static void sendPong(final ByteBuffer data, final WebSocketChannel wsChannel, final WebSocketCallback<Void> callback) {
        sendInternal(data, WebSocketFrameType.PONG, wsChannel, callback, null, -1);
    }

    /**
     * Sends a complete pong message, invoking the callback when complete
     *
     * @param data The data to send
     * @param wsChannel The web socket channel
     * @param callback The callback to invoke on completion
     * @param context The context object that will be passed to the callback on completion
     */
    public static <T> void sendPong(final ByteBuffer data, final WebSocketChannel wsChannel, final WebSocketCallback<T> callback, T context) {
        sendInternal(data, WebSocketFrameType.PONG, wsChannel, callback, context, -1);
    }

    /**
     * Sends a complete pong message, invoking the callback when complete
     *
     * @param data The data to send
     * @param wsChannel The web socket channel
     * @param callback The callback to invoke on completion
     * @param timeoutmillis the timeout in milliseconds
     */
    public static void sendPong(final ByteBuffer data, final WebSocketChannel wsChannel, final WebSocketCallback<Void> callback, long timeoutmillis) {
        sendInternal(data, WebSocketFrameType.PONG, wsChannel, callback, null, timeoutmillis);
    }

    /**
     * Sends a complete pong message, invoking the callback when complete
     *
     * @param data The data to send
     * @param wsChannel The web socket channel
     * @param callback The callback to invoke on completion
     * @param context The context object that will be passed to the callback on completion
     * @param timeoutmillis the timeout in milliseconds
     */
    public static <T> void sendPong(final ByteBuffer data, final WebSocketChannel wsChannel, final WebSocketCallback<T> callback, T context, long timeoutmillis) {
        sendInternal(data, WebSocketFrameType.PONG, wsChannel, callback, context, timeoutmillis);
    }

    /**
     * Sends a complete pong message, invoking the callback when complete
     *
     * @param data The data to send
     * @param wsChannel The web socket channel
     * @param callback The callback to invoke on completion
     */
    public static void sendPong(final ByteBuffer[] data, final WebSocketChannel wsChannel, final WebSocketCallback<Void> callback) {
        sendInternal(mergeBuffers(data), WebSocketFrameType.PONG, wsChannel, callback, null, -1);
    }

    /**
     * Sends a complete pong message, invoking the callback when complete
     *
     * @param data The data to send
     * @param wsChannel The web socket channel
     * @param callback The callback to invoke on completion
     * @param context The context object that will be passed to the callback on completion
     */
    public static <T> void sendPong(final ByteBuffer[] data, final WebSocketChannel wsChannel, final WebSocketCallback<T> callback, T context) {
        sendInternal(mergeBuffers(data), WebSocketFrameType.PONG, wsChannel, callback, context, -1);
    }

    /**
     * Sends a complete pong message, invoking the callback when complete
     *
     * @param data The data to send
     * @param wsChannel The web socket channel
     * @param callback The callback to invoke on completion
     * @param timeoutmillis the timeout in milliseconds
     */
    public static void sendPong(final ByteBuffer[] data, final WebSocketChannel wsChannel, final WebSocketCallback<Void> callback, long timeoutmillis) {
        sendInternal(mergeBuffers(data), WebSocketFrameType.PONG, wsChannel, callback, null, timeoutmillis);
    }

    /**
     * Sends a complete pong message, invoking the callback when complete
     *
     * @param data The data to send
     * @param wsChannel The web socket channel
     * @param callback The callback to invoke on completion
     * @param context The context object that will be passed to the callback on completion
     * @param timeoutmillis the timeout in milliseconds
     */
    public static <T> void sendPong(final ByteBuffer[] data, final WebSocketChannel wsChannel, final WebSocketCallback<T> callback, T context, long timeoutmillis) {
        sendInternal(mergeBuffers(data), WebSocketFrameType.PONG, wsChannel, callback, context, timeoutmillis);
    }

    /**
     * Sends a complete pong message, invoking the callback when complete
     * Automatically frees the pooled byte buffer when done.
     *
     * @param pooledData The data to send, it will be freed when done
     * @param wsChannel The web socket channel
     * @param callback The callback to invoke on completion
     */
    public static void sendPong(final PooledByteBuffer pooledData, final WebSocketChannel wsChannel, final WebSocketCallback<Void> callback) {
        sendInternal(pooledData, WebSocketFrameType.PONG, wsChannel, callback, null, -1);
    }

    /**
     * Sends a complete pong message, invoking the callback when complete
     * Automatically frees the pooled byte buffer when done.
     *
     * @param pooledData The data to send, it will be freed when done
     * @param wsChannel The web socket channel
     * @param callback The callback to invoke on completion
     * @param context The context object that will be passed to the callback on completion
     */
    public static <T> void sendPong(final PooledByteBuffer pooledData, final WebSocketChannel wsChannel, final WebSocketCallback<T> callback, T context) {
        sendInternal(pooledData, WebSocketFrameType.PONG, wsChannel, callback, context, -1);
    }

    /**
     * Sends a complete pong message, invoking the callback when complete
     * Automatically frees the pooled byte buffer when done.
     *
     * @param pooledData The data to send, it will be freed when done
     * @param wsChannel The web socket channel
     * @param callback The callback to invoke on completion
     * @param timeoutmillis the timeout in milliseconds
     */
    public static void sendPong(final PooledByteBuffer pooledData, final WebSocketChannel wsChannel, final WebSocketCallback<Void> callback, long timeoutmillis) {
        sendInternal(pooledData, WebSocketFrameType.PONG, wsChannel, callback, null, timeoutmillis);
    }

    /**
     * Sends a complete pong message, invoking the callback when complete
     * Automatically frees the pooled byte buffer when done.
     *
     * @param pooledData The data to send, it will be freed when done
     * @param wsChannel The web socket channel
     * @param callback The callback to invoke on completion
     * @param context The context object that will be passed to the callback on completion
     * @param timeoutmillis the timeout in milliseconds
     */
    public static <T> void sendPong(final PooledByteBuffer pooledData, final WebSocketChannel wsChannel, final WebSocketCallback<T> callback, T context, long timeoutmillis) {
        sendInternal(pooledData, WebSocketFrameType.PONG, wsChannel, callback, context, timeoutmillis);
    }

    /**
     * Sends a complete pong message using blocking IO
     *
     * @param data The data to send
     * @param wsChannel The web socket channel
     */
    public static void sendPongBlocking(final ByteBuffer data, final WebSocketChannel wsChannel) throws IOException {
        sendBlockingInternal(data, WebSocketFrameType.PONG, wsChannel);
    }

    /**
     * Sends a complete pong message using blocking IO
     *
     * @param data The data to send
     * @param wsChannel The web socket channel
     */
    public static void sendPongBlocking(final ByteBuffer[] data, final WebSocketChannel wsChannel) throws IOException {
        sendBlockingInternal(mergeBuffers(data), WebSocketFrameType.PONG, wsChannel);
    }

    /**
     * Sends a complete pong message using blocking IO
     * Automatically frees the pooled byte buffer when done.
     *
     * @param pooledData The data to send, it will be freed when done
     * @param wsChannel The web socket channel
     */
    public static void sendPongBlocking(final PooledByteBuffer pooledData, final WebSocketChannel wsChannel) throws IOException {
        sendBlockingInternal(pooledData, WebSocketFrameType.PONG, wsChannel);
    }

    /**
     * Sends a complete binary message, invoking the callback when complete
     *
     * @param data The data to send
     * @param wsChannel The web socket channel
     * @param callback The callback to invoke on completion
     */
    public static void sendBinary(final ByteBuffer data, final WebSocketChannel wsChannel, final WebSocketCallback<Void> callback) {
        sendInternal(data, WebSocketFrameType.BINARY, wsChannel, callback, null, -1);
    }

    /**
     * Sends a complete binary message, invoking the callback when complete
     *
     * @param data The data to send
     * @param wsChannel The web socket channel
     * @param callback The callback to invoke on completion
     * @param context The context object that will be passed to the callback on completion
     */
    public static <T> void sendBinary(final ByteBuffer data, final WebSocketChannel wsChannel, final WebSocketCallback<T> callback, T context) {
        sendInternal(data, WebSocketFrameType.BINARY, wsChannel, callback, context, -1);
    }

    /**
     * Sends a complete binary message, invoking the callback when complete
     *
     * @param data The data to send
     * @param wsChannel The web socket channel
     * @param callback The callback to invoke on completion
     * @param timeoutmillis the timeout in milliseconds
     */
    public static void sendBinary(final ByteBuffer data, final WebSocketChannel wsChannel, final WebSocketCallback<Void> callback, long timeoutmillis) {
        sendInternal(data, WebSocketFrameType.BINARY, wsChannel, callback, null, timeoutmillis);
    }

    /**
     * Sends a complete binary message, invoking the callback when complete
     *
     * @param data The data to send
     * @param wsChannel The web socket channel
     * @param callback The callback to invoke on completion
     * @param context The context object that will be passed to the callback on completion
     * @param timeoutmillis the timeout in milliseconds
     */
    public static <T> void sendBinary(final ByteBuffer data, final WebSocketChannel wsChannel, final WebSocketCallback<T> callback, T context, long timeoutmillis) {
        sendInternal(data, WebSocketFrameType.BINARY, wsChannel, callback, context, timeoutmillis);
    }

    /**
     * Sends a complete binary message, invoking the callback when complete
     *
     * @param data The data to send
     * @param wsChannel The web socket channel
     * @param callback The callback to invoke on completion
     */
    public static void sendBinary(final ByteBuffer[] data, final WebSocketChannel wsChannel, final WebSocketCallback<Void> callback) {
        sendInternal(mergeBuffers(data), WebSocketFrameType.BINARY, wsChannel, callback, null, -1);
    }

    /**
     * Sends a complete binary message, invoking the callback when complete
     *
     * @param data The data to send
     * @param wsChannel The web socket channel
     * @param callback The callback to invoke on completion
     * @param context The context object that will be passed to the callback on completion
     */
    public static <T> void sendBinary(final ByteBuffer[] data, final WebSocketChannel wsChannel, final WebSocketCallback<T> callback, T context) {
        sendInternal(mergeBuffers(data), WebSocketFrameType.BINARY, wsChannel, callback, context, -1);
    }

    /**
     * Sends a complete binary message, invoking the callback when complete
     *
     * @param data The data to send
     * @param wsChannel The web socket channel
     * @param callback The callback to invoke on completion
     * @param timeoutmillis the timeout in milliseconds
     */
    public static void sendBinary(final ByteBuffer[] data, final WebSocketChannel wsChannel, final WebSocketCallback<Void> callback, long timeoutmillis) {
        sendInternal(mergeBuffers(data), WebSocketFrameType.BINARY, wsChannel, callback, null, timeoutmillis);
    }

    /**
     * Sends a complete binary message, invoking the callback when complete
     *
     * @param data The data to send
     * @param wsChannel The web socket channel
     * @param callback The callback to invoke on completion
     * @param context The context object that will be passed to the callback on completion
     * @param timeoutmillis the timeout in milliseconds
     */
    public static <T> void sendBinary(final ByteBuffer[] data, final WebSocketChannel wsChannel, final WebSocketCallback<T> callback, T context, long timeoutmillis) {
        sendInternal(mergeBuffers(data), WebSocketFrameType.BINARY, wsChannel, callback, context, timeoutmillis);
    }

    /**
     * Sends a complete binary message, invoking the callback when complete
     * Automatically frees the pooled byte buffer when done.
     *
     * @param pooledData The data to send, it will be freed when done
     * @param wsChannel The web socket channel
     * @param callback The callback to invoke on completion
     */
    public static void sendBinary(final PooledByteBuffer pooledData, final WebSocketChannel wsChannel, final WebSocketCallback<Void> callback) {
        sendInternal(pooledData, WebSocketFrameType.BINARY, wsChannel, callback, null, -1);
    }

    /**
     * Sends a complete binary message, invoking the callback when complete
     * Automatically frees the pooled byte buffer when done.
     *
     * @param pooledData The data to send, it will be freed when done
     * @param wsChannel The web socket channel
     * @param callback The callback to invoke on completion
     * @param context The context object that will be passed to the callback on completion
     */
    public static <T> void sendBinary(final PooledByteBuffer pooledData, final WebSocketChannel wsChannel, final WebSocketCallback<T> callback, T context) {
        sendInternal(pooledData, WebSocketFrameType.BINARY, wsChannel, callback, context, -1);
    }

    /**
     * Sends a complete binary message, invoking the callback when complete
     * Automatically frees the pooled byte buffer when done.
     *
     * @param pooledData The data to send, it will be freed when done
     * @param wsChannel The web socket channel
     * @param callback The callback to invoke on completion
     * @param timeoutmillis the timeout in milliseconds
     */
    public static void sendBinary(final PooledByteBuffer pooledData, final WebSocketChannel wsChannel, final WebSocketCallback<Void> callback, long timeoutmillis) {
        sendInternal(pooledData, WebSocketFrameType.BINARY, wsChannel, callback, null, timeoutmillis);
    }

    /**
     * Sends a complete binary message, invoking the callback when complete
     * Automatically frees the pooled byte buffer when done.
     *
     * @param pooledData The data to send, it will be freed when done
     * @param wsChannel The web socket channel
     * @param callback The callback to invoke on completion
     * @param context The context object that will be passed to the callback on completion
     * @param timeoutmillis the timeout in milliseconds
     */
    public static <T> void sendBinary(final PooledByteBuffer pooledData, final WebSocketChannel wsChannel, final WebSocketCallback<T> callback, T context, long timeoutmillis) {
        sendInternal(pooledData, WebSocketFrameType.BINARY, wsChannel, callback, context, timeoutmillis);
    }

    /**
     * Sends a complete binary message using blocking IO
     *
     * @param data The data to send
     * @param wsChannel The web socket channel
     */
    public static void sendBinaryBlocking(final ByteBuffer data, final WebSocketChannel wsChannel) throws IOException {
        sendBlockingInternal(data, WebSocketFrameType.BINARY, wsChannel);
    }

    /**
     * Sends a complete binary message using blocking IO
     *
     * @param data The data to send
     * @param wsChannel The web socket channel
     */
    public static void sendBinaryBlocking(final ByteBuffer[] data, final WebSocketChannel wsChannel) throws IOException {
        sendBlockingInternal(mergeBuffers(data), WebSocketFrameType.BINARY, wsChannel);
    }

    /**
     * Sends a complete binary message using blocking IO
     * Automatically frees the pooled byte buffer when done.
     *
     * @param pooledData The data to send, it will be freed when done
     * @param wsChannel The web socket channel
     */
    public static void sendBinaryBlocking(final PooledByteBuffer pooledData, final WebSocketChannel wsChannel) throws IOException {
        sendBlockingInternal(pooledData, WebSocketFrameType.BINARY, wsChannel);
    }

    /**
     * Sends a complete close message, invoking the callback when complete
     *
     * @param data The data to send
     * @param wsChannel The web socket channel
     * @param callback The callback to invoke on completion
     */
    public static void sendClose(final ByteBuffer data, final WebSocketChannel wsChannel, final WebSocketCallback<Void> callback) {
        CloseMessage sm = new CloseMessage(data);
        sendClose(sm, wsChannel, callback);
    }

    /**
     * Sends a complete close message, invoking the callback when complete
     *
     * @param data The data to send
     * @param wsChannel The web socket channel
     * @param callback The callback to invoke on completion
     * @param context The context object that will be passed to the callback on completion
     */
    public static <T> void sendClose(final ByteBuffer data, final WebSocketChannel wsChannel, final WebSocketCallback<T> callback, T context) {
        CloseMessage sm = new CloseMessage(data);
        sendClose(sm, wsChannel, callback, context);
    }

    /**
     * Sends a complete close message, invoking the callback when complete
     *
     * @param data The data to send
     * @param wsChannel The web socket channel
     * @param callback The callback to invoke on completion
     */
    public static void sendClose(final ByteBuffer[] data, final WebSocketChannel wsChannel, final WebSocketCallback<Void> callback) {
        CloseMessage sm = new CloseMessage(data);
        sendClose(sm, wsChannel, callback);
    }

    /**
     * Sends a complete close message, invoking the callback when complete
     *
     * @param data The data to send
     * @param wsChannel The web socket channel
     * @param callback The callback to invoke on completion
     * @param context The context object that will be passed to the callback on completion
     */
    public static <T> void sendClose(final ByteBuffer[] data, final WebSocketChannel wsChannel, final WebSocketCallback<T> callback, T context) {
        CloseMessage sm = new CloseMessage(data);
        sendClose(sm, wsChannel, callback, context);
    }

    /**
     * Sends a complete close message, invoking the callback when complete
     *
     * @param code The close code
     * @param wsChannel The web socket channel
     * @param callback The callback to invoke on completion
     */
    public static void sendClose(final int code, String reason, final WebSocketChannel wsChannel, final WebSocketCallback<Void> callback) {
        sendClose(new CloseMessage(code, reason), wsChannel, callback);
    }

    /**
     * Sends a complete close message, invoking the callback when complete
     *
     * @param code The close code
     * @param wsChannel The web socket channel
     * @param callback The callback to invoke on completion
     * @param context The context object that will be passed to the callback on completion
     */
    public static <T> void sendClose(final int code, String reason, final WebSocketChannel wsChannel, final WebSocketCallback<T> callback, T context) {
        sendClose(new CloseMessage(code, reason), wsChannel, callback, context);
    }

    /**
     * Sends a complete close message, invoking the callback when complete
     *
     * @param closeMessage The close message
     * @param wsChannel The web socket channel
     * @param callback The callback to invoke on completion
     */
    public static void sendClose(final CloseMessage closeMessage, final WebSocketChannel wsChannel, final WebSocketCallback<Void> callback) {
        sendClose(closeMessage, wsChannel, callback, null);
    }

    /**
     * Sends a complete close message, invoking the callback when complete
     *
     * @param closeMessage The close message
     * @param wsChannel The web socket channel
     * @param callback The callback to invoke on completion
     * @param context The context object that will be passed to the callback on completion
     */
    public static <T> void sendClose(final CloseMessage closeMessage, final WebSocketChannel wsChannel, final WebSocketCallback<T> callback, T context) {
        wsChannel.setCloseCode(closeMessage.getCode());
        wsChannel.setCloseReason(closeMessage.getReason());
        sendInternal(closeMessage.toByteBuffer(), WebSocketFrameType.CLOSE, wsChannel, callback, context, -1);
    }

    /**
     * Sends a complete close message, invoking the callback when complete
     *
     * @param closeMessage the close message
     * @param wsChannel The web socket channel
     */
    public static void sendCloseBlocking(final CloseMessage closeMessage, final WebSocketChannel wsChannel) throws IOException {
        wsChannel.setCloseReason(closeMessage.getReason());
        wsChannel.setCloseCode(closeMessage.getCode());
        sendBlockingInternal(closeMessage.toByteBuffer(), WebSocketFrameType.CLOSE, wsChannel);
    }
    /**
     * Sends a complete close message, invoking the callback when complete
     *
     * @param code
     * @param wsChannel The web socket channel
     */
    public static void sendCloseBlocking(final int code, String reason, final WebSocketChannel wsChannel) throws IOException {
        sendCloseBlocking(new CloseMessage(code, reason), wsChannel);
    }
    /**
     * Sends a complete close message, invoking the callback when complete
     *
     * @param data The data to send
     * @param wsChannel The web socket channel
     */
    public static void sendCloseBlocking(final ByteBuffer data, final WebSocketChannel wsChannel) throws IOException {
        sendCloseBlocking(new CloseMessage(data), wsChannel);
    }

    /**
     * Sends a complete close message, invoking the callback when complete
     *
     * @param data The data to send
     * @param wsChannel The web socket channel
     */
    public static void sendCloseBlocking(final ByteBuffer[] data, final WebSocketChannel wsChannel) throws IOException {
        sendCloseBlocking(new CloseMessage(data), wsChannel);
    }

    private static <T> void sendInternal(final ByteBuffer data, WebSocketFrameType type, final WebSocketChannel wsChannel, final WebSocketCallback<T> callback, T context, long timeoutmillis) {
        sendInternal(new ImmediatePooledByteBuffer(data), type, wsChannel, callback, context, timeoutmillis);
    }

    private static <T> void sendInternal(final PooledByteBuffer pooledData, WebSocketFrameType type, final WebSocketChannel wsChannel, final WebSocketCallback<T> callback, T context, long timeoutmillis) {
        boolean closePooledData = true;
        try {
            StreamSinkFrameChannel channel = wsChannel.send(type);
            // TODO chunk data into some MTU-like thing to control packet size
            closePooledData = false; // channel.send takes ownership of pooledData so it no longer needs to be closed
            if(!channel.send(pooledData)) {
                throw WebSocketMessages.MESSAGES.unableToSendOnNewChannel();
            }
            flushChannelAsync(wsChannel, callback, channel, context, timeoutmillis);
        } catch (IOException e) {
            if (callback != null) {
                callback.onError(wsChannel, context, e);
            } else {
                IoUtils.safeClose(wsChannel);
            }
        } finally {
            if ( closePooledData ) {
                pooledData.close();
            }
        }
    }

    private static <T> void flushChannelAsync(final WebSocketChannel wsChannel, final WebSocketCallback<T> callback, StreamSinkFrameChannel channel, final T context, long timeoutmillis) throws IOException {
        final WebSocketFrameType type = channel.getType();
        channel.shutdownWrites();
        if (!channel.flush()) {
            channel.getWriteSetter().set(flushingChannelListener(
                    new ChannelListener<StreamSinkFrameChannel>() {
                        @Override
                        public void handleEvent(StreamSinkFrameChannel channel) {
                            if (callback != null) {
                                callback.complete(wsChannel, context);
                            }
                            if (type == WebSocketFrameType.CLOSE && wsChannel.isCloseFrameReceived()) {
                                IoUtils.safeClose(wsChannel);
                            }
                            //we explicitly set the channel to null, as in some situations this
                            //listener may get invoked twice
                            channel.getWriteSetter().set(null);
                        }
                    }, new ChannelExceptionHandler<StreamSinkFrameChannel>() {
                        @Override
                        public void handleException(StreamSinkFrameChannel channel, IOException exception) {
                            if (callback != null) {
                                callback.onError(wsChannel, context, exception);
                            }
                            IoUtils.safeClose(channel, wsChannel);
                            //we explicitly set the channel to null, as in some situations this
                            //listener may get invoked twice
                            channel.getWriteSetter().set(null);
                        }
                    }
            ));
            if(timeoutmillis > 0) {
                setupTimeout(channel, timeoutmillis);
            }
            channel.resumeWrites();
            return;
        }
        if (callback != null) {
            callback.complete(wsChannel, context);
        }
    }

    private static void setupTimeout(final StreamSinkFrameChannel channel, long timeoutmillis) {
        final XnioExecutor.Key key = WorkerUtils.executeAfter(channel.getIoThread(), new Runnable() {
            @Override
            public void run() {
                if (channel.isOpen()) {
                    IoUtils.safeClose(channel);
                }
            }
        }, timeoutmillis, TimeUnit.MILLISECONDS);
        channel.getCloseSetter().set(new ChannelListener<StreamSinkFrameChannel>() {
            @Override
            public void handleEvent(StreamSinkFrameChannel channel) {
                key.remove();
            }
        });
    }

    private static void sendBlockingInternal(final ByteBuffer data, WebSocketFrameType type, final WebSocketChannel wsChannel) throws IOException {
        sendBlockingInternal(new ImmediatePooledByteBuffer(data), type, wsChannel);
    }

    private static void sendBlockingInternal(final PooledByteBuffer pooledData, WebSocketFrameType type, final WebSocketChannel wsChannel) throws IOException {
        boolean closePooledData = true;
        try {
            StreamSinkFrameChannel channel = wsChannel.send(type);
            // TODO chunk data into some MTU-like thing to control packet size
            closePooledData = false; // channel.send takes ownership of pooledData so it no longer needs to be closed
            if(!channel.send(pooledData)) {
                throw WebSocketMessages.MESSAGES.unableToSendOnNewChannel();
            }
            channel.shutdownWrites();
            while (!channel.flush()) {
                channel.awaitWritable();
            }
            if (type == WebSocketFrameType.CLOSE && wsChannel.isCloseFrameReceived()) {
                IoUtils.safeClose(wsChannel);
            }
        } finally {
            if (closePooledData) {
                pooledData.close();
            }
        }
    }

    private WebSockets() {

    }

    public static ByteBuffer mergeBuffers(ByteBuffer... payload) {
        int size = (int) Buffers.remaining(payload);
        if (size == 0) {
            return Buffers.EMPTY_BYTE_BUFFER;
        }
        ByteBuffer buffer = ByteBuffer.allocate(size);
        for (ByteBuffer buf : payload) {
            buffer.put(buf);
        }
        buffer.flip();
        return buffer;
    }
}
