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

import java.io.IOException;
import java.net.URI;
import java.util.Iterator;

import org.activeio.AcceptListener;
import org.activeio.AsynchChannel;
import org.activeio.AsynchChannelListener;
import org.activeio.AsynchChannelServer;
import org.activeio.Channel;
import org.activeio.FilterAsynchChannel;
import org.activeio.FilterAsynchChannelServer;
import org.activeio.Packet;
import org.activeio.SynchChannel;
import org.activeio.adapter.AsynchToSynchChannelAdapter;
import org.activeio.adapter.SynchToAsynchChannelAdapter;
import org.activeio.filter.PushbackSynchChannel;
import org.activeio.packet.AppendedPacket;

import EDU.oswego.cs.dl.util.concurrent.ConcurrentHashMap;

/**
 * Allows multiple protocols share a single ChannelServer.  All protocols sharing the server 
 * must have a distinct magic number at the beging of the client's request.
 * 
 * TODO: handle the case where a client opens a connection but sends no data down the stream.  We need
 * to timeout that client.
 * 
 * @version $Revision$
 */
final public class OnePortAsynchChannelServer extends FilterAsynchChannelServer {
    
    /**
     * The OnePortAsynchChannelServer listens for incoming connection
     * from a normal AsynchChannelServer.  This s the listner used 
     * to receive the accepted channels.
     */
    final private class OnePortAcceptListener implements AcceptListener {
        
        public void onAccept(Channel channel) {
            try {
                AsynchChannel asynchChannel = SynchToAsynchChannelAdapter.adapt(channel);
                ProtocolInspectingAsynchChannel inspector = new ProtocolInspectingAsynchChannel(asynchChannel);
                inspector.start();                
            } catch (IOException e) {
                onAcceptError(e);
            }                
        }
        
        public void onAcceptError(IOException error) {
            dispose();
        }
    }

    /**
     * This channel filter sniffs the first few bytes of the byte stream 
     * to see if a ProtocolRecognizer recognizes the protocol.  If it does not
     * it just closes the channel, otherwise the associated SubPortAsynchChannelServer
     * is notified that it accepted a channel.
     *
     */
    final private class ProtocolInspectingAsynchChannel extends FilterAsynchChannel {
        private Packet buffer;

        public ProtocolInspectingAsynchChannel(AsynchChannel next) throws IOException {
            super(next);
            setAsynchChannelListener(new AsynchChannelListener() {
                public void onPacket(Packet packet) {
                    if (buffer == null) {
                        buffer = packet;
                    } else {
                        buffer = AppendedPacket.join(buffer, packet);
                    }
                    findMagicNumber();
                }

                public void onPacketError(IOException error) {
                    dispose();
                }
            });
        }

        private void findMagicNumber() {
            for (Iterator iter = recognizerMap.keySet().iterator(); iter.hasNext();) {
                ProtocolRecognizer recognizer = (ProtocolRecognizer) iter.next();
                if (recognizer.recognizes(buffer.duplicate())) {

                    if( UnknownRecognizer.UNKNOWN_RECOGNIZER == recognizer ) {
                        // Dispose the channel.. don't know what to do with it.
                        dispose();
                    }
                    
                    SubPortAsynchChannelServer onePort = (SubPortAsynchChannelServer) recognizerMap.get(recognizer);
                    if( onePort == null ) {
                        // Dispose the channel.. don't know what to do with it.
                        dispose();
                    }

                    // Once the magic number is found:
                    // Stop the channel so that a decision can be taken on what to
                    // do with the
                    // channel. When the channel is restarted, the buffered up
                    // packets wiil get
                    // delivered.
                    try {
                        stop(NO_WAIT_TIMEOUT);
                        setAsynchChannelListener(null);
                    } catch (IOException e) {                        
                        getAsynchChannelListener().onPacketError(e);
                    }
                    
                    Channel channel = getNext();
                    channel = AsynchToSynchChannelAdapter.adapt(channel);
                    channel = new PushbackSynchChannel((SynchChannel) channel, buffer);
                    channel = SynchToAsynchChannelAdapter.adapt(channel);
                    
                    onePort.onAccept(channel);
                    break;
                }
            }
        }
    }    

    /**
     * Clients bind against the OnePortAsynchChannelServer and get 
     * SubPortAsynchChannelServer which can be used to accept connections.
     */
    final private class SubPortAsynchChannelServer implements AsynchChannelServer {
        
        private final ProtocolRecognizer recognizer;
        private AcceptListener acceptListener;
        private boolean started;
        
        /**
         * @param recognizer
         */
        public SubPortAsynchChannelServer(ProtocolRecognizer recognizer) {
            this.recognizer = recognizer;
        }

        public void setAcceptListener(AcceptListener acceptListener) {
            this.acceptListener = acceptListener;
        }
        
        public URI getBindURI() {
            return next.getBindURI();
        }
        
        public URI getConnectURI() {
            return next.getConnectURI();
        }
        
        public void dispose() {
            started = false;
            recognizerMap.remove(recognizer);
        }
        
        public void start() throws IOException {
            started = true;
        }
        public void stop(long timeout) throws IOException {
            started = false;
        }
        
        void onAccept(Channel channel) {
            if( started && acceptListener!=null ) {
                acceptListener.onAccept(channel);
            } else {
                // Dispose the channel.. don't know what to do with it.
                channel.dispose();
            }
        }
        
        public Object narrow(Class target) {
            if( target.isAssignableFrom(getClass()) ) {
                return this;
            }
            return OnePortAsynchChannelServer.this.narrow(target);
        }    
        
    }
    
    
    private final ConcurrentHashMap recognizerMap = new ConcurrentHashMap();

    public OnePortAsynchChannelServer(AsynchChannelServer server) throws IOException {
        super(server);
        super.setAcceptListener(new OnePortAcceptListener());
    }
    
    public void setAcceptListener(AcceptListener acceptListener) {
        throw new IllegalAccessError("Not supported");
    }    
    
    public AsynchChannelServer bindAsynchChannel(ProtocolRecognizer recognizer) throws IOException {
        
        if( recognizerMap.contains(recognizer) ) 
            throw new IOException("That recognizer is allredy bound.");
        
        SubPortAsynchChannelServer server = new SubPortAsynchChannelServer(recognizer);
        Object old = recognizerMap.put(recognizer, server);
        return server;
    }
    
    
}
