/*******************************************************************************
 * Copyright (c) 2010, 2017 SAP AG and others
 *
 * This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License 2.0
 * which accompanies this distribution, and is available at
 * https://www.eclipse.org/legal/epl-2.0/
 *
 * SPDX-License-Identifier: EPL-2.0
 * 
 * Contributors:
 *     Lazar Kirchev, SAP AG - initial API and implementation  
 *     IBM Corporation - ongoing development
 *******************************************************************************/
package org.eclipse.equinox.console.telnet;

import org.eclipse.equinox.console.common.ConsoleInputStream;
import org.eclipse.equinox.console.common.ConsoleOutputStream;
import org.eclipse.equinox.console.common.KEYS;
import org.eclipse.equinox.console.common.Scanner;
import org.eclipse.equinox.console.common.terminal.TerminalTypeMappings;

import java.io.IOException;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.Set;

/**
 * This class performs the processing of the telnet commands,
 * and updates respectively what is displayed in the output. Also, it performs 
 * terminal type negotiation with the telnet client. This is important for some of the escape sequences, 
 * which are different for the different terminal types. Without such negotiation,
 * some keys (such as backspace, del, insert, home, etc.) may not be correctly
 * interpreted by the telnet server. Currently the supported terminal types are
 * ANSI, VT100, VT220, VT320, XTERM and SCO. The support is limited to the following 
 * keys - BACKSPACE, DEL, INSERT, HOME, END, PAGEUP, PAGEDOWN, ARROWS.
 */
public class TelnetInputScanner extends Scanner {

    private boolean isCommand = false;
    private boolean isReadingTtype = false; 
    private boolean shouldFinish = false;
    private boolean tTypeNegotiationStarted = false;
    private int lastRead = -1;
    private ArrayList<Integer> currentTerminalType = new ArrayList<>();
    private ArrayList<Integer> lastTerminalType = null;
    private Set<String> supportedTerminalTypes = new HashSet<>();
    private Callback callback;

    public TelnetInputScanner(ConsoleInputStream toShell, ConsoleOutputStream toTelnet, Callback callback) {
        super(toShell, toTelnet);
        initializeSupportedTerminalTypes();
        TerminalTypeMappings currentMapping = supportedEscapeSequences.get(DEFAULT_TTYPE);
    	currentEscapesToKey = currentMapping.getEscapesToKey();
    	escapes = currentMapping.getEscapes();
    	setBackspace(currentMapping.getBackspace());
    	setDel(currentMapping.getDel());
    	this.callback = callback;
    }
    
    private void initializeSupportedTerminalTypes() {
        supportedTerminalTypes.add("ANSI");
        supportedTerminalTypes.add("VT100");
        supportedTerminalTypes.add("VT220");
        supportedTerminalTypes.add("VT320");
        supportedTerminalTypes.add("XTERM");
        supportedTerminalTypes.add("SCO");     
    }

    @Override
	public void scan(int b) throws IOException {
        b &= 0xFF;

        if (isEsc) {
            scanEsc(b);
        } else if (isCommand) {
            scanCommand(b);
        } else if (b == IAC) {
            startCommand();
        } else {
        	switch (b) {
                case ESC:
                    startEsc();
                    toShell.add(new byte[]{(byte) b});
                    break;
                default:
                    if (b >= SPACE && b < MAX_CHAR) {
                        echo((byte) b);
                        flush();
                    }
                    toShell.add(new byte[]{(byte) b});
            }

        }
        lastRead = b;
    }

    /* Telnet command codes are described in RFC 854, TELNET PROTOCOL SPECIFICATION
     * available at http://www.ietf.org/rfc/rfc854.txt
     * 
     * Telnet terminal type negotiation option is described in RFC 1091, Telnet Terminal-Type Option
     * available at http://www.ietf.org/rfc/rfc1091.txt
     */
    private static final int SE = 240;
    private static final int EC = 247;
    private static final int EL = 248;
    private static final int SB = 250;
    private static final int WILL = 251;
    private static final int WILL_NOT = 252;
    private static final int DO = 253;
    private static final int DO_NOT = 254;
    private static final int TTYPE = 24;
    private static final int SEND = 1;
    private static final int IAC = 255;
    private static final int IS = 0;

    private boolean isNegotiation;
    private boolean isWill;
    
