/*******************************************************************************
 * Copyright (c) 2000, 2015 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
 *     Lars Vogel <Lars.Vogel@vogella.com> - Bug 440270
 *******************************************************************************/
package org.eclipse.jface.viewers;

import static org.eclipse.swt.events.SelectionListener.widgetSelectedAdapter;

import java.text.MessageFormat;	// Not using ICU to support standalone JFace scenario

import org.eclipse.jface.resource.ImageDescriptor;
import org.eclipse.jface.resource.ImageRegistry;
import org.eclipse.jface.resource.JFaceResources;
import org.eclipse.swt.SWT;
import org.eclipse.swt.events.FocusEvent;
import org.eclipse.swt.events.FocusListener;
import org.eclipse.swt.events.KeyAdapter;
import org.eclipse.swt.events.KeyEvent;
import org.eclipse.swt.graphics.Color;
import org.eclipse.swt.graphics.Font;
import org.eclipse.swt.graphics.Point;
import org.eclipse.swt.graphics.Rectangle;
import org.eclipse.swt.widgets.Button;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Control;
import org.eclipse.swt.widgets.Label;
import org.eclipse.swt.widgets.Layout;

/**
 * An abstract cell editor that uses a dialog. Dialog cell editors usually have
 * a label control on the left and a button on the right. Pressing the button
 * opens a dialog window (for example, a color dialog or a file dialog) to
 * change the cell editor's value. The cell editor's value is the value of the
 * dialog.
 * <p>
 * Subclasses may override the following methods:
 * </p>
 * <ul>
 * <li><code>createButton</code>: creates the cell editor's button control</li>
 * <li><code>createContents</code>: creates the cell editor's 'display value'
 * control</li>
 * <li><code>updateContents</code>: updates the cell editor's 'display value'
 * control after its value has changed</li>
 * <li><code>openDialogBox</code>: opens the dialog box when the end user
 * presses the button</li>
 * </ul>
 */
public abstract class DialogCellEditor extends CellEditor {

	/**
	 * Image registry key for three dot image (value <code>"cell_editor_dots_button_image"</code>).
	 */
	public static final String CELL_EDITOR_IMG_DOTS_BUTTON = "cell_editor_dots_button_image";//$NON-NLS-1$

	/**
	 * The editor control.
	 */
	private Composite editor;

	/**
	 * The current contents.
	 */
	private Control contents;

	/**
	 * The label that gets reused by <code>updateLabel</code>.
	 */
	private Label defaultLabel;

	/**
	 * The button.
	 */
	private Button button;

	/**
	 * Listens for 'focusLost' events and  fires the 'apply' event as long
	 * as the focus wasn't lost because the dialog was opened.
	 */
	private FocusListener buttonFocusListener;

	/**
	 * The value of this cell editor; initially <code>null</code>.
	 */
	private Object value = null;

	static {
		ImageRegistry reg = JFaceResources.getImageRegistry();
		reg.put(CELL_EDITOR_IMG_DOTS_BUTTON, ImageDescriptor.createFromFile(
				DialogCellEditor.class, "images/dots_button.png"));//$NON-NLS-1$
	}

	/**
	 * Internal class for laying out the dialog.
	 */
	private class DialogCellLayout extends Layout {
		@Override
		public void layout(Composite editor, boolean force) {
			Rectangle bounds = editor.getClientArea();
			Point size = button.computeSize(SWT.DEFAULT, SWT.DEFAULT, force);
			if (contents != null) {
				contents.setBounds(0, 0, bounds.width - size.x, bounds.height);
			}
			button.setBounds(bounds.width - size.x, 0, size.x, bounds.height);
		}

		@Override
		public Point computeSize(Composite editor, int wHint, int hHint,
				boolean force) {
			if (wHint != SWT.DEFAULT && hHint != SWT.DEFAULT) {
				return new Point(wHint, hHint);
			}
			Point contentsSize = contents.computeSize(SWT.DEFAULT, SWT.DEFAULT,
					force);
			Point buttonSize = button.computeSize(SWT.DEFAULT, SWT.DEFAULT,
					force);
			return new Point(buttonSize.x, Math.max(contentsSize.y,
					buttonSize.y));
		}
	}

	/**
	 * Default DialogCellEditor style
	 */
	private static final int defaultStyle = SWT.NONE;

	/**
	 * Creates a new dialog cell editor with no control
	 * @since 2.1
	 */
	public DialogCellEditor() {
		setStyle(defaultStyle);
	}

	/**
	 * Creates a new dialog cell editor parented under the given control.
	 * The cell editor value is <code>null</code> initially, and has no
	 * validator.
	 *
	 * @param parent the parent control
	 */
	protected DialogCellEditor(Composite parent) {
		this(parent, defaultStyle);
	}

	/**
	 * Creates a new dialog cell editor parented under the given control.
	 * The cell editor value is <code>null</code> initially, and has no
	 * validator.
	 *
	 * @param parent the parent control
	 * @param style the style bits
	 * @since 2.1
	 */
	protected DialogCellEditor(Composite parent, int style) {
		super(parent, style);
	}

