import java.applet.Applet;
import java.awt.*;
import java.io.*;
import java.util.Vector;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;

//
// ConsoleAreaText implements the component drawing the text
//
class ConsoleAreaText extends Canvas {
    protected ConsoleArea parent;
    protected boolean changed = false;

    public ConsoleAreaText(ConsoleArea p) { parent = p; }

    // call setChanged() when the next repaint should clear the
    // whole area
    public void setChanged() { changed = true; }

    public void paint(Graphics g) {
        if(changed) {
            g.clearRect(0, 0, this.size().width, this.size().height);
            changed = false;
        }
        FontMetrics f = g.getFontMetrics();
        g.drawRect(0, 0, this.size().width - 1, this.size().height - 1);
        int y = f.getMaxAscent() + ConsoleArea.BORDER_WID;
        int x = -parent.getOffsX() + ConsoleArea.BORDER_WID;
        for(int i = parent.getOffsY(); y < this.size().height; i++) {
            String line = parent.getLine(i);
            if(line == null) break;
            g.drawString(line, x, y);
            y += f.getHeight();
        }
    }
}

//
// ConsoleArea implements the entire scrollable area where the user can
// type. It maintains all the text to be printed.
//
class ConsoleArea extends Frame {
    public static final int BORDER_WID = 3;
    public static final int RT_WIDTH = 23;

    protected Scrollbar hbar;
    protected Scrollbar vbar;
    public ConsoleAreaText canvas;

    protected Vector lines = new Vector(); // the lines of text
    int max_width = 1; // the longest line in pixels

    int offs_x = 0; // in pixels
    int offs_y = 0; // in lines
    int height = 1; // in lines
    int width = 1; // in pixels

    protected static final int BUFFER_RESET_LENGTH = 80;
    StringBuffer buffer = new StringBuffer(); // input buffer
    int buffer_pos = 0; // position in input buffer

    public ConsoleArea() {
        super("Console");
        
        // create the graphics region
        GridBagLayout layout = new GridBagLayout();
        GridBagConstraints c = new GridBagConstraints();
        c.fill = GridBagConstraints.BOTH;
        this.setLayout(layout);
        canvas = new ConsoleAreaText(this);
        c.gridx = 0; c.gridy = 0;
        c.weightx = 1.0; c.weighty = 1.0;
        layout.setConstraints(canvas, c);
        this.add(canvas);

        hbar = new Scrollbar(Scrollbar.HORIZONTAL);
        c.gridx = 0; c.gridy = 1;
        c.weightx = 1.0; c.weighty = 0.0;
        layout.setConstraints(hbar, c);
        this.add(hbar);

        vbar = new Scrollbar(Scrollbar.VERTICAL);
        c.gridx = 1; c.gridy = 0;
        c.weightx = 0.0; c.weighty = 1.0;
        layout.setConstraints(vbar, c);
        this.add(vbar);

        // initialize the text data
        lines.addElement("");
    }
    
    public Dimension getPreferredSize() {
        return new Dimension(400,250);
    }
    public Dimension getMinimumSize() {
        return new Dimension(100,50);
    }

    public void reset() {
        // reset the variables
        lines = new Vector();
        lines.addElement("");
        max_width = 0;
        offs_x = 0;
        offs_y = 0;
        synchronized(buffer) {
            buffer = new StringBuffer();
            buffer_pos = 0;
        }

        // repair the appearance
        canvas.setChanged();
        sizeBars();
        this.update(this.getGraphics());
    }

    public int getOffsX() { return offs_x; }
    public int getOffsY() { return offs_y; }
    public String getLine(int which) {
        if(which >= lines.size()) return null;
        else return (String) lines.elementAt(which);
    }

    public void reshape(int x, int y, int wid, int ht) {
        super.reshape(x, y, wid, ht);

        // determine how many lines fit vertically
        int line_height = canvas.getGraphics().getFontMetrics().getHeight();
        height = (ht - hbar.size().height - 2 * BORDER_WID) / line_height;
        width = wid - vbar.size().width - 2 * BORDER_WID;

        sizeBars();
    }

    protected void sizeBars() {
        hbar.setValues(offs_x, width, 0, width >= max_width ? 1 : max_width);
        hbar.setPageIncrement(max_width <= 1 ? 1 : max_width / 2);

        vbar.setValues(offs_y, height, 0, lines.size());
        vbar.setPageIncrement(height == 1 ? 1 : height - 1);
    }

    public void paint(Graphics g) {
        canvas.paint(canvas.getGraphics());
    }

    public int read() {
        char ret;
        // wait until a character is available
        while(buffer_pos >= buffer.length()) {
            try {
                synchronized(this) { wait(); }
            } catch(InterruptedException e) { }
        }
        // consume this character
        synchronized(buffer) {
            ret = buffer.charAt(buffer_pos);
            ++buffer_pos;
            if(buffer_pos >= BUFFER_RESET_LENGTH) {
                // remove the first buffer_pos characters at this point
                if(buffer_pos < buffer.length()) {
                    String new_val = new String(buffer).substring(buffer_pos);
                    buffer = new StringBuffer(new_val);
                } else {
                    buffer = new StringBuffer();
                }
                buffer_pos = 0;
            }
        }
        return ret;
    }

