/*******************************************************************************
 * 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.beans.PropertyChangeListener;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;

import org.eclipse.swt.SWT;
import org.eclipse.swt.events.SelectionAdapter;
import org.eclipse.swt.events.SelectionEvent;
import org.eclipse.swt.graphics.GC;
import org.eclipse.swt.graphics.Image;
import org.eclipse.swt.graphics.ImageData;
import org.eclipse.swt.graphics.Point;
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.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.internal.util.ImageUtil;
import org.eclipse.tips.ui.internal.util.ResourceManager;
import org.eclipse.tips.ui.internal.util.SWTResourceManager;

@SuppressWarnings("restriction")
public class Slider extends Composite {

	private static final int RESIZE_RELOAD_DELAY = 100;
	private Composite fScroller;
	private TipProvider fSelectedProvider;
	private int fSpacing = 5;
	private int fSliderIndex = 0;
	private List<ProviderSelectionListener> fListeners = new ArrayList<>();
	private TipManager fTipManager;
	private Button fLeftButton;
	private Button fRightButton;
	private Composite fSelectedProviderButton;
	private HashMap<String, Image> fProviderImageCache = new HashMap<>();
	private int fIconSize = 48;
	private PropertyChangeListener fPropertyChangeListener;
	private int fLeftRightButtonWidth;
	private long fLastResizeEventTime;
	private boolean fResizeRequestPending;

	/**
	 * Constructor for the Slider widget.
	 *
	 * @param parent the parent
	 * @param style  the SWT style bits
	 */
	public Slider(Composite parent, int style) {
		super(parent, style);

		GridLayout layout = new GridLayout(3, false);
		layout.marginHeight = 0;
		setLayout(layout);

		fLeftButton = new Button(this, SWT.FLAT);
		fLeftRightButtonWidth = fIconSize / 2 + 8;
		setLeftRightButtonGridData(fLeftButton, fLeftRightButtonWidth);
		fLeftButton.setImage(getImage("icons/" + fIconSize + "/aleft.png")); //$NON-NLS-1$ //$NON-NLS-2$
		fLeftButton.addSelectionListener(new SelectionAdapter() {
			@Override
			public void widgetSelected(SelectionEvent e) {
				scrollLeft();
			}
		});

		fScroller = new Composite(this, SWT.DOUBLE_BUFFERED);
		GridData layoutData = new GridData(SWT.FILL, SWT.FILL, true, true, 1, 1);
		layoutData.heightHint = fIconSize + 4;
		fScroller.setLayoutData(layoutData);

		fRightButton = new Button(this, SWT.FLAT);
		setLeftRightButtonGridData(fRightButton, fLeftRightButtonWidth);
		fRightButton.setImage(getImage("icons/" + fIconSize + "/aright.png")); //$NON-NLS-1$ //$NON-NLS-2$
		fRightButton.addSelectionListener(new SelectionAdapter() {
			@Override
			public void widgetSelected(SelectionEvent e) {
				scrollRight();
			}
		});

		setupDisposeListener();
		setupProviderListener();
		setupResizeListener();
	}

	// Recalculation of slider is expensive. Make sure it is done only once when
	// resizing.
	private void setupResizeListener() {
		addListener(SWT.Resize, event -> {
			fLastResizeEventTime = System.currentTimeMillis();
			if (!fResizeRequestPending) {
				fResizeRequestPending = true;
				submitResizeExecution();
			}
		});
	}

	private void submitResizeExecution() {
		getDisplay().timerExec(RESIZE_RELOAD_DELAY / 2, () -> {
			long currentTime = System.currentTimeMillis();
			if (currentTime - fLastResizeEventTime > RESIZE_RELOAD_DELAY) {
				load();
				fResizeRequestPending = false;
			}
			if (fResizeRequestPending) {
				submitResizeExecution();
			}
		});
	}

	private void setLeftRightButtonGridData(Button pButton, int pWidth) {
		GridData gridData = new GridData(SWT.FILL, SWT.FILL, false, false, 1, 1);
		gridData.widthHint = pWidth;
		gridData.heightHint = fIconSize;
		pButton.setLayoutData(gridData);
	}

	private void setupDisposeListener() {
		addListener(SWT.Dispose, event -> {
			fTipManager.getChangeSupport().removePropertyChangeListener(fPropertyChangeListener);
			fProviderImageCache.values().forEach(img -> {
				if (!img.isDisposed()) {
					img.dispose();
				}
			});
		});
	}

	private void setupProviderListener() {
		fPropertyChangeListener = provider -> {
			if (!isDisposed()) {
				getDisplay().asyncExec(this::load);
			}
		};
	}

	private static Image getImage(String icon) {
		return ResourceManager.getPluginImage("org.eclipse.tips.ui", icon); //$NON-NLS-1$
	}

	/**
	 * Loads or reloads the provider list. If you want to update the read count of
	 * the current button or all buttons then check the {@link #updateButton()} and
	 * {@link #updateButtons()} methods.
	 *
	 */
	public void load() {
		if (isDisposed() || fScroller.isDisposed() || fTipManager == null) {
			return;
		}
		Arrays.stream(fScroller.getChildren()).filter(control -> !control.isDisposed())
				.forEach(Control::dispose);
		List<TipProvider> providers = fTipManager.getProviders();
		int spaceCount = Math.floorDiv(fScroller.getBounds().width, (fIconSize + fSpacing));
		int providerCount = providers.size();

		if (fSliderIndex > 0 && spaceCount >= providerCount) {
			fSliderIndex = 0;
		}

		if (spaceCount >= providerCount) {
			if (fRightButton.isEnabled()) {
				enableLeftRightButtons(false);
			}
		} else {
			if (!fRightButton.isEnabled()) {
				enableLeftRightButtons(true);
			}
		}

		int emptyPixelsLeft = Math.floorMod(fScroller.getBounds().width, (fIconSize + fSpacing));
		int newSpacing = fSpacing + (emptyPixelsLeft / (spaceCount + 1));
		for (int i = 0; i < Math.min(providerCount - fSliderIndex, spaceCount); i++) {
			TipProvider provider = providers.get(i + fSliderIndex);
			if (fSelectedProvider == null && !provider.getTips().isEmpty()) {
				fSelectedProvider = provider;
				notifyListeners(fSelectedProvider);
			}
			createProviderButton(providers.get(i + fSliderIndex), newSpacing, i);
		}
		setBackground(fScroller.getBackground());
	}

	private void enableLeftRightButtons(boolean enable) {
		fRightButton.setEnabled(enable);
		fRightButton.setVisible(enable);
		setLeftRightButtonGridData(fRightButton, enable ? fLeftRightButtonWidth : 0);
		fLeftButton.setEnabled(enable);
		fLeftButton.setVisible(enable);
		setLeftRightButtonGridData(fLeftButton, enable ? fLeftRightButtonWidth : 0);
		fRightButton.requestLayout();
		fLeftButton.requestLayout();
	}

	private Composite createProviderButton(TipProvider provider, int spacing, int index) {
		Composite button = new Composite(fScroller, SWT.DOUBLE_BUFFERED);
		button.setToolTipText(provider.getDescription());
		button.setBackground(fScroller.getBackground());
		button.setSize(fIconSize + 4, fIconSize + 4);
		button.setLocation((index * (fIconSize + spacing) + spacing - fSpacing), 2);
		button.addPaintListener(e -> {
			if (fSelectedProvider == provider) {
				fSelectedProviderButton = button;
			}
			paintButton(e.gc, button, provider);
		});
		button.addListener(SWT.MouseEnter, event -> button.redraw());
		button.addListener(SWT.MouseExit, event -> button.redraw());
		button.addListener(SWT.MouseUp, event -> {
			if (fSelectedProvider == provider) {
				return;
			}
			fSelectedProvider = provider;
			if (fSelectedProviderButton != null && !fSelectedProviderButton.isDisposed()) {
				fSelectedProviderButton.redraw();
			}
			fSelectedProviderButton = button;
			fSelectedProviderButton.redraw();
			notifyListeners(provider);
		});
		if (fSelectedProvider == provider) {
			fSelectedProviderButton = button;
		}
		return button;
	}

	/**
	 * Updates the read count of the currently selected button.
	 */
	public void updateButton() {
		if (fSelectedProviderButton != null && !fSelectedProviderButton.isDisposed()) {
			getDisplay().asyncExec(() -> fSelectedProviderButton.redraw());
		}
	}

	/**
	 * Calls redraw on all buttons to update the badge counter.
	 */
	public void updateButtons() {
		if (!isDisposed()) {
			getDisplay().asyncExec(() -> {
				if (!fScroller.isDisposed()) {
					for (Control control : fScroller.getChildren()) {
						if (control instanceof Composite && !control.isDisposed()) {
							control.redraw();
						}
					}
				}
			});
		}
	}

	/**
	 * Sets the {@link TipManager}.
	 *
	 * @param tipManager the {@link TipManager}
	 * @return this
	 */
	public Slider setTipManager(TipManager tipManager) {
		fTipManager = tipManager;
		fTipManager.getChangeSupport().addPropertyChangeListener(TipProvider.PROP_READY, fPropertyChangeListener);
		fIconSize = 48;
		load();
		return this;
	}

	private void notifyListeners(TipProvider provider) {
		fListeners.forEach(listener -> {
			try {
				listener.selected(provider);
			} catch (Exception e) {
				fTipManager.log(LogUtil.error(getClass(), e));
			}
		});
	}

	@Override
	public Point computeSize(int wHint, int hHint, boolean changed) {
		Point point = new Point(fIconSize * 3, fIconSize + fSpacing + fSpacing);
		return point;
	}

	protected void scrollRight() {
		if (fSliderIndex < fTipManager.getProviders().size() - 1) {
			fSliderIndex++;
			load();
		}
	}

	protected void scrollLeft() {
		if (fSliderIndex > 0) {
			fSliderIndex--;
			load();
		}
	}

	/**
	 * @return the current selected {@link TipProvider}.
	 */
	public TipProvider getTipProvider() {
		return fSelectedProvider;
	}

	/**
	 * Sets the passed provider as the selected provider in the slider.
	 *
	 * @param provider the new provider for the slider.
	 *
	 * @return this
	 */
	public Slider setTipProvider(TipProvider provider) {
		fSelectedProvider = provider;
		updateButtons();
		return this;
	}

	/**
	 * Adds the listener to the list of listeners to be called when an event with a
	 * {@link TipProvider} occurs.
	 *
	 * @param listener the {@link ProviderSelectionListener}
	 * @return this
	 */
	public Slider addTipProviderListener(ProviderSelectionListener listener) {
		fListeners.add(listener);
		return this;
	}

	/**
	 * Removes the listener from the list.
	 *
	 * @param listener the {@link ProviderSelectionListener}
	 * @return this
	 */
	public Slider removeTipProviderListener(ProviderSelectionListener listener) {
		fListeners.remove(listener);
		return this;
	}

	private void paintButton(GC gc, Composite providerButton, TipProvider provider) {
		gc.setAdvanced(true);
		if (!gc.getAdvanced()) {
			throw new RuntimeException(Messages.Slider_13);
		}
		if (provider.equals(fSelectedProvider)) {
			gc.setForeground(getDisplay().getSystemColor(SWT.COLOR_LIST_SELECTION_TEXT));
			gc.setBackground(getDisplay().getSystemColor(SWT.COLOR_LIST_SELECTION));
			gc.drawRectangle(0, 0, fIconSize + 3, fIconSize + 3);
		} else {
			gc.setForeground(fLeftButton.getForeground());
			gc.setBackground(fLeftButton.getBackground());
			boolean mouseIn = getDisplay().getCursorControl() == providerButton;
			if (mouseIn) {
				gc.drawRectangle(0, 0, fIconSize + 3, fIconSize + 3);
			} else {
				gc.setBackground(fScroller.getBackground());
			}
		}
		gc.fillRectangle(2, 2, fIconSize, fIconSize);
		Image overlay = getUnreadOverlay(providerButton, provider);
		gc.drawImage(overlay, 2, 2);
		if (overlay != getProviderImage(provider, selectProviderImage(provider))) {
			overlay.dispose();
		}
	}

	private TipImage selectProviderImage(TipProvider provider) {
		return provider.getImage();
	}

	private Image getProviderImage(TipProvider provider, TipImage image) {
		if (!fProviderImageCache.containsKey(provider.getID())) {
			try {
				fProviderImageCache.put(provider.getID(),
						new Image(getDisplay(), ImageUtil.decodeToImage(image.getBase64Image())));
			} catch (Exception e) {
				fTipManager.log(LogUtil.error(getClass(), e));
				return null;
			}
		}
		return fProviderImageCache.get(provider.getID());
	}

	private Image getUnreadOverlay(Composite providerButton, TipProvider provider) {
		if (provider.getTips().isEmpty()) {
			return getProviderImage(provider, selectProviderImage(provider));
		}

		int tipCount = provider.getTips().size();
		Point textExtent = getTipCountTextSize(providerButton, tipCount);

		Image image = null;
		int imageHeight = textExtent.y + 5;
		if (tipCount > 9) {
			image = new Image(getDisplay(), textExtent.x + 8, imageHeight);
		} else {
			image = new Image(getDisplay(), imageHeight, imageHeight);
		}
		ImageData data = image.getImageData();
		data.transparentPixel = data.getPixel(0, 0);
		image.dispose();
		image = new Image(getDisplay(), data);
		GC gc = new GC(image);
		gc.setAdvanced(true);
		if (fTipManager.mustServeReadTips()) {
			gc.setBackground(getDisplay().getSystemColor(SWT.COLOR_DARK_GREEN));
		} else {
			gc.setBackground(getDisplay().getSystemColor(SWT.COLOR_RED));
		}
		gc.setForeground(getDisplay().getSystemColor(SWT.COLOR_WHITE));
		gc.setFont(SWTResourceManager.getBoldFont(gc.getFont()));
		gc.setAlpha(200);
		gc.setTextAntialias(SWT.ON);
		if (tipCount > 9) {
			gc.fillOval(0, 0, textExtent.x + 8, imageHeight);
			gc.drawText(tipCount + "", 4, 2, true); //$NON-NLS-1$
		} else {
			gc.fillOval(0, 0, imageHeight, imageHeight);
			gc.drawText(tipCount + "", (imageHeight - textExtent.x + 1) / 2, 2, true); //$NON-NLS-1$
		}
		Image result = ResourceManager.decorateImage(getProviderImage(provider, selectProviderImage(provider)), image,
				SWTResourceManager.TOP_RIGHT);
		image.dispose();
		gc.dispose();
		return result;
	}

	private Point getTipCountTextSize(Composite providerButton, int tipCount) {
		GC gc2 = new GC(providerButton);
		gc2.setAdvanced(true);
		gc2.setFont(SWTResourceManager.getBoldFont(gc2.getFont()));
		Point textExtent = gc2.textExtent(tipCount + ""); //$NON-NLS-1$
		gc2.dispose();
		return textExtent;
	}
}