package com.jscape.inet.terminal;

import com.jscape.inet.telnet.*;
import com.jscape.inet.terminal.commands.ControlCommand;
import com.jscape.inet.terminal.events.CellEvent;
import com.jscape.inet.terminal.events.CommandEvent;
import com.jscape.inet.terminal.events.ExceptionEvent;
import com.jscape.inet.terminal.events.TerminalEvent;

import java.io.IOException;
import java.util.Observable;

/**
 * Main emulator class. Contains <code>Telnet</code> object as low-level
 * transport, <code>Screen</code> object as terminal screen,
 * two references to <code>CharacterSet</code> objects as G0 and G1 registers
 * of terminal and reference to <code>CommandSet</code> object as current
 * command set (ANSI or VT52).
 * Together with <code>TerminalListener</code> implements "Observer" pattern
 * ([Gamma94]).
 * In general this class parses incoming characters which may
 * be directed to terminal screen or be interpreted as control character or
 * a part of control sequence. In last two cases <code>Terminal</code>
 * class uses current <code>CommandSet</code> object to find and execute
 * corresponded command ("Command" pattern [Gamma94]);
 */

public class Terminal
        extends Observable
        implements TelnetListener {

    public static final String DEFAULT_NAME = "VT100";

    private final ControlSequence controlSequence = new ControlSequence();
    private final String terminalName;
    private final Telnet transport;
    private final Screen screen;
    private CommandSet commandSet = VT100Set.getInstance();
    private CellAttributes cellAttributes = CellAttributes.getInstance();
    private CharacterSet g0 = USAscii.getInstance();
    private CharacterSet g1 = SpecialGraphic.getInstance();
    private CharacterSet charSet = this.g0;
    private boolean autoWrapMode = false;
    private boolean lnmMode = true;
    private Exception exception = null;

    //----------------------------------------------------------------------------

    /**
     * Creates <code>Terminal</code> object giving its name, host and port to
     * connect, screen dimensions and history size.
     *
     * @param terminalName terminal name.
     * @param host         host to connect.
     * @param port         port to connect.
     * @param rowNum       screen row dimension.
     * @param colNum       screen column dimension.
     * @param historySize  history size.
     */
    public Terminal(
            String terminalName,
            String host,
            int port,
            int rowNum,
            int colNum,
            int historySize) {
        if (terminalName == null) {
            throw new NullPointerException("Terminal.constructor(): null passed");
        }

        this.terminalName = terminalName;
        this.transport = new Telnet(host, port);
        this.transport.addTelnetListener(this);
        this.screen = new Screen(rowNum, colNum, historySize);
    }

    //----------------------------------------------------------------------------

    /**
     * Creates <code>Terminal</code> object giving host and port to
     * connect, history size.
     *
     * @param host        host to connect.
     * @param port        port to connect.
     * @param historySize history size.
     */
    public Terminal(String host, int port, int historySize) {
        this(
                DEFAULT_NAME,
                host,
                port,
                Screen.DEFAULT_MAX_ROW,
                Screen.DEFAULT_MAX_COL,
                historySize);
    }

    //----------------------------------------------------------------------------

    /**
     * Creates <code>Terminal</code> object giving host and port to
     * connect, screen dimensions.
     *
     * @param host   host to connect.
     * @param port   port to connect.
     * @param rowNum screen row dimension.
     * @param colNum screen column dimension.
     */
    public Terminal(String host, int port, int rowNum, int colNum) {
        this(
                DEFAULT_NAME,
                host,
                port,
                rowNum,
                colNum,
                Screen.DEFAULT_HISTORY_SIZE);
    }

    //----------------------------------------------------------------------------

    /**
     * Creates <code>Terminal</code> object giving host and port to
     * connect, screen dimensions.
     *
     * @param host host to connect.
     * @param port port to connect.
     */
    public Terminal(String host, int port) {
        this(host, port, Screen.DEFAULT_HISTORY_SIZE);
    }

    //----------------------------------------------------------------------------

    /**
     * Connects to remote host.
     *
     * @throws TelnetException if underlying transport exception occurs.
     */
    public void connect()
            throws TelnetException {
        this.transport.connect();
    }

    //----------------------------------------------------------------------------

    /**
     * Disconnects from remote host.
     */
    public void disconnect() {
        this.transport.disconnect();
    }

    //----------------------------------------------------------------------------

    /**
     * Returns current screen.
     *
     * @return current screen.
     */
    public Screen getScreen() {
        return this.screen;
    }

    //----------------------------------------------------------------------------

    /**
     * Returns current screen copy.
     *
     * @return current screen copy.
     */
    public Screen getScreenCopy() {
        return new Screen(this.screen);
    }

    //----------------------------------------------------------------------------

    /**
     * Returns current exception.
     *
     * @return current exception.
     */
    public Exception getException() {
        return this.exception;
    }

    //----------------------------------------------------------------------------

    /**
     * Returns current character set in G0 register.
     *
     * @return current character set in G0 register.
     */
    public CharacterSet getG0() {
        return this.g0;
    }

    //----------------------------------------------------------------------------

    /**
     * Sets character set to G0 register.
     *
     * @param g0 new character set for G0 register.
     */
    public void setG0(CharacterSet g0) {
        if (g0 == null) {
            throw new NullPointerException("Terminal.setG0(): null passed");
        }

        this.g0 = g0;
    }

    //----------------------------------------------------------------------------

    /**
     * Returns current character set in G1 register.
     *
     * @return current character set in G1 register.
     */
    public CharacterSet getG1() {
        return this.g1;
    }

    //----------------------------------------------------------------------------

    /**
     * Sets character set to G1 register.
     *
     * @param g1 new character set for G1 register.
     */
    public void setG1(CharacterSet g1) {
        if (g1 == null) {
            throw new NullPointerException("Terminal.setG1(): null passed");
        }

        this.g1 = g1;
    }

    //----------------------------------------------------------------------------

    /**
     * Returns current character set which is used to encode incoming characters.
     *
     * @return current character set.
     */
    public CharacterSet getCharSet() {
        return this.charSet;
    }

    //----------------------------------------------------------------------------

    /**
     * Sets new character set which is used to encode incoming characters.
     *
     * @param charSet new character set.
     */
    public void setCharSet(CharacterSet charSet) {
        if (charSet == null) {
            throw new NullPointerException("Terminal.setCharSet(): null passed");
        }

        this.charSet = charSet;
    }

    //----------------------------------------------------------------------------

    /**
     * Checks if AutoWrap mode is on.
     *
     * @return true if AutoWrap mode is on.
     */
    public boolean isAutoWrapMode() {
        return this.autoWrapMode;
    }

    //----------------------------------------------------------------------------

    /**
     * Sets AutoWrap mode.
     *
     * @param autoWrapMode true if AutoWrap mode must be on.
     */
    public void setAutoWrapMode(boolean autoWrapMode) {
        this.autoWrapMode = autoWrapMode;
    }

    //----------------------------------------------------------------------------

    /**
     * Checks if Line Feed/New Line Mode is set.
     *
     * @return true if Line Feed/New Line Mode is set.
     */
    public boolean isLNMMode() {
        return lnmMode;
    }

    //----------------------------------------------------------------------------

    /**
     * Sets Line Feed/New Line Mode.
     *
     * @param lnmMode
     */
    public void setLNMMode(boolean lnmMode) {
        this.lnmMode = lnmMode;
    }

    //----------------------------------------------------------------------------

    /**
     * Returns current command set.
     *
     * @return current command set.
     */
    public CommandSet getCommandSet() {
        return this.commandSet;
    }

    //----------------------------------------------------------------------------

    /**
     * Sets new command set.
     *
     * @param commandSet new command set.
     */
    public void setCommandSet(CommandSet commandSet) {
        if (commandSet == null) {
            throw new NullPointerException("Terminal.setCommandSet(): null passed");
        }

        this.commandSet = commandSet;
    }

    //----------------------------------------------------------------------------

    /**
     * Sends char sequence to remote host.
     *
     * @param sequence char sequence to be sent.
     */
    public void send(String sequence) {
        if (sequence == null) {
            throw new NullPointerException("Terminal.send(): null passed");
        }

        try {
            this.transport.getOutputStream().write(sequence.getBytes());
        } catch (Exception ex) {
            this.exception = ex;
            notify(new ExceptionEvent(ex));
        }
    }

    //----------------------------------------------------------------------------

    /**
     * Sends char to remote host.
     *
     * @param ch char to be sent.
     */
    public void send(char ch) {
        try {
            this.transport.getOutputStream().write(ch);
        } catch (Exception ex) {
            this.exception = ex;
            notify(new ExceptionEvent(ex));
        }
    }

    //----------------------------------------------------------------------------

    /**
     * Performs new control sequence start.
     */
    public void newSequence() {
        this.controlSequence.startNew();
    }

    //----------------------------------------------------------------------------

    /**
     * Breaks current control sequence.
     */
    public void breakSequence() {
        this.controlSequence.reset();
    }

    //----------------------------------------------------------------------------

    /**
     * Returns current screen cell attributes.
     *
     * @return current screen cell attributes.
     */
    public CellAttributes getCellAttributes() {
        return this.cellAttributes;
    }

    //----------------------------------------------------------------------------

    /**
     * Sets new screen cell attributes.
     *
     * @param attributes new screen cell attributes.
     */
    public void setCellAttributes(CellAttributes attributes) {
        if (attributes == null) {
            throw new NullPointerException("Terminal.setCellAttributes(): null passed");
        }

        this.cellAttributes = attributes;
    }

    //----------------------------------------------------------------------------

    /**
     * Handles "connected" event from underlying transport.
     *
     * @param event <code>TelnetConnectedEvent</code> event.
     */
    public void connected(TelnetConnectedEvent event) {
    }

    //----------------------------------------------------------------------------

    /**
     * Handles "disconnected" event from underlying transport.
     *
     * @param event <code>TelnetDisconnectedEvent</code> event.
     */
    public void disconnected(TelnetDisconnectedEvent event) {
    }

    //----------------------------------------------------------------------------

    /**
     * Handles "doOption" event from underlying transport.
     *
     * @param event <code>DoOptionEvent</code> event.
     */
    public void doOption(DoOptionEvent event) {
        TelnetOption opt = event.getOption();

        switch (opt.getOptionCode()) {
            case 1:
                doOption(opt, true);
                break;
            case 24:
                doOption(opt, true);
                break;
            case 31:
                doOption(opt, true);
                sendWSOption();
                break;
            default:
                this.transport.sendWontOption(opt);
                break;
        }
    }

    //----------------------------------------------------------------------------

    /**
     * Handles "dontOption" event from underlying transport.
     *
     * @param event <code>DontOptionEvent</code> event.
     */
    public void dontOption(DontOptionEvent event) {
        TelnetOption opt = event.getOption();

        switch (opt.getOptionCode()) {
            case 1:
                doOption(opt, false);
                break;
            default:
                this.transport.sendWontOption(opt);
                break;
        }
    }

    //----------------------------------------------------------------------------

    /**
     * Handles "willOption" event from underlying transport.
     *
     * @param event <code>WillOptionEvent</code> event.
     */
    public void willOption(WillOptionEvent event) {
        TelnetOption opt = event.getOption();

        switch (opt.getOptionCode()) {
            case 1:
                willOption(opt, true);
                break;
            case 3:
                willOption(opt, true);
                break;
            default:
                this.transport.sendDontOption(opt);
                break;
        }
    }

    //----------------------------------------------------------------------------

    /**
     * Handles "wontOption" event from underlying transport.
     *
     * @param event <code>WontOptionEvent</code> event.
     */
    public void wontOption(WontOptionEvent event) {
        TelnetOption opt = event.getOption();

        switch (opt.getOptionCode()) {
            case 1:
                willOption(opt, false);
                break;
            case 3:
                willOption(opt, false);
                break;
            default:
                this.transport.sendDontOption(opt);
                break;
        }

    }

    //----------------------------------------------------------------------------

    /**
     * Handles "doSubOption" event from underlying transport.
     *
     * @param event <code>DoSubOptionEvent</code> event.
     */
    public void doSubOption(DoSubOptionEvent event) {
        TelnetOption opt = event.getOption();

        switch (opt.getOptionCode()) {
            case 24:
                sendTerminalType();
                break;
            case 31:
                sendWSOption();
                break;
            default:
                break;
        }
    }

    //----------------------------------------------------------------------------

    /**
     * Handles "dataReceived" event from underlying transport.
     *
     * @param event <code>TelnetDataReceivedEvent</code> event.
     */
    public void dataReceived(TelnetDataReceivedEvent event) {
        if (event == null) {
            throw new NullPointerException("Terminal.dataReceived(): null passed");
        }

        String data = event.getData();

        if (data == null) {
            throw new NullPointerException("Terminal.dataReceived(): no data in event");
        }

        int len = data.length();

        for (int i = 0; i < len; ++i) {
            processChar(data.charAt(i));
        }
    }

    //----------------------------------------------------------------------------

    /**
     * Parses and processes incoming char.
     *
     * @param ch incoming char.
     */
    private void processChar(char ch) {
        if (this.commandSet.isControlChar(ch)) {
            processCommand(makeString(ch), null);
        } else if (this.controlSequence.inProgress()) {
            processSequence(ch);
        } else {
            directCharToScreen(ch);
        }
    }

    //----------------------------------------------------------------------------

    /**
     * Processes incoming control sequence.
     *
     * @param ch control sequence char.
     */
    private void processSequence(char ch) {
        this.controlSequence.append(ch);

        if (this.controlSequence.isComplete()) {
            String sequence = this.controlSequence.getCanonicalForm();
            int[] params = this.controlSequence.getParameters();
            processCommand(sequence, params);
        }
    }

    //----------------------------------------------------------------------------

    /**
     * Processes incoming control commands.
     *
     * @param sequence control sequence.
     * @param params   control command parameters.
     */
    private void processCommand(String sequence, int[] params) {
        processCommand(sequence, sequence, params);
    }

    //----------------------------------------------------------------------------

    /**
     * Processes incoming control commands.
     *
     * @param shortForm "canonical" form of command.
     * @param fullForm  full command line.
     * @param params    command parameters.
     */
    private void processCommand(
            String shortForm,
            String fullForm,
            int[] params) {
        notifyObservers(new CommandEvent(shortForm, fullForm));
        ControlCommand cmd = this.commandSet.getCommand(shortForm);
        cmd.execute(params, this);
    }

    //----------------------------------------------------------------------------

    /**
     * Makes <code>String</code> object from char.
     *
     * @param ch char.
     *
     * @return string with giving char.
     */
    private String makeString(char ch) {
        return String.valueOf(ch);
    }

    //----------------------------------------------------------------------------

    /**
     * Directs character to terminal screen.
     *
     * @param ch char.
     */
    private void directCharToScreen(char ch) {
        Screen.Cursor c = this.screen.getCursor();
        if (c.column() == (this.screen.getMaxCol() - 1) && this.autoWrapMode) {
            c.scrollUp();
            c.moveHome();
        }
        Cell cell = c.getCell();
        cell.setValue(this.charSet.encode(ch));
        cell.setAttributes(this.cellAttributes);
        int row = c.row();
        int col = c.column();
        c.moveRight(1);
        notify(new CellEvent(row, col));
    }

    //----------------------------------------------------------------------------

    /**
     * Notifies registered listeners about event.
     *
     * @param event generated event.
     */
    private void notify(TerminalEvent event) {
        setChanged();
        notifyObservers(event);
    }

    //----------------------------------------------------------------------------

    /**
     * Process "will/wont" option event.
     *
     * @param opt   option.
     * @param apply true if this is "will" event.
     */
    private void willOption(TelnetOption opt, boolean apply) {
        if (apply) {
            this.transport.sendDoOption(opt);
        } else {
            this.transport.sendDontOption(opt);
        }
    }

    //----------------------------------------------------------------------------

    /**
     * Process "do/dont" option event.
     *
     * @param opt   option.
     * @param apply true if this is "do" event.
     */
    private void doOption(TelnetOption opt, boolean apply) {
        if (apply) {
            this.transport.sendWillOption(opt);
        } else {
            this.transport.sendWontOption(opt);
        }
    }

    //----------------------------------------------------------------------------

    /**
     * Sends terminal type to remote host.
     */
    private void sendTerminalType() {
        try {
            this.transport.sendCommand(Telnet.TC_SB, 24);
            this.transport.getOutputStream().write(0);
            this.transport.getOutputStream().write(
                    this.terminalName.getBytes());
            this.transport.sendCommand(Telnet.TC_SE);
        } catch (IOException ex) {
            this.exception = ex;
            notifyObservers(new ExceptionEvent(ex));
        }
    }

    //----------------------------------------------------------------------------

    /**
     * Sends NAWS sub-option to remote host.
     */
    private void sendWSOption() {
        try {
            this.transport.sendCommand(Telnet.TC_SB, 31);
            this.transport.getOutputStream().write(
                    iToN(this.screen.getMaxCol()));
            this.transport.getOutputStream().write(
                    iToN(this.screen.getMaxRow()));
            this.transport.sendCommand(Telnet.TC_SE);
        } catch (IOException ex) {
            this.exception = ex;
            notify(new ExceptionEvent(ex));
        }
    }

    //----------------------------------------------------------------------------

    /**
     * Converts <code>int</code> value to network byte order.
     *
     * @param i
     *
     * @return
     */
    private byte[] iToN(int i) {
        byte[] result = new byte[2];
        result[0] = (byte) (i >> 8);
        result[1] = (byte) i;

        return result;
    }

}