/*******************************************************************************
 * Copyright (c) 2000, 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
 *******************************************************************************/
package org.eclipse.ui.internal.forms.widgets;

import org.eclipse.jface.action.IMenuManager;
import org.eclipse.jface.action.MenuManager;
import org.eclipse.osgi.service.environment.Constants;
import org.eclipse.swt.SWT;
import org.eclipse.swt.custom.StyledText;
import org.eclipse.swt.dnd.DragSource;
import org.eclipse.swt.dnd.DragSourceEffect;
import org.eclipse.swt.dnd.DragSourceEvent;
import org.eclipse.swt.dnd.DragSourceListener;
import org.eclipse.swt.dnd.DropTarget;
import org.eclipse.swt.dnd.DropTargetListener;
import org.eclipse.swt.dnd.Transfer;
import org.eclipse.swt.events.FocusListener;
import org.eclipse.swt.events.MouseEvent;
import org.eclipse.swt.events.MouseMoveListener;
import org.eclipse.swt.events.MouseTrackListener;
import org.eclipse.swt.events.PaintEvent;
import org.eclipse.swt.graphics.Color;
import org.eclipse.swt.graphics.Font;
import org.eclipse.swt.graphics.FontMetrics;
import org.eclipse.swt.graphics.GC;
import org.eclipse.swt.graphics.Image;
import org.eclipse.swt.graphics.Point;
import org.eclipse.swt.graphics.Rectangle;
import org.eclipse.swt.widgets.Canvas;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Control;
import org.eclipse.swt.widgets.Label;
import org.eclipse.swt.widgets.Layout;
import org.eclipse.swt.widgets.Menu;
import org.eclipse.ui.forms.IFormColors;
import org.eclipse.ui.forms.widgets.ILayoutExtension;
import org.eclipse.ui.forms.widgets.SizeCache;
import org.eclipse.ui.forms.widgets.Twistie;
import org.eclipse.ui.internal.forms.IMessageToolTipManager;

/**
 * Form heading title.
 */
public class TitleRegion extends Canvas {
	public static final int STATE_NORMAL = 0;
	public static final int STATE_HOVER_LIGHT = 1;
	public static final int STATE_HOVER_FULL = 2;
	private int hoverState;
	private static final int HMARGIN = 1;
	private static final int VMARGIN = 5;
	private static final int SPACING = 5;
	private static final int ARC_WIDTH = 20;
	private static final int ARC_HEIGHT = 20;
	private Image image;
	private BusyIndicator busyLabel;
	private Control currentTitleControl;
	private Label titleLabel;
	private StyledText titleText;
	private SizeCache titleCache;
	private int fontHeight = -1;
	private int fontBaselineHeight = -1;
	private MenuHyperlink menuHyperlink;
	private MenuManager menuManager;
	private boolean dragSupport;
	private int dragOperations;
	private Transfer[] dragTransferTypes;
	private DragSourceListener dragListener;
	private DragSource dragSource;

	private class HoverListener implements MouseTrackListener,
			MouseMoveListener {

		@Override
		public void mouseEnter(MouseEvent e) {
			setHoverState(STATE_HOVER_FULL);
		}

		@Override
		public void mouseExit(MouseEvent e) {
			setHoverState(STATE_NORMAL);
		}

		@Override
		public void mouseHover(MouseEvent e) {
		}

		@Override
		public void mouseMove(MouseEvent e) {
			if (e.button > 0)
				setHoverState(STATE_NORMAL);
			else
				setHoverState(STATE_HOVER_FULL);
		}
	}

	private class MenuHyperlink extends Twistie {
		private boolean firstTime = true;

		public MenuHyperlink(Composite parent, int style) {
			super(parent, style);
			setExpanded(true);
		}

		@Override
		public void setExpanded(boolean expanded) {
			if (firstTime) {
				super.setExpanded(expanded);
				firstTime = false;
			} else {
				Menu menu = menuManager.createContextMenu(menuHyperlink);
				menu.setVisible(true);
			}
		}
	}

