package scone.util.tokenstream;


import java.io.IOException;
import java.io.InputStream;
import java.io.Reader;
import java.io.UnsupportedEncodingException;
import java.util.Hashtable;

import com.ibm.wbi.Copyright;
import com.ibm.wbi.MegObject;
import com.ibm.wbi.NotCharDataException;
import com.ibm.wbi.RequestEvent;


/**
 * writes Tokens into a TokenBuffer
 * 
 * @author Volkert Buchmann (ported from code by Team WBI), 
 *         IBM Almaden Research Center, 
 *         (c) Copyright IBM Corp 2000
 * @version 1.0
 */

public class TokenOutputStream implements MegObject {

    private final static String COPYRIGHT = Copyright.COPYRIGHT;

    private TokenBuffer buffer = null;
    private Object bufferContext = null;
    private boolean closed = false;
    private long timeout = 0;
    private Hashtable metaInfo = null;

    /**
     * creates a new TokenOutputStream with a standard size TokenBuffer.
     */
    public TokenOutputStream() {
        this.buffer = new TokenBuffer(256);
        this.bufferContext = buffer.subscribe();
    }

    /**
     * creates a TokenOutputStream that references another TokenOutputStream's
     * buffer. This constructor is used for cloning. Since the TokenOutputStream 
     * is a MegObject it must be able to create clones that access the same 
     * TokenBuffer in order to allow more than one TokenInputStream to read from it.
     * Thus, TokenOutputStreams are not cloned to write to the same buffer, but
     * to allow different TokenInputStreams to access this buffer.
     * @param buffer the TokenBuffer
     * @param timeout the timeout
     */
    TokenOutputStream(TokenBuffer buffer, long timeout) {
        this.buffer = buffer;
        this.timeout = timeout;
        this.bufferContext = buffer.subscribe(); 
        buffer.mark(bufferContext, -1);         // sad but true..
    }

    /**
     * Returns a TokenOutputStream that allows to pass tokens to the next MEG.
     * @param e the RequestEvent
     * @return the TokenOutputStream
     */
    public static synchronized TokenOutputStream create(RequestEvent e) throws Exception {
        TokenOutputStream out = new TokenOutputStream();
        e.putMegObject(out);
        return out;
    }

    /**
     * provides interoperation with "old" MEGs that do not understand the
     * MegObject and must process the data as a MegInputStream.
     * @return InputStream access to the MEG data.
     */
    public InputStream getInputStream() {
        return new TokenStreamer(this);
    }

    /**
     * getReader provides access to the MegObject as character data rather than bytes.
     * @return Reader access to the MEG data.
     */
    public Reader getReader() 
            throws UnsupportedEncodingException, NotCharDataException {
        return null;
    }
    
    /**
     * getReader provides access to the MegObject as character data using the specified
     * character encoding.
     * @param encoding the desired character encoding.
     * @return Reader access to the MEG data.
     */
    public Reader getReader(String encoding) 
            throws UnsupportedEncodingException, NotCharDataException {
        return getReader();
    }
  
    /**
     * returns a copy of this MegObject to preserve the data
     * in its original state.
     * @return the clone
     */
    public MegObject getClone() {
        return new TokenOutputStream(buffer, timeout);
    }

    /**
     * Write a portion of the passed Token buffer array.
     * @param   buf     The Token buffer array.
     * @param   offset  Offset in the array to start writing from.
     * @param   length  The amount from offset to write.
     * @exception   IOException     If unable to write to stream.
     */
    public void write(Token buf[], int offset, int length) throws IOException {
        if (closed) {
            throw new IOException("TokenOutputStream is closed to write.");
        }
        try {
            if (length > 0) {
                int written = 0;

                // we may not be able write everything at once
                while (written < length) {
                    written += buffer.write(buf, offset + written, length - written);
                }
            }
        } catch (TokenBufferClosedException e) {
            throw new IOException(e.getMessage());
        }
    }

