/**
 *
 * 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.activeio.packet;

import java.io.DataOutput;
import java.io.IOException;
import java.io.OutputStream;
import java.lang.reflect.Constructor;
import java.nio.ByteBuffer;


/**
 * Provides a Packet implementation that is backed by a {@see java.nio.ByteBuffer}
 * 
 * @version $Revision$
 */
final public class ByteBufferPacket implements Packet {

	public static final int DEFAULT_BUFFER_SIZE = Integer.parseInt(System.getProperty("org.apache.activeio.DefaultByteBufferSize", ""+(64*1024)));
	public static final int DEFAULT_DIRECT_BUFFER_SIZE = Integer.parseInt(System.getProperty("org.apache.activeio.DefaultDirectByteBufferSize", ""+(8*1024)));

    private final ByteBuffer buffer;
    private static final int TEMP_BUFFER_SIZE = 64*1024;

    public ByteBufferPacket(ByteBuffer buffer) {
        this.buffer = buffer;
        clear();
    }
    
    public ByteBuffer getByteBuffer() {
        return buffer;
    }
    
    public static ByteBufferPacket createDefaultBuffer(boolean direct) {
    	if( direct )
    		return new ByteBufferPacket( ByteBuffer.allocateDirect(DEFAULT_DIRECT_BUFFER_SIZE) );
    	return new ByteBufferPacket( ByteBuffer.allocate(DEFAULT_BUFFER_SIZE)  );
    }
    
    public void writeTo(OutputStream out) throws IOException {
        if( buffer.hasArray() ) {
            
            // If the buffer is backed by an array.. then use it directly.
            out.write(buffer.array(), position(), remaining());
            position(limit());
            
        } else {
            
            // It's not backed by a buffer.. We can only dump it to a OutputStream via a byte[] so,
            // create a temp buffer that we can use to chunk it out.            
            byte temp[] = new byte[TEMP_BUFFER_SIZE];            
            while( buffer.hasRemaining() ) {
                int maxWrite = buffer.remaining() > temp.length ? temp.length : buffer.remaining();
	            buffer.get(temp, 0, maxWrite);
	            out.write(temp,0, maxWrite);
            }
            
        }        
    }
    
    public void writeTo(DataOutput out) throws IOException {
        if( buffer.hasArray() ) {
            
            // If the buffer is backed by an array.. then use it directly.
            out.write(buffer.array(), position(), remaining());
            position(limit());
            
        } else {
            
            // It's not backed by a buffer.. We can only dump it to a OutputStream via a byte[] so,
            // create a temp buffer that we can use to chunk it out.            
            byte temp[] = new byte[TEMP_BUFFER_SIZE];            
            while( buffer.hasRemaining() ) {
                int maxWrite = buffer.remaining() > temp.length ? temp.length : buffer.remaining();
                buffer.get(temp, 0, maxWrite);
                out.write(temp,0, maxWrite);
            }
            
        }        
    }

    public int capacity() {
        return buffer.capacity();
    }

    public void clear() {
        buffer.clear();
    }

    public Packet compact() {
        buffer.compact();
        return this;
    }

    public void flip() {
        buffer.flip();
    }

    public boolean hasRemaining() {
        return buffer.hasRemaining();
    }

    public boolean isDirect() {
        return buffer.isDirect();
    }

    public boolean isReadOnly() {
        return buffer.isReadOnly();
    }

    public int limit() {
        return buffer.limit();
    }

    public void limit(int arg0) {
        buffer.limit(arg0);
    }

    public Packet mark() {
        buffer.mark();
        return this;
    }

    public int position() {
        return buffer.position();
    }

    public void position(int arg0) {
        buffer.position(arg0);
    }

    public int remaining() {
        return buffer.remaining();
    }

    public void rewind() {
        buffer.rewind();
    }

    public Packet slice() {
        return new ByteBufferPacket(buffer.slice());
    }

    public Packet duplicate() {
        return new ByteBufferPacket(buffer.duplicate());
    }

    public Object duplicate(ClassLoader cl) throws IOException {
        try {
            Class clazz = cl.loadClass(ByteBufferPacket.class.getName());
            Constructor constructor = clazz.getConstructor(new Class[]{ByteBuffer.class});
            return constructor.newInstance(new Object[]{buffer.duplicate()});
        } catch (Throwable e) {
            throw (IOException)new IOException("Could not duplicate packet in a different classloader: "+e).initCause(e);
        }

    }
    

    /**
     * @see org.apache.activeio.packet.Packet#read()
     */
    public int read() {
        if( !buffer.hasRemaining() )
            return -1;
        return buffer.get() & 0xff;
    }

    /**
     * @see org.apache.activeio.packet.Packet#read(byte[], int, int)
     */
    public int read(byte[] data, int offset, int length) {
        if( !hasRemaining() )
            return -1;
        
        int copyLength = Math.min(length, remaining());
        buffer.get(data, offset, copyLength);
        return copyLength;
    }

    /**
     * @see org.apache.activeio.packet.Packet#write(int)
     */
    public boolean write(int data) {
        if( !buffer.hasRemaining() )
            return false;
        buffer.put((byte)data);
        return true;
    }

    /**
     * @see org.apache.activeio.packet.Packet#write(byte[], int, int)
     */
    public int write(byte[] data, int offset, int length) {
        if( !hasRemaining() )
            return -1;

        int copyLength = Math.min(length, remaining());
        buffer.put(data, offset, copyLength);
        return copyLength;
    }

    /**
     * @see org.apache.activeio.packet.Packet#asByteSequence()
     */
    public ByteSequence asByteSequence() {
        if( buffer.hasArray() ) {
            byte[] bs = buffer.array();
            return new ByteSequence(bs, buffer.position(), buffer.remaining());
        } else {
            byte[] bs = new byte[buffer.remaining()];
        	int p = buffer.position();
        	buffer.get(bs);
        	buffer.position(p);
        	return new ByteSequence(bs, 0, bs.length);
        }
    }
    
    /**
     * @see org.apache.activeio.packet.Packet#sliceAsBytes()
     */
    public byte[] sliceAsBytes() {
        // TODO Auto-generated method stub
        return null;
    }

    /**
     * @param dest
     * @return the number of bytes read into the dest.
     */
    public int read(Packet dest) {
        
	    int rc = Math.min(dest.remaining(), remaining()); 
		if( rc > 0 ) {
		    
	        if( dest.getClass() == ByteBufferPacket.class ) {            

			    // Adjust our limit so that we don't overflow the dest buffer. 
				int limit = limit();
				limit(position()+rc);
				
	            ((ByteBufferPacket)dest).buffer.put(buffer);

	            // restore the limit.
				limit(limit);
	            
	            return 0;
	        } else {	            
	            ByteSequence sequence = dest.asByteSequence();
	            rc = read(sequence.getData(), sequence.getOffset(), sequence.getLength());
	            dest.position(dest.position()+rc);
	        }
		}
		return rc;
    }
	
    public String toString() {
        return "{position="+position()+",limit="+limit()+",capacity="+capacity()+"}";
    }

    public Object getAdapter(Class target) {
        if( target.isAssignableFrom(getClass()) ) {
            return this;
        }
        return null;
    }
    
    public void dispose() {        
    }
}
