/*******************************************************************************
 * Copyright (c) 2004, 2015 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.themes;

import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.ResourceBundle;
import org.eclipse.core.runtime.IConfigurationElement;
import org.eclipse.core.runtime.IExtensionRegistry;
import org.eclipse.core.runtime.Platform;
import org.eclipse.jface.resource.StringConverter;
import org.eclipse.ui.PlatformUI;
import org.eclipse.ui.internal.WorkbenchPlugin;
import org.eclipse.ui.internal.registry.IWorkbenchRegistryConstants;
import org.eclipse.ui.internal.registry.RegistryReader;
import org.eclipse.ui.themes.IColorFactory;

/**
 * Registry reader for themes.
 *
 * @since 3.0
 */
public class ThemeRegistryReader extends RegistryReader {

	/**
	 * The translation bundle in which to look up internationalized text.
	 */
	private static final ResourceBundle RESOURCE_BUNDLE = ResourceBundle.getBundle(ThemeRegistryReader.class.getName());

	private Collection<ThemeElementCategory> categoryDefinitions = new HashSet<>();

	private Collection<ColorDefinition> colorDefinitions = new HashSet<>();

	private Collection<FontDefinition> fontDefinitions = new HashSet<>();

	private ThemeDescriptor themeDescriptor = null;

	private ThemeRegistry themeRegistry;

	private Map<String, String> dataMap = new HashMap<>();

	/**
	 * ThemeRegistryReader constructor comment.
	 */
	public ThemeRegistryReader() {
		super();
	}

	/**
	 * Returns the category definitions.
	 *
	 * @return the category definitions
	 */
	public Collection<ThemeElementCategory> getCategoryDefinitions() {
		return categoryDefinitions;
	}

	/**
	 * Returns the color definitions.
	 *
	 * @return the color definitions
	 */
	public Collection<ColorDefinition> getColorDefinitions() {
		return colorDefinitions;
	}

	/**
	 * Returns the data map.
	 *
	 * @return the data map
	 */
	public Map<String, String> getData() {
		return dataMap;
	}

	/**
	 * Returns the font definitions.
	 *
	 * @return the font definitions
	 */
	public Collection<FontDefinition> getFontDefinitions() {
		return fontDefinitions;
	}

	/**
	 * Read a category.
	 *
	 * @param element the element to read
	 * @return the new category
	 */
	private ThemeElementCategory readCategory(IConfigurationElement element) {
		String name = element.getAttribute(IWorkbenchRegistryConstants.ATT_LABEL);

		String id = element.getAttribute(IWorkbenchRegistryConstants.ATT_ID);
		String parentId = element.getAttribute(IWorkbenchRegistryConstants.ATT_PARENT_ID);

		String description = null;

		IConfigurationElement[] descriptions = element.getChildren(IWorkbenchRegistryConstants.TAG_DESCRIPTION);

		if (descriptions.length > 0) {
			description = descriptions[0].getValue();
		}

		return new ThemeElementCategory(name, id, parentId, description, element.getContributor().getName(), element);
	}

	/**
	 * Read a color.
	 *
	 * @param element the element to read
	 * @return the new color
	 */
	private ColorDefinition readColor(IConfigurationElement element) {
		String name = element.getAttribute(IWorkbenchRegistryConstants.ATT_LABEL);

		String id = element.getAttribute(IWorkbenchRegistryConstants.ATT_ID);

		String defaultMapping = element.getAttribute(IWorkbenchRegistryConstants.ATT_DEFAULTS_TO);

		String value = getPlatformSpecificColorValue(element.getChildren(IWorkbenchRegistryConstants.TAG_COLORVALUE));

		if (value == null) {
			value = getColorValue(element);
		}

		if ((value == null && defaultMapping == null) || (value != null && defaultMapping != null)) {
			logError(element, RESOURCE_BUNDLE.getString("Colors.badDefault")); //$NON-NLS-1$
			return null;
		}

		String categoryId = element.getAttribute(IWorkbenchRegistryConstants.ATT_CATEGORY_ID);

		String description = null;
		boolean isEditable = true;
		String isEditableString = element.getAttribute(IWorkbenchRegistryConstants.ATT_IS_EDITABLE);
		if (isEditableString != null) {
			isEditable = Boolean.parseBoolean(isEditableString);
		}

		IConfigurationElement[] descriptions = element.getChildren(IWorkbenchRegistryConstants.TAG_DESCRIPTION);

		if (descriptions.length > 0) {
			description = descriptions[0].getValue();
		}

		return new ColorDefinition(name, id, defaultMapping, value, categoryId, isEditable, description,
				element.getDeclaringExtension().getContributor().getName());
	}

