/*******************************************************************************
 * Copyright (c) 2007, 2018 Wind River Systems, Inc. and others.
 * All rights reserved. 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:
 * Michael Scharf (Wind River) - initial implementation
 * Michael Scharf (Wing River) - [211659] Add field assist to terminal input field
 * Michael Scharf (Wing River) - [196447] The optional terminal input line should be resizeable
 * Martin Oberhuber (Wind River) - [168197] Fix Terminal for CDC-1.1/Foundation-1.1
 * Michael Scharf (Wing River) - [236458] Fix 168197 lost the last entry
 * Anton Leherbauer (Wind River) - [220971] The optional terminal input line has redraw problems when resizing
 *******************************************************************************/
package org.eclipse.tm.internal.terminal.control;

import java.util.ArrayList;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import java.util.StringTokenizer;

import org.eclipse.jface.fieldassist.IContentProposal;
import org.eclipse.jface.fieldassist.IContentProposalProvider;
import org.eclipse.jface.fieldassist.TextContentAdapter;
import org.eclipse.swt.SWT;
import org.eclipse.swt.events.KeyEvent;
import org.eclipse.swt.events.KeyListener;
import org.eclipse.swt.graphics.Font;
import org.eclipse.swt.graphics.Rectangle;
import org.eclipse.swt.layout.GridData;
import org.eclipse.swt.layout.GridLayout;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Sash;
import org.eclipse.swt.widgets.Text;
import org.eclipse.ui.fieldassist.ContentAssistCommandAdapter;

/**
 * Manages the Command History for the command line input
 * of the terminal control.
 * <li>
 * <ul>Navigate with ARROW_UP,ARROW_DOWN,PAGE_UP,PAGE_DOWN
 * <ul>ESC to cancel history editing
 * <ul>History can be edited (by moving up and edit) but changes are
 * not persistent (like in bash).
 * <ul>If the same command is entered multiple times in a row,
 * only one entry is kept in the history.
 * </li>
 *
 */
public class CommandInputFieldWithHistory implements ICommandInputField {
	private class FieldAssist implements IContentProposalProvider {

		@Override
		public IContentProposal[] getProposals(String contents, int position) {
			String prefix = contents.substring(0, position);
			List<Proposal> result = new ArrayList<>();
			// show an entry only once
			Set<String> seen = new HashSet<>();
			for (Iterator<String> iterator = fHistory.iterator(); iterator.hasNext();) {
				String history = iterator.next();
				if (history.startsWith(prefix) && !seen.contains(history)) {
					// the content is the rest of the history item
					String content = history.substring(prefix.length());
					result.add(new Proposal(content, history));
					// don't add this proposal again
					seen.add(history);
				}
			}
			return result.toArray(new IContentProposal[result.size()]);
		}

	}

	private static class Proposal implements IContentProposal {

		private final String fContent;
		private final String fLabel;

		Proposal(String content, String label) {
			fContent = content;
			fLabel = label;
		}

		@Override
		public String getContent() {
			return fContent;
		}

		@Override
		public String getLabel() {
			return fLabel;
		}

		@Override
		public String getDescription() {
			return null;
		}

		@Override
		public int getCursorPosition() {
			return fContent.length();
		}
	}

	final List<String> fHistory = new ArrayList<>();
	/**
	 * Keeps a modifiable history while in history editing mode
	 */
	List<Object> fEditedHistory;
	/**
	 * The current position in the edit history
	 */
	private int fEditHistoryPos = 0;
	/**
	 * The limit of the history.
	 */
	private final int fMaxSize;
	/**
	 * The input text field.
	 */
	private Text fInputField;
	private Sash fSash;
	private Composite fPanel;

	public CommandInputFieldWithHistory(int maxHistorySize) {
		fMaxSize = maxHistorySize;
	}

	/**
	 * Add a line to the history.
	 * @param line The line to be added to the history.
	 */
	protected void pushLine(String line) {
		endHistoryMode();
		// anything to remember?
		if (line == null || line.trim().length() == 0)
			return;
		fHistory.add(0, line);
		// ignore if the same as last
		if (fHistory.size() > 1 && line.equals(fHistory.get(1)))
			fHistory.remove(0);
		// limit the history size.
		if (fHistory.size() >= fMaxSize)
			fHistory.remove(fHistory.size() - 1);
	}

	/**
	 * Sets the history
	 * @param history or null
	 */
	public void setHistory(String history) {
		endHistoryMode();
		fHistory.clear();
		if (history == null)
			return;
		// add history entries separated by '\n'
		// fHistory.addAll(Arrays.asList(history.split("\n"))); //$NON-NLS-1$
		//<J2ME CDC-1.1 Foundation-1.1 variant>
		StringTokenizer tok = new StringTokenizer(history, "\n"); //$NON-NLS-1$
		while (tok.hasMoreElements())
			fHistory.add((String) tok.nextElement());
		//</J2ME CDC-1.1 Foundation-1.1 variant>
	}

	/**
	 * @return the current content of the history buffer and new line separated list
	 */
	public String getHistory() {
		StringBuffer buff = new StringBuffer();
		boolean sep = false;
		for (Iterator<String> iterator = fHistory.iterator(); iterator.hasNext();) {
			String line = iterator.next();
			if (line.length() > 0) {
				if (sep)
					buff.append("\n"); //$NON-NLS-1$
				else
					sep = true;
				buff.append(line);
			}
		}
		return buff.toString();
	}

