// Copyright (c) 2007-Present Pivotal Software, Inc.  All rights reserved.
//
// This software, the RabbitMQ Java client library, is triple-licensed under the
// Mozilla Public License 1.1 ("MPL"), the GNU General Public License version 2
// ("GPL") and the Apache License version 2 ("ASL"). For the MPL, please see
// LICENSE-MPL-RabbitMQ. For the GPL, please see LICENSE-GPL2.  For the ASL,
// please see LICENSE-APACHE2.
//
// This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY KIND,
// either express or implied. See the LICENSE file for specific language governing
// rights and limitations of this software.
//
// If you have any questions regarding licensing, please contact us at
// info@rabbitmq.com.

package com.rabbitmq.client.impl;

import com.rabbitmq.client.AMQP;

import java.io.*;
import java.net.InetAddress;
import java.net.Socket;
import java.net.SocketException;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;

/**
 * A socket-based frame handler.
 */

public class SocketFrameHandler implements FrameHandler {
    /** The underlying socket */
    private final Socket _socket;

    /**
     * Optional {@link ExecutorService} for final flush.
     */
    private final ExecutorService _shutdownExecutor;

    /** Socket's inputstream - data from the broker - synchronized on */
    private final DataInputStream _inputStream;

    /** Socket's outputstream - data to the broker - synchronized on */
    private final DataOutputStream _outputStream;

    /** Time to linger before closing the socket forcefully. */
    public static final int SOCKET_CLOSING_TIMEOUT = 1;

    /**
     * @param socket the socket to use
     */
    public SocketFrameHandler(Socket socket) throws IOException {
        this(socket, null);
    }

    /**
     * @param socket the socket to use
     */
    public SocketFrameHandler(Socket socket, ExecutorService shutdownExecutor) throws IOException {
        _socket = socket;
        _shutdownExecutor = shutdownExecutor;

        _inputStream = new DataInputStream(new BufferedInputStream(socket.getInputStream()));
        _outputStream = new DataOutputStream(new BufferedOutputStream(socket.getOutputStream()));
    }

    @Override
    public InetAddress getAddress() {
        return _socket.getInetAddress();
    }

    @Override
    public InetAddress getLocalAddress() {
        return _socket.getLocalAddress();
    }

    // For testing only
    public DataInputStream getInputStream() {
        return _inputStream;
    }

    @Override
    public int getPort() {
        return _socket.getPort();
    }

    @Override
    public int getLocalPort() {
        return _socket.getLocalPort();
    }

    @Override
    public void setTimeout(int timeoutMs)
        throws SocketException
    {
        _socket.setSoTimeout(timeoutMs);
    }

    @Override
    public int getTimeout()
        throws SocketException
    {
        return _socket.getSoTimeout();
    }

    /**
     * Write a 0-8-style connection header to the underlying socket,
     * containing the specified version information, kickstarting the
     * AMQP protocol version negotiation process.
     *
     * @param major major protocol version number
     * @param minor minor protocol version number
     * @throws IOException if there is a problem accessing the connection
     * @see #sendHeader()
     */
    public void sendHeader(int major, int minor) throws IOException {
        synchronized (_outputStream) {
            _outputStream.write("AMQP".getBytes("US-ASCII"));
            _outputStream.write(1);
            _outputStream.write(1);
            _outputStream.write(major);
            _outputStream.write(minor);
            _outputStream.flush();
        }
    }

   /**
     * Write a 0-9-1-style connection header to the underlying socket,
     * containing the specified version information, kickstarting the
     * AMQP protocol version negotiation process.
     *
     * @param major major protocol version number
     * @param minor minor protocol version number
     * @param revision protocol revision number
     * @throws IOException if there is a problem accessing the connection
     * @see #sendHeader()
     */
  public void sendHeader(int major, int minor, int revision) throws IOException {
        synchronized (_outputStream) {
            _outputStream.write("AMQP".getBytes("US-ASCII"));
            _outputStream.write(0);
            _outputStream.write(major);
            _outputStream.write(minor);
            _outputStream.write(revision);
            _outputStream.flush();
        }
    }

    @Override
    public void sendHeader() throws IOException {
        sendHeader(AMQP.PROTOCOL.MAJOR, AMQP.PROTOCOL.MINOR, AMQP.PROTOCOL.REVISION);
    }

    @Override
    public void initialize(AMQConnection connection) {
        connection.startMainLoop();
    }

    @Override
    public Frame readFrame() throws IOException {
        synchronized (_inputStream) {
            return Frame.readFrom(_inputStream);
        }
    }

    @Override
    public void writeFrame(Frame frame) throws IOException {
        synchronized (_outputStream) {
            frame.writeTo(_outputStream);
        }
    }

    @Override
    public void flush() throws IOException {
        _outputStream.flush();
    }

    @Override
    public void close() {
        try { _socket.setSoLinger(true, SOCKET_CLOSING_TIMEOUT); } catch (Exception _e) {}
        // async flush if possible
        // see https://github.com/rabbitmq/rabbitmq-java-client/issues/194
        Callable<Void> flushCallable = new Callable<Void>() {
            @Override
            public Void call() throws Exception {
                flush();
                return null;
            }
        };
        Future<Void> flushTask = null;
        try {
            if(this._shutdownExecutor == null) {
                flushCallable.call();
            } else {
                flushTask = this._shutdownExecutor.submit(flushCallable);
                flushTask.get(SOCKET_CLOSING_TIMEOUT, TimeUnit.SECONDS);
            }
        } catch(Exception e) {
            if(flushTask != null) {
                flushTask.cancel(true);
            }
        }
        try { _socket.close();                                   } catch (Exception _e) {}
    }
}
