/*******************************************************************************
 * Copyright (c) 2015 Red Hat Inc.
 *
 * 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:
 *     Mickael Istria (Red Hat Inc.) - 469918 Zoom In/Out
 *******************************************************************************/
package org.eclipse.ui.texteditor;

import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.Map;
import java.util.Set;

import org.eclipse.swt.graphics.FontData;

import org.eclipse.core.commands.AbstractHandler;
import org.eclipse.core.commands.ExecutionEvent;
import org.eclipse.core.commands.ExecutionException;
import org.eclipse.core.expressions.IEvaluationContext;

import org.eclipse.core.runtime.IConfigurationElement;
import org.eclipse.core.runtime.Platform;
import org.eclipse.core.runtime.Status;

import org.eclipse.jface.resource.FontDescriptor;
import org.eclipse.jface.resource.FontRegistry;
import org.eclipse.jface.resource.JFaceResources;

import org.eclipse.ui.IEditorPart;
import org.eclipse.ui.ISources;
import org.eclipse.ui.handlers.HandlerUtil;
import org.eclipse.ui.part.AbstractMultiEditor;
import org.eclipse.ui.part.MultiPageEditorPart;

/**
 * Abstract handler to change the default font size on Text editors.
 */
abstract class AbstractTextZoomHandler extends AbstractHandler {

	private static Map<String, String> fgFontToDefault;

	private static Map<String, Set<String>> fgDefaultToFonts;

	private int fFontSizeOffset;

	/**
	 * Implementations of this class have to specify in the constructor how much the font would
	 * change when the handler is executed.
	 *
	 * @param fontSizeOffset how many points the default font will change. The offset can be
	 *            negative, to reduce font size (zoom out) or positive to increase font size (zoom
	 *            in)
	 */
	protected AbstractTextZoomHandler(int fontSizeOffset) {
		fFontSizeOffset= fontSizeOffset;
	}

	@Override
	public Object execute(ExecutionEvent event) throws ExecutionException {
		AbstractTextEditor textEditor= getActiveTextEditor(event);
		if (textEditor == null) {
			return null;
		}
		FontRegistry fontRegistry= textEditor.getSite().getWorkbenchWindow().getWorkbench().getThemeManager().getCurrentTheme().getFontRegistry();
		String fontProperty= textEditor.getSymbolicFontName();
		if (fontProperty == null) {
			fontProperty= JFaceResources.TEXT_FONT;
		}
		Set<String> fontsToSet= getAffectedFontNames(fontProperty, fontRegistry);
		FontData[] initialFontData= null;
		String currentFontName= fontProperty;
		while (currentFontName != null && (initialFontData= fontRegistry.getFontData(currentFontName)) == null) {
			currentFontName= fgFontToDefault.get(currentFontName);
		}

		FontData[] newFontData= createFontDescriptor(initialFontData).getFontData();
		if (newFontData != null) {
			fontsToSet.stream().forEach(fontName -> fontRegistry.put(fontName, newFontData));
		}
		return Status.OK_STATUS;
	}

	private FontDescriptor createFontDescriptor(FontData[] initialFontData) {
		int destFontSize= initialFontData[0].getHeight() + fFontSizeOffset;
		if (destFontSize <= 0) {
			return FontDescriptor.createFrom(initialFontData);
		}
		return FontDescriptor.createFrom(initialFontData).setHeight(destFontSize);
	}

	private AbstractTextEditor getActiveTextEditor(ExecutionEvent event) {
		return getActiveTextEditor(HandlerUtil.getActiveEditor(event));
	}

	private AbstractTextEditor getActiveTextEditor(IEditorPart part) {
		if (part instanceof AbstractTextEditor) {
			return (AbstractTextEditor)part;
		} else if ((part instanceof AbstractMultiEditor) && ((AbstractMultiEditor)part).getActiveEditor() instanceof AbstractTextEditor) {
			return (AbstractTextEditor)((AbstractMultiEditor)part).getActiveEditor();
		} else if ((part instanceof MultiPageEditorPart) && ((MultiPageEditorPart)part).getSelectedPage() instanceof AbstractTextEditor) {
			return (AbstractTextEditor)((MultiPageEditorPart)part).getSelectedPage();
		}
		return part != null ? part.getAdapter(AbstractTextEditor.class) : null;
	}

