/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF 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
 *
 *   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 org.apache.qpid.jms.test.testpeer;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.nio.ByteBuffer;

import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLServerSocket;
import javax.net.ssl.SSLServerSocketFactory;

import org.apache.qpid.jms.test.testpeer.basictypes.AmqpError;
import org.apache.qpid.proton.amqp.Binary;
import org.apache.qpid.proton.engine.impl.AmqpHeader;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

class TestAmqpPeerRunner implements Runnable
{
    private static final Logger LOGGER = LoggerFactory.getLogger(TestAmqpPeerRunner.class);

    private static final int PORT = 25672;

    private final ServerSocket _serverSocket;
    private final boolean useFixedPort = Boolean.getBoolean("testPeerUsesFixedPort");

    private Socket _clientSocket;
    private OutputStream _networkOutputStream;

    private final Object _inputHandlingLock = new Object();
    private final TestAmqpPeer _peer;
    private final TestFrameParser _testFrameParser;
    private volatile boolean _suppressReadExceptionOnClose;
    private volatile boolean _exitReadLoopEarly;
    private volatile boolean _sendSaslHeaderPreEmptively;

    private volatile Throwable _throwable;

    private boolean needClientCert;

    public TestAmqpPeerRunner(TestAmqpPeer peer, SSLContext sslContext, boolean needClientCert) throws IOException
    {
        int port = useFixedPort ? PORT : 0;
        this.needClientCert = needClientCert;

        if (sslContext == null)
        {
            _serverSocket = new ServerSocket(port);
        }
        else
        {
            SSLServerSocketFactory socketFactory = sslContext.getServerSocketFactory();
            _serverSocket = socketFactory.createServerSocket(port);

            SSLServerSocket sslServerSocket = (SSLServerSocket) _serverSocket;
            if (this.needClientCert)
            {
                sslServerSocket.setNeedClientAuth(true);
            }
        }

        _testFrameParser = new TestFrameParser(peer);
        _peer = peer;
    }

    @Override
    public void run()
    {
        boolean attemptingRead = false;
        try
        (
            Socket clientSocket = _serverSocket.accept();
            InputStream networkInputStream = clientSocket.getInputStream();
            OutputStream networkOutputStream = clientSocket.getOutputStream();
        )
        {
            _clientSocket = clientSocket;
            _clientSocket.setTcpNoDelay(true);
            _networkOutputStream = networkOutputStream;

            if (_sendSaslHeaderPreEmptively) {
                byte[] bytes = AmqpHeader.SASL_HEADER;
                LOGGER.debug("Sending header pre-emptively: {}", new Binary(bytes));
                _networkOutputStream.write(bytes);
            }

            int bytesRead;
            byte[] networkInputBytes = new byte[1024];

            LOGGER.trace("Attempting read");
            attemptingRead = true;
            while((bytesRead = networkInputStream.read(networkInputBytes)) != -1)
            {
                attemptingRead = false;
                //prevent stop() from killing the socket while the frame parser might be using it handling input
                synchronized(_inputHandlingLock)
                {
                    ByteBuffer networkInputByteBuffer = ByteBuffer.wrap(networkInputBytes, 0, bytesRead);

                    LOGGER.debug("Read: {} ({} bytes)", new Binary(networkInputBytes, 0, bytesRead), bytesRead);

                    try {
                        _testFrameParser.input(networkInputByteBuffer);
                    } catch (Exception ex) {
                        try {
                            _peer.sendConnectionCloseImmediately(AmqpError.INTERNAL_ERROR, "Problem in peer: " + ex);
                        } catch (Exception ex2) {
                            LOGGER.debug("Exception while sending close frame during handling of previous exception", ex2);
                        }

                        throw ex;
                    }
                }

                if(_exitReadLoopEarly)
                {
                    LOGGER.trace("Exiting read loop early");
                    break;
                }

                LOGGER.trace("Attempting read");
                attemptingRead = true;
            }

            LOGGER.trace("Exited read loop");
        }
        catch (Throwable t)
        {
            if (attemptingRead && _suppressReadExceptionOnClose && t instanceof IOException)
            {
                LOGGER.debug("Caught exception during read, suppressing as expected: " + t, t);
            }
            else if(!_serverSocket.isClosed())
            {
                LOGGER.error("Problem in peer", t);
                _throwable = t;
            }
            else
            {
                if(t instanceof IOException)
                {
                    LOGGER.debug("Caught throwable, ignoring as socket is closed: " + t, t);
                }
                else
                {
                    LOGGER.debug("Caught throwable after socket is closed: " + t);
                    _throwable = t;
                }
            }
        }
        finally
        {
            try
            {
                _serverSocket.close();
            }
            catch (IOException e)
            {
                LOGGER.error("Unable to close server socket", e);
            }
        }
    }

    public void stop() throws IOException
    {
        //wait for the frame parser to handle any input it already had
        synchronized(_inputHandlingLock)
        {
            try
            {
                _serverSocket.close();
            }
            finally
            {
                if(_clientSocket != null)
                {
                    _clientSocket.close();
                }
            }
        }
    }

    public void expectHeader()
    {
        _testFrameParser.expectHeader();
    }

    public void sendBytes(byte[] bytes)
    {
        LOGGER.debug("Sending: {} ({} bytes)", new Binary(bytes), bytes.length);
        try
        {
            _networkOutputStream.write(bytes);
            _networkOutputStream.flush();
        }
        catch (IOException e)
        {
            throw new RuntimeException(e);
        }
    }

    public Throwable getException()
    {
        return _throwable;
    }

    public int getServerPort()
    {
        if (_serverSocket != null)
        {
            return _serverSocket.getLocalPort();
        }

        return -1;
    }

    public Socket getClientSocket()
    {
        return _clientSocket;
    }

    public void setSuppressReadExceptionOnClose(boolean suppress)
    {
        _suppressReadExceptionOnClose = suppress;
    }

    public boolean isNeedClientCert() {
        return needClientCert;
    }

    public void exitReadLoopEarly() {
        _exitReadLoopEarly = true;
    }

    public void setSendSaslHeaderPreEmptively(boolean sendSaslHeaderPreEmptively) {
        _sendSaslHeaderPreEmptively = sendSaslHeaderPreEmptively;
    }

    public boolean isSendSaslHeaderPreEmptively() {
        return _sendSaslHeaderPreEmptively;
    }

    public boolean isSSL() {
        return _serverSocket instanceof SSLServerSocket;
    }
}
