/*
 * @author Ranides Atterwim <ranides@gmail.com>
 * @copyright Ranides Atterwim
 * @license WTFPL
 * @url http://ranides.net/projects/assira
 */
package net.ranides.assira.io;

import java.io.EOFException;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.ReadOnlyBufferException;
import java.nio.channels.ClosedChannelException;
import java.nio.channels.SeekableByteChannel;

/**
 *
 * @author Ranides Atterwim <ranides@gmail.com>
 */
public final class ChannelAdapter {
    
    private ChannelAdapter() {
        // utility class
    }
    
    public static SeekableByteChannel wrap(byte[] input) {
        // preferred to BufferAdapter because we can write transfers to/from array
        // in a bit more efficient way.
        return new AChannel(input);
    }

    public static SeekableByteChannel wrap(ByteBuffer input) {
        return BufferAdapter.asChannel(input);
    }
    
    public static SeekableByteChannel wrap(char[] input) {
        // preferred to CSChannel adapter because we want to support writes
        return new CAChannel(input);
    }
    
    /**
     * read only
     * @param input
     * @return 
     */
    public static SeekableByteChannel wrap(CharSequence input) {
        return new CSChannel(input);
    }

    
    private static void assertIsOpen(Object object) throws IOException  {
        if(null == object) {
            throw new ClosedChannelException();
        }
    }
    
    private static class AChannel implements SeekableByteChannel {
        
        private byte[] array;
        private int position;
        private int limit;

        public AChannel(byte[] array) { // NOPMD wrapper
            this.array = array;
            this.position = 0;
            this.limit = array.length;
        }
        
        @Override
        public boolean isOpen() {
            return array != null;
        }

        @Override
        public void close() throws IOException {
            assertIsOpen(array);
            array = null;
        }

        @Override
        public int read(ByteBuffer dst) throws IOException {
            assertIsOpen(array);
            if( position >= limit) {
                return -1;
            }
            int max = Math.min(limit, position+dst.remaining());
            int ret = max - position;
            dst.put(array, position, ret);
            position += ret;
            return ret;
        }

        @Override
        public int write(ByteBuffer src) throws IOException {
            assertIsOpen(array);
            if( position >= limit) {
                throw new EOFException();
            }
            int ret = Math.min(src.remaining(), limit-position);
            src.get(array, position, ret);
            position += ret;
            return ret;
        }

        @Override
        public long position() throws IOException {
            assertIsOpen(array);
            return position;
        }

        @Override
        public SeekableByteChannel position(long newPosition) throws IOException {
            assertIsOpen(array);
            assert newPosition == (int)newPosition;
            position = (int)newPosition;
            return this;
        }

        @Override
        public long size() throws IOException {
            assertIsOpen(array);
            return limit;
        }

        @Override
        public SeekableByteChannel truncate(long size) throws IOException {
            assertIsOpen(array);
            assert size == (int)size;
            if( size > limit) {
                return this;
            }
            limit = (int)size;
            if(position > limit) {
                position = limit;
            }
            return this;
        }
        
    }
    
    private static class CSChannel implements SeekableByteChannel {
        
        private CharSequence chars;
        private int position;
        private int limit;

        public CSChannel(CharSequence chars) { // NOPMD wrapper
            this.chars = chars;
            this.position = 0;
            this.limit = chars.length() * 2;
        }
        
        @Override
        public boolean isOpen() {
            return chars != null;
        }

        @Override
        public void close() throws IOException {
            assertIsOpen(chars);
            chars = null;
        }

        @Override
        public int read(ByteBuffer dst) throws IOException {
            assertIsOpen(chars);
            if( position >= limit) {
                return -1;
            }
            int max = Math.min(limit, position+dst.remaining());
            int ret = max - position;
            if(0 == ret) {
                return ret;
            }
            if(1 == position % 2) {
                dst.put( char1(position, dst.order()) );
                position++;
            }
            for(int i=position/2, n=max/2; i<n; i++) {
                dst.putChar( chars.charAt(i) );
            }
            if(1 == max % 2) {
                dst.put( char0(max, dst.order()) );
            }

            position = max;
            return ret;
        }
        
        @Override
        public int write(ByteBuffer src) throws IOException {
            throw new ReadOnlyBufferException();
        }

        @Override
        public long position() throws IOException {
            assertIsOpen(chars);
            return position;
        }

