//
//  ========================================================================
//  Copyright (c) 1995-2015 Mort Bay Consulting Pty. Ltd.
//  ------------------------------------------------------------------------
//  All rights reserved. This program and the accompanying materials
//  are made available under the terms of the Eclipse Public License v1.0
//  and Apache License v2.0 which accompanies this distribution.
//
//      The Eclipse Public License is available at
//      http://www.eclipse.org/legal/epl-v10.html
//
//      The Apache License v2.0 is available at
//      http://www.opensource.org/licenses/apache2.0.php
//
//  You may elect to redistribute this code under either of these licenses.
//  ========================================================================
//

package org.eclipse.jetty.io.nio;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.net.SocketException;
import java.nio.ByteBuffer;
import java.nio.channels.ByteChannel;
import java.nio.channels.GatheringByteChannel;
import java.nio.channels.SelectableChannel;
import java.nio.channels.SocketChannel;

import org.eclipse.jetty.io.Buffer;
import org.eclipse.jetty.io.EndPoint;
import org.eclipse.jetty.util.log.Log;
import org.eclipse.jetty.util.log.Logger;

/**
 * Channel End Point.
 * <p>Holds the channel and socket for an NIO endpoint.
 *
 */
public class ChannelEndPoint implements EndPoint
{
    private static final Logger LOG = Log.getLogger(ChannelEndPoint.class);

    private static final String ALL_INTERFACES = "0.0.0.0";

    protected final ByteChannel _channel;
    protected final ByteBuffer[] _gather2=new ByteBuffer[2];
    protected final Socket _socket;
    protected final InetSocketAddress _local;
    protected final InetSocketAddress _remote;
    protected volatile int _maxIdleTime;
    private volatile boolean _ishut;
    private volatile boolean _oshut;

    protected ChannelEndPoint(ByteChannel channel, int maxIdleTime) throws IOException
    {
        this._channel = channel;
        _maxIdleTime=maxIdleTime;
        _socket=(channel instanceof SocketChannel)?((SocketChannel)channel).socket():null;
        if (_socket!=null)
        {
            _local=(InetSocketAddress)_socket.getLocalSocketAddress();
            _remote=(InetSocketAddress)_socket.getRemoteSocketAddress();
            _socket.setSoTimeout(_maxIdleTime);
        }
        else
        {
            _local=_remote=null;
        }
    }

    public boolean isBlocking()
    {
        return  !(_channel instanceof SelectableChannel) || ((SelectableChannel)_channel).isBlocking();
    }

    public boolean blockWritable(long millisecs) throws IOException
    {
        return true;
    }

    /*
     * @see org.eclipse.io.EndPoint#isOpen()
     */
    public boolean isOpen()
    {
        return _channel.isOpen();
    }

    /** Shutdown the channel Input.
     * Cannot be overridden. To override, see {@link #shutdownInput()}
     * @throws IOException
     */
    protected final void shutdownChannelInput() throws IOException
    {
        LOG.debug("ishut {}", this);
        _ishut = true;
        if (_channel.isOpen())
        {
            if (_socket != null)
            {
                try
                {
                    if (!_socket.isInputShutdown())
                    {
                        _socket.shutdownInput();
                    }
                }
                catch (SocketException e)
                {
                    LOG.debug(e.toString());
                    LOG.ignore(e);
                }
                finally
                {
                    if (_oshut)
                    {
                        close();
                    }
                }
            }
        }
    }

    /* (non-Javadoc)
     * @see org.eclipse.io.EndPoint#close()
     */
    public void shutdownInput() throws IOException
    {
        shutdownChannelInput();
    }

    protected final void shutdownChannelOutput() throws IOException
    {
        LOG.debug("oshut {}",this);
        _oshut = true;
        if (_channel.isOpen())
        {
            if (_socket != null)
            {
                try
                {
                    if (!_socket.isOutputShutdown())
                    {
                        _socket.shutdownOutput();
                    }
                }
                catch (SocketException e)
                {
                    LOG.debug(e.toString());
                    LOG.ignore(e);
                }
                finally
                {
                    if (_ishut)
                    {
                        close();
                    }
                }
            }
        }
    }

