/*******************************************************************************
 * Copyright (c) 2000, 2018 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.jface.contentassist;

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

import org.eclipse.swt.SWT;
import org.eclipse.swt.custom.VerifyKeyListener;
import org.eclipse.swt.events.KeyEvent;
import org.eclipse.swt.events.KeyListener;
import org.eclipse.swt.events.VerifyEvent;
import org.eclipse.swt.graphics.Image;
import org.eclipse.swt.widgets.Control;
import org.eclipse.swt.widgets.Event;
import org.eclipse.swt.widgets.Listener;

import org.eclipse.core.runtime.Assert;
import org.eclipse.core.runtime.Platform;

import org.eclipse.jface.fieldassist.ControlDecoration;
import org.eclipse.jface.resource.ImageDescriptor;
import org.eclipse.jface.viewers.ILabelProvider;
import org.eclipse.jface.viewers.ILabelProviderListener;
import org.eclipse.jface.viewers.LabelProviderChangedEvent;

import org.eclipse.jface.text.IEventConsumer;


/**
 * An <code>AbstractControlContentAssistSubjectAdapter</code> delegates assistance requests from a
 * {@linkplain org.eclipse.jface.text.contentassist.ContentAssistant content assistant}
 * to a <code>Control</code>.
 *
 * A visual feedback can be configured via {@link #setContentAssistCueProvider(ILabelProvider)}.
 *
 * @since 3.0
 * @deprecated As of 3.2, replaced by Platform UI's field assist support
 */
@Deprecated
public abstract class AbstractControlContentAssistSubjectAdapter implements IContentAssistSubjectControl {

	protected static final boolean DEBUG= "true".equalsIgnoreCase(Platform.getDebugOption("org.eclipse.jface.text/debug/ContentAssistSubjectAdapters"));  //$NON-NLS-1$//$NON-NLS-2$

	/**
	 * VerifyKeyListeners for the control.
	 */
	private List<VerifyKeyListener> fVerifyKeyListeners;
	/**
	 * KeyListeners for the control.
	 */
	private Set<KeyListener> fKeyListeners;
	/**
	 * The Listener installed on the control which passes events to
	 * {@link #fVerifyKeyListeners fVerifyKeyListeners} and {@link #fKeyListeners}.
	 */
	private Listener fControlListener;
	/**
	 * The cue label provider, or <code>null</code> iff none.
	 * @since 3.3
	 */
	private ILabelProvider fCueLabelProvider;
	/**
	 * The control decoration, or <code>null</code> iff fCueLabelProvider is null.
	 * @since 3.3
	 */
	private ControlDecoration fControlDecoration;
	/**
	 * The default cue image, or <code>null</code> if not cached yet.
	 * @since 3.3
	 */
	private Image fCachedDefaultCueImage;

	/**
	 * Creates a new {@link AbstractControlContentAssistSubjectAdapter}.
	 */
	public AbstractControlContentAssistSubjectAdapter() {
		fVerifyKeyListeners= new ArrayList<>(1);
		fKeyListeners= new HashSet<>(1);
	}

	@Override
	public abstract Control getControl();

	@Override
	public void addKeyListener(KeyListener keyListener) {
		fKeyListeners.add(keyListener);

		if (DEBUG)
			System.out.println("AbstractControlContentAssistSubjectAdapter#addKeyListener()"); //$NON-NLS-1$

		installControlListener();
	}

	@Override
	public void removeKeyListener(KeyListener keyListener) {
		boolean deleted= fKeyListeners.remove(keyListener);

		if (DEBUG) {
			if (!deleted)
				System.out.println("removeKeyListener -> wasn't here"); //$NON-NLS-1$
			System.out.println("AbstractControlContentAssistSubjectAdapter#removeKeyListener() -> " + fKeyListeners.size()); //$NON-NLS-1$
		}

		uninstallControlListener();
	}

	@Override
	public boolean supportsVerifyKeyListener() {
		return true;
	}

	@Override
	public boolean appendVerifyKeyListener(final VerifyKeyListener verifyKeyListener) {
		fVerifyKeyListeners.add(verifyKeyListener);

		if (DEBUG)
			System.out.println("AbstractControlContentAssistSubjectAdapter#appendVerifyKeyListener() -> " + fVerifyKeyListeners.size()); //$NON-NLS-1$

		installControlListener();
		return true;
	}

	@Override
	public boolean prependVerifyKeyListener(final VerifyKeyListener verifyKeyListener) {
		fVerifyKeyListeners.add(0, verifyKeyListener);

		if (DEBUG)
			System.out.println("AbstractControlContentAssistSubjectAdapter#prependVerifyKeyListener() -> " + fVerifyKeyListeners.size()); //$NON-NLS-1$

		installControlListener();
		return true;
	}

