/*
Copyright (C) 2005 David Green <green@couchpotato.net>

This file is part of I3J.

I3J is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.

I3J is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with I3J; if not, write to the Free Software
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
*/

package com.aelfengard.i3;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.SocketChannel;
import java.util.HashSet;
import java.util.NoSuchElementException;
import java.util.Set;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;

import com.aelfengard.i3.packet.I3Packet;

public class I3Connection {

    static final String DEFAULT_HOST = "us-1.i3.intermud.org";
    static final int DEFAULT_PORT = 9000;
    
    private final BlockingQueue<I3Packet> writeQueue = new LinkedBlockingQueue<I3Packet>();
    private final Selector selector;
    private final SelectionKey key;
    private final SocketChannel channel;
    
    private final Set<I3PacketListener> listeners = new HashSet<I3PacketListener>();

    public I3Connection() throws IOException {
        this(DEFAULT_HOST, DEFAULT_PORT);
    }
    
    public I3Connection(String host, int port) throws IOException {
        channel = SocketChannel.open(new InetSocketAddress(host, port));
        selector = Selector.open();
        channel.configureBlocking(false);
        key = channel.register(selector, SelectionKey.OP_READ);
        new ConnectionThread().start();
    }
    
    public void send(I3Packet packet) {
        writeQueue.add(packet);
        key.interestOps(SelectionKey.OP_READ | SelectionKey.OP_WRITE);
        selector.wakeup();
    }
    
    private void packetReceived(I3Packet packet) {
        Set<I3PacketListener> set;
        synchronized(this) {
            set = new HashSet<I3PacketListener>(listeners);
        }
        for (I3PacketListener l : set) {
            l.packetReceived(this, packet);
        }
    }
    
    private void unrecognizedPacket(String packet) {
        Set<I3PacketListener> set;
        synchronized(this) {
            set = new HashSet<I3PacketListener>(listeners);
        }
        for (I3PacketListener l : set) {
            l.unrecognizedPacket(this, packet);
        }
    }
    
    private void connectionClosed() {
        Set<I3PacketListener> set;
        synchronized(this) {
            set = new HashSet<I3PacketListener>(listeners);
        }
        for (I3PacketListener l : set) {
            l.connectionClosed(this);
        }
    }

    public synchronized void addPacketListener(I3PacketListener l) {
        listeners.add(l);
    }
    
    public synchronized void removePacketListener(I3PacketListener l) {
        listeners.remove(l);
    }
    
    private class ConnectionThread extends Thread {
        
        private final ByteBuffer lengthBuffer = ByteBuffer.allocate(4);
        private ByteBuffer readBuffer = lengthBuffer; // start by reading length
        private ByteBuffer[] writeBuffers = null;
        
        public ConnectionThread() {
            setDaemon(true);
        }
        
        public void run() {
            try {
                while (true) {
                    selector.select();
                    selector.selectedKeys().clear();
                    if (key.isReadable()) {
                        while (true) {
                            channel.read(readBuffer);
                            if (readBuffer.hasRemaining()) {
                                break; // out of stuff to read
                            }
                            if (readBuffer == lengthBuffer) {
                                readBuffer = ByteBuffer.allocate(readBuffer.getInt(0));
                            }
                            else {
                                byte[] b = readBuffer.array();
                                removeNonPrintables(b);
                                String packetString = new String(b, "ISO-8859-1");
                                if (System.getProperty("i3.debug") != null) {
                                    System.err.println("I3 Packet Received: " + packetString);
                                }
                                I3Packet packet = I3Packet.forString(packetString);
                                if (packet == null) {
                                    unrecognizedPacket(packetString);
                                }
                                else {
                                    packetReceived(packet);
                                }
                                lengthBuffer.clear();
                                readBuffer = lengthBuffer;
                            }
                        }
                    }
                    else if (key.isWritable()) {
                        while (true) {
                            if (writeBuffers != null) {
                                channel.write(writeBuffers);
                                if (writeBuffers[0].hasRemaining() || writeBuffers[1].hasRemaining()) {
                                    break; // can't write anymore
                                }
                            }
                            I3Packet packet;
                            try {
                                packet = writeQueue.remove();
                                String packetString = packet.toString();
                                if (System.getProperty("i3.debug") != null) {
                                    System.err.println("I3 Packet Sent: " + packetString);
                                }
                                byte[] b = packetString.getBytes("ISO-8859-1");
                                removeNonPrintables(b);
                                writeBuffers = new ByteBuffer[] {
                                        ByteBuffer.allocate(4),
                                        ByteBuffer.wrap(b)};
                                writeBuffers[0].putInt(0, b.length);
                            }
                            catch (NoSuchElementException ex) {
                                // out of stuff to write
                                writeBuffers = null;
                                key.interestOps(SelectionKey.OP_READ);
                                break;
                            }
                            
                        }
                    }
                }
            }
            catch (IOException ex) {
                ex.printStackTrace();
            }
            finally {
                try {
                    channel.close();
                }
                catch (IOException ex) {
                    ex.printStackTrace();
                }
                try {
                    selector.close();
                }
                catch (IOException ex) {
                    ex.printStackTrace();
                }
                connectionClosed();
            }
        }
    }
    
    private static void removeNonPrintables(byte[] b) {
        // remove non-printables, per spec
        for (int i = 0; i < b.length; i++) {
            // 160 is a non-breaking space. We'll consider that "printable".
            if (b[i] < 32 || (b[i] >= 127 && b[i] <= 159)) {
                // Java uses it as a replacement 
                // character, so it's probably ok 
                // for us too.
                b[i] = '?';
            }
        }
    }
    
}