    private byte[] tTypeRequest = {(byte)IAC, (byte)SB, (byte)TTYPE, (byte)SEND, (byte)IAC, (byte)SE};

    private void scanCommand(final int b) throws IOException {
        if (isNegotiation) {
            scanNegotiation(b);
        } else if (isWill) {
            isWill = false;
            isCommand = false;
            if(b == TTYPE && tTypeNegotiationStarted == false) {
        		sendRequest();
        	}
        } else {
            switch (b) {
                case WILL:
                	isWill = true;
                	break;
                case WILL_NOT:
                	break;
                case DO:
                	break;
                case DO_NOT:
                    break;
                case SB:
                    isNegotiation = true;
                    break;
                case EC:
                    eraseChar();
                    isCommand = false;
                    break;
                case EL:
                default:
                    isCommand = false;
                    break;
            }
        }
    }

    private void scanNegotiation(final int b) {
    	if (lastRead == SB && b == TTYPE) {
    		isReadingTtype = true;
    	} else if (b == IS) {
    		
    	} else if (b == IAC) {
    		
    	} else if (b == SE) {
            isNegotiation = false;
            isCommand = false;
            if (isReadingTtype == true) {
				isReadingTtype = false;
				if (shouldFinish == true) {
					setCurrentTerminalType();
					shouldFinish = false;
					return;
				}
				boolean isMatch = isTerminalTypeSupported();
				boolean isLast = isLast();
				if (isMatch == true) {
					setCurrentTerminalType();
					return;
				}
				lastTerminalType = currentTerminalType;
				currentTerminalType = new ArrayList<>();
				if (isLast == true && isMatch == false) {
					shouldFinish = true;
					sendRequest();
				} else if (isLast == false && isMatch == false) {
					sendRequest();
				}
			}
        } else if (isReadingTtype == true){
        	currentTerminalType.add(b);
        }
    }
    
    private boolean isTerminalTypeSupported() {
    	byte[] tmp = new byte[currentTerminalType.size()];
    	int idx = 0;
    	for(Integer i : currentTerminalType) {
    		tmp[idx] = i.byteValue();
    		idx++;
    	}
    	String tType = new String(tmp);
    	
    	for(String terminal : supportedTerminalTypes) {
    		if(tType.toUpperCase().contains(terminal)) {
    			return true;
    		}
    	}
    	
    	return false;
    }
    
    private boolean isLast() {
    	if(currentTerminalType.equals(lastTerminalType)) {
    		return true;
    	} else {
    		return false;
    	}
    }
    
    private void setCurrentTerminalType() {
    	byte[] tmp = new byte[currentTerminalType.size()];
    	int idx = 0;
    	for(Integer i : currentTerminalType) {
    		tmp[idx] = i.byteValue();
    		idx++;
    	}
    	String tType = new String(tmp);
    	String term = null;
    	for(String terminal : supportedTerminalTypes) {
    		if(tType.toUpperCase().contains(terminal)) {
    			term = terminal;
    		}
    	}
    	TerminalTypeMappings currentMapping = supportedEscapeSequences.get(term);
    	if(currentMapping == null) {
    		currentMapping = supportedEscapeSequences.get(DEFAULT_TTYPE);
    	}
    	currentEscapesToKey = currentMapping.getEscapesToKey();
    	escapes = currentMapping.getEscapes();
    	setBackspace(currentMapping.getBackspace());
    	setDel(currentMapping.getDel());
    	if(callback != null) {
    		callback.finished();
    	}
    }
    
    private void sendRequest() {
    	try {
			toTelnet.write(tTypeRequest);
			toTelnet.flush();
			if(tTypeNegotiationStarted == false) {
				tTypeNegotiationStarted = true;
			}
		} catch (IOException e) {
			
			e.printStackTrace();
		}
    }

    private void startCommand() {
        isCommand = true;
        isNegotiation = false;
        isWill = false;
    }

    private void eraseChar() throws IOException {
        toShell.add(new byte[]{BS});
    }

    @Override
	protected void scanEsc(int b) throws IOException {
        esc += (char) b;
        toShell.add(new byte[]{(byte) b});
        KEYS key = checkEscape(esc);
        if (key == KEYS.UNFINISHED) {
            return;
        }
        if (key == KEYS.UNKNOWN) {
            isEsc = false;
            scan(b);
            return;
        }
        isEsc = false;
    }

}