	@Override
	public void removeVerifyKeyListener(VerifyKeyListener verifyKeyListener) {
		fVerifyKeyListeners.remove(verifyKeyListener);

		if (DEBUG)
			System.out.println("AbstractControlContentAssistSubjectAdapter#removeVerifyKeyListener() -> " + fVerifyKeyListeners.size()); //$NON-NLS-1$

		uninstallControlListener();
	}

	@Override
	public void setEventConsumer(IEventConsumer eventConsumer) {
		// this is not supported
		if (DEBUG)
			System.out.println("AbstractControlContentAssistSubjectAdapter#setEventConsumer()"); //$NON-NLS-1$
	}

	@Override
	public String getLineDelimiter() {
		return System.getProperty("line.separator"); //$NON-NLS-1$
	}

	/**
	 * Installs <code>fControlListener</code>, which handles VerifyEvents and KeyEvents by
	 * passing them to {@link #fVerifyKeyListeners} and {@link #fKeyListeners}.
	 */
	private void installControlListener() {
		if (DEBUG)
			System.out.println("AbstractControlContentAssistSubjectAdapter#installControlListener() -> k: " + fKeyListeners.size() + ", v: " + fVerifyKeyListeners.size()); //$NON-NLS-1$ //$NON-NLS-2$

		if (fControlListener != null)
			return;

		fControlListener= new Listener() {
			@Override
			public void handleEvent(Event e) {
				if (! getControl().isFocusControl())
					return; //SWT.TRAVERSE_MNEMONIC events can also come in to inactive widgets
				VerifyEvent verifyEvent= new VerifyEvent(e);
				KeyEvent keyEvent= new KeyEvent(e);
				switch (e.type) {
					case SWT.Traverse :

						if (DEBUG)
							dump("before traverse", e, verifyEvent); //$NON-NLS-1$

						verifyEvent.doit= true;
						for (VerifyKeyListener verifyKeyListener : fVerifyKeyListeners) {
							verifyKeyListener.verifyKey(verifyEvent);
							if (! verifyEvent.doit) {
								e.detail= SWT.TRAVERSE_NONE;
								e.doit= true;
								if (DEBUG)
									dump("traverse eaten by verify", e, verifyEvent); //$NON-NLS-1$
								return;
							}

							if (DEBUG)
								dump("traverse OK", e, verifyEvent); //$NON-NLS-1$
						}
						break;

					case SWT.KeyDown:
						for (VerifyKeyListener verifyKeyListener : fVerifyKeyListeners) {
							verifyKeyListener.verifyKey(verifyEvent);
							if (! verifyEvent.doit) {
								e.doit= verifyEvent.doit;
								if (DEBUG)
									dump("keyDown eaten by verify", e, verifyEvent); //$NON-NLS-1$
								return;
							}
						}

						if (DEBUG)
							dump("keyDown OK", e, verifyEvent); //$NON-NLS-1$

						for (KeyListener keyListener : fKeyListeners) {
							keyListener.keyPressed(keyEvent);
						}
						break;

					default :
						Assert.isTrue(false);
				}
			}

			/**
			 * Dump the given events to "standard" output.
			 *
			 * @param who who dump's
			 * @param e the event
			 * @param ve the verify event
			 */
			private void dump(String who, Event e, VerifyEvent ve) {
				StringBuilder sb= new StringBuilder("--- [AbstractControlContentAssistSubjectAdapter]\n"); //$NON-NLS-1$
				sb.append(who);
				sb.append(" - e: keyCode="+e.keyCode+hex(e.keyCode)); //$NON-NLS-1$
				sb.append("; character="+e.character+hex(e.character)); //$NON-NLS-1$
				sb.append("; stateMask="+e.stateMask+hex(e.stateMask)); //$NON-NLS-1$
				sb.append("; doit="+e.doit); //$NON-NLS-1$
				sb.append("; detail="+e.detail+hex(e.detail)); //$NON-NLS-1$
				sb.append("; widget="+e.widget); //$NON-NLS-1$
				sb.append("\n"); //$NON-NLS-1$
				sb.append("  verifyEvent keyCode="+e.keyCode+hex(e.keyCode)); //$NON-NLS-1$
				sb.append("; character="+e.character+hex(e.character)); //$NON-NLS-1$
				sb.append("; stateMask="+e.stateMask+hex(e.stateMask)); //$NON-NLS-1$
				sb.append("; doit="+ve.doit); //$NON-NLS-1$
				sb.append("; widget="+e.widget); //$NON-NLS-1$
				System.out.println(sb);
			}

			private String hex(int i) {
				return "[0x" + Integer.toHexString(i) + ']'; //$NON-NLS-1$
			}
		};
		getControl().addListener(SWT.Traverse, fControlListener);
		getControl().addListener(SWT.KeyDown, fControlListener);

		if (DEBUG)
			System.out.println("AbstractControlContentAssistSubjectAdapter#installControlListener() - installed"); //$NON-NLS-1$
	}