    /**
     * Write a Token to the stream.
     * @param   token the Token to write.
     * @exception   IOException     If unable to write to stream.
     */
    public void write(Token token) throws IOException {
        Token tArray[] = {token};

        write(tArray, 0, 1);
    }

    /**
     * Write a Token array to the stream.
     * @param   buf     The Token array to write.
     * @exception   IOException     If unable to write to stream.
     */
    public void write(Token buf[]) throws IOException {
        write(buf, 0, buf.length);
    }

    /**
     * Set a timeout value for write operations.  Writes will not block longer
     * than the timeout time.  If a timeout occurs, then an InterruptedIOException
     * will be thrown.
     * @param timeout The maximum time to block for write operations, in milliseconds.
     *                A value of zero (the default) means to never timeout.
     */
    public void setTimeout(long timeout) {
        this.timeout = timeout;
    }

    /**
     * Access the timeout value for write operations.
     * @return The maximum time to block for write operations, in milliseconds.  A value
     *         of zero means to never timeout.
     */
    public long getTimeout() {
        return timeout;
    }

    /**
     * Close the token output stream.
     * @exception   IOException     If error occurred closing stream.
     */
    public void close() throws IOException {
        if (!closed) {

            closed = true;
            // Tell token buffer that it will not be written to anymore.
            buffer.done();
        }
    }

    /**
     * Determine if the stream is closed.
     * @return  <code>true</code> if closed.
     */
    public boolean isClosed() {
        return closed;
    }

    /**
     * Flush the stream.
     */
    public void flush() throws IOException {}

    /**
     * Get the Token buffer where the data is stored. This method is used 
     * by TokenInputStreamBufferImpl
     * @return      The Token buffer.
     */
    TokenBuffer getTokenBuffer() {
        return buffer;
    }

    /**
     * Get the buffer context. This method is used 
     * by TokenInputStreamBufferImpl
     * @return      The buffer context.
     */    
    protected Object getBufferContext() {
        return bufferContext;
    }

    /**
     * converts a token stream into an InputStream
     */
    class TokenStreamer extends InputStream {
        TokenInputStream in = null;
        StringBuffer buffer = new StringBuffer();
        int pos = 0;
	
        /**
         * creates a new TokenStreamer from a TokenOutputStream.
         */
        public TokenStreamer(TokenOutputStream out) {
            // create an input stream first!

            in = new TokenInputStreamBufferImpl(out);	

        }

        /**
         * reads into a byte array. 
         */
        public int read(byte[] b, int off, int len)throws IOException {

            /*
             first, we try to read characters from our StringBuffer buffer.
             If that buffer is empty, we fill it with the String representation of a Token.
             Those characters are then converted into bytes and written to the byte array.
             */

            // make sure the buffer is not empty
            Token token = null;

            while (buffer.length() - pos == 0) {
                pos = 0;
                token = in.read();
                if (token == null) { 
                    return -1;
                } else {
                    buffer = new StringBuffer(token.toString());
                }
            }
	    
            // trim len, if it would exceed the buffer
            if (len > buffer.length() - pos) {
                len = buffer.length() - pos;
            }
	    
            // read the characters from the buffer
            for (int i = 0; i < len; i++) {
                b[off + i] = (byte) buffer.charAt(pos + i);
            }
	    
            // adjust our position in the buffer
            pos = pos + len;

            // return the amount of bytes read
            return len;
        }

        // this one works like the other read method
        public int read() throws IOException {
            byte[] b = new byte[1];
            int l = read(b, 0, 1);

            if (l < 0) {
                return l;
            } else {
                int bret = b[0];

                if (bret < 0) {
                    bret += 256;
                }
                return bret;
            }
        }
	
        // close the InputStream.
        public void close() throws IOException {
            in.close();
        }
    }
   
    public Hashtable getMetaInfo() {
        return metaInfo;
    }
   
    protected void setMetaInfo(Hashtable metaInfo) {
        this.metaInfo = metaInfo;
    }
}