	/**
	 * @param currLine Line of text to be moved in history
	 * @param count (+1 or -1) for forward and backward movement. -1 goes back
	 * @return the new string to be displayed in the command line or null,
	 * if the limit is reached.
	 */
	public String move(String currLine, int count) {
		if (!inHistoryMode()) {
			fEditedHistory = new ArrayList<>(fHistory.size() + 1);
			fEditedHistory.add(currLine);
			fEditedHistory.addAll(fHistory);
			fEditHistoryPos = 0;
		}
		fEditedHistory.set(fEditHistoryPos, currLine);
		if (fEditHistoryPos + count >= fEditedHistory.size())
			return null;
		if (fEditHistoryPos + count < 0)
			return null;
		fEditHistoryPos += count;
		return (String) fEditedHistory.get(fEditHistoryPos);
	}

	private boolean inHistoryMode() {
		return fEditedHistory != null;
	}

	/**
	 * Exit the history movements and go to position 0;
	 * @return the string to be shown in the command line
	 */
	protected String escape() {
		if (!inHistoryMode())
			return null;
		String line = (String) fEditedHistory.get(0);
		endHistoryMode();
		return line;
	}

	/**
	 * End history editing
	 */
	private void endHistoryMode() {
		fEditedHistory = null;
		fEditHistoryPos = 0;
	}

	@Override
	public void createControl(final Composite parent, final ITerminalViewControl terminal) {
		//		fSash = new Sash(parent,SWT.HORIZONTAL|SWT.SMOOTH);
		fSash = new Sash(parent, SWT.HORIZONTAL);
		final GridData gd_sash = new GridData(SWT.FILL, SWT.CENTER, true, false);
		gd_sash.heightHint = 5;
		fSash.setLayoutData(gd_sash);
		fSash.addListener(SWT.Selection, e -> {
			if (e.detail == SWT.DRAG) {
				// don't redraw during drag, it causes paint errors - bug 220971
				return;
			}
			// no idea why this is needed
			GridData gdata = (GridData) fInputField.getLayoutData();
			Rectangle sashRect = fSash.getBounds();
			Rectangle containerRect = parent.getClientArea();

			int h = fInputField.getLineHeight();
			// make sure the input filed height is a multiple of the line height
			gdata.heightHint = Math.max(((containerRect.height - e.y - sashRect.height) / h) * h, h);
			// do not show less then one line
			e.y = Math.min(e.y, containerRect.height - h);
			fInputField.setLayoutData(gdata);
			parent.layout();
			// else the content assist icon will be replicated
			parent.redraw();
		});
		fPanel = new Composite(parent, SWT.NONE);
		GridLayout layout = new GridLayout();
		layout.marginWidth = 0;
		layout.marginHeight = 0;
		layout.marginTop = 0;
		layout.marginBottom = 2;
		fPanel.setLayout(layout);
		fPanel.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, false));
		fInputField = new Text(fPanel, SWT.MULTI | SWT.BORDER | SWT.WRAP | SWT.V_SCROLL);
		GridData data = new GridData(SWT.FILL, SWT.FILL, true, false);
		boolean installDecoration = true;
		if (installDecoration) {
			// The ContentAssistCommandAdapter says: "The client is responsible for
			// ensuring that adequate space is reserved for the decoration."
			// TODO: what is the "adequate space"???
			data.horizontalIndent = 6;
		}
		fInputField.setLayoutData(data);
		fInputField.setFont(terminal.getFont());
		// Register field assist *before* the key listener.
		// Else the ENTER key is sent *first* to the input field
		// and then to the field assist popup.
		// (https://bugs.eclipse.org/bugs/show_bug.cgi?id=211659)
		new ContentAssistCommandAdapter(fInputField, new TextContentAdapter(), new FieldAssist(), null, null,
				installDecoration);
		fInputField.addKeyListener(new KeyListener() {
			@Override
			public void keyPressed(KeyEvent e) {
				// if the field assist has handled the key already then
				// ignore it (https://bugs.eclipse.org/bugs/show_bug.cgi?id=211659)
				if (!e.doit)
					return;
				if (e.character == SWT.CR || e.character == SWT.LF) {
					e.doit = false;
					String line = fInputField.getText();
					if (!terminal.pasteString(line + '\r'))
						return;
					pushLine(line);
					setCommand("");//$NON-NLS-1$
				} else if (e.keyCode == SWT.ARROW_UP || e.keyCode == SWT.PAGE_UP) {
					e.doit = false;
					setCommand(move(fInputField.getText(), 1));
				} else if (e.keyCode == SWT.ARROW_DOWN || e.keyCode == SWT.PAGE_DOWN) {
					e.doit = false;
					setCommand(move(fInputField.getText(), -1));
				} else if (e.keyCode == SWT.ESC) {
					e.doit = false;
					setCommand(escape());
				}
			}

			private void setCommand(String line) {
				if (line == null)
					return;
				fInputField.setText(line);
				fInputField.setSelection(fInputField.getCharCount());
			}

			@Override
			public void keyReleased(KeyEvent e) {
			}
		});
	}

	@Override
	public void setFont(Font font) {
		fInputField.setFont(font);
		fInputField.getParent().layout(true);
	}

	@Override
	public void dispose() {
		fSash.dispose();
		fSash = null;
		fPanel.dispose();
		fPanel = null;
		fInputField.dispose();
		fInputField = null;
	}
}