	/**
	 * Uninstalls <code>fControlListener</code> iff there are no <code>KeyListener</code>s and no
	 * <code>VerifyKeyListener</code>s registered.
	 * Otherwise does nothing.
	 */
	private void uninstallControlListener() {
		if (fControlListener == null || fKeyListeners.size() + fVerifyKeyListeners.size() != 0) {

			if (DEBUG)
				System.out.println("AbstractControlContentAssistSubjectAdapter#uninstallControlListener() -> k: " + fKeyListeners.size() + ", v: " + fVerifyKeyListeners.size()); //$NON-NLS-1$ //$NON-NLS-2$

			return;
		}
		getControl().removeListener(SWT.Traverse, fControlListener);
		getControl().removeListener(SWT.KeyDown, fControlListener);
		fControlListener= null;

		if (DEBUG)
			System.out.println("AbstractControlContentAssistSubjectAdapter#uninstallControlListener() - done"); //$NON-NLS-1$
	}

	/**
	 * Sets the visual feedback provider for content assist.
	 * The given {@link ILabelProvider} methods are called with
	 * {@link #getControl()} as argument.
	 *
	 * <ul>
	 *   <li><code>getImage(Object)</code> provides the visual cue image.
	 *     The image can maximally be 5 pixels wide and 8 pixels high.
	 *     If <code>getImage(Object)</code> returns <code>null</code>, a default image is used.
	 *   </li>
	 *   <li><code>getText(Object)</code> provides the hover info text.
	 *     It is shown when hovering over the cue image or the adapted {@link Control}.
	 *     No info text is shown if <code>getText(Object)</code> returns <code>null</code>.
	 *   </li>
	 * </ul>
	 * <p>
	 * The given {@link ILabelProvider} becomes owned by the {@link AbstractControlContentAssistSubjectAdapter},
	 * i.e. it gets disposed when the adapted {@link Control} is disposed
	 * or when another {@link ILabelProvider} is set.
	 * </p>
	 *
	 * @param labelProvider a {@link ILabelProvider}, or <code>null</code>
	 * 	if no visual feedback should be shown
	 */
	public void setContentAssistCueProvider(final ILabelProvider labelProvider) {
		if (fCueLabelProvider != null) {
			fCueLabelProvider.dispose();
		}

		fCueLabelProvider= labelProvider;

		if (labelProvider == null) {
			if (fControlDecoration != null) {
				fControlDecoration.dispose();
				fControlDecoration= null;
			}

		} else {
			if (fControlDecoration == null) {
				fControlDecoration= new ControlDecoration(getControl(), (SWT.TOP | SWT.LEFT));
				getControl().addDisposeListener(e -> {
					if (fCueLabelProvider != null) {
						fCueLabelProvider.dispose();
						fCueLabelProvider= null;
					}
					if (fControlDecoration != null) {
						fControlDecoration.dispose();
						fControlDecoration= null;
					}
					if (fCachedDefaultCueImage != null) {
						fCachedDefaultCueImage.dispose();
						fCachedDefaultCueImage= null;
					}
				});
				fControlDecoration.setShowHover(true);
				fControlDecoration.setShowOnlyOnFocus(true);
			}

			ILabelProviderListener listener= event -> {
				fControlDecoration.setDescriptionText(labelProvider.getText(getControl()));
				Image image= labelProvider.getImage(getControl());
				if (image == null)
					image= getDefaultCueImage();
				fControlDecoration.setImage(image);
			};
			labelProvider.addListener(listener);
			//initialize control decoration:
			listener.labelProviderChanged(new LabelProviderChangedEvent(labelProvider));
		}
	}

	/**
	 * Returns the default cue image.
	 *
	 * @return the default cue image
	 * @since 3.3
	 */
	private Image getDefaultCueImage() {
		if (fCachedDefaultCueImage == null) {
			ImageDescriptor cueID= ImageDescriptor.createFromFile(AbstractControlContentAssistSubjectAdapter.class, "images/content_assist_cue.png"); //$NON-NLS-1$
			fCachedDefaultCueImage= cueID.createImage(getControl().getDisplay());
		}
		return fCachedDefaultCueImage;
	}
}