	private class TitleRegionLayout extends Layout implements ILayoutExtension {

		@Override
		protected Point computeSize(Composite composite, int wHint, int hHint,
				boolean flushCache) {
			return layout(composite, false, 0, 0, wHint, hHint, flushCache);
		}

		@Override
		protected void layout(Composite composite, boolean flushCache) {
			Rectangle carea = composite.getClientArea();
			layout(composite, true, carea.x, carea.y, carea.width,
					carea.height, flushCache);
		}

		private Point layout(Composite composite, boolean move, int x, int y,
				int width, int height, boolean flushCache) {
			int iwidth = width == SWT.DEFAULT ? SWT.DEFAULT : width - HMARGIN
					* 2;
			Point bsize = null;
			Point tsize = null;
			Point msize = null;

			if (busyLabel != null) {
				bsize = busyLabel.computeSize(SWT.DEFAULT, SWT.DEFAULT);
			}
			if (menuManager != null) {
				menuHyperlink.setVisible(!menuManager.isEmpty()
						&& currentTitleControl.getVisible());
				if (menuHyperlink.getVisible())
					msize = menuHyperlink.computeSize(SWT.DEFAULT, SWT.DEFAULT);
			}
			if (flushCache)
				titleCache.flush();
			titleCache.setControl(currentTitleControl);
			int twidth = iwidth == SWT.DEFAULT ? iwidth : iwidth - SPACING * 2;
			if (bsize != null && twidth != SWT.DEFAULT)
				twidth -= bsize.x + SPACING;
			if (msize != null && twidth != SWT.DEFAULT)
				twidth -= msize.x + SPACING;
			if (currentTitleControl.getVisible()) {
				tsize = titleCache.computeSize(twidth, SWT.DEFAULT);
				if (twidth != SWT.DEFAULT) {
					// correct for the case when width hint is larger
					// than the maximum width - this is when the text
					// can be rendered on one line with width to spare
					int maxWidth = titleCache.computeSize(SWT.DEFAULT,
							SWT.DEFAULT).x;
					tsize.x = Math.min(tsize.x, maxWidth);
					// System.out.println("twidth="+twidth+",
					// tsize.x="+tsize.x); //$NON-NLS-1$//$NON-NLS-2$
				}
			} else
				tsize = new Point(0, 0);
			Point size = new Point(width, height);
			if (!move) {
				// compute size
				size.x = tsize.x > 0 ? HMARGIN * 2 + SPACING * 2 + tsize.x : 0;
				size.y = tsize.y;
				if (bsize != null) {
					size.x += bsize.x + SPACING;
					size.y = Math.max(size.y, bsize.y);
				}
				if (msize != null) {
					size.x += msize.x + SPACING;
					size.y = Math.max(size.y, msize.y);
				}
				if (size.y > 0)
					size.y += VMARGIN * 2;
				// System.out.println("Compute size: width="+width+",
				// size.x="+size.x); //$NON-NLS-1$ //$NON-NLS-2$
			} else {
				// position controls
				int xloc = x + HMARGIN + SPACING;
				int yloc = y + VMARGIN;
				if (bsize != null) {
					busyLabel.setBounds(xloc,
							// yloc + height / 2 - bsize.y / 2,
							yloc + (getFontHeight() >= bsize.y ? getFontHeight() : bsize.y) - 1 - bsize.y,
							bsize.x, bsize.y);
					xloc += bsize.x + SPACING;
				}
				if (currentTitleControl.getVisible()) {
					int tw = width - HMARGIN * 2 - SPACING * 2;
					String os = System.getProperty("os.name"); //$NON-NLS-1$
					if (Constants.OS_LINUX.equalsIgnoreCase(os)) {
						tw += 1; // See Bug 342610
					}
					if (bsize != null)
						tw -= bsize.x + SPACING;
					if (msize != null)
						tw -= msize.x + SPACING;
					currentTitleControl.setBounds(xloc,
					// yloc + height / 2 - tsize.y / 2,
							yloc, tw, tsize.y);
					// System.out.println("tw="+tw); //$NON-NLS-1$
					xloc += tw + SPACING;
				}
				if (msize != null) {
					menuHyperlink.setBounds(xloc, yloc
							+ getFontHeight() / 2 - msize.y / 2,
							msize.x, msize.y);
				}
			}
			return size;
		}

