/*******************************************************************************
 * Copyright (c) 2007, 2016 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
 *     Manumitting Technologies Inc - bug 488721
 ******************************************************************************/

package org.eclipse.ui.internal.quickaccess;

import org.eclipse.jface.resource.DeviceResourceException;
import org.eclipse.jface.resource.ImageDescriptor;
import org.eclipse.jface.resource.ResourceManager;
import org.eclipse.jface.viewers.StyledCellLabelProvider;
import org.eclipse.jface.viewers.StyledString;
import org.eclipse.swt.SWT;
import org.eclipse.swt.custom.StyleRange;
import org.eclipse.swt.graphics.Color;
import org.eclipse.swt.graphics.Image;
import org.eclipse.swt.graphics.Rectangle;
import org.eclipse.swt.graphics.TextLayout;
import org.eclipse.swt.graphics.TextStyle;
import org.eclipse.swt.widgets.Event;
import org.eclipse.swt.widgets.Table;
import org.eclipse.swt.widgets.TableItem;
import org.eclipse.ui.internal.IWorkbenchGraphicConstants;
import org.eclipse.ui.internal.WorkbenchImages;
import org.eclipse.ui.internal.WorkbenchPlugin;
import org.eclipse.ui.internal.quickaccess.providers.CommandElement;
import org.eclipse.ui.quickaccess.QuickAccessElement;

/**
 *
 * @since 3.5
 *
 */
public class QuickAccessEntry {
	boolean firstInCategory;
	boolean lastInCategory;
	QuickAccessElement element;
	QuickAccessProvider provider;
	int[][] elementMatchRegions;
	int[][] providerMatchRegions;

	/**
	 * Provides a rough indicator of how good of a match this entry was to its
	 * filter. Lower values indicate better match quality. A value of 0 indicates
	 * the filter string was an exact match to the label or that there is no filter
	 * being applied.
	 */
	private int matchQuality;

	/**
	 * Indicates the filter string was a perfect match to the label or there is no
	 * filter applied
	 *
	 * @see #getMatchQuality()
	 */
	public static final int MATCH_PERFECT = 0;

	/**
	 * Indicates this entry is very relevant for the filter string. Recommended
	 * value for when the filter was found at the start of the element's label or a
	 * complete case sensitive camel case match.
	 *
	 * @see #getMatchQuality()
	 */
	public static final int MATCH_EXCELLENT = 5;

	/**
	 * Indicates this entry is relevant for the filter string. Recommended value for
	 * when the complete filter was found somewhere inside the element's label or
	 * provider.
	 *
	 * @see #getMatchQuality()
	 */
	public static final int MATCH_GOOD = 10;

	/**
	 * Indicates only part of the filter string matches to the element's label.
	 *
	 * @see #getMatchQuality()
	 */
	public static final int MATCH_PARTIAL = 15;

	/**
	 * Creates a new quick access entry from the given element and provider. If no
	 * filter was used to match this entry the element/provider match regions may be
	 * empty and the match quality should be {@link #MATCH_PERFECT}
	 *
	 * @param element              the element this entry will represent
	 * @param provider             the provider that owns this entry
	 * @param elementMatchRegions  list of text regions the filter string matched in
	 *                             the element label, possibly empty
	 * @param providerMatchRegions list of text regions the filter string matches in
	 *                             the provider label, possible empty
	 * @param matchQuality         a rough indication of how closely the filter
	 *                             matched, lower values indicate a better match. It
	 *                             is recommended to use the constants available on
	 *                             this class: {@link #MATCH_PERFECT},
	 *                             {@link #MATCH_EXCELLENT}, {@link #MATCH_GOOD},
	 *                             {@link #MATCH_PARTIAL}
	 */
	public QuickAccessEntry(QuickAccessElement element, QuickAccessProvider provider, int[][] elementMatchRegions,
			int[][] providerMatchRegions, int matchQuality) {
		this.element = element;
		this.provider = provider;
		this.elementMatchRegions = elementMatchRegions;
		this.providerMatchRegions = providerMatchRegions;
		this.matchQuality = matchQuality;
	}

	Image getImage(QuickAccessElement element, ResourceManager resourceManager) {
		Image image = findOrCreateImage(element.getImageDescriptor(), resourceManager);
		if (image == null) {
			image = WorkbenchImages.getImage(IWorkbenchGraphicConstants.IMG_OBJ_ELEMENT);
		}
		return image;
	}

	private Image findOrCreateImage(ImageDescriptor imageDescriptor, ResourceManager resourceManager) {
		if (imageDescriptor == null) {
			return null;
		}
		Image image = resourceManager.find(imageDescriptor);
		if (image == null) {
			try {
				image = resourceManager.create(imageDescriptor);
			} catch (DeviceResourceException e) {
				WorkbenchPlugin.log(e);
			}
		}
		return image;
	}

