/*******************************************************************************
 * 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;
	}

}