        @Override
        public SeekableByteChannel position(long newPosition) throws IOException {
            assertIsOpen(chars);
            assert newPosition == (int)newPosition;
            position = (int)newPosition;
            return this;
        }

        @Override
        public long size() throws IOException {
            assertIsOpen(chars);
            return limit;
        }

        @Override
        public SeekableByteChannel truncate(long size) throws IOException {
            assertIsOpen(chars);
            assert size == (int)size;
            if( size > limit) {
                return this;
            }
            limit = (int)size;
            if(position > limit) {
                position = limit;
            }
            return this;
        }
        
        private byte char0(int index, ByteOrder order) {
            char c = chars.charAt(index/2);
            return (byte)(order != ByteOrder.BIG_ENDIAN ? c : c>>8);
        }

        private byte char1(int index, ByteOrder order) {
            char c = chars.charAt(index/2);
            return (byte)(order == ByteOrder.BIG_ENDIAN ? c : c>>8);
        }
        
    }
    
    private static class CAChannel implements SeekableByteChannel {
        
        private char[] chars;
        private int position;
        private int limit;

        public CAChannel(char[] chars) { // NOPMD wrapper
            this.chars = chars;
            this.position = 0;
            this.limit = chars.length * 2;
        }
        
        @Override
        public boolean isOpen() {
            return chars != null;
        }

        @Override
        public void close() throws IOException {
            assertIsOpen(chars);
            chars = null;
        }

        @Override
        public int read(ByteBuffer dst) throws IOException {
            assertIsOpen(chars);
            if( position >= limit) {
                return -1;
            }
            int max = Math.min(limit, position+dst.remaining());
            int ret = max - position;
            if(0 == ret) {
                return ret;
            }
            if(1 == position % 2) {
                dst.put( char1(position, dst.order()) );
                position++;
            }
            for(int i=position/2, n=max/2; i<n; i++) {
                dst.putChar( chars[i] );
            }
            if(1 == max % 2) {
                dst.put( char0(max, dst.order()) );
            }

            position = max;
            return ret;
        }
        
        @Override
        public int write(ByteBuffer src) throws IOException {
            assertIsOpen(chars);
            if( position >= limit) {
                throw new EOFException();
            }
            int ret = Math.min(src.remaining(), limit-position);
            if(0 == ret) {
                return ret;
            }
            int max = position + ret;
            if(1 == position %2) {
                char1s(position, src.order(), src.get());
                position++;
            }
            for(int i=position/2, n=max/2; i<n; i++) {
                chars[i] = src.getChar();
            }
            if(1 == max % 2) {
                char0s(max, src.order(), src.get());
            }
            
            position = max;
            return ret;
        }

        @Override
        public long position() throws IOException {
            assertIsOpen(chars);
            return position;
        }

        @Override
        public SeekableByteChannel position(long newPosition) throws IOException {
            assertIsOpen(chars);
            assert newPosition == (int)newPosition;
            position = (int)newPosition;
            return this;
        }

        @Override
        public long size() throws IOException {
            assertIsOpen(chars);
            return limit;
        }

        @Override
        public SeekableByteChannel truncate(long size) throws IOException {
            assertIsOpen(chars);
            assert size == (int)size;
            if( size > limit) {
                return this;
            }
            limit = (int)size;
            if(position > limit) {
                position = limit;
            }
            return this;
        }
        
        private void char0s(int index, ByteOrder order, byte value) {
            if( order != ByteOrder.BIG_ENDIAN ) {
                chars[index/2] = (char)((chars[index/2] & 0xFF00) | (value & 0xFF));
            } else {
                chars[index/2] = (char)((chars[index/2] & 0x00FF) | ((value & 0xFF)<<8));
            }
        }

        private void char1s(int index, ByteOrder order, byte value) {
            if( order == ByteOrder.BIG_ENDIAN ) {
                chars[index/2] = (char)((chars[index/2] & 0xFF00) | (value & 0xFF));
            } else {
                chars[index/2] = (char)((chars[index/2] & 0x00FF) | ((value & 0xFF)<<8));
            }
        }
        
        private byte char0(int index, ByteOrder order) {
            return (byte)(order != ByteOrder.BIG_ENDIAN ? chars[index/2] : chars[index/2]>>8);
        }

        private byte char1(int index, ByteOrder order) {
            return (byte)(order == ByteOrder.BIG_ENDIAN ? chars[index/2] : chars[index/2]>>8);
        }
        
    }
     
}
