/*
 * 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.protocol.server;

import io.undertow.server.DefaultByteBufferPool;
import io.undertow.server.HttpHandler;
import io.undertow.server.protocol.http.HttpOpenListener;
import io.undertow.util.Transfer;
import io.undertow.websockets.core.StreamSinkFrameChannel;
import io.undertow.websockets.core.StreamSourceFrameChannel;
import io.undertow.websockets.core.WebSocketChannel;
import io.undertow.websockets.core.WebSocketFrameType;
import io.undertow.websockets.WebSocketConnectionCallback;
import io.undertow.websockets.WebSocketProtocolHandshakeHandler;
import io.undertow.websockets.spi.WebSocketHttpExchange;
import org.xnio.ChannelExceptionHandler;
import org.xnio.ChannelListener;
import org.xnio.ChannelListeners;
import org.xnio.OptionMap;
import org.xnio.Options;
import org.xnio.StreamConnection;
import org.xnio.Xnio;
import org.xnio.XnioWorker;
import org.xnio.channels.AcceptingChannel;

import java.io.IOException;
import java.net.InetSocketAddress;

/**
 * This class is intended for use with testing against the Python
 * <a href="http://www.tavendo.de/autobahn/testsuite.html">AutoBahn test suite</a>.
 * <p/>
 * Autobahn installation documentation can be found <a href="http://autobahn.ws/testsuite/installation">here</a>.
 * <p/>
 * <h3>How to run the tests on Linux/OSX.</h3>
 * <p/>
 * <p>01. Install AutoBahn: <tt>sudo easy_install autobahntestsuite</tt>.  Test using <tt>wstest --help</tt>.
 * <p/>
 * <p>02. Create a directory for test configuration and results: <tt>mkdir ~/autobahn</tt> <tt>cd ~/autobahn</tt>.
 * <p/>
 * <p>03. Create <tt>fuzzing_client_spec.json</tt> in the above directory
 * {@code
 * {
 * "options": {"failByDrop": false},
 * "outdir": "./reports/servers",
 * <p/>
 * "servers": [
 * {"agent": "Netty4",
 * "url": "ws://localhost:9000",
 * "options": {"version": 18}}
 * ],
 * <p/>
 * "cases": ["*"],
 * "exclude-cases": [],
 * "exclude-agent-cases": {}
 * }
 * }
 * <p/>
 * <p>04. Run the <tt>AutobahnServer</tt> located in this package. If you are in Eclipse IDE, right click on
 * <tt>AutobahnServer.java</tt> and select Run As > Java Application.
 * <p/>
 * <p>05. Run the Autobahn test <tt>wstest -m fuzzingclient -s fuzzing_client_spec.json</tt>.
 * <p/>
 * <p>06. See the results in <tt>./reports/servers/index.html</tt>
 *
 * @author <a href="mailto:nmaurer@redhat.com">Norman Maurer</a>
 */
public class AutobahnWebSocketServer {
    private HttpOpenListener openListener;
    private XnioWorker worker;
    private AcceptingChannel<StreamConnection> server;
    private Xnio xnio;
    private final int port;

    public static WebSocketChannel current;

    public AutobahnWebSocketServer(int port) {
        this.port = port;
    }

    private static final ChannelExceptionHandler<StreamSinkFrameChannel> W_H = new ChannelExceptionHandler<StreamSinkFrameChannel>() {
        @Override
        public void handleException(StreamSinkFrameChannel channel, IOException exception) {
            exception.printStackTrace();
        }
    };

    private static final ChannelExceptionHandler<StreamSourceFrameChannel> R_H = new ChannelExceptionHandler<StreamSourceFrameChannel>() {
        @Override
        public void handleException(StreamSourceFrameChannel channel, IOException exception) {
            exception.printStackTrace();
        }
    };

    public void run() {
        xnio = Xnio.getInstance();
        try {
            worker = xnio.createWorker(OptionMap.builder()
                    .set(Options.WORKER_WRITE_THREADS, 4)
                    .set(Options.WORKER_READ_THREADS, 4)
                    .set(Options.CONNECTION_HIGH_WATER, 1000000)
                    .set(Options.CONNECTION_LOW_WATER, 1000000)
                    .set(Options.WORKER_TASK_CORE_THREADS, 10)
                    .set(Options.WORKER_TASK_MAX_THREADS, 12)
                    .set(Options.TCP_NODELAY, true)
                    .set(Options.CORK, true)
                    .getMap());

            OptionMap serverOptions = OptionMap.builder()
                    .set(Options.WORKER_ACCEPT_THREADS, 4)
                    .set(Options.TCP_NODELAY, true)
                    .set(Options.REUSE_ADDRESSES, true)
                    .getMap();
            openListener = new HttpOpenListener(new DefaultByteBufferPool(false, 8192));
            ChannelListener acceptListener = ChannelListeners.openListenerAdapter(openListener);
            server = worker.createStreamConnectionServer(new InetSocketAddress(port), acceptListener, serverOptions);


            setRootHandler(getRootHandler());
            server.resumeAccepts();
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    public static WebSocketProtocolHandshakeHandler getRootHandler() {
        return new WebSocketProtocolHandshakeHandler(new WebSocketConnectionCallback() {
            @Override
            public void onConnect(final WebSocketHttpExchange exchange, final WebSocketChannel channel) {
                current = channel;
                channel.getReceiveSetter().set(new Receiver());
                channel.resumeReceives();
            }
        });
    }

    private static final class Receiver implements ChannelListener<WebSocketChannel> {

        @Override
        public void handleEvent(final WebSocketChannel channel) {
            try {
                final StreamSourceFrameChannel ws = channel.receive();
                if (ws != null) {
                    StreamSinkFrameChannel target;
                    if (ws.getType() == WebSocketFrameType.PING ||
                            ws.getType() == WebSocketFrameType.CLOSE) {
                        target = channel.send(ws.getType() == WebSocketFrameType.PING ? WebSocketFrameType.PONG : WebSocketFrameType.CLOSE);
                    } else if (ws.getType() == WebSocketFrameType.PONG) {
                        ws.getReadSetter().set(ChannelListeners.drainListener(Long.MAX_VALUE, null, null));
                        ws.wakeupReads();
                        return;
                    } else {
                        target = channel.send(ws.getType());
                    }
                    Transfer.initiateTransfer(ws, target, null, ChannelListeners.writeShutdownChannelListener(new ChannelListener<StreamSinkFrameChannel>() {
                        @Override
                        public void handleEvent(StreamSinkFrameChannel c) {
                            channel.resumeReceives();
                        }
                    }, W_H), R_H, W_H, channel.getBufferPool());

                }
            } catch (IOException e) {
                e.printStackTrace();
                //IoUtils.safeClose(channel);
            }
        }
    }

    /**
     * Sets the root handler for the default web server
     *
     * @param rootHandler The handler to use
     */
    private void setRootHandler(HttpHandler rootHandler) {
        openListener.setRootHandler(rootHandler);
    }

    public static void main(String[] args) {
        new AutobahnWebSocketServer(7777).run();
    }


}