	/**
	 * Creates the button for this cell editor under the given parent control.
	 * <p>
	 * The default implementation of this framework method creates the button
	 * display on the right hand side of the dialog cell editor. Subclasses
	 * may extend or reimplement.
	 * </p>
	 *
	 * @param parent the parent control
	 * @return the new button control
	 */
	protected Button createButton(Composite parent) {
		Button result = new Button(parent, SWT.DOWN);
		result.setText("..."); //$NON-NLS-1$
		return result;
	}

	/**
	 * Creates the controls used to show the value of this cell editor.
	 * <p>
	 * The default implementation of this framework method creates
	 * a label widget, using the same font and background color as the parent control.
	 * </p>
	 * <p>
	 * Subclasses may reimplement.  If you reimplement this method, you
	 * should also reimplement <code>updateContents</code>.
	 * </p>
	 *
	 * @param cell the control for this cell editor
	 * @return the underlying control
	 */
	protected Control createContents(Composite cell) {
		defaultLabel = new Label(cell, SWT.LEFT);
		defaultLabel.setFont(cell.getFont());
		defaultLabel.setBackground(cell.getBackground());
		return defaultLabel;
	}

	@Override
	protected Control createControl(Composite parent) {

		Font font = parent.getFont();
		Color bg = parent.getBackground();

		editor = new Composite(parent, getStyle());
		editor.setFont(font);
		editor.setBackground(bg);
		editor.setLayout(new DialogCellLayout());

		contents = createContents(editor);
		updateContents(value);

		button = createButton(editor);
		button.setFont(font);

		button.addKeyListener(new KeyAdapter() {

			@Override
			public void keyReleased(KeyEvent e) {
				if (e.character == '\u001b') { // Escape
					fireCancelEditor();
				}
			}
		});

		button.addFocusListener(getButtonFocusListener());

		button.addSelectionListener(widgetSelectedAdapter(event -> {
			// Remove the button's focus listener since it's guaranteed
			// to lose focus when the dialog opens
			button.removeFocusListener(getButtonFocusListener());

			Object newValue = openDialogBox(editor);

			// Re-add the listener once the dialog closes
			button.addFocusListener(getButtonFocusListener());

			if (newValue != null) {
				boolean newValidState = isCorrect(newValue);
				if (newValidState) {
					markDirty();
					doSetValue(newValue);
				} else {
					// try to insert the current value into the error message.
					setErrorMessage(MessageFormat.format(getErrorMessage(), newValue.toString()));
				}
				fireApplyEditorValue();
			}
		}));

		setValueValid(true);

		return editor;
	}

	@Override
	public void deactivate() {
		if (button != null && !button.isDisposed()) {
			button.removeFocusListener(getButtonFocusListener());
		}

		super.deactivate();
	}

	@Override
	protected Object doGetValue() {
		return value;
	}

	@Override
	protected void doSetFocus() {
		button.setFocus();

		// add a FocusListener to the button
		button.addFocusListener(getButtonFocusListener());
	}

	/**
	 * Return a listener for button focus.
	 * @return FocusListener
	 */
	private FocusListener getButtonFocusListener() {
		if (buttonFocusListener == null) {
			buttonFocusListener = new FocusListener() {

				@Override
				public void focusGained(FocusEvent e) {
					// Do nothing
				}

				@Override
				public void focusLost(FocusEvent e) {
					DialogCellEditor.this.focusLost();
				}
			};
		}

		return buttonFocusListener;
	}

	@Override
	protected void doSetValue(Object value) {
		this.value = value;
		updateContents(value);
	}

	/**
	 * Returns the default label widget created by <code>createContents</code>.
	 *
	 * @return the default label widget
	 */
	protected Label getDefaultLabel() {
		return defaultLabel;
	}

	/**
	 * Opens a dialog box under the given parent control and returns the
	 * dialog's value when it closes, or <code>null</code> if the dialog
	 * was canceled or no selection was made in the dialog.
	 * <p>
	 * This framework method must be implemented by concrete subclasses.
	 * It is called when the user has pressed the button and the dialog
	 * box must pop up.
	 * </p>
	 *
	 * @param cellEditorWindow the parent control cell editor's window
	 *   so that a subclass can adjust the dialog box accordingly
	 * @return the selected value, or <code>null</code> if the dialog was
	 *   canceled or no selection was made in the dialog
	 */
	protected abstract Object openDialogBox(Control cellEditorWindow);

	/**
	 * Updates the controls showing the value of this cell editor.
	 * <p>
	 * The default implementation of this framework method just converts
	 * the passed object to a string using <code>toString</code> and
	 * sets this as the text of the label widget.
	 * </p>
	 * <p>
	 * Subclasses may reimplement.  If you reimplement this method, you
	 * should also reimplement <code>createContents</code>.
	 * </p>
	 *
	 * @param value the new value of this cell editor
	 */
	protected void updateContents(Object value) {
		if (defaultLabel == null) {
			return;
		}

		String text = "";//$NON-NLS-1$
		if (value != null) {
			text = value.toString();
		}
		defaultLabel.setText(text);
	}
}
