/* -*- Mode: java; indent-tabs-mode: t; c-basic-offset: 4; tab-width: 4 -*- */

// Copyright 2001-2003, Danger, Inc.  All Rights Reserved.
// This file is subject to the Danger, Inc. Sample Code License,
// which is provided in the file SAMPLE_CODE_LICENSE.
// Copies are also available from http://developer.danger.com/

package com.danger.terminal;

import java.io.InputStream;
import java.io.OutputStream;
import java.io.IOException;

public class TelnetSession implements Session {
    public TelnetSession (ServerInfo info, InputStream in, OutputStream out, int width, int height,
        String terminalType) throws IOException
    {
        mCanDoWindowSize = false;
        fRawInput = in;
        fRawOutput = out;
        mColumns = width;
        mRows = height;
        mTerminalType = terminalType;
        mInfo = info;
        mInfo.setStatus(ServerInfo.STATUS_OPEN);
        sendInitialOptions();
    }

    /* package */ void
    setConsole (TerminalConsole console)
    {
        mConsole = console;
    }

    public void
    refreshShow()
    {
        // pass
    }

    public InputStream
    getInputStream()
    {
        return new TelnetInputStream();
    }
    
    public OutputStream
    getOutputStream()
    {
        return new TelnetOutputStream();
    }

    public void
    resize (int cols, int rows)
    {
        mColumns = cols;
        mRows = rows;
        if (mCanDoWindowSize) {
            byte[] geometry = new byte[4];
            geometry[0] = (byte)(mColumns >> 8);
            geometry[1] = (byte) mColumns;
            geometry[2] = (byte) (mRows >> 8);
            geometry[3] = (byte) mRows;
            try {
                sendOption(kSuboptionBegin, kWindowSize, geometry);
            } catch (IOException x) {
                // pass
            }
        }
    }

    private void
    write(int ch) throws IOException
    {
        synchronized (this) {
            fRawOutput.write(ch);
        }
    }
    
    private void
    write(byte[] stuff) throws IOException
    {
        synchronized (this) {
            fRawOutput.write(stuff);
        }
    }

    public void
    write(byte[] stuff, int offset, int length) throws IOException
    {
        synchronized (this) {
            fRawOutput.write(stuff, offset, length);
        }
    }

    // OOPS! bug in ProxySocketInputStream, so don't use 'int read()'
    private int
    rawRead ()
        throws IOException
    {
        byte[] buf = new byte[1];
        int ch = fRawInput.read(buf, 0, 1);
        if (ch == 1) {
            return ((int) buf[0]) & 0xff;
        }
        return ch;
    }
    
    private int
    read() throws IOException
    {
        int ch;
        
        for (;;) {
            ch = rawRead();
            synchronized (this) {
                if (ch != kInterpretAsCommand)
                    break;

                processCommand();
            }
        }

        if (mWaitingForOptions) {
            // assume dumb remote host, turn on line mode
            mWaitingForOptions = false;
            mConsole.setLineMode(true);
        }

        return ch;
    }

    private int
    read (byte[] b, int off, int len)
        throws IOException
    {
        if (len == 0) {
            return 0;
        }

        int count = 0;

        /* do a blocking call on the first byte */
        int ch = read();
        synchronized (this) {
            if (ch == -1) {
                throw new IOException("End of stream");
            }
            b[off++] = (byte)ch;
            count++;

            /* after the first byte has been read, check available() and only
             * read in what's buffered.  this is to mimic standard read(byte[])
             * sematics.
             */
            while ((count < len) && (fRawInput.available() > 0)) {
                ch = read();
                if (ch == -1) {
                    return count;
                }
                b[off++] = (byte)ch;
                count++;
            }
        }

        return count;
    }

    private void
    sendInitialOptions() throws IOException
    {
        synchronized (this) {
            sendOption(kWill, kTerminalType);
            sendOption(kWill, kWindowSize);
            // request to get out of line-mode
            mWaitingForOptions = true;
            sendOption(kWill, kSuppressGoAhead);
        }
    }
    
