/*******************************************************************************
 * Copyright (c) 2018 Remain Software
 *
 * 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:
 *     wim.jongman@remainsoftware.com - initial API and implementation
 *******************************************************************************/
package org.eclipse.tips.ui.internal;

import java.io.IOException;
import java.net.URL;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Timer;
import java.util.TimerTask;
import java.util.function.Function;

import org.eclipse.core.runtime.FileLocator;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Status;
import org.eclipse.core.runtime.jobs.Job;
import org.eclipse.swt.SWT;
import org.eclipse.swt.browser.Browser;
import org.eclipse.swt.browser.BrowserFunction;
import org.eclipse.swt.custom.StackLayout;
import org.eclipse.swt.events.SelectionAdapter;
import org.eclipse.swt.events.SelectionEvent;
import org.eclipse.swt.graphics.Image;
import org.eclipse.swt.graphics.Point;
import org.eclipse.swt.graphics.Rectangle;
import org.eclipse.swt.layout.FillLayout;
import org.eclipse.swt.layout.GridData;
import org.eclipse.swt.layout.GridLayout;
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.Menu;
import org.eclipse.swt.widgets.MenuItem;
import org.eclipse.swt.widgets.ToolBar;
import org.eclipse.swt.widgets.ToolItem;
import org.eclipse.tips.core.IHtmlTip;
import org.eclipse.tips.core.IUrlTip;
import org.eclipse.tips.core.Tip;
import org.eclipse.tips.core.TipAction;
import org.eclipse.tips.core.TipImage;
import org.eclipse.tips.core.TipProvider;
import org.eclipse.tips.core.internal.LogUtil;
import org.eclipse.tips.core.internal.TipManager;
import org.eclipse.tips.ui.IBrowserFunctionProvider;
import org.eclipse.tips.ui.ISwtTip;
import org.eclipse.tips.ui.internal.util.ImageUtil;
import org.eclipse.tips.ui.internal.util.ResourceManager;

@SuppressWarnings("restriction")
public class TipComposite extends Composite implements ProviderSelectionListener {
	private static final String EMPTY = ""; //$NON-NLS-1$
	private static final int READ_TIMER = 2000;
	private TipProvider fProvider;
	private Browser fBrowser;
	private Slider fSlider;
	private TipManager fTipManager;
	private Tip fCurrentTip;
	private Button fUnreadOnly;
	private Button fPreviousTipButton;
	private Composite fSWTComposite;
	private Composite fBrowserComposite;
	private StackLayout fContentStack;
	private Button fMultiActionMenuButton;
	private Composite fNavigationBar;
	private StackLayout fActionStack;
	private Composite fEmptyActionComposite;
	private Composite fSingleActionComposite;
	private Composite fMultiActionComposite;
	private Button fSingleActionButton;
	private Button fMultiActionButton;
	private Composite fContentComposite;
	private List<Image> fActionImages = new ArrayList<>();
	private List<BrowserFunction> fBrowserFunctions = new ArrayList<>();
	private Menu fActionMenu;
	private ToolBar ftoolBar;
	private ToolItem fStartupItem;