    /* (non-Javadoc)
     * @see org.eclipse.io.EndPoint#close()
     */
    public void shutdownOutput() throws IOException
    {
        shutdownChannelOutput();
    }

    public boolean isOutputShutdown()
    {
        return _oshut || !_channel.isOpen() || _socket != null && _socket.isOutputShutdown();
    }

    public boolean isInputShutdown()
    {
        return _ishut || !_channel.isOpen() || _socket != null && _socket.isInputShutdown();
    }

    /* (non-Javadoc)
     * @see org.eclipse.io.EndPoint#close()
     */
    public void close() throws IOException
    {
        LOG.debug("close {}",this);
        _channel.close();
    }

    /* (non-Javadoc)
     * @see org.eclipse.io.EndPoint#fill(org.eclipse.io.Buffer)
     */
    public int fill(Buffer buffer) throws IOException
    {
        if (_ishut)
            return -1;
        Buffer buf = buffer.buffer();
        int len=0;
        if (buf instanceof NIOBuffer)
        {
            final NIOBuffer nbuf = (NIOBuffer)buf;
            final ByteBuffer bbuf=nbuf.getByteBuffer();

            //noinspection SynchronizationOnLocalVariableOrMethodParameter
            try
            {
                synchronized(bbuf)
                {
                    try
                    {
                        bbuf.position(buffer.putIndex());
                        len=_channel.read(bbuf);
                    }
                    finally
                    {
                        buffer.setPutIndex(bbuf.position());
                        bbuf.position(0);
                    }
                }

                if (len<0 && isOpen())
                {
                    if (!isInputShutdown())
                        shutdownInput();
                    if (isOutputShutdown())
                        _channel.close();
                }
            }
            catch (IOException x)
            {
                LOG.debug("Exception while filling", x);
                try
                {
                    if (_channel.isOpen())
                        _channel.close();
                }
                catch (Exception xx)
                {
                    LOG.ignore(xx);
                }

                if (len>0)
                    throw x;
                len=-1;
            }
        }
        else
        {
            throw new IOException("Not Implemented");
        }

        return len;
    }

    /* (non-Javadoc)
     * @see org.eclipse.io.EndPoint#flush(org.eclipse.io.Buffer)
     */
    public int flush(Buffer buffer) throws IOException
    {
        Buffer buf = buffer.buffer();
        int len=0;
        if (buf instanceof NIOBuffer)
        {
            final NIOBuffer nbuf = (NIOBuffer)buf;
            final ByteBuffer bbuf=nbuf.getByteBuffer().asReadOnlyBuffer();
            try
            {
                bbuf.position(buffer.getIndex());
                bbuf.limit(buffer.putIndex());
                len=_channel.write(bbuf);
            }
            finally
            {
                if (len>0)
                    buffer.skip(len);
            }
        }
        else if (buffer.array()!=null)
        {
            ByteBuffer b = ByteBuffer.wrap(buffer.array(), buffer.getIndex(), buffer.length());
            len=_channel.write(b);
            if (len>0)
                buffer.skip(len);
        }
        else
        {
            throw new IOException("Not Implemented");
        }
        return len;
    }

    /* (non-Javadoc)
     * @see org.eclipse.io.EndPoint#flush(org.eclipse.io.Buffer, org.eclipse.io.Buffer, org.eclipse.io.Buffer)
     */
    public int flush(Buffer header, Buffer buffer, Buffer trailer) throws IOException
    {
        int length=0;

        Buffer buf0 = header==null?null:header.buffer();
        Buffer buf1 = buffer==null?null:buffer.buffer();

        if (_channel instanceof GatheringByteChannel &&
            header!=null && header.length()!=0 && buf0 instanceof NIOBuffer &&
            buffer!=null && buffer.length()!=0 && buf1 instanceof NIOBuffer)
        {
            length = gatheringFlush(header,((NIOBuffer)buf0).getByteBuffer(),buffer,((NIOBuffer)buf1).getByteBuffer());
        }
        else
        {
            // flush header
            if (header!=null && header.length()>0)
                length=flush(header);

            // flush buffer
            if ((header==null || header.length()==0) &&
                 buffer!=null && buffer.length()>0)
                length+=flush(buffer);

            // flush trailer
            if ((header==null || header.length()==0) &&
                (buffer==null || buffer.length()==0) &&
                 trailer!=null && trailer.length()>0)
                length+=flush(trailer);
        }

        return length;
    }