    public boolean handleEvent(Event e) {
        if(e.id == Event.KEY_PRESS) {
            canvas.requestFocus();
            addInput((char) e.key);
            return true;
        }
        if(e.target == hbar) {
            switch(e.id) {
            case Event.SCROLL_LINE_UP:
            case Event.SCROLL_LINE_DOWN:
            case Event.SCROLL_PAGE_UP:
            case Event.SCROLL_PAGE_DOWN:
            case Event.SCROLL_ABSOLUTE:
                offs_x = ((Integer) e.arg).intValue();
                break;
            }
            this.update(canvas.getGraphics());
            return true;
        } else if(e.target == vbar) {
            switch(e.id) {
            case Event.SCROLL_LINE_UP:
            case Event.SCROLL_LINE_DOWN:
            case Event.SCROLL_PAGE_UP:
            case Event.SCROLL_PAGE_DOWN:
            case Event.SCROLL_ABSOLUTE:
                offs_y = ((Integer) e.arg).intValue();
                break;
            }
            this.update(canvas.getGraphics());
            return true;
        }
        return false;
    }

    public void addInput(char ch) {
        if(ch == '\010' || ch == '\077') {
            // handle a backspace character
            synchronized(buffer) {
                if(buffer_pos <= buffer.length() - 1
                        && buffer.length() > 0
                        && (buffer.charAt(buffer.length() - 1) != '\n'
                            && buffer.charAt(buffer.length() - 1) != '\r')) {
                    buffer.setLength(buffer.length() - 1);
                }
            }
            String old_line = getLine(lines.size() - 1);
            if(old_line.length() > 0) {
                String new_line = old_line.substring(0, old_line.length() - 1);
                lines.setElementAt(new_line, lines.size() - 1);
                canvas.setChanged();
                this.update(this.getGraphics());
            }
        } else {
            // add a character to the input stream
            synchronized(buffer) { buffer.append(ch); }
            appendText(ch);
            if(ch == '\n' || ch == '\r') { synchronized(this) { notify(); } }
        }
    }

    public synchronized void appendText(String s) {
        boolean lines_added = false; // true if any lines added
        boolean width_changed = false; // true if hbar should be resized
        boolean scroll_down = (offs_y + height >= lines.size());

        // add a line for each newline in the text
        int last_pos = 0;
        for(int i = 0; i < s.length(); i++) {
            if(s.charAt(i) == '\n' || s.charAt(i) == '\r') {
                String old_line = getLine(lines.size() - 1);
                String new_line = old_line + s.substring(last_pos, i);
                lines.setElementAt(new_line, lines.size() - 1);
                int wid = canvas.getGraphics().getFontMetrics().stringWidth(new_line) + RT_WIDTH;
                if(wid > max_width) { width_changed = true; max_width = wid; }
                lines.addElement("");
                last_pos = i + 1;
                lines_added = true;
            }
        }

        // add any remaining characters past the last newline
        String old_line = getLine(lines.size() - 1);
        String new_line = old_line + s.substring(last_pos);
        lines.setElementAt(new_line, lines.size() - 1);
        int wid = canvas.getGraphics().getFontMetrics().stringWidth(new_line) + RT_WIDTH;
        if(wid > max_width) { width_changed = true; max_width = wid; }

        // rework the appearance appropriately
        if(scroll_down && lines_added && lines.size() - height > 0) {
            // scroll down for user
            canvas.setChanged();
            offs_y = lines.size() - height + 1;
        }
        if(lines_added || width_changed) sizeBars();
        this.update(this.getGraphics());
    }
    public void appendText(char ch) {
        appendText(new Character(ch).toString());
    }
}

//
// ConsoleOutput is an implementation of OutputStream, so that
// we can use PrintStream in Console
//
class ConsoleOutput extends OutputStream {
    protected ConsoleArea area;
    public ConsoleOutput(ConsoleArea a) { area = a; }
    public void write(int b) {
        area.appendText((char) b);
    }
    public void write(byte[] bytes) {
        area.appendText(new String(bytes, 0));
    }
    public void write(byte[] bytes, int offs, int len) {
        area.appendText(new String(bytes, 0, offs, len));
    }
}

//
// Console implements the entire interface, plus the methods called
// by the console applet
//
public class Console extends Frame {
    protected static PrintStream out = System.out;
    protected static ConsoleArea area = null;

    static {
        // add the typing area
        area = new ConsoleArea();
        area.pack();
        area.show();

        // set up the PrintStream
        out = new PrintStream(new ConsoleOutput(area));
    }

    public static void print(int what)     { out.print(what); }
    public static void print(double what)  { out.print(what); }
    public static void print(char what)    { out.print(what); }
    public static void print(boolean what) { out.print(what); }
    public static void print(String what)  { out.print(what); }

    public static void println(int what)     { out.println(what); }
    public static void println(double what)  { out.println(what); }
    public static void println(char what)    { out.println(what); }
    public static void println(boolean what) { out.println(what); }
    public static void println(String what)  { out.println(what); }

    // read a single character
    protected static char getRawChar() {
        return (char) area.read();
    }

    // read in a string with no spaces
    protected static String getRawToken() {
        // skip initial spaces
        char ch; // charact
        ch = getRawChar();
        while(ch != '\0' && Character.isSpace(ch)) {
            ch = getRawChar();
        }

        // everything until the next space is part of the token
        StringBuffer ret = new StringBuffer();
        while(ch != '\0' && !Character.isSpace(ch)) {
            ret.append(ch);
            ch = getRawChar();
        }
        return new String(ret);
    }

    public static int readInt() {
        try {
            return Integer.parseInt(getRawToken());
        } catch(NumberFormatException e) {
            return Integer.MIN_VALUE;
        }
    }
    public static double readDouble() {
        try {
            return Double.valueOf(getRawToken()).doubleValue();
        } catch(NumberFormatException e) {
            return Double.NEGATIVE_INFINITY;
        }
    }
    public static String readString() { return getRawToken(); }
    public static char readChar() { return getRawChar(); }

    public static boolean readBoolean() {
        // 'y' or 'Y' is true; others are false
        return Character.toUpperCase(getRawChar()) == 'Y';
    }

}
