/*******************************************************************************
 * Copyright (c) 2000, 2011 IBM Corporation 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:
 *     IBM Corporation - initial API and implementation
 *******************************************************************************/
package org.eclipse.jdt.text.tests.performance;

import org.eclipse.swt.SWT;
import org.eclipse.swt.layout.FillLayout;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Event;
import org.eclipse.swt.widgets.Shell;
import org.eclipse.swt.widgets.Text;



/**
 * @since 3.1
 */
public class KeyboardProbe {

	private static final boolean TRACE= false;
	private static final char FAKE_CHAR= '$';

	public static void main(String[] args) {
		new KeyboardProbe().getKeycodes();
	}

	private Display fDisplay;
	private char[][] fCodes= null;
	private int fKeyCode;
	private static final int NONE= 0;
	private static final int SHIFT= 1;
	private boolean fKeyContinue;
	private boolean fTextContinue;
	private boolean fDisposeDisplay= false;
	private Shell fShell;

	/**
	 * Returns the characters being input into a text component by injecting key
	 * events into the system event queue. For each character, the array holds
	 * the resulting character when pressed alone and when pressed together with
	 * shift.
	 *
	 * @return the characters input by pressing the keys. The key code is the
	 *         index into the array.
	 */
	public synchronized char[][] getKeycodes() {
		initialize();
		return fCodes;
	}

	/**
	 * Initializes this keyboard probe.
	 */
	public void initialize() {
		if (fCodes == null) {
			try {
				probe();
			} finally {
				if (fDisposeDisplay && fDisplay != null) {
					fDisplay.dispose();
				}
				fDisplay= null;

				if (fShell != null && !fShell.isDisposed()) {
					fShell.dispose();
				}
				fShell= null;
			}
		}
	}

	/**
	 * Returns the character resulting from pressing the character 'key' with
	 * or without shift being pressed.
	 *
	 * @param key the key to press
	 * @param pressShift whether pressed together with shift
	 * @return the char resulting from the given key combo, or 0 if there is no equivalent
	 */
	public char getCharForKeybinding(char key, boolean pressShift) {
		char[][] keycodes= getKeycodes();
		if (key < keycodes.length)
			return keycodes[key][pressShift ? SHIFT : NONE];

		return 0;
	}

	/**
	 * Returns a keybinding combo that will produce the wanted character. Note
	 * that there may be more than one combo that can produce the wanted
	 * binding, any one is returned. The last integer in the returned array is
	 * a character that must be set to the Event.character field, any integers
	 * before are SWT constants describing modifier keys that must be pressed
	 * to get the desired result.
	 *
	 * @param expected the char to be input
	 * @return the corresponding key combo, or an empty array if there is no combo
	 */
	public int[] getKeybindingForChar(char expected) {
		char[][] keycodes= getKeycodes();
		for (int i= 0; i < keycodes.length; i++) {
			if (keycodes[i][NONE] == expected)
				return new int[] {i};
			else if (keycodes[i][SHIFT] == expected)
				return new int[] { SWT.SHIFT, i };
		}
		return new int[0];
	}

	/**
	 * Presses a key combination such that the expected character is input by
	 * simulating key events on the given display. Returns <code>true</code>
	 * if a key combo could be found, <code>false</code> if not.
	 *
	 * @param expected the expected character
	 * @param display the display to simulate the events on
	 * @return <code>true</code> if there was a key combo to press,
	 *         <code>false</code> if <code>expected</code> has no combo on
	 *         the current keyboard layout.
	 */
	public boolean pressChar(char expected, Display display) {
		return pressChar(expected, new int[0], display);
	}

	/**
	 * Presses a key combination such that the expected character is input by
	 * simulating key events on the given display. Returns <code>true</code>
	 * if a key combo could be found, <code>false</code> if not.
	 *
	 * @param expected the expected character
	 * @param modifiers additional modifiers to press
	 * @param display the display to simulate the events on
	 * @return <code>true</code> if there was a key combo to press,
	 *         <code>false</code> if <code>expected</code> has no combo on
	 *         the current keyboard layout.
	 */
	public boolean pressChar(char expected, int[] modifiers, Display display) {
		int[] charkeys= getKeybindingForChar(expected);
		int[] combo= new int[charkeys.length + modifiers.length];
		System.arraycopy(modifiers, 0, combo, 0, modifiers.length);
		System.arraycopy(charkeys, 0, combo, modifiers.length, charkeys.length);

		for (int i= 0; i <= combo.length - 2; i++) {
			Event event= new Event();
			event.type= SWT.KeyDown;
			event.keyCode= combo[i];
			display.post(event);
		}

		if (combo.length > 0) {
			Event event= new Event();
			event.type= SWT.KeyDown;
			event.character= (char) combo[combo.length - 1];
			display.post(event);

			event.type= SWT.KeyUp;
			display.post(event);
		}

		for (int i= combo.length - 2; i >= 0; i--) {
			Event event= new Event();
			event.type= SWT.KeyUp;
			event.keyCode= combo[i];
			display.post(event);
		}

		return combo.length > 0;
	}