    private void
    processCommand() throws IOException
    {
        int command = rawRead();
        int option;
        switch (command) {
            case kNOP:
                break;

            case kWill:
            case kWont:
            case kDo:
            case kDont:
                option = rawRead();
                if (processOption(command, option, null)) {
                    if (command == kWill)
                        sendOption(kDo, option);
                } else {
                    if (command == kWill)
                        sendOption(kDont, option);
                    else if (command == kDo)
                        sendOption(kWont, option);
                }
                
                break;

            case kSuboptionBegin:
                do {
                    option = rawRead();
                    if (option == kSuboptionEnd)
                        break;
                        
                    byte[] optionData = new byte[64];
                    int offs = 0;
                    for (;;) {
                        int ch = rawRead();
                        if (ch == kInterpretAsCommand)
                            break;
                        
                        optionData[offs++] = (byte) ch;
                    }

                    processOption(kSuboptionBegin, option, optionData);
                } while (true);
                break;
                
            default:
                System.out.println("Unknown command " + command);
        }
    }
    
    private boolean
    processOption(int command, int option, byte data[])
        throws IOException
    {
        System.out.println("RECV: " + command2String(command) + " " + option2String(option));
        switch (option) {
            case kEcho:
                if (command == kWill) {
                    // turn off local echo
                    mConsole.setLocalEcho(false);
                } else if (command == kWont) {
                    // turn on local echo
                    mConsole.setLocalEcho(true);
                } else if (command == kDo) {
                    sendOption(kWill, option);
                    mConsole.setLocalEcho(true);
                }
                break;

            case kSuppressGoAhead:
                mWaitingForOptions = false;
                if (command == kDo) {
                    sendOption(kWill, option);
                } else if (command == kWill) {
                    mConsole.setLineMode(false);
                } else {
                    mConsole.setLineMode(true);
                }
                break;

            case kLogout:
                if (command == kDo)
                    sendOption(kWill, option);

                break;
                
            case kTerminalType:
                if (command == kSuboptionBegin && data[0] == kSend) {
                    byte[] termString = mTerminalType.getBytes();
                    byte[] optionval = new byte[termString.length + 1];
                    System.arraycopy(termString, 0, optionval, 1, termString.length);
                    optionval[0] = kIs;
                    sendOption(kSuboptionBegin, kTerminalType, optionval);
                } else if (command == kDo) {
                    sendOption(kWill, kTerminalType);
                }
                break;
                
            case kWindowSize:
                if (command == kDo) {
                    mCanDoWindowSize = true;
                    byte[] geometry = new byte[4];
                    geometry[0] = (byte) (mColumns >> 8);
                    geometry[1] = (byte) mColumns;
                    geometry[2] = (byte) (mRows >> 8);
                    geometry[3] = (byte) mRows;
                    sendOption(kSuboptionBegin, kWindowSize, geometry);
                }
                break;
                
            case kEnvironment:
                if (command == kSuboptionBegin && data[0] == kSend) {
                    byte[] env = new byte[1];
                    env[0] = kIs;
                    sendOption(kSuboptionBegin, kEnvironment, env);
                } else if (command == kDo)
                    sendOption(kWill, kEnvironment);
                
                break;
                
            case kExtendedOptions:
                // Falls through for now.
                System.out.println("oops");

            default:    // Unrecognized option
                return false;
        }

        return true;
    }
    
    private void
    sendOption(int command, int option) throws IOException
    {
        sendOption(command, option, null);
    }
    
    private void
    sendOption(int command, int option, byte data[]) throws IOException
    {
        System.out.println("SEND: " + command2String(command) + " " + option2String(option));
        synchronized (this) {
            fRawOutput.write(kInterpretAsCommand);
            switch (command) {
                case kSuboptionBegin:
                    fRawOutput.write(kSuboptionBegin);
                    fRawOutput.write(option);
                    fRawOutput.write(data);
                    fRawOutput.write(kInterpretAsCommand);
                    fRawOutput.write(kSuboptionEnd);
                    break;

                default:
                    fRawOutput.write(command);
                    fRawOutput.write(option);
            }
        }
    }
    
    private String
    command2String(int command)
    {
        switch (command) {
            case 240:
                return "SE";
            case 241:
                return "NOP";
            case 242:
                return "MARK";
            case 243:
                return "BRK";
            case 244:
                return "IP";
            case 245:
                return "AO";
            case 246:
                return "AYT";
            case 247:
                return "EC";
            case 248:
                return "EL";
            case 249:
                return "GA";
            case 250:
                return "SB";
            case 251:
                return "WILL";
            case 252:
                return "WONT";
            case 253:
                return "DO";
            case 254:
                return "DONT";
            case 255:
                return "IAC";
            default:
                return "" + command;
        }
    }