	@Override
	public void setEnabled(Object evaluationContext) {
		boolean enabled = false;
		if (evaluationContext instanceof IEvaluationContext) {
			Object activeEditor = ((IEvaluationContext) evaluationContext).getVariable(ISources.ACTIVE_EDITOR_NAME);
			if (activeEditor instanceof IEditorPart) {
				enabled = getActiveTextEditor((IEditorPart) activeEditor) != null;
			}
		}
		setBaseEnabled(enabled);
	}

	/**
	 *
	 * @param referenceFontName the font name on which change is initially requested
	 * @param fontRegistry the font registry
	 * @return the names of the fonts that should be modified together with the referenceFontName.
	 *         Those are parent fonts from which the reference font inherit, or children font that
	 *         are set to default or inherit from reference font or a common parent that is affected
	 *         too.
	 */
	private Set<String> getAffectedFontNames(String referenceFontName, FontRegistry fontRegistry) {
		synchronized (AbstractTextZoomHandler.class) {
			if (fgFontToDefault == null) {
				// TODO: This should rely on ThemeRegistry and IThemeElementDefinition,
				// but those aren't visible at that time. So we're recreating the font hierarchy
				fgFontToDefault= new HashMap<>();
				fgDefaultToFonts= new HashMap<>();
				IConfigurationElement[] themeElements= Platform.getExtensionRegistry().getConfigurationElementsFor("org.eclipse.ui.themes"); //$NON-NLS-1$
				for (int i= 0; i < themeElements.length; i++) {
					IConfigurationElement extension= themeElements[i];
					if ("fontDefinition".equals(extension.getName())) { //$NON-NLS-1$
						String fontId= extension.getAttribute("id"); //$NON-NLS-1$
						String defaultsTo= extension.getAttribute("defaultsTo"); //$NON-NLS-1$
						if (defaultsTo != null) {
							fgFontToDefault.put(fontId, defaultsTo);
							if (!fgDefaultToFonts.containsKey(defaultsTo)) {
								fgDefaultToFonts.put(defaultsTo, new HashSet<>());
							}
							fgDefaultToFonts.get(defaultsTo).add(fontId);
						}

					}
				}
			}
		}
		Set<String> res= new HashSet<>();
		FontData[] referenceFontData= fontRegistry.getFontData(referenceFontName);
		if (fontRegistry.hasValueFor(referenceFontName)) {
			res.add(referenceFontName);
		}
		String currentFontName= referenceFontName;
		String rootFontName= referenceFontName;
		// identify "root" font to change
		do {
			currentFontName= fgFontToDefault.get(currentFontName);
			if (currentFontName != null && Arrays.equals(referenceFontData, fontRegistry.getFontData(currentFontName))) {
				rootFontName= currentFontName;
			}
		} while (currentFontName != null);
		LinkedList<String> fontsToProcess= new LinkedList<>();
		fontsToProcess.add(rootFontName);
		// propage to "children" fonts
		Set<String> alreadyProcessed= new HashSet<>();
		while (!fontsToProcess.isEmpty()) {
			currentFontName= fontsToProcess.get(0);
			fontsToProcess.remove(0);
			// with recent Java, use currentFOntName = fontsToProcess.poll instead of the 2 lines above
			if (!alreadyProcessed.contains(currentFontName)) { // avoid infinite loop
				alreadyProcessed.add(currentFontName);
				FontData[] currentFontData= fontRegistry.getFontData(currentFontName);
				if (currentFontData == null || Arrays.equals(referenceFontData, currentFontData)) {
					if (fontRegistry.hasValueFor(currentFontName)) {
						res.add(currentFontName);
					}
					Set<String> children= fgDefaultToFonts.get(currentFontName);
					if (children != null) {
						fontsToProcess.addAll(children);
					}
				}
			}
		}
		return res;
	}

}