	void probe() {
		fCodes= new char[128][];

		fDisplay= createDisplay();
		Text text= createControl(fDisplay);

		for (fKeyCode= 0; fKeyCode < 128; fKeyCode++) {
			fCodes[fKeyCode]= new char[2];

			postNaturalKeyPress(fKeyCode);
			char c= getContent(text);
			if (TRACE) System.out.println("" + fKeyCode + "content[NONE]: " + c);
			fCodes[fKeyCode][NONE]= c;
			clearText(text);

			postShiftKeyPress(fKeyCode);
			c= getContent(text);
			if (TRACE) System.out.println("" + fKeyCode + "content[SHIFT]: " + c);
			fCodes[fKeyCode][SHIFT]= c;
			clearText(text);

		}

	}

	private char getContent(Text text) {
		String content= text.getText();
		char c;
		if (content.length() == 2) {
			c= content.charAt(0);
			if (TRACE)
				if (content.charAt(1) != FAKE_CHAR)
					System.out.println("second char was '" + content.charAt(1) + "'");
		} else if (content.length() > 2) {
			c= content.charAt(0);
			if (TRACE)
				System.out.println("rest content was '" + content.substring(1) + "'");
		} else if (content.length() == 1) {
			c= '\0';
			if (TRACE)
				if (content.charAt(0) != FAKE_CHAR)
					System.out.println("second char was '" + content.charAt(0) + "'");
		} else {
			c= '\0';
			if (TRACE) System.out.println("no content");
		}
		return c;
	}

	private void clearText(Text text) {
		fTextContinue= false;
		text.setText("");
		do
			driveEventQueue();
		while (!fTextContinue);
	}

	private Display createDisplay() {
		Display display= Display.getCurrent();
		if (display == null) {
			display= Display.getDefault();
			fDisposeDisplay= true;
		}
		return display;
	}

	private void addListeners(Text control) {
		control.addListener(SWT.KeyDown, this::onKeyDown);
		control.addListener(SWT.KeyUp, this::onKeyUp);
		control.addListener(SWT.Modify, event -> onModify());
	}

	private Text createControl(Display display) {
		fShell= new Shell(display);
		fShell.setSize(300, 200);
		fShell.setText("Keyboard Probe"); //$NON-NLS-1$
		fShell.setLayout(new FillLayout());
		Text text= new Text(fShell, SWT.MULTI | SWT.LEFT | SWT.WRAP);
		text.setSize(300, 200);
		fShell.setVisible(true);
		fShell.forceActive();
		fShell.forceFocus();
		addListeners(text);

		text.setFocus();
		return text;
	}

	private void postNaturalKeyPress(int i) {

		fKeyContinue= false;

		Event event= new Event();
		event.type= SWT.KeyDown;
		event.keyCode= i;
		event.character= (char) i;
		fDisplay.post(event);

		event.type= SWT.KeyUp;
		fDisplay.post(event);

		Event event2= new Event();
		event2.type= SWT.KeyDown;
//		event2.keyCode= i;
		event2.character= FAKE_CHAR;
		fDisplay.post(event2);

		event2.type= SWT.KeyUp;
		fDisplay.post(event2);

		do
			driveEventQueue();
		while (!fKeyContinue);

	}

	private void postShiftKeyPress(int i) {
		fKeyContinue= false;

		Event shift= new Event();
		shift.type= SWT.KeyDown;
		shift.keyCode= SWT.SHIFT;
		fDisplay.post(shift);

		Event event= new Event();
		event.type= SWT.KeyDown;
		event.character= (char) i;
		fDisplay.post(event);

		event.type= SWT.KeyUp;
		fDisplay.post(event);

		shift.type= SWT.KeyUp;
		fDisplay.post(shift);

		Event event2= new Event();
		event2.type= SWT.KeyDown;
		event2.character= FAKE_CHAR;
		fDisplay.post(event2);

		event2.type= SWT.KeyUp;
		fDisplay.post(event2);

		do
			driveEventQueue();
		while (!fKeyContinue);
	}

	private boolean fShiftPressed= false;

	private void onKeyDown(Event event) {
		if (event.keyCode == SWT.SHIFT)
			fShiftPressed= true;
	}

	private void onKeyUp(Event event) {
		if (!fShiftPressed || event.keyCode == SWT.SHIFT)
			fKeyContinue= true;
		fShiftPressed= false;
	}

	private void onModify() {
		fTextContinue= true;
	}

	private void driveEventQueue() {
		while (fDisplay.readAndDispatch()) {}
		try {
			Thread.sleep(10);
		} catch (InterruptedException e) {
			// continue
		}
	}

}
