package com.jscape.inet.telnet;

import java.io.BufferedInputStream;
import java.io.IOException;
import java.io.InputStream;


/**
 * Specialised input stream used to interpret telnet commands in ASCII stream.
 */
public class TelnetInputStream
        extends InputStream {
    /**
     * The BufferedInputStream underlying this TelnetInputStream
     */
    BufferedInputStream is = null;

    /**
     * The previous character read
     */
    int prevchar = -1;

    /**
     * The Telnet instance mapped to this TelnetInputStream
     */
    Telnet telnet = null;

    /**
     * Creates a new TelnetInputStream object.
     *
     * @param tn a Telnet
     * @param in an InputStream
     */
    public TelnetInputStream(Telnet tn, InputStream in) {
        this.telnet = tn;
        is = new BufferedInputStream(in);
    }

    /**
     * Returns underlying stream.
     *
     * @return an InputStream
     */
    public InputStream getStream() {
        return is;
    }

    /**
     * Read data from telnet stream with escaping control characters.
     *
     * @return data read
     *
     * @throws IOException if an I/O related error occurs
     */
    public int read()
            throws IOException {
        // read next byte
        int a = tread();

        // if byte is a command then handle properly
        if (a == Telnet.TSC_IAC) {
            int b = is.read();

            if (b == Telnet.TSC_IAC) {
                return Telnet.TSC_IAC; // this was IAC escaped by another IAC
            } else {
                // handle command                
                telnet.commandRead(b);

                // after command is read, we can return next character
                // this character can also be command, so the best way
                // is to recurse read() untill a character is read.
                // this is consistent with normal read operation;
                // read returns only when character is read, end of stream
                // has been reached or exception occurs.
                return read();
            }
        }

        return a;
    }

    /**
     * Read data from telnet stream without escaping control characters.
     *
     * @return data read
     *
     * @throws IOException if an I/O related error occurs
     */
    public int tread()
            throws IOException {
        // the following code removes NULL from CR NULL sequence
        int a = is.read();

        while ((prevchar == Telnet.TSC_CR) && (a == 0)) {
            prevchar = a;
            a = is.read();
        }

        prevchar = a;

        return a;
    }

    /**
     * Read data from telnet stream with escaping control characters.
     *
     * @param b buffer to read data from
     *
     * @return length of bytes read
     *
     * @throws IOException if an I/O related error occurs
     */
    public int read(byte[] b)
            throws IOException {
        return read(b, 0, b.length);
    }

    /**
     * Read data from telnet stream with escaping control characters.
     *
     * @param b   buffer to read data from
     * @param off offset in buffer to start reading from
     * @param len length of data to read from offset
     *
     * @return total number of bytes read
     *
     * @throws IOException if an I/O related error occurs
     */
    public int read(byte[] b, int off, int len)
            throws IOException {
        for (int i = off; i < len; i++) {
            int a = read();

            if (a == -1) {
                return i - off; //we have read i-off bytes so far
            }

            b[i] = (byte) a;
        }

        return len;
    }

    /**
     * Checks if mark is supported in this TelnetInputStream
     *
     * @return true if mark supported, false otherwise
     */
    public boolean markSupported() {
        return is.markSupported();
    }

    /**
     * Skips over and discards n bytes of data from this input stream.
     *
     * @param n the number of bytes to skip
     *
     * @return the actual number of bytes skipped
     *
     * @throws IOException if an I/O error occurs
     * @see java.io.InputStream#skip(long)
     */
    public synchronized long skip(long n)
            throws IOException {
        return is.skip(n);
    }

    /**
     * Marks the current position in this input stream.
     *
     * @param readlimit the maximum limit of bytes that can be read before the mark position becomes invalid.
     *
     * @see java.io.InputStream#mark(int)
     */
    public synchronized void mark(int readlimit) {
        is.mark(readlimit);
    }

    /**
     * Returns the number of bytes that can be read (or skipped over) from this input stream
     * without blocking by the next caller of a method for this input stream.
     *
     * @return the number of bytes that can be read from this input stream without blocking.
     *
     * @throws IOException if an I/O error occurs.
     * @see java.io.InputStream#available
     */
    public int available()
            throws IOException {
        return is.available();
    }

    /**
     * Reads until the amount of data available is greater than 0 bytes
     */
    public void flush()
            throws IOException {
        while (available() > 0) {
            read();
        }

    }

    /**
     * Repositions this stream to the position at the time the mark method was
     * last called on this input stream.
     *
     * @throws IOException if an I/O related error occurs
     * @see java.io.InputStream#reset
     */
    public synchronized void reset()
            throws IOException {
        is.reset();
    }

    /**
     * Reads from the stream. This method will read from the stream until one
     * of the supplied strings occured.
     *
     * @param lbr Line break sequences the method is expected to recognise
     *
     * @return a line from the underlying InputStream
     *
     * @throws IOException if an I/O related error occurs
     */
    public String readLine(String[] lbr)
            throws IOException {
        StringBuffer buf = new StringBuffer();
        int[] matched = new int[lbr.length];

        for (int i = 0; i < matched.length; i++) {
            matched[i] = 0;
        }


        while (true) {
            for (int i = 0; i < matched.length; i++) {
                if (matched[i] == lbr[i].length()) {
                    return buf.toString();
                }
            }

            int a = read();

            if (a == -1) {
                throw new IOException("End of stream");
            }

            char ch = (char) a;

            for (int i = 0; i < matched.length; i++) {
                if (ch == lbr[i].charAt(matched[i])) {
                    matched[i]++;
                }
            }

            if ((ch != '\n') && (ch != '\r')) {
                buf.append(ch);
            }
        }
    }

    /**
     * Reads from the stream. This method will read from the stream until one
     * of the supplied strings occured or no more data is available in the
     * stream.
     *
     * @param lbr Array of strings to match against
     *
     * @return reads data from stream until one of the supplied strings is found
     *
     * @throws IOException if an I/O error occurs
     */
    public String readBlock(String[] lbr)
            throws IOException {
        StringBuffer buf = new StringBuffer();
        int[] matched = new int[lbr.length];

        for (int i = 0; i < matched.length; i++) {
            matched[i] = 0;
        }


        while (true) {
            for (int i = 0; i < matched.length; i++) {
                if (matched[i] == lbr[i].length()) {
                    return buf.toString();
                }
            }

            if (available() < 1) {
                return buf.toString();
            }

            int a = read();

            if (a == -1) {
                throw new IOException("End of stream");
            }

            char ch = (char) a;

            for (int i = 0; i < matched.length; i++) {
                if (ch == lbr[i].charAt(matched[i])) {
                    matched[i]++;
                }
            }
        }
    }
}