	public void measure(Event event, TextLayout textLayout, ResourceManager resourceManager, TextStyle boldStyle) {
		Table table = ((TableItem) event.item).getParent();
		textLayout.setFont(table.getFont());
		event.width = 0;
		switch (event.index) {
		case 0:
			if (firstInCategory || providerMatchRegions.length > 0) {
				textLayout.setText(provider.getName());
				if (boldStyle != null) {
					for (int[] matchRegion : providerMatchRegions) {
						textLayout.setStyle(boldStyle, matchRegion[0], matchRegion[1]);
					}
				}
			} else {
				textLayout.setText(""); //$NON-NLS-1$
			}
			break;
		case 1:
			textLayout.setText(element.getLabel());
			// Two situations for measuring icons:
			// - command with very large icon image (500x500) [scale down]
			// - command with normal image (16x16) but small text-height (8pt)
			Image image = getImage(element, resourceManager);
			Rectangle imageRect = image.getBounds();
			Rectangle textBounds = textLayout.getBounds();
			int iconSize = imageRect.height;
			// Heuristic: only scale image if has double the pixels
			if (iconSize > 16 && iconSize >= 2 * textBounds.height) {
				// image will be scaled down to fit
				iconSize = textBounds.height;
			}
			// include additional line 1 for category separator
			event.height = Math.max(event.height, iconSize + 3);
			event.width += iconSize + 4;
			if (boldStyle != null) {
				for (int[] matchRegion : elementMatchRegions) {
					textLayout.setStyle(boldStyle, matchRegion[0], matchRegion[1]);
				}
			}
			break;
		}
		Rectangle rect = textLayout.getBounds();
		event.width += rect.width + 4;
		event.height = Math.max(event.height, rect.height + 2);
	}

	public void paint(Event event, TextLayout textLayout, ResourceManager resourceManager, TextStyle boldStyle,
			Color grayColor) {
		final Table table = ((TableItem) event.item).getParent();
		textLayout.setFont(table.getFont());
		switch (event.index) {
		case 0:
			if (firstInCategory || providerMatchRegions.length > 0) {
				textLayout.setText(provider.getName());
				if (boldStyle != null) {
					for (int[] matchRegion : providerMatchRegions) {
						textLayout.setStyle(boldStyle, matchRegion[0], matchRegion[1]);
					}
				}
				if (grayColor != null && providerMatchRegions.length > 0 && !firstInCategory) {
					event.gc.setForeground(grayColor);
				}
				Rectangle availableBounds = ((TableItem) event.item).getTextBounds(event.index);
				Rectangle requiredBounds = textLayout.getBounds();
				textLayout.draw(event.gc, availableBounds.x + 1,
						availableBounds.y + (availableBounds.height - requiredBounds.height) / 2);
			}
			break;
		case 1:
			String label = element.getLabel();
			if (element instanceof CommandElement) {
				CommandElement commandElement = (CommandElement) element;
				String binding = commandElement.getBinding();
				if (binding != null) {
					StyledString styledString = StyledCellLabelProvider.styleDecoratedString(label,
							StyledString.QUALIFIER_STYLER, new StyledString(commandElement.getCommand()));
					StyleRange[] styleRanges = styledString.getStyleRanges();
					for (StyleRange styleRange : styleRanges) {
						textLayout.setStyle(styleRange, styleRange.start, styleRange.start + styleRange.length);
					}
				}
			}
			// draw images to fit square area sized by the text area
			Image image = getImage(element, resourceManager);
			Rectangle availableBounds = ((TableItem) event.item).getTextBounds(event.index);
			Rectangle requiredBounds = textLayout.getBounds();
			Rectangle imageBounds = image.getBounds();
			// 3 = top + bottom + category lines
			int maxImageSize = availableBounds.height - 3;
			// preserve aspect ratio
			int destHeight = Math.min(imageBounds.height, maxImageSize);
			int destWidth = destHeight * imageBounds.width / imageBounds.height;
			// and centre within available space; remove 1 from height for
			// category separator
			int startX = (maxImageSize - destWidth) / 2;
			int startY = (availableBounds.height - 1 - destHeight) / 2;
			event.gc.drawImage(image, 0, 0, imageBounds.width, imageBounds.height, availableBounds.x + startX,
					availableBounds.y + startY, destWidth, destHeight);
			textLayout.setText(label);
			if (boldStyle != null) {
				for (int[] matchRegion : elementMatchRegions) {
					textLayout.setStyle(boldStyle, matchRegion[0], matchRegion[1]);
				}
			}
			textLayout.draw(event.gc, availableBounds.x + maxImageSize + 4,
					availableBounds.y + (availableBounds.height - requiredBounds.height) / 2);
			break;
		}
		if (lastInCategory) {
			if (grayColor != null)
				event.gc.setForeground(grayColor);
			Rectangle bounds = ((TableItem) event.item).getBounds(event.index);
			event.gc.drawLine(Math.max(0, bounds.x - 1), bounds.y + bounds.height - 1, bounds.x + bounds.width,
					bounds.y + bounds.height - 1);
		}
	}

	/**
	 * @param event
	 */
	public void erase(Event event) {
		// We are only custom drawing the foreground.
		event.detail &= ~SWT.FOREGROUND;
	}

	/**
	 * Provides a rough indicator of how good of a match this entry was to its
	 * filter. Lower values indicate better match quality. A value of
	 * {@link #MATCH_PERFECT} indicates the filter string was an exact match to the
	 * label or that there is no filter being applied.
	 *
	 * @return Returns the match quality
	 */
	public int getMatchQuality() {
		return matchQuality;
	}

}