/*
 * Copyright 2014-2017 Real Logic Ltd.
 *
 * 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.aeron.driver.media;

import io.aeron.driver.Configuration;
import io.aeron.driver.status.ChannelEndpointStatus;
import io.aeron.protocol.HeaderFlyweight;
import org.agrona.LangUtil;
import org.agrona.concurrent.UnsafeBuffer;
import org.agrona.concurrent.errors.DistinctErrorLog;
import org.agrona.concurrent.status.AtomicCounter;

import java.io.IOException;
import java.net.*;
import java.nio.ByteBuffer;
import java.nio.channels.ClosedChannelException;
import java.nio.channels.DatagramChannel;
import java.nio.channels.SelectionKey;

import static io.aeron.logbuffer.FrameDescriptor.frameVersion;
import static java.net.StandardSocketOptions.*;

public abstract class UdpChannelTransport implements AutoCloseable
{
    protected final UdpChannel udpChannel;
    protected final AtomicCounter invalidPackets;
    protected final DistinctErrorLog errorLog;
    protected UdpTransportPoller transportPoller;

    protected SelectionKey selectionKey;
    protected InetSocketAddress bindAddress;
    protected InetSocketAddress endPointAddress;
    protected InetSocketAddress connectAddress;
    protected DatagramChannel sendDatagramChannel;
    protected DatagramChannel receiveDatagramChannel;
    protected int multicastTtl = 0;

    public UdpChannelTransport(
        final UdpChannel udpChannel,
        final InetSocketAddress endPointAddress,
        final InetSocketAddress bindAddress,
        final InetSocketAddress connectAddress,
        final DistinctErrorLog errorLog,
        final AtomicCounter invalidPackets)
    {
        this.udpChannel = udpChannel;
        this.errorLog = errorLog;
        this.endPointAddress = endPointAddress;
        this.bindAddress = bindAddress;
        this.connectAddress = connectAddress;
        this.invalidPackets = invalidPackets;
    }

    /**
     * Create the underlying channel for reading and writing.
     *
     * @param statusIndicator to set for status
     */
    public void openDatagramChannel(final AtomicCounter statusIndicator)
    {
        try
        {
            sendDatagramChannel = DatagramChannel.open(udpChannel.protocolFamily());
            receiveDatagramChannel = sendDatagramChannel;
            if (udpChannel.isMulticast())
            {
                final NetworkInterface localInterface = udpChannel.localInterface();

                if (null != connectAddress)
                {
                    receiveDatagramChannel = DatagramChannel.open(udpChannel.protocolFamily());
                }

                receiveDatagramChannel.setOption(StandardSocketOptions.SO_REUSEADDR, true);
                receiveDatagramChannel.bind(new InetSocketAddress(endPointAddress.getPort()));
                receiveDatagramChannel.join(endPointAddress.getAddress(), localInterface);
                sendDatagramChannel.setOption(StandardSocketOptions.IP_MULTICAST_IF, localInterface);

                if (0 != udpChannel.multicastTtl())
                {
                    sendDatagramChannel.setOption(StandardSocketOptions.IP_MULTICAST_TTL, udpChannel.multicastTtl());
                    multicastTtl = getOption(StandardSocketOptions.IP_MULTICAST_TTL);
                }

                if (null != connectAddress)
                {
                    sendDatagramChannel.connect(connectAddress);
                }
            }
            else
            {
                sendDatagramChannel.bind(bindAddress);

                if (null != connectAddress)
                {
                    sendDatagramChannel.connect(connectAddress);
                }
            }

            if (0 != Configuration.SOCKET_SNDBUF_LENGTH)
            {
                sendDatagramChannel.setOption(SO_SNDBUF, Configuration.SOCKET_SNDBUF_LENGTH);
            }

            if (0 != Configuration.SOCKET_RCVBUF_LENGTH)
            {
                receiveDatagramChannel.setOption(SO_RCVBUF, Configuration.SOCKET_RCVBUF_LENGTH);
            }

            sendDatagramChannel.configureBlocking(false);
            receiveDatagramChannel.configureBlocking(false);
        }
        catch (final IOException ex)
        {
            statusIndicator.setOrdered(ChannelEndpointStatus.ERRORED);
            throw new RuntimeException("Erroneous channel: " + udpChannel.originalUriString(), ex);
        }
    }

    /**
     * Register this transport for reading from a {@link UdpTransportPoller}.
     *
     * @param transportPoller to register read with
     */
    public void registerForRead(final UdpTransportPoller transportPoller)
    {
        this.transportPoller = transportPoller;
        selectionKey = transportPoller.registerForRead(this);
    }

    /**
     * Return underlying {@link UdpChannel}
     *
     * @return underlying channel
     */
    public UdpChannel udpChannel()
    {
        return udpChannel;
    }

    /**
     * The {@link DatagramChannel} for this transport channel.
     *
     * @return {@link DatagramChannel} for this transport channel.
     */
    public DatagramChannel receiveDatagramChannel()
    {
        return receiveDatagramChannel;
    }

    /**
     * Get the multicast TTL value for sending datagrams on the channel.
     *
     * @return the multicast TTL value for sending datagrams on the channel.
     */
    public int multicastTtl()
    {
        return multicastTtl;
    }

    /**
     * Close transport, canceling any pending read operations and closing channel
     */
    public void close()
    {
        try
        {
            if (null != selectionKey)
            {
                selectionKey.cancel();
            }

            if (null != transportPoller)
            {
                transportPoller.cancelRead(this);
                transportPoller.selectNowWithoutProcessing();
            }

            if (null != sendDatagramChannel)
            {
                sendDatagramChannel.close();
            }

            if (receiveDatagramChannel != sendDatagramChannel && null != receiveDatagramChannel)
            {
                receiveDatagramChannel.close();
            }
        }
        catch (final IOException ex)
        {
            errorLog.record(ex);
        }
    }

    /**
     * Is transport representing a multicast media or unicast
     *
     * @return if transport is multicast media
     */
    public boolean isMulticast()
    {
        return udpChannel.isMulticast();
    }

    /**
     * Is the received frame valid. This method will do some basic checks on the header and can be
     * overridden in a subclass for further validation.
     *
     * @param buffer containing the frame.
     * @param length of the frame.
     * @return true if the frame is believed valid otherwise false.
     */
    public boolean isValidFrame(final UnsafeBuffer buffer, final int length)
    {
        boolean isFrameValid = true;

        if (frameVersion(buffer, 0) != HeaderFlyweight.CURRENT_VERSION)
        {
            isFrameValid = false;
            invalidPackets.increment();
        }
        else if (length < HeaderFlyweight.HEADER_LENGTH)
        {
            isFrameValid = false;
            invalidPackets.increment();
        }

        return isFrameValid;
    }

    /**
     * Receive a datagram from the media layer.
     *
     * @param buffer into which the datagram will be received.
     * @return the source address of the datagram if one is available otherwise false.
     */
    public InetSocketAddress receive(final ByteBuffer buffer)
    {
        buffer.clear();

        InetSocketAddress address = null;
        try
        {
            address = (InetSocketAddress)receiveDatagramChannel.receive(buffer);
        }
        catch (final PortUnreachableException | ClosedChannelException ignored)
        {
            // do nothing
        }
        catch (final Exception ex)
        {
            LangUtil.rethrowUnchecked(ex);
        }

        return address;
    }

    /**
     * Return socket option value
     *
     * @param socketOption of the socket option
     * @param <T>          type of option
     * @return option value
     */
    protected <T> T getOption(final SocketOption<T> socketOption)
    {
        T option = null;
        try
        {
            option = sendDatagramChannel.getOption(socketOption);
        }
        catch (final IOException ex)
        {
            LangUtil.rethrowUnchecked(ex);
        }

        return option;
    }
}

@@ -4828,26 +4828,65 @@
 ion(
-%22Erroneous channel
+%0A                %22Channel error: %22 + ex.getMessage() + %22 
 : %22 

Add a more informative channel error.