		@Override
		public int computeMaximumWidth(Composite parent, boolean changed) {
			return computeSize(parent, SWT.DEFAULT, SWT.DEFAULT, changed).x;
		}

		@Override
		public int computeMinimumWidth(Composite parent, boolean changed) {
			return computeSize(parent, 0, SWT.DEFAULT, changed).x;
		}
	}

	public TitleRegion(Composite parent) {
		super(parent, SWT.NULL);
		titleLabel = new Label(this, SWT.WRAP);
		titleLabel.setVisible(false);
		titleText = new StyledText(this, SWT.WRAP);
		titleText.setCursor(getDisplay().getSystemCursor(SWT.CURSOR_IBEAM));
		titleText.setEditable(false);
		titleText.addFocusListener(FocusListener.focusLostAdapter(e -> titleText.setSelection(0)));
		titleText.setVisible(false);
		currentTitleControl = titleLabel;
		titleCache = new SizeCache();
		super.setLayout(new TitleRegionLayout());
		hookHoverListeners();
	}

	@Override
	public boolean forceFocus() {
		return false;
	}

	private Color getColor(String key) {
		return ((FormHeading) getParent()).colors.get(key);
	}

	private void hookHoverListeners() {
		HoverListener listener = new HoverListener();
		addMouseTrackListener(listener);
		addMouseMoveListener(listener);
		titleLabel.addMouseTrackListener(listener);
		titleLabel.addMouseMoveListener(listener);
		addPaintListener(this::onPaint);
	}

	private void onPaint(PaintEvent e) {
		if (hoverState == STATE_NORMAL)
			return;
		GC gc = e.gc;
		Rectangle carea = getClientArea();
		gc.setBackground(getHoverBackground());
		int savedAntialias = gc.getAntialias();
		FormUtil.setAntialias(gc, SWT.ON);
		gc.fillRoundRectangle(carea.x + HMARGIN, carea.y + 2, carea.width
				- HMARGIN * 2, carea.height - 4, ARC_WIDTH, ARC_HEIGHT);
		FormUtil.setAntialias(gc, savedAntialias);
	}

	private Color getHoverBackground() {
		if (hoverState == STATE_NORMAL)
			return null;
		Color color = getColor(hoverState == STATE_HOVER_FULL ? IFormColors.H_HOVER_FULL
				: IFormColors.H_HOVER_LIGHT);
		if (color == null)
			color = getDisplay()
					.getSystemColor(
							hoverState == STATE_HOVER_FULL ? SWT.COLOR_WIDGET_BACKGROUND
									: SWT.COLOR_WIDGET_LIGHT_SHADOW);
		return color;
	}

	public void setHoverState(int state) {
		if (dragSource == null || this.hoverState == state)
			return;
		this.hoverState = state;
		Color color = getHoverBackground();
		titleLabel.setBackground(color != null ? color
				: getColor(FormHeading.COLOR_BASE_BG));
		titleText.setBackground(color != null ? color
				: getColor(FormHeading.COLOR_BASE_BG));
		if (busyLabel != null)
			busyLabel.setBackground(color != null ? color
					: getColor(FormHeading.COLOR_BASE_BG));
		if (menuHyperlink != null)
			menuHyperlink.setBackground(color != null ? color
					: getColor(FormHeading.COLOR_BASE_BG));
		redraw();
	}

