/* -*- 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.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.InetSocketAddress;
import java.net.Socket;
import danger.app.Event;
import danger.app.Listener;
import danger.net.SocketControl;
import danger.ui.Font;
import danger.ui.TextField;
import danger.util.format.StringFormat;

/**
 * TerminalConsole is just a descendant of VirtualConsole that tracks some
 * other Terminal-specific information per console.  It also handles the
 * socket connecting, and attaching a specific "session" to each virtual
 * console.
 */

/* package */ class TerminalConsole
    extends VirtualConsole
    implements TerminalEvents, Runnable
{
    public interface ErrorReporter
    {
        public void reportError (String s);
        public void reportStatus (String s);
    }
    
    
    private class MyErrorReporter
        implements ErrorReporter
    {
        public void
        reportError (String s)
        {
            insertError(s);
        }
        
        public void
        reportStatus (String s)
        {
            insertStatus(s);
        }        
    }
    
    
    public
    TerminalConsole (String fontname, int width, int height, int scroll_lines)
    {
        super(fontname, width, height, scroll_lines);

        mHistory = new String[MAX_HISTORY];
        mOutBuffer = new byte[128];
    }

    public void
    refreshShow()
    {
        if (mSession != null) {
            mSession.refreshShow();
        }
    }

    public void
    resize (int width, int height)
    {
        if (mTextField != null) {
            mTextField.setSize(width, 0);
            mTextField.setPosition(0, height - mTextField.getHeight());
            height -= mTextField.getHeight() + TEXT_FIELD_PADDING;
        }

        super.resize(width, height);
        Terminal.instance().getTerminalScreen().hideScrollbar();
        if (mSession != null) {
            mSession.resize(mColumns, mRows);
        }
    }

    public void
    setFont (String fontName)
    {
        if (mTextField != null) {
            mHeight += mTextField.getHeight() + TEXT_FIELD_PADDING;
            mTextField.setFont(Font.findFont(fontName));
            mTextField.invalidate();
        }
        super.setFont(fontName);
    }

    /* package */ void
    setLocalEcho (boolean localEcho)
    {
        mLocalEcho = localEcho;
    }

    /* package */ void
    setLineMode (boolean lineMode)
    {
        boolean inLineMode = (mTextField != null);
        if (inLineMode != lineMode) {
            Terminal.instance().getTerminalScreen().toggleTextField(this);
        }
    }

    public void
    setCodec (String codec)
    {
        super.setCodec(codec);
        mEncoder = CharacterEncoder.getEncoder(codec);
    }

    public void
    reconnect()
    {
        if (!mIsOpen) {            
            mIsOpen = true;
            mInfo.setStatus(ServerInfo.STATUS_CONNECTING);
            mInfo.consoleChanged(this);
            startSession(mInfo);
        }
    }
    
    public void
    connect (ServerInfo info)
    {
        mInfo = info;
        mIsOpen = true;

        info.addConsole(this);
        info.setStatus(ServerInfo.STATUS_CONNECTING);
        info.consoleChanged(this);

        setFont(info.getFont());

        startSession(info);
    }

    public void
    disconnect ()
    {
        //mThread.interrupt();
        if (mSocket != null) {
            try {
                mSocket.close();
            } catch (IOException x) { }
        }
        
        mInfo.setStatus(ServerInfo.STATUS_CLOSED);
        mInfo.removeConsole(this);
    }

    public final boolean
    isOpen()
    {
        return mIsOpen;
    }
    
    public final boolean
    isPaused ()
    {
        return mIsOpen && mIsPaused;
    }

    public Session
    getSession ()
    {
        return mSession;
    }

    public ServerInfo
    getHostInfo()
    {
        return mInfo;
    }

    /* package */ TextField
    getTextField ()
    {
        return mTextField;
    }

    /* package */ void
    setTextField (TextField tf)
    {
        mTextField = tf;
    }

    /* package */ void
    previousHistory ()
    {
        if (mCurrentHistoryIndex == mNextHistoryIndex) {
            // just starting
            mHistory[mCurrentHistoryIndex] = mTextField.toString();
        }
        mCurrentHistoryIndex = (mCurrentHistoryIndex + MAX_HISTORY - 1) % MAX_HISTORY;
        if (mHistory[mCurrentHistoryIndex] == null) {
            // can't go back any further -- reverse.
            mCurrentHistoryIndex = (mCurrentHistoryIndex + 1) % MAX_HISTORY;
            return;
        }
        mTextField.setText(mHistory[mCurrentHistoryIndex]);
    }

    /* package */ void
    nextHistory ()
    {
        if (mCurrentHistoryIndex == mNextHistoryIndex) {
            // can't go down any further
            return;
        }
        mCurrentHistoryIndex = (mCurrentHistoryIndex + 1) % MAX_HISTORY;
        mTextField.setText(mHistory[mCurrentHistoryIndex]);
    }

    /* package */ void
    commitHistory ()
    {
        mHistory[mNextHistoryIndex] = mTextField.toString();
        mNextHistoryIndex = (mNextHistoryIndex + 1) % MAX_HISTORY;
        mCurrentHistoryIndex = mNextHistoryIndex;
    }

    private void
    insertStatus (String s)
    {
        insert("\033[K");
        insert(s);
        insert("\r\n");
    }

    private synchronized void
    insertError (String s)
    {
        if (mWroteError) {
            // avoid writing an error status out more than once
            return;
        }
        
        mWroteError = true;
        //mTerminalEmulator.dropToBottom();
        insert("\033[K\r\n\033[0;34m");
        for (int i = 0; i < mColumns; i++) {
            mTerminalEmulator.write('\uffff');
        }
        insert("\033[K");
        insert(s);
        insert("\033[K\033[0m\r\n");
    }
    
    private synchronized void
    clearError ()
    {
        if (mWroteError) {
            insert("\033[K\r\n");
        }
        mWroteError = false;
    }
    
    public void
    writeKey (int key)
    {
        if (mOutputStream == null) {
            return;
        }

        write(mTerminalEmulator.getKeyEncoding(key));
    }

    public void
    write (int ch)
    {
        if (mOutputStream == null) {
            // don't even try if we're not connected
            return;
        }

        stopScrolling();

        if (mLocalEcho) {
            insert(ch);
            if (ch == 13) {
                insert(10);
            }
        }

        int x = 0;
        mEncoder.feed((char)ch);
        for (int c = mEncoder.next(); c >= 0; c = mEncoder.next()) {
            mOutBuffer[x++] = (byte)c;
        }

        try {
            mOutputStream.write(mOutBuffer, 0, x);
            if ((ch == 13) && getAutoLFMode()) {
                mOutputStream.write(10);
            }
        } catch (IOException exc) {    }
    }

    public void
    write (char[] stuff)
    {
        if (mOutputStream == null) {
            // don't even try if we're not connected
            return;
        }

        stopScrolling();

        if (mLocalEcho) {
            insert(stuff);
        }

        int x = 0, len = stuff.length, buflen = mOutBuffer.length;
        for (int i = 0; i < len; i++) {
            mEncoder.feed(stuff[i]);
            for (int c = mEncoder.next(); c >= 0; c = mEncoder.next()) {
                if (x == buflen) {
                    try {
                        mOutputStream.write(mOutBuffer);
                    } catch (IOException exc) { }
                    x = 0;
                }
                mOutBuffer[x++] = (byte)c;
            }
        }
        if (x > 0) {
            try {
                mOutputStream.write(mOutBuffer, 0, x);
            } catch (IOException exc) { }
        }

        if ((stuff.length > 0) && (stuff[stuff.length - 1] == '\r') && getAutoLFMode()) {
            write(10);
        }
    }

    public void
    write (String s)
    {
        write(s.toCharArray());
    }

    private void
    startSession (ServerInfo info)
    {
        // clear out any existing session
        if (mSession != null) {
            if (mSocket != null) {
                try {
                    mSocket.close();
                } catch (IOException x) { }
                mSocket = null;
            }
            mSession = null;
        }

        // do the hard work in another thread
        new Thread(this, "Terminal: " + mInfo.getHostname()).start();
    }

    public boolean
    receiveEvent (Event e)
    {
        switch (e.type) {
        case EVENT_PAUSED:
            mIsPaused = true;
            System.err.println("PAUSED");
            Terminal.instance().getTerminalScreen().refresh(true);
            return true;
        case EVENT_RESUMED:
            mIsPaused = false;
            System.err.println("RESUMED");
            Terminal.instance().getTerminalScreen().refresh(true);
            return true;
        }
        return false;
    }

    public void
    run ()
    {
        InputStream istream = null;
        clearError();
        mIsPaused = false;
        insertStatus(StringFormat.fromRsrc(STRING_TRYING, mInfo.getHostname()));

        try {
            // resumable sockets :) yay!
            mSocket = new Socket();
            SocketControl.setResumable(mSocket, true);
            // sneak around java single-inheritance restrictions
            Listener l = new Listener () {
                public boolean receiveEvent(Event e) { return TerminalConsole.this.receiveEvent(e); }
            };
            SocketControl.setPauseEvent(mSocket, new Event(l, EVENT_PAUSED));
            SocketControl.setResumeEvent(mSocket, new Event(l, EVENT_RESUMED));

            mSocket.connect(new InetSocketAddress(mInfo.getHostname(), mInfo.getPort()));
            insertStatus(StringFormat.fromRsrc(STRING_CONNECTED_TO, mInfo.getHostname()));

            switch (mInfo.getProtocol()) {
            case ServerInfo.PROT_TELNET:
                mSession = new TelnetSession(mInfo, mSocket.getInputStream(), mSocket.getOutputStream(),
                                             mColumns, mRows, EMULATOR_NAMES[mInfo.getEmulator()]);
                ((TelnetSession)mSession).setConsole(this);
                istream = mSession.getInputStream();
                mOutputStream = mSession.getOutputStream();
                setAutoLFMode(true);
                mLocalEcho = true;
                break;
            case ServerInfo.PROT_SSH2:
                mSession = new JaramikoSession((SSHServerInfo)mInfo, mSocket,
                                               mColumns, mRows, EMULATOR_NAMES[mInfo.getEmulator()]);
                istream = mSession.getInputStream();
                mOutputStream = mSession.getOutputStream();
                mLocalEcho = false;
                ((JaramikoSession) mSession).setErrorReporter(new MyErrorReporter());
                ((JaramikoSession) mSession).startSession();
                break;
            default:
                mSession = new RawSession(mInfo, mSocket.getInputStream(), mSocket.getOutputStream());
                istream = mSession.getInputStream();
                mOutputStream = mSession.getOutputStream();
                setAutoLFMode(true);
                mLocalEcho = true;
                break;
            }
        } catch (Exception e) {
            insertError(Terminal.instance().getString(STRING_CONNECTION_FAILED));
            System.err.println("connect exception: " + e.toString());
            mIsOpen = false;
            mIsPaused = false;
            mInfo.setStatus(ServerInfo.STATUS_CLOSED);
            mInfo.consoleChanged(this);
            return;
        }

        // now just pump bytes into VirtualConsole...
        byte[] buffer = new byte[2048];
        try {
            for (;;) {
                int n = istream.read(buffer);
                if (n < 0) {
                    // odd java way to indicate EOF
                    break;
                }
                if (n > 0) {
                    insert(buffer, 0, n);
                }
            }
        } catch (IOException x) {
            try {
                istream.close();
            } catch (IOException ignored) { }
            istream = null;
        }

        mIsOpen = false;
        mIsPaused = false;
        setLineMode(false);

        mInfo.setStatus(ServerInfo.STATUS_CLOSED);
        mInfo.consoleChanged(this);
        try {
            mSocket.close();
        } catch (IOException x) { }
        mSocket = null;
        mSession = null;
        mOutputStream = null;
        insertError(Terminal.instance().getString(STRING_CONNECTION_CLOSED));
    }


    private Session                 mSession;
    private Socket                  mSocket;
    private Thread                  mThread;
    private ServerInfo              mInfo;
    private OutputStream            mOutputStream;
    private TextField               mTextField;
    private boolean                 mIsOpen = false;
    private boolean                 mIsPaused = false;
    private boolean                 mLocalEcho = false;
    private CharacterEncoder        mEncoder;
    private byte[]                  mOutBuffer;
    private boolean                 mWroteError = false;
    
    // line-entry history
    private static final int        MAX_HISTORY = 20;

    private String[]                mHistory;
    private int                     mNextHistoryIndex = 0;
    private int                     mCurrentHistoryIndex = 0;

    private final static int        TEXT_FIELD_PADDING = 1;
}