	/**
	 * Gets the color value, either via the value attribute or from a color factory.
	 *
	 * @param element the element to check
	 * @return the color string
	 */
	private String getColorValue(IConfigurationElement element) {
		if (element == null) {
			return null;
		}

		String value = element.getAttribute(IWorkbenchRegistryConstants.ATT_VALUE);
		if (value == null) {
			value = checkColorFactory(element);
		}
		return value;
	}

	/**
	 * Check for platform specific color values. This will return the "best match"
	 * for the current platform.
	 *
	 * @param elements the elements to check
	 * @return the platform specific color, if any
	 */
	private String getPlatformSpecificColorValue(IConfigurationElement[] elements) {
		return getColorValue(getBestPlatformMatch(elements));
	}

	/**
	 * Get the element that has os/ws attributes that best match the current
	 * platform.
	 *
	 * @param elements the elements to check
	 * @return the best match, if any
	 */
	private IConfigurationElement getBestPlatformMatch(IConfigurationElement[] elements) {
		IConfigurationElement match = null;

		String osname = Platform.getOS();
		String wsname = Platform.getWS();

		for (IConfigurationElement element : elements) {
			String elementOs = element.getAttribute(IWorkbenchRegistryConstants.ATT_OS);
			String elementWs = element.getAttribute(IWorkbenchRegistryConstants.ATT_WS);

			if (osname.equalsIgnoreCase(elementOs)) {
				if (wsname.equalsIgnoreCase(elementWs)) {
					// best possible match. Return
					return element;
				}
				match = element;
			} else if (wsname.equalsIgnoreCase(elementWs)) {
				match = element;
			}
		}

		return match;
	}

	@Override
	public boolean readElement(IConfigurationElement element) {
		String elementName = element.getName();
		if (themeDescriptor == null && elementName.equals(IWorkbenchRegistryConstants.TAG_COLORDEFINITION)) {
			ColorDefinition definition = readColor(element);
			if (definition != null) {
				if (!colorDefinitions.contains(definition)) {
					colorDefinitions.add(definition);
					themeRegistry.add(definition);
				}
			}
			return true;
		} else if (themeDescriptor != null && elementName.equals(IWorkbenchRegistryConstants.TAG_COLOROVERRIDE)) {
			ColorDefinition definition = readColor(element);
			if (definition != null) {
				themeDescriptor.add(definition);
			}
			return true;
		} else if (themeDescriptor == null && elementName.equals(IWorkbenchRegistryConstants.TAG_FONTDEFINITION)) {
			FontDefinition definition = readFont(element);
			if (definition != null) {
				if (!fontDefinitions.contains(definition)) {
					fontDefinitions.add(definition);
					themeRegistry.add(definition);
				}
			}
			return true;
		} else if (themeDescriptor != null && elementName.equals(IWorkbenchRegistryConstants.TAG_FONTOVERRIDE)) {
			FontDefinition definition = readFont(element);
			if (definition != null) {
				themeDescriptor.add(definition);
			}
			return true;
		} else if (themeDescriptor == null && elementName.equals(IWorkbenchRegistryConstants.TAG_CATEGORYDEFINITION)) {
			ThemeElementCategory definition = readCategory(element);
			if (definition != null) {
				if (!categoryDefinitions.contains(definition)) {
					categoryDefinitions.add(definition);
					themeRegistry.add(definition);
				}
			}
			return true;
		} else if (element.getName().equals(IWorkbenchRegistryConstants.TAG_THEME)) {
			if (themeDescriptor != null) {
				logError(element, RESOURCE_BUNDLE.getString("Themes.badNesting")); //$NON-NLS-1$
			} else {
				themeDescriptor = readTheme(element);
				if (themeDescriptor != null) {
					readElementChildren(element);
					themeDescriptor = null;
				}
				return true;
			}
		} else if (themeDescriptor != null && elementName.equals(IWorkbenchRegistryConstants.TAG_DESCRIPTION)) {
			themeDescriptor.setDescription(element.getValue());
			return true;
		} else if (elementName.equals(IWorkbenchRegistryConstants.TAG_DATA)) {
			String name = element.getAttribute(IWorkbenchRegistryConstants.ATT_NAME);
			String value = element.getAttribute(IWorkbenchRegistryConstants.ATT_VALUE);
			if (name == null || value == null) {
				logError(element, RESOURCE_BUNDLE.getString("Data.badData")); //$NON-NLS-1$
			} else if (themeDescriptor != null) {
				themeDescriptor.setData(name, value);
			} else {
				themeRegistry.setData(name, value);
				if (!dataMap.containsKey(name)) {
					dataMap.put(name, value);
				}
			}
			return true;
		}

		return false;
	}