    private String
    option2String(int option)
    {
        switch (option) {
            case 0: return "BIN";
            case 1: return "Echo"; 
            case 2: return "reconnect";   
            case 3: return "Suppress Go Ahead";
            case 4: return "Approx Message Size";
            case 5: return "Status";
            case 6: return "Timing Mark"; 
            case 7: return "Remote Controlled Trans and Echo";
            case 8: return "Output Line Width";   
            case 9: return "Output Page Size";
            case 10: return "Output Carriage-Return Disposition"; 
            case 11: return "Output Horizontal Tab Stops"; 
            case 12: return "Output Horizontal Tab Disposition"; 
            case 13: return "Output Formfeed Disposition"; 
            case 14: return "Output Vertical Tabstops"; 
            case 15: return "Output Vertical Tab Disposition"; 
            case 16: return "Output Linefeed Disposition"; 
            case 17: return "Extended ASCII"; 
            case 18: return "Logout"; 
            case 19: return "Byte Macro"; 
            case 20: return "Data Entry Terminal";
            case 21: return "SUPDUP"; 
            case 22: return "SUPDUP Output"; 
            case 23: return "Send Location"; 
            case 24: return "Terminal Type"; 
            case 25: return "End of Record"; 
            case 26: return "TACACS User Identification"; 
            case 27: return "Output Marking"; 
            case 28: return "Terminal Location Number"; 
            case 29: return "Telnet 3270 Regime";
            case 30: return "X.3 PAD"; 
            case 31: return "NAWS";
            case 32: return "Terminal Speed"; 
            case 33: return "Remote Flow Control"; 
            case 34: return "Linemode";
            case 35: return "X Display Location"; 
            case 36: return "Environment"; 
            case 37: return "Authentication";
            case 38: return "Encryption"; 
            case 39: return "New Environment"; 
            case 255: return "EXT";
            default:
                return "" + option;
        }
    }


    private InputStream fRawInput;
    private OutputStream fRawOutput;
    
    // Current configuration
    private String mTerminalType;
    private int mColumns;
    private int mRows;
    private boolean mCanDoWindowSize;
    private boolean mWaitingForOptions;

    private ServerInfo mInfo;
    private TerminalConsole mConsole;

    // Telnet commands
    private static final int kSuboptionEnd = 240;
    private static final int kNOP = 241;
    private static final int kDataMark = 242;
    private static final int kBreak = 243;
    private static final int kInterrupt = 244;
    private static final int kAbort = 245;
    private static final int kAreYouThere = 246;
    private static final int kEraseChar = 247;
    private static final int kEraseLine = 248;
    private static final int kGoAhead = 249;
    private static final int kSuboptionBegin = 250;
    private static final int kWill = 251;
    private static final int kWont = 252;
    private static final int kDo = 253;
    private static final int kDont = 254;
    private static final int kInterpretAsCommand = 255;

    // Telnet options
    private static final int kEcho = 1;
    private static final int kSuppressGoAhead = 3;
    private static final int kLogout = 18;
    private static final int kTerminalType = 24;
    private static final int kWindowSize = 31;
    private static final int kTerminalSpeed = 32;        // we don't use this
    private static final int kLineMode = 34;
    private static final int kEnvironment = 36;
    private static final int kAuthentication = 37;
    private static final int kExtendedOptions = 255;

    // Suboption specific parameters
    private static final int kIs = 0;
    private static final int kSend = 1;

    class TelnetInputStream extends InputStream
    {
        public int read() throws IOException {
            return TelnetSession.this.read();
        }

        public int read (byte[] b, int off, int len) throws IOException {
            return TelnetSession.this.read(b, off, len);
        }
        
        public int read (byte[] b) throws IOException {
            return TelnetSession.this.read(b, 0, b.length);
        }
    }

    class TelnetOutputStream extends OutputStream
    {
        public void write(byte[] stuff) throws IOException {
            TelnetSession.this.write(stuff);
        }
        
        public void write(int value) throws IOException {
            TelnetSession.this.write(value);
        }
        
        public void write(byte[] stuff, int offset, int length) throws IOException {
            TelnetSession.this.write(stuff, offset, length);
        }
    }
}

