/** 
 * 
 * Copyright 2004 Hiram Chirino
 * 
 * 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 org.activeio.net;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.InetAddress;
import java.net.Socket;
import java.net.SocketAddress;
import java.net.SocketException;
import java.net.SocketTimeoutException;

import org.activeio.Packet;
import org.activeio.SynchChannel;
import org.activeio.SynchChannelServer;
import org.activeio.Packet.ByteSequence;
import org.activeio.packet.ByteArrayPacket;
import org.activeio.packet.EOSPacket;
import org.activeio.packet.EmptyPacket;

/**
 * A {@see org.activeio.SynchChannel} implementation that uses a {@see java.net.Socket}
 *  to talk to the network.
 * 
 * @version $Revision$
 */
public class SocketSynchChannel implements SynchChannel, SocketMetadata {

    protected static final int DEFAULT_BUFFER_SIZE = 64 * 1024;

    private final Socket socket;
    private final OutputStream out;
    private final InputStream in;    
    private boolean disposed;
    private int curentSoTimeout;

    private Packet inputPacket;
    
    protected SocketSynchChannel(Socket socket) throws IOException {
        this.socket = socket;
        socket.setReceiveBufferSize(DEFAULT_BUFFER_SIZE);
        socket.setSendBufferSize(DEFAULT_BUFFER_SIZE);
        in = socket.getInputStream();
        out = socket.getOutputStream();        
    }

    /**
     * @see org.activeio.SynchChannel#read(long)
     */
    public org.activeio.Packet read(long timeout) throws IOException {
        try {
            
            if( timeout==SynchChannelServer.WAIT_FOREVER_TIMEOUT )
                setSoTimeout( 0 );
            else if( timeout==SynchChannelServer.NO_WAIT_TIMEOUT )
                setSoTimeout( 1 );
            else 
                setSoTimeout( (int)timeout );

            if( inputPacket==null || !inputPacket.hasRemaining() ) {
                inputPacket = allocatePacket();
            }
            
            ByteSequence sequence = inputPacket.asByteSequence();
            int size = in.read(sequence.getData(), sequence.getOffset(), sequence.getLength());
            if( size == -1 )
                return EOSPacket.EOS_PACKET;
            if( size == 0 )
                return EmptyPacket.EMPTY_PACKET;
            inputPacket.position(size);
            
            Packet remaining = inputPacket.slice();
            inputPacket.flip();
            Packet data = inputPacket.slice();

            // Keep the remaining buffer around to fill with data.
            inputPacket = remaining;
            return data;
            
        } catch (SocketTimeoutException e) {
            return null;
        }
    }

    private Packet allocatePacket() {
        return new ByteArrayPacket(new byte[DEFAULT_BUFFER_SIZE]);
    }

    protected void setSoTimeout(int i) throws SocketException {
        if( curentSoTimeout != i ) {
            socket.setSoTimeout(i);
            curentSoTimeout = i;
        }
    }
    
    /**
     * @see org.activeio.Channel#write(org.activeio.channel.Packet)
     */
    public void write(Packet packet) throws IOException {
        packet.writeTo(out);
    }

    /**
     * @see org.activeio.Channel#flush()
     */
    public void flush() throws IOException {
        out.flush();
    }

    /**
     * @see org.activeio.Disposable#dispose()
     */
    public void dispose() {
        if (disposed)
            return;

        try {
            out.close();
        } catch (IOException ignore) {
        }
        try {
            in.close();
        } catch (IOException ignore) {
        }
        try {
            socket.close();
        } catch (IOException ignore) {
        }
        disposed = true;
    }

    public void start() throws IOException {
    }
    public void stop(long timeout) throws IOException {
    }
    
    public InetAddress getInetAddress() {
        return socket.getInetAddress();
    }
    public boolean getKeepAlive() throws SocketException {
        return socket.getKeepAlive();
    }
    public InetAddress getLocalAddress() {
        return socket.getLocalAddress();
    }
    public int getLocalPort() {
        return socket.getLocalPort();
    }
    public SocketAddress getLocalSocketAddress() {
        return socket.getLocalSocketAddress();
    }
    public boolean getOOBInline() throws SocketException {
        return socket.getOOBInline();
    }
    public int getPort() {
        return socket.getPort();
    }
    public int getReceiveBufferSize() throws SocketException {
        return socket.getReceiveBufferSize();
    }
    public SocketAddress getRemoteSocketAddress() {
        return socket.getRemoteSocketAddress();
    }
    public boolean getReuseAddress() throws SocketException {
        return socket.getReuseAddress();
    }
    public int getSendBufferSize() throws SocketException {
        return socket.getSendBufferSize();
    }
    public int getSoLinger() throws SocketException {
        return socket.getSoLinger();
    }
    public int getSoTimeout() throws SocketException {
        return socket.getSoTimeout();
    }
    public boolean getTcpNoDelay() throws SocketException {
        return socket.getTcpNoDelay();
    }
    public int getTrafficClass() throws SocketException {
        return socket.getTrafficClass();
    }
    public boolean isBound() {
        return socket.isBound();
    }
    public boolean isClosed() {
        return socket.isClosed();
    }
    public boolean isConnected() {
        return socket.isConnected();
    }
    public void setKeepAlive(boolean on) throws SocketException {
        socket.setKeepAlive(on);
    }
    public void setOOBInline(boolean on) throws SocketException {
        socket.setOOBInline(on);
    }
    public void setReceiveBufferSize(int size) throws SocketException {
        socket.setReceiveBufferSize(size);
    }
    public void setReuseAddress(boolean on) throws SocketException {
        socket.setReuseAddress(on);
    }
    public void setSendBufferSize(int size) throws SocketException {
        socket.setSendBufferSize(size);
    }
    public void setSoLinger(boolean on, int linger) throws SocketException {
        socket.setSoLinger(on, linger);
    }
    public void setTcpNoDelay(boolean on) throws SocketException {
        socket.setTcpNoDelay(on);
    }
    public void setTrafficClass(int tc) throws SocketException {
        socket.setTrafficClass(tc);
    }
    
    public Object narrow(Class target) {
        if( target.isAssignableFrom(getClass()) ) {
            return this;
        }
        return null;
    }

    public String toString() {
        return "Socket Connection: "+getLocalSocketAddress()+" -> "+getRemoteSocketAddress();
    }
}