	/**
	 * Constructor.
	 *
	 * @param parent the parent
	 * @param style  the style
	 */
	public TipComposite(Composite parent, int style) {
		super(parent, style);
		GridLayout gridLayout_1 = new GridLayout(1, false);
		gridLayout_1.marginWidth = 2;
		gridLayout_1.marginHeight = 2;
		setLayout(gridLayout_1);

		fContentComposite = new Composite(this, SWT.NONE);
		fContentStack = new StackLayout();
		fContentComposite.setLayout(fContentStack);
		GridData gd_gridComposite = new GridData(SWT.FILL, SWT.FILL, true, true, 1, 1);
		gd_gridComposite.widthHint = 600;
		gd_gridComposite.heightHint = 400;
		fContentComposite.setLayoutData(gd_gridComposite);

		fBrowserComposite = new Composite(fContentComposite, SWT.NONE);
		fBrowserComposite.setLayout(new FillLayout(SWT.HORIZONTAL));

		fBrowser = new Browser(fBrowserComposite, SWT.NONE);
		fBrowser.setJavascriptEnabled(true);

		fSWTComposite = new Composite(fContentComposite, SWT.NONE);
		fSWTComposite.setLayout(new FillLayout(SWT.HORIZONTAL));

		fNavigationBar = new Composite(this, SWT.NONE);
		fNavigationBar.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false, 1, 1));
		GridLayout gl_NavigationBar = new GridLayout(2, false);
		gl_NavigationBar.horizontalSpacing = 0;
		gl_NavigationBar.marginHeight = 0;
		gl_NavigationBar.verticalSpacing = 0;
		gl_NavigationBar.marginWidth = 0;
		fNavigationBar.setLayout(gl_NavigationBar);

		Composite preferenceBar = new Composite(fNavigationBar, SWT.NONE);
		FillLayout fl_composite_3 = new FillLayout(SWT.HORIZONTAL);
		fl_composite_3.marginWidth = 5;
		fl_composite_3.spacing = 5;
		preferenceBar.setLayout(fl_composite_3);

		final Menu menu = new Menu(getShell(), SWT.POP_UP);
		menu.addListener(SWT.Show, event -> startupMenuAboutToShow(menu));

		ftoolBar = new ToolBar(preferenceBar, SWT.FLAT | SWT.RIGHT);

		fStartupItem = new ToolItem(ftoolBar, SWT.DROP_DOWN);
		fStartupItem.setText(Messages.TipComposite_13);
		fStartupItem.addListener(SWT.Selection, event -> {
			showStartupOptions(menu);
		});

		fUnreadOnly = new Button(preferenceBar, SWT.CHECK);
		fUnreadOnly.setText(Messages.TipComposite_2);
		fUnreadOnly.addSelectionListener(new SelectionAdapter() {
			@Override
			public void widgetSelected(SelectionEvent e) {
				fTipManager.setServeReadTips(!fUnreadOnly.getSelection());
				fPreviousTipButton.setEnabled(fTipManager.mustServeReadTips());
				fSlider.load();
				getNextTip();
			}
		});

		Composite buttonBar = new Composite(fNavigationBar, SWT.NONE);
		buttonBar.setLayoutData(new GridData(SWT.RIGHT, SWT.CENTER, true, false, 1, 1));
		GridLayout gl_buttonBar = new GridLayout(4, true);
		gl_buttonBar.marginHeight = 0;
		buttonBar.setLayout(gl_buttonBar);

		Composite actionComposite = new Composite(buttonBar, SWT.NONE);
		fActionStack = new StackLayout();
		actionComposite.setLayout(fActionStack);
		actionComposite.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false, 1, 1));

		fSingleActionComposite = new Composite(actionComposite, SWT.NONE);
		GridLayout gl_SingleActionComposite = new GridLayout(1, false);
		gl_SingleActionComposite.marginWidth = 0;
		fSingleActionComposite.setLayout(gl_SingleActionComposite);

		fSingleActionButton = new Button(fSingleActionComposite, SWT.NONE);
		fSingleActionButton.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false, 1, 1));
		fSingleActionButton.setText(Messages.TipComposite_3);
		fSingleActionButton.addSelectionListener(new SelectionAdapter() {
			@Override
			public void widgetSelected(SelectionEvent e) {
				runTipAction(fCurrentTip.getActions().get(0));
			}
		});

		fMultiActionComposite = new Composite(actionComposite, SWT.NONE);
		GridLayout gl_MultiActionComposite = new GridLayout(2, false);
		gl_MultiActionComposite.marginWidth = 0;
		gl_MultiActionComposite.verticalSpacing = 0;
		gl_MultiActionComposite.horizontalSpacing = 0;
		fMultiActionComposite.setLayout(gl_MultiActionComposite);

		fMultiActionButton = new Button(fMultiActionComposite, SWT.NONE);
		fMultiActionButton.setText(Messages.TipComposite_4);
		fMultiActionButton.addSelectionListener(new SelectionAdapter() {
			@Override
			public void widgetSelected(SelectionEvent e) {
				runTipAction(fCurrentTip.getActions().get(0));
			}
		});

		fMultiActionMenuButton = new Button(fMultiActionComposite, SWT.NONE);
		fMultiActionMenuButton.setImage(ResourceManager.getPluginImage("org.eclipse.tips.ui", "icons/popup_menu.png")); //$NON-NLS-1$ //$NON-NLS-2$
		fMultiActionMenuButton.addSelectionListener(new SelectionAdapter() {
			@Override
			public void widgetSelected(SelectionEvent e) {
				showActionMenu();
			}
		});

		fEmptyActionComposite = new Composite(actionComposite, SWT.NONE);
		fEmptyActionComposite.setLayout(new FillLayout(SWT.HORIZONTAL));

		fPreviousTipButton = new Button(buttonBar, SWT.NONE);
		fPreviousTipButton.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, false, false, 1, 1));
		fPreviousTipButton.setText(Messages.TipComposite_7);
		fPreviousTipButton.addSelectionListener(new SelectionAdapter() {

			@Override
			public void widgetSelected(SelectionEvent e) {
				getPreviousTip();
			}
		});
		fPreviousTipButton.setEnabled(false);

		Button btnNextTip = new Button(buttonBar, SWT.NONE);
		btnNextTip.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, false, false, 1, 1));
		btnNextTip.setText(Messages.TipComposite_8);
		btnNextTip.addSelectionListener(new SelectionAdapter() {
			@Override
			public void widgetSelected(SelectionEvent e) {
				getNextTip();
			}
		});

		Button btnClose = new Button(buttonBar, SWT.NONE);
		btnClose.addSelectionListener(new SelectionAdapter() {
			@Override
			public void widgetSelected(SelectionEvent e) {
				getParent().dispose();
			}
		});
		btnClose.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, false, false, 1, 1));
		btnClose.setText(Messages.TipComposite_9);

		Label label_1 = new Label(this, SWT.SEPARATOR | SWT.HORIZONTAL);
		label_1.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, false, false, 1, 1));

		fSlider = new Slider(this, SWT.NONE);
		GridLayout gridLayout = (GridLayout) fSlider.getLayout();
		gridLayout.verticalSpacing = 0;
		gridLayout.marginWidth = 0;
		gridLayout.horizontalSpacing = 0;
		fSlider.setLayoutData(new GridData(SWT.FILL, SWT.BOTTOM, false, false, 1, 1));
		fContentStack.topControl = fBrowserComposite;
		fSlider.addTipProviderListener(this);

		loadWaitingScript();
	}

	private void showStartupOptions(final Menu menu) {
		Rectangle rect = fStartupItem.getBounds();
		Point pt = new Point(rect.x, rect.y + rect.height);
		pt = ftoolBar.toDisplay(pt);
		menu.setLocation(pt.x, pt.y);
		menu.setVisible(true);
	}

	private Image getStartupItemImage(int startup) {
		switch (startup) {
		case 1:
			return ResourceManager.getPluginImage("org.eclipse.tips.ui", "icons/lightbulb.png"); //$NON-NLS-1$ //$NON-NLS-2$
		case 2:
			return ResourceManager.getPluginImage("org.eclipse.tips.ui", "icons/stop.png"); //$NON-NLS-1$ //$NON-NLS-2$
		default:
			return ResourceManager.getPluginImage("org.eclipse.tips.ui", "icons/run_exc.png"); //$NON-NLS-1$ //$NON-NLS-2$
		}
	}

	private void startupMenuAboutToShow(final Menu menu) {
		Arrays.asList(menu.getItems()).forEach(item -> item.dispose());

		MenuItem item0 = new MenuItem(menu, SWT.CHECK);
		item0.setText(Messages.TipComposite_1);
		item0.setSelection(fTipManager.getStartupBehavior() == TipManager.START_DIALOG);
		item0.addListener(SWT.Selection, event -> fTipManager.setStartupBehavior(TipManager.START_DIALOG));
		item0.setImage(getStartupItemImage(TipManager.START_DIALOG));

		MenuItem item1 = new MenuItem(menu, SWT.CHECK);
		item1.setText(Messages.TipComposite_5);
		item1.setSelection(fTipManager.getStartupBehavior() == TipManager.START_BACKGROUND);
		item1.addListener(SWT.Selection, event -> fTipManager.setStartupBehavior(TipManager.START_BACKGROUND));
		item1.setImage(getStartupItemImage(TipManager.START_BACKGROUND));

		MenuItem item2 = new MenuItem(menu, SWT.CHECK);
		item2.setText(Messages.TipComposite_6);
		item2.setSelection(fTipManager.getStartupBehavior() == TipManager.START_DISABLE);
		item2.addListener(SWT.Selection, event -> fTipManager.setStartupBehavior(TipManager.START_DISABLE));
		item2.setImage(getStartupItemImage(TipManager.START_DISABLE));
	}

	private void showActionMenu() {
		Rectangle rect = fMultiActionButton.getBounds();
		Point pt = new Point(rect.x - 1, rect.y + rect.height);
		pt = fMultiActionButton.toDisplay(pt);
		fActionMenu.setLocation(pt.x, pt.y);
		fActionMenu.setVisible(true);
	}

	private void runTipAction(TipAction tipAction) {
		Job job = new Job(MessageFormat.format(Messages.TipComposite_10, tipAction.getTooltip())) {
			@Override
			protected IStatus run(IProgressMonitor monitor) {
				try {
					tipAction.getRunner().run();
				} catch (Exception e) {
					IStatus status = LogUtil.error(getClass(), e);
					fTipManager.log(status);
					return status;
				}
				return Status.OK_STATUS;
			}
		};
		job.setUser(true);
		job.schedule();
	}

	/**
	 * Sets the selected provider.
	 *
	 * @param provider the {@link TipProvider}
	 */
	public void setProvider(TipProvider provider) {
		fProvider = provider;
		fSlider.setTipProvider(provider);
		getCurrentTip();
	}

	/**
	 * Schedules a TimerTask that is executed after {@value #READ_TIMER}
	 * milliseconds after which the tip is marked as read.
	 */
	private void hitTimer() {
		Tip timerTip = fCurrentTip;
		Timer timer = new Timer(Messages.TipComposite_11);
		timer.schedule(new TimerTask() {
			@Override
			public void run() {
				if (timerTip == fCurrentTip) {
					fTipManager.setAsRead(timerTip);
					fSlider.updateButtons();
				}
				timer.cancel();
			}
		}, READ_TIMER);
	}

	private void getPreviousTip() {
		processTip(fProvider.getPreviousTip());
	}

	private void getNextTip() {
		if (fProvider.getTips().isEmpty() && !fTipManager.getProviders().isEmpty()) {
			fProvider.getNextTip(); // advance current tip
			for (TipProvider provider : fTipManager.getProviders()) {
				if (!provider.getTips().isEmpty()) {
					setProvider(provider);
					break;
				}
			}
		}
		processTip(fProvider.getNextTip());
	}

	private void getCurrentTip() {
		processTip(fProvider.getCurrentTip());
	}

	private void processTip(Tip tip) {
		fCurrentTip = tip;
		hitTimer();
		enableActionButtons(tip);
		prepareForHTML();
		loadContent(tip);
	}

	private void loadContent(Tip tip) {
		disposeBrowserFunctions();
		if (tip instanceof ISwtTip) {
			loadContentSWT(tip);
		} else if (tip instanceof IHtmlTip) {
			loadContentHtml((IHtmlTip) tip);
			applyBrowserFunctions(tip);
		} else if (tip instanceof IUrlTip) {
			loadContentUrl((IUrlTip) tip);
			applyBrowserFunctions(tip);
		} else {
			fTipManager.log(LogUtil.error(getClass(), Messages.TipComposite_12 + tip));
		}

		fContentComposite.requestLayout();
	}

	private void applyBrowserFunctions(Tip tip) {
		if (tip instanceof IBrowserFunctionProvider) {
			((IBrowserFunctionProvider) tip).getBrowserFunctions()
					.forEach((name, function) -> fBrowserFunctions.add(createBrowserFunction(name, function)));
		}
	}

	private BrowserFunction createBrowserFunction(String functionName, Function<Object[], Object> function) {
		return new BrowserFunction(getBrowser(), functionName) {
			@Override
			public Object function(Object[] arguments) {
				return function.apply(arguments);
			}
		};
	}

	private void loadContentHtml(IHtmlTip tip) {
		fBrowser.setText(getHTML(tip).trim());
	}

	private void loadContentUrl(IUrlTip tip) {
		try {
			String url = FileLocator.resolve(new URL(tip.getURL())).toString();
			fBrowser.setUrl(url);
		} catch (IOException e) {
			fTipManager.log(LogUtil.error(getClass(), e));
		}
	}

	private void loadContentSWT(Tip tip) {
		for (Control control : fSWTComposite.getChildren()) {
			control.dispose();
		}
		fContentStack.topControl = fSWTComposite;
		((ISwtTip) tip).createControl(fSWTComposite);
		fSWTComposite.requestLayout();
	}

	private void prepareForHTML() {
		fContentStack.topControl = fBrowserComposite;
		loadTimeOutScript();

		fBrowserComposite.requestLayout();
	}

	private void disposeBrowserFunctions() {
		fBrowserFunctions.forEach(BrowserFunction::dispose);
		fBrowserFunctions.clear();
	}

	/**
	 * Sets content in the browser that displays a message after 1500ms if the Tip
	 * could not load fast enough.
	 */
	private void loadTimeOutScript() {
		fBrowser.setText(getLoadingScript(500));
		while (!isDisposed()) {
			if (!getDisplay().readAndDispatch()) {
				break;
			}
		}
	}

	/**
	 * Sets content in the browser that displays a message after 1500ms if tips
	 * could not be loaded.
	 */
	private void loadWaitingScript() {
		fBrowser.setText(getWaitingScript(1500));
		while (!isDisposed()) {
			if (!getDisplay().readAndDispatch()) {
				break;
			}
		}
	}

	/**
	 * Get the timeout script in case the tips are not loading.
	 *
	 * @param timeout the timeout in milliseconds
	 * @return the script
	 */
	private static String getWaitingScript(int timeout) {
		return "<style>div{height: 90vh;display: flex;justify-content: center;align-items: center;}</style>" //$NON-NLS-1$
				+ "<div id=\"txt\"></div>" //$NON-NLS-1$
				+ "<script>var wss=function(){document.getElementById(\"txt\").innerHTML=\"" //$NON-NLS-1$
				+ Messages.TipComposite_14 //
				+ "\"};window.setTimeout(wss," //$NON-NLS-1$
				+ timeout //
				+ ");</script>"; //$NON-NLS-1$
	}

	private void enableActionButtons(Tip tip) {
		disposeActionImages();
		if (tip.getActions().isEmpty()) {
			fActionStack.topControl = fEmptyActionComposite;
		} else if (tip.getActions().size() == 1) {
			TipAction action = tip.getActions().get(0);
			fActionStack.topControl = fSingleActionComposite;
			fSingleActionButton.setImage(getActionImage(action.getTipImage()));
			fSingleActionButton.setText(action.getText());
			fSingleActionButton.setToolTipText(action.getTooltip());
		} else {
			TipAction action = tip.getActions().get(0);
			fActionStack.topControl = fMultiActionComposite;
			fMultiActionButton.setImage(getActionImage(tip.getActions().get(0).getTipImage()));
			fMultiActionButton.setText(action.getText());
			fMultiActionButton.setToolTipText(action.getTooltip());
			loadActionMenu(tip);
		}
		fEmptyActionComposite.getParent().requestLayout();
		fNavigationBar.requestLayout();
	}

	private void disposeActionImages() {
		fActionImages.forEach(img -> img.dispose());
	}

	private void loadActionMenu(Tip pTip) {
		if (fActionMenu != null) {
			fActionMenu.dispose();
		}
		fActionMenu = new Menu(fContentComposite.getShell(), SWT.POP_UP);
		pTip.getActions().subList(1, pTip.getActions().size()).forEach(action -> {
			MenuItem item = new MenuItem(fActionMenu, SWT.PUSH);
			item.setText(action.getText());
			item.setToolTipText(action.getTooltip());
			item.setText(action.getText());
			item.setImage(getActionImage(action.getTipImage()));
			item.addListener(SWT.Selection, e -> runTipAction(action));
		});
	}

	private Image getActionImage(TipImage tipImage) {
		if (tipImage == null) {
			return null;
		}
		try {
			Image image = new Image(getDisplay(), ImageUtil.decodeToImage(tipImage.getBase64Image()));
			if (image != null) {
				fActionImages.add(image);
				return image;
			}
		} catch (IOException e) {
			fTipManager.log(LogUtil.error(getClass(), e));
		}
		return null;
	}

	/**
	 * Get the timeout script in case a tip takes time to load.
	 *
	 * @param timeout the timeout in milliseconds
	 * @return the script
	 */
	private static String getLoadingScript(int timeout) {
		return "<style>div{position:fixed;top:50%;left:40%}</style>" //$NON-NLS-1$
				+ "<div id=\"txt\"></div>" //$NON-NLS-1$
				+ "<script>var wss=function(){document.getElementById(\"txt\").innerHTML=\"" //$NON-NLS-1$
				+ Messages.TipComposite_0 //
				+ "\"};window.setTimeout(wss," //$NON-NLS-1$
				+ timeout //
				+ ");</script>"; //$NON-NLS-1$
	}

	private String getHTML(IHtmlTip tip) {
		String encodedImage = encodeImage(tip);
		return tip.getHTML() + encodedImage;
	}

	private String encodeImage(IHtmlTip tip) {
		TipImage image = tip.getImage();
		if (image == null) {
			return EMPTY;
		}
		return encodeImageFromBase64(image);
	}

	private String encodeImageFromBase64(TipImage image) {
		int width = fBrowser.getClientArea().width;
		int height = Math.min(fBrowser.getClientArea().height / 2, (2 * (width / 3)));
		String attributes = image.getIMGAttributes(width, height).trim();
		String encoded = EMPTY + "<center> <img " // //$NON-NLS-1$
				+ attributes //
				+ " src=\"" // //$NON-NLS-1$
				+ image.getBase64Image() //
				+ "\"></center><br/>"; //$NON-NLS-1$
		return encoded;
	}

	@Override
	protected void checkSubclass() {
	}

	/**
	 * @return the {@link Browser} widget
	 */
	public Browser getBrowser() {
		return fBrowser;
	}

	/**
	 * @return the {@link Slider} widget
	 */
	public Slider getSlider() {
		return fSlider;
	}

	@Override
	public void selected(TipProvider provider) {
		setProvider(provider);
	}

	/**
	 * Sets the {@link TipManager}
	 *
	 * @param tipManager the {@link TipManager} that opened the dialog.
	 */
	public void setTipManager(TipManager tipManager) {
		fTipManager = tipManager;

		getDisplay().syncExec(() -> {
			fSlider.setTipManager(fTipManager);
			fUnreadOnly.setSelection(!fTipManager.mustServeReadTips());
			fPreviousTipButton.setEnabled(fTipManager.mustServeReadTips());
		});
	}

	@Override
	public void dispose() {
		disposeActionImages();
		super.dispose();
	}
}