    protected int gatheringFlush(Buffer header, ByteBuffer bbuf0, Buffer buffer, ByteBuffer bbuf1) throws IOException
    {
        int length;

        synchronized(this)
        {
            // Adjust position indexs of buf0 and buf1
            bbuf0=bbuf0.asReadOnlyBuffer();
            bbuf0.position(header.getIndex());
            bbuf0.limit(header.putIndex());
            bbuf1=bbuf1.asReadOnlyBuffer();
            bbuf1.position(buffer.getIndex());
            bbuf1.limit(buffer.putIndex());

            _gather2[0]=bbuf0;
            _gather2[1]=bbuf1;

            // do the gathering write.
            length=(int)((GatheringByteChannel)_channel).write(_gather2);

            int hl=header.length();
            if (length>hl)
            {
                header.clear();
                buffer.skip(length-hl);
            }
            else if (length>0)
            {
                header.skip(length);
            }
        }
        return length;
    }

    /* ------------------------------------------------------------ */
    /**
     * @return Returns the channel.
     */
    public ByteChannel getChannel()
    {
        return _channel;
    }


    /* ------------------------------------------------------------ */
    /*
     * @see org.eclipse.io.EndPoint#getLocalAddr()
     */
    public String getLocalAddr()
    {
        if (_socket==null)
            return null;
       if (_local==null || _local.getAddress()==null || _local.getAddress().isAnyLocalAddress())
           return ALL_INTERFACES;
        return _local.getAddress().getHostAddress();
    }

    /* ------------------------------------------------------------ */
    /*
     * @see org.eclipse.io.EndPoint#getLocalHost()
     */
    public String getLocalHost()
    {
        if (_socket==null)
            return null;
       if (_local==null || _local.getAddress()==null || _local.getAddress().isAnyLocalAddress())
           return ALL_INTERFACES;
        return _local.getAddress().getCanonicalHostName();
    }

    /* ------------------------------------------------------------ */
    /*
     * @see org.eclipse.io.EndPoint#getLocalPort()
     */
    public int getLocalPort()
    {
        if (_socket==null)
            return 0;
        if (_local==null)
            return -1;
        return _local.getPort();
    }

    /* ------------------------------------------------------------ */
    /*
     * @see org.eclipse.io.EndPoint#getRemoteAddr()
     */
    public String getRemoteAddr()
    {
        if (_socket==null)
            return null;
        if (_remote==null)
            return null;
        return _remote.getAddress().getHostAddress();
    }

    /* ------------------------------------------------------------ */
    /*
     * @see org.eclipse.io.EndPoint#getRemoteHost()
     */
    public String getRemoteHost()
    {
        if (_socket==null)
            return null;
        if (_remote==null)
            return null;
        return _remote.getAddress().getCanonicalHostName();
    }

    /* ------------------------------------------------------------ */
    /*
     * @see org.eclipse.io.EndPoint#getRemotePort()
     */
    public int getRemotePort()
    {
        if (_socket==null)
            return 0;
        return _remote==null?-1:_remote.getPort();
    }

    /* ------------------------------------------------------------ */
    public void flush()
        throws IOException
    {
    }

    /* ------------------------------------------------------------ */
    public int getMaxIdleTime()
    {
        return _maxIdleTime;
    }

    /* ------------------------------------------------------------ */
    /**
     * @see org.eclipse.jetty.io.bio.StreamEndPoint#setMaxIdleTime(int)
     */
    public void setMaxIdleTime(int timeMs) throws IOException
    {
        if (_socket!=null && timeMs!=_maxIdleTime)
            _socket.setSoTimeout(timeMs>0?timeMs:0);
        _maxIdleTime=timeMs;
    }
}