	/**
	 * Fully delegates the size computation to the internal layout manager.
	 */
	@Override
	public final Point computeSize(int wHint, int hHint, boolean changed) {
		return ((TitleRegionLayout) getLayout()).computeSize(this, wHint,
				hHint, changed);
	}

	@Override
	public final void setLayout(Layout layout) {
		// do nothing
	}

	public Image getImage() {
		return image;
	}

	public void setImage(Image image) {
		this.image = image;
	}

	public void updateImage(Image newImage, boolean doLayout) {
		Image theImage = newImage != null ? newImage : this.image;

		if (theImage != null) {
			ensureBusyLabelExists();
		} else if (busyLabel != null) {
			if (!busyLabel.isBusy()) {
				busyLabel.dispose();
				busyLabel = null;
			}
		}
		if (busyLabel != null) {
			busyLabel.setImage(theImage);
		}
		if (doLayout)
			layout();
	}

	public void updateToolTip(String toolTip) {
		if (busyLabel != null)
			busyLabel.setToolTipText(toolTip);
	}

	@Override
	public void setBackground(Color bg) {
		super.setBackground(bg);
		titleLabel.setBackground(bg);
		titleText.setBackground(bg);
		if (busyLabel != null)
			busyLabel.setBackground(bg);
		if (menuHyperlink != null)
			menuHyperlink.setBackground(bg);
	}

	@Override
	public void setForeground(Color fg) {
		super.setForeground(fg);
		titleLabel.setForeground(fg);
		titleText.setForeground(fg);
		if (menuHyperlink != null)
			menuHyperlink.setForeground(fg);
	}

	public void setText(String text) {
		if (text != null) {
			titleLabel.setText(text);
			titleText.setText(text);
		}
		currentTitleControl.setVisible(text != null);
		layout();
		redraw();
	}

	public String getText() {
		return titleLabel.getText();
	}

	/**
	 * Sets whether ther text in the title region should be selectable.
	 * <p>
	 * Note: If {@link #addDragSupport(int, Transfer[], DragSourceListener) drag
	 * support} is also enabled, text selection has priority. Dragging still works
	 * in the non-text parts of the title area.
	 *
	 * @param selectable whether the title text should be selectable
	 */
	public void setTextSelectable(boolean selectable) {
		if (selectable) {
			this.titleText.setVisible(currentTitleControl.getVisible());
			this.titleLabel.setVisible(false);
			this.currentTitleControl = this.titleText;
		} else {
			this.titleLabel.setVisible(currentTitleControl.getVisible());
			this.titleText.setVisible(false);
			this.currentTitleControl = this.titleLabel;
		}
		layout();
		redraw();
	}

	@Override
	public void setFont(Font font) {
		super.setFont(font);
		titleLabel.setFont(font);
		titleText.setFont(font);
		fontHeight = -1;
		fontBaselineHeight = -1;
		layout();
	}

	private void ensureBusyLabelExists() {
		if (busyLabel == null) {
			busyLabel = new BusyIndicator(this, SWT.NULL);
			busyLabel.setBackground(getColor(FormHeading.COLOR_BASE_BG));
			HoverListener listener = new HoverListener();
			busyLabel.addMouseTrackListener(listener);
			busyLabel.addMouseMoveListener(listener);
			if (menuManager != null)
				busyLabel.setMenu(menuManager.createContextMenu(this));
			if (dragSupport)
				addDragSupport(busyLabel, dragOperations, dragTransferTypes, dragListener);
			IMessageToolTipManager mng = ((FormHeading) getParent())
					.getMessageToolTipManager();
			if (mng != null)
				mng.createToolTip(busyLabel, true);
		}
	}