	/**
	 * Read a font.
	 *
	 * @param element the element to read
	 * @return the new font
	 */
	private FontDefinition readFont(IConfigurationElement element) {
		String name = element.getAttribute(IWorkbenchRegistryConstants.ATT_LABEL);

		String id = element.getAttribute(IWorkbenchRegistryConstants.ATT_ID);

		String defaultMapping = element.getAttribute(IWorkbenchRegistryConstants.ATT_DEFAULTS_TO);

		String value = getPlatformSpecificFontValue(element.getChildren(IWorkbenchRegistryConstants.TAG_FONTVALUE));
		if (value == null) {
			value = element.getAttribute(IWorkbenchRegistryConstants.ATT_VALUE);
		}

		if (value != null && defaultMapping != null) {
			logError(element, RESOURCE_BUNDLE.getString("Fonts.badDefault")); //$NON-NLS-1$
			return null;
		}

		String categoryId = element.getAttribute(IWorkbenchRegistryConstants.ATT_CATEGORY_ID);

		boolean isEditable = true;
		String isEditableString = element.getAttribute(IWorkbenchRegistryConstants.ATT_IS_EDITABLE);
		if (isEditableString != null) {
			isEditable = Boolean.parseBoolean(isEditableString);
		}

		String description = null;

		IConfigurationElement[] descriptions = element.getChildren(IWorkbenchRegistryConstants.TAG_DESCRIPTION);

		if (descriptions.length > 0) {
			description = descriptions[0].getValue();
		}

		return new FontDefinition(name, id, defaultMapping, value, categoryId, isEditable, description);
	}

	/**
	 * Check for platform specific font values. This will return the "best match"
	 * for the current platform.
	 *
	 * @param elements the elements to check
	 * @return the platform specific font, if any
	 */
	private String getPlatformSpecificFontValue(IConfigurationElement[] elements) {
		return getFontValue(getBestPlatformMatch(elements));
	}

	/**
	 * Gets the font valu from the value attribute.
	 *
	 * @param element the element to check
	 * @return the font string
	 */
	private String getFontValue(IConfigurationElement element) {
		if (element == null) {
			return null;
		}

		return element.getAttribute(IWorkbenchRegistryConstants.ATT_VALUE);
	}

	/**
	 * Attempt to load the color value from the colorFactory attribute.
	 *
	 * @param element the element to load from
	 * @return the value, or null if it could not be obtained
	 */
	private String checkColorFactory(IConfigurationElement element) {
		String value = null;
		if (element.getAttribute(IWorkbenchRegistryConstants.ATT_COLORFACTORY) != null
				|| element.getChildren(IWorkbenchRegistryConstants.ATT_COLORFACTORY).length > 0) {
			try {
				IColorFactory factory = (IColorFactory) element
						.createExecutableExtension(IWorkbenchRegistryConstants.ATT_COLORFACTORY);
				value = StringConverter.asString(factory.createColor());
			} catch (Exception e) {
				WorkbenchPlugin.log(RESOURCE_BUNDLE.getString("Colors.badFactory"), //$NON-NLS-1$
						WorkbenchPlugin.getStatus(e));
			}
		}
		return value;
	}

	/**
	 * Read a theme.
	 *
	 * @param element the element to read
	 * @return the new theme
	 */
	protected ThemeDescriptor readTheme(IConfigurationElement element) {
		ThemeDescriptor desc = null;

		String id = element.getAttribute(ThemeDescriptor.ATT_ID);
		if (id == null) {
			return null;
		}
		// see if the theme has already been created in another extension
		desc = (ThemeDescriptor) themeRegistry.findTheme(id);
		// if not, create it
		if (desc == null) {
			desc = new ThemeDescriptor(id);
			themeRegistry.add(desc);
		}
		// set the name as applicable
		desc.extractName(element);

		return desc;
	}

	/**
	 * Read the theme extensions within a registry.
	 *
	 * @param in  the registry to read
	 * @param out the registry to write to
	 */
	public void readThemes(IExtensionRegistry in, ThemeRegistry out) {
		// this does not seem to really ever be throwing an the exception
		setRegistry(out);
		readRegistry(in, PlatformUI.PLUGIN_ID, IWorkbenchRegistryConstants.PL_THEMES);

		// support for old font definitions
		readRegistry(in, PlatformUI.PLUGIN_ID, IWorkbenchRegistryConstants.PL_FONT_DEFINITIONS);
	}

	/**
	 * Set the output registry.
	 *
	 * @param out the output registry
	 */
	public void setRegistry(ThemeRegistry out) {
		themeRegistry = out;
	}
}