	private void createMenuHyperlink() {
		menuHyperlink = new MenuHyperlink(this, SWT.NULL);
		menuHyperlink.setBackground(getColor(FormHeading.COLOR_BASE_BG));
		menuHyperlink.setDecorationColor(getForeground());
		menuHyperlink.setHoverDecorationColor(getDisplay().getSystemColor(SWT.COLOR_LIST_FOREGROUND));
		HoverListener listener = new HoverListener();
		menuHyperlink.addMouseTrackListener(listener);
		menuHyperlink.addMouseMoveListener(listener);
		if (dragSupport)
			addDragSupport(menuHyperlink, dragOperations, dragTransferTypes, dragListener);
	}

	/**
	 * Sets the form's busy state. Busy form will display 'busy' animation in
	 * the area of the title image.
	 *
	 * @param busy
	 *            the form's busy state
	 */
	public boolean setBusy(boolean busy) {
		if (busy)
			ensureBusyLabelExists();
		else if (busyLabel == null)
			return false;
		if (busy == busyLabel.isBusy())
			return false;
		busyLabel.setBusy(busy);
		if (busyLabel.getImage() == null) {
			layout();
			return true;
		}
		return false;
	}

	public boolean isBusy() {
		return busyLabel != null && busyLabel.isBusy();
	}

	/*
	 * Returns the complete height of the font.
	 */
	public int getFontHeight() {
		if (fontHeight == -1) {
			Font font = getFont();
			GC gc = new GC(getDisplay());
			gc.setFont(font);
			FontMetrics fm = gc.getFontMetrics();
			fontHeight = fm.getHeight();
			gc.dispose();
		}
		return fontHeight;
	}

	/*
	 * Returns the height of the font starting at the baseline,
	 * i.e. without the descent.
	 */
	public int getFontBaselineHeight() {
		if (fontBaselineHeight == -1) {
			Font font = getFont();
			GC gc = new GC(getDisplay());
			gc.setFont(font);
			FontMetrics fm = gc.getFontMetrics();
			fontBaselineHeight = fm.getHeight() - fm.getDescent();
			gc.dispose();
		}
		return fontBaselineHeight;
	}

	public IMenuManager getMenuManager() {
		if (menuManager == null) {
			menuManager = new MenuManager();
			Menu menu = menuManager.createContextMenu(this);
			setMenu(menu);
			titleLabel.setMenu(menu);
			titleText.setMenu(menu);
			if (busyLabel != null)
				busyLabel.setMenu(menu);
			createMenuHyperlink();
		}
		return menuManager;
	}

	public void addDragSupport(int operations, Transfer[] transferTypes,
			DragSourceListener listener) {
		dragSupport = true;
		dragOperations = operations;
		dragTransferTypes = transferTypes;
		dragListener = listener;
		dragSource = addDragSupport(titleLabel, operations, transferTypes,
				listener);
		addDragSupport(this, operations, transferTypes, listener);
		if (busyLabel != null)
			addDragSupport(busyLabel, operations, transferTypes, listener);
		if (menuHyperlink != null)
			addDragSupport(menuHyperlink, operations, transferTypes, listener);
	}

	private DragSource addDragSupport(Control control, int operations,
			Transfer[] transferTypes, DragSourceListener listener) {
		DragSource source = new DragSource(control, operations);
		source.setTransfer(transferTypes);
		source.addDragListener(listener);
		source.setDragSourceEffect(new DragSourceEffect(control) {
			@Override
			public void dragStart(DragSourceEvent event) {
				event.image = createDragEffectImage();
			}
		});
		return source;
	}

	private Image createDragEffectImage() {
		/*
		 * if (dragImage != null) { dragImage.dispose(); } GC gc = new GC(this);
		 * Point size = getSize(); dragImage = new Image(getDisplay(), size.x,
		 * size.y); gc.copyArea(dragImage, 0, 0); gc.dispose(); return
		 * dragImage;
		 */
		return null;
	}

	public void addDropSupport(int operations, Transfer[] transferTypes,
			DropTargetListener listener) {
		final DropTarget target = new DropTarget(this, operations);
		target.setTransfer(transferTypes);
		target.addDropListener(listener);
	}
}