/*******************************************************************************
 * Copyright (c) 2005, 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
 *     EclipseSource Corporation - ongoing enhancements
 *******************************************************************************/
package org.eclipse.pde.internal.ui.wizards.product;

import java.lang.reflect.InvocationTargetException;
import java.util.Locale;

import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Status;
import org.eclipse.osgi.util.NLS;
import org.eclipse.pde.core.IBaseModel;
import org.eclipse.pde.core.plugin.IPluginAttribute;
import org.eclipse.pde.core.plugin.IPluginBase;
import org.eclipse.pde.core.plugin.IPluginElement;
import org.eclipse.pde.core.plugin.IPluginExtension;
import org.eclipse.pde.core.plugin.IPluginModelBase;
import org.eclipse.pde.core.plugin.IPluginObject;
import org.eclipse.pde.core.plugin.PluginRegistry;
import org.eclipse.pde.internal.core.TargetPlatformHelper;
import org.eclipse.pde.internal.core.iproduct.IAboutInfo;
import org.eclipse.pde.internal.core.iproduct.ICSSInfo;
import org.eclipse.pde.internal.core.iproduct.IPreferencesInfo;
import org.eclipse.pde.internal.core.iproduct.IProduct;
import org.eclipse.pde.internal.core.iproduct.ISplashInfo;
import org.eclipse.pde.internal.core.iproduct.IWindowImages;
import org.eclipse.pde.internal.core.plugin.WorkspacePluginModelBase;
import org.eclipse.pde.internal.core.product.SplashInfo;
import org.eclipse.pde.internal.core.text.plugin.PluginElementNode;
import org.eclipse.pde.internal.ui.PDEPlugin;
import org.eclipse.pde.internal.ui.PDEUIMessages;
import org.eclipse.pde.internal.ui.util.ModelModification;
import org.eclipse.pde.internal.ui.util.PDEModelUtility;
import org.eclipse.pde.internal.ui.util.TemplateFileGenerator;
import org.eclipse.swt.widgets.Shell;
import org.eclipse.ui.branding.IProductConstants;

public class ProductDefinitionOperation extends BaseManifestOperation {

	private static final String APPLICATION_CSS = "applicationCSS"; //$NON-NLS-1$

	private final String fProductId;
	private final String fApplication;
	private final IProduct fProduct;

	protected IProject fProject;

	private UpdateSplashHandlerAction fUpdateSplashAction;

	private RemoveSplashHandlerBindingAction fRemoveSplashAction;

	private UpdateSplashProgressOperation fUpdateSplashProgressOperation;

	public ProductDefinitionOperation(IProduct product, String pluginId, String productId, String application, Shell shell) {
		super(shell, pluginId);
		fProductId = productId;
		fApplication = application;
		fProduct = product;
		fProject = null;
	}

	public ProductDefinitionOperation(IProduct product, String pluginId, String productId, String application, Shell shell, IProject project) {
		super(shell, pluginId);
		fProductId = productId;
		fApplication = application;
		fProduct = product;
		// Needed for splash handler updates (file copying)
		fProject = project;
	}

	protected String getFormattedPackageName(String id) {
		StringBuilder buffer = new StringBuilder();
		for (int i = 0; i < id.length(); i++) {
			char ch = id.charAt(i);
			if (buffer.length() == 0) {
				if (Character.isJavaIdentifierStart(ch))
					buffer.append(Character.toLowerCase(ch));
			} else {
				if (Character.isJavaIdentifierPart(ch) || ch == '.')
					buffer.append(ch);
			}
		}
		return buffer.toString().toLowerCase(Locale.ENGLISH);
	}

	protected String createTargetPackage() {
		// Package name addition to create a location for containing
		// any classes required by the splash handlers.
		String packageName = getFormattedPackageName(fPluginId);
		// Unqualifed
		if (packageName.length() == 0) {
			return ISplashHandlerConstants.F_UNQUALIFIED_EXTENSION_ID;
		}
		// Qualified
		return packageName + '.' + ISplashHandlerConstants.F_UNQUALIFIED_EXTENSION_ID;
	}

	/**
	 * @return fully-qualified class (with package)
	 */
	private String createAttributeValueClass() {
		String targetPackage = createTargetPackage();
		String targetClass = createTargetClass();
		// Ensure target class is defined
		if (targetClass == null) {
			return null;
		}

		return targetPackage + "." + //$NON-NLS-1$
				targetClass;
	}

	/**
	 * @return unqualified class
	 */
	private String createTargetClass() {
		// Get the splash handler type
		String splashHandlerType = getSplashHandlerType();
		// Ensure splash handler type was specfied
		if (splashHandlerType == null) {
			return null;
		}
		// Update the class name depending on the splash screen type
		for (int i = 0; i < ISplashHandlerConstants.F_SPLASH_SCREEN_TYPE_CHOICES.length; i++) {
			String choice = ISplashHandlerConstants.F_SPLASH_SCREEN_TYPE_CHOICES[i][0];
			if (splashHandlerType.equals(choice)) {
				return ISplashHandlerConstants.F_SPLASH_SCREEN_CLASSES[i];
			}
		}
		return null;
	}

	/**
	 * @return splash screen type qualified with package name
	 */
	private String createAttributeValueID() {
		// Create the ID based on the splash screen type
		return createTargetPackage() + "." + //$NON-NLS-1$
				getSplashHandlerType();
	}

	private UpdateSplashProgressOperation getUpdateSplashProgressOperation() {
		if (fUpdateSplashProgressOperation == null) {
			fUpdateSplashProgressOperation = new UpdateSplashProgressOperation();
		} else {
			fUpdateSplashProgressOperation.reset();
		}
		return fUpdateSplashProgressOperation;
	}

	private void updateSplashProgress(IPluginModelBase model, IProgressMonitor monitor) throws CoreException {
		// Sanity checks
		if (fProject == null) {
			return;
		} else if (model == null) {
			return;
		} else if (monitor == null) {
			return;
		}
		// Get the action
		UpdateSplashProgressOperation operation = getUpdateSplashProgressOperation();
		operation.setModel(model);
		operation.setShowProgress(isProgressDefined());
		operation.setProject(fProject);
		operation.setProductID(fProduct.getProductId());
		operation.setPluginID(fPluginId);
		// Execute the action
		operation.run(monitor);
	}

	private boolean isProgressDefined() {
		// Get the splash info from the model
		ISplashInfo info = fProduct.getProduct().getSplashInfo();
		// Ensure splash info was defined
		if (info == null) {
			return false;
		}
		// Ensure splash progress was defined
		return info.isDefinedGeometry();
	}

	private String getSplashHandlerType() {
		// Get the splash info from the model
		ISplashInfo info = fProduct.getProduct().getSplashInfo();
		// Ensure splash info was defined
		if (info == null) {
			return null;
		}
		// Ensure splash type was defined
		if (info.isDefinedSplashHandlerType() == false) {
			return null;
		}
		return info.getFieldSplashHandlerType();
	}

	private void updateSplashHandler(IPluginModelBase model, IProgressMonitor monitor) throws CoreException {
		// Copy the applicable splash handler artifacts and perform parameter
		// substitution (like in templates plug-in)
		// Artifacts may include code, images and extension point schemas
		updateSplashHandlerFiles(model, monitor);
		// Update the plug-in model with the applicable splash handler extension
		// and extension point related mark-up
		updateSplashHandlerModel(model, monitor);
	}

	private void updateSplashHandlerFiles(IPluginModelBase model, IProgressMonitor monitor) throws CoreException {
		// If the project is not defined, abort this operation
		if (fProject == null) {
			return;
		}
		// Get the splash handler type
		String splashHandlerType = getSplashHandlerType();
		// If the splash handler type was not defined, abort this operation
		if (splashHandlerType == null) {
			return;
		}
		// Create and configure the template file generator
		// Note: Plug-in ID must be passed in separately from model, because
		// the underlying model does not contain the ID (even when it is
		// a workspace model)
		TemplateFileGenerator generator = new TemplateFileGenerator(fProject, model, fPluginId, createTargetPackage(), createTargetClass(), splashHandlerType);
		// Generate the necessary files
		generator.generateFiles(monitor);
	}

	private void updateSplashHandlerModel(IPluginModelBase model, IProgressMonitor monitor) throws CoreException {
		// Get the splash handler type
		String splashHandlerType = getSplashHandlerType();
		// If the splash handler type is not defined, abort this operation
		if (splashHandlerType == null) {
			runRemoveSplashAction(model, monitor);
		} else {
			runUpdateSplashAction(model, monitor, splashHandlerType);
		}
	}

	private void runRemoveSplashAction(IPluginModelBase model, IProgressMonitor monitor) throws CoreException {
		// Create the remove splash handler action
		fRemoveSplashAction = new RemoveSplashHandlerBindingAction();
		// Configure the action
		fRemoveSplashAction.setFieldProductID(fProduct.getProductId());
		fRemoveSplashAction.setFieldTargetPackage(createTargetPackage());

		fRemoveSplashAction.setModel(model);
		fRemoveSplashAction.setMonitor(monitor);
		// Execute the action
		fRemoveSplashAction.run();
		// If an core exception was thrown and caught, release it
		fRemoveSplashAction.hasException();
	}

	private void runUpdateSplashAction(IPluginModelBase model, IProgressMonitor monitor, String splashHandlerType) throws CoreException {
		// Create the update splash handler action
		fUpdateSplashAction = new UpdateSplashHandlerAction();
		// Configure the action
		String id = createAttributeValueID();
		fUpdateSplashAction.setFieldID(id);
		fUpdateSplashAction.setFieldClass(createAttributeValueClass());
		fUpdateSplashAction.setFieldSplashID(id);
		fUpdateSplashAction.setFieldProductID(fProduct.getProductId());
		fUpdateSplashAction.setFieldTemplate(splashHandlerType);
		fUpdateSplashAction.setFieldPluginID(fPluginId);

		fUpdateSplashAction.setModel(model);
		fUpdateSplashAction.setMonitor(monitor);
		// Execute the action
		fUpdateSplashAction.run();
		// If an core exception was thrown and caught, release it
		fUpdateSplashAction.hasException();
	}

	@Override
	public void run(IProgressMonitor monitor) throws InvocationTargetException, InterruptedException {
		try {
			IFile file = getFile();
			if (!file.exists()) {
				createNewFile(file, monitor);
			} else {
				modifyExistingFile(file, monitor);
			}
			updateSingleton(monitor);
		} catch (CoreException e) {
			throw new InvocationTargetException(e);
		}
	}

	private void createNewFile(IFile file, IProgressMonitor monitor) throws CoreException {
		WorkspacePluginModelBase model = (WorkspacePluginModelBase) getModel(file);
		IPluginBase base = model.getPluginBase();
		base.setSchemaVersion(TargetPlatformHelper.getSchemaVersion());
		base.add(createExtension(model));
		// Update the splash handler.  Update plug-in model and copy files
		updateSplashHandler(model, monitor);
		// Update splash progress.  Update plug-in model and copy files
		updateSplashProgress(model, monitor);

		model.save();
	}

	private IPluginExtension createExtension(IPluginModelBase model) throws CoreException {
		IPluginExtension extension = model.getFactory().createExtension();
		extension.setPoint("org.eclipse.core.runtime.products"); //$NON-NLS-1$
		extension.setId(fProductId);
		extension.add(createExtensionContent(extension));
		return extension;
	}

	private IPluginElement createExtensionContent(IPluginExtension extension) throws CoreException {
		IPluginElement element = extension.getModel().getFactory().createElement(extension);
		element.setName("product"); //$NON-NLS-1$
		element.setAttribute("name", fProduct.getName()); //$NON-NLS-1$
		element.setAttribute("application", fApplication); //$NON-NLS-1$

		IPluginElement child = createElement(element, IProductConstants.WINDOW_IMAGES, getWindowImagesString());
		if (child != null)
			element.add(child);

		child = createElement(element, IProductConstants.ABOUT_TEXT, getAboutText());
		if (child != null)
			element.add(child);

		child = createElement(element, IProductConstants.ABOUT_IMAGE, getAboutImage());
		if (child != null)
			element.add(child);

		child = createElement(element, IProductConstants.STARTUP_FOREGROUND_COLOR, getForegroundColor());
		if (child != null)
			element.add(child);

		child = createElement(element, IProductConstants.STARTUP_PROGRESS_RECT, getProgressRect());
		if (child != null)
			element.add(child);

		child = createElement(element, IProductConstants.STARTUP_MESSAGE_RECT, getMessageRect());
		if (child != null)
			element.add(child);

		child = createElement(element, IProductConstants.PREFERENCE_CUSTOMIZATION, getPreferenceCustomization());
		if (child != null)
			element.add(child);

		child = createElement(element, APPLICATION_CSS, getApplicationCSS());
		if (child != null)
			element.add(child);

		return element;
	}

	private IPluginElement createElement(IPluginElement parent, String name, String value) throws CoreException {
		IPluginElement element = null;
		if (value != null && value.length() > 0) {
			element = parent.getModel().getFactory().createElement(parent);
			element.setName("property"); //$NON-NLS-1$
			element.setAttribute("name", name); //$NON-NLS-1$
			element.setAttribute("value", value); //$NON-NLS-1$
		}
		return element;
	}

	private String getAboutText() {
		IAboutInfo info = fProduct.getAboutInfo();
		if (info != null) {
			String text = info.getText();
			return text == null || text.length() == 0 ? null : text;
		}
		return null;
	}

	private String getAboutImage() {
		IAboutInfo info = fProduct.getAboutInfo();
		return info != null ? getURL(info.getImagePath()) : null;
	}

	private String getURL(String location) {
		if (location == null || location.trim().length() == 0)
			return null;
		IPath path = IPath.fromOSString(location);
		if (!path.isAbsolute())
			return location;
		String projectName = path.segment(0);
		IProject project = PDEPlugin.getWorkspace().getRoot().getProject(projectName);
		if (project.exists()) {
			IPluginModelBase model = PluginRegistry.findModel(project);
			if (model != null) {
				String id = model.getPluginBase().getId();
				if (fPluginId.equals(id))
					return path.removeFirstSegments(1).toString();
				return "platform:/plugin/" + id + "/" + path.removeFirstSegments(1); //$NON-NLS-1$ //$NON-NLS-2$
			}
		}
		return location;
	}

	private String getFullyQualifiedURL(String location) {
		if (location == null || location.trim().length() == 0)
			return null;
		IPath path = IPath.fromOSString(location);
		if (!path.isAbsolute())
			return location;
		String projectName = path.segment(0);
		IProject project = PDEPlugin.getWorkspace().getRoot().getProject(projectName);
		if (project.exists()) {
			IPluginModelBase model = PluginRegistry.findModel(project);
			if (model != null) {
				String id = model.getPluginBase().getId();
				return "platform:/plugin/" + id + "/" + path.removeFirstSegments(1); //$NON-NLS-1$ //$NON-NLS-2$
			}
		}
		return location;
	}

	private String getWindowImagesString() {
		IWindowImages images = fProduct.getWindowImages();
		StringBuilder buffer = new StringBuilder();
		if (images != null) {
			for (int i = 0; i < IWindowImages.TOTAL_IMAGES; i++) {
				String image = getURL(images.getImagePath(i));
				if (image != null) {
					if (buffer.length() > 0)
						buffer.append(","); //$NON-NLS-1$
					buffer.append(image);
				}

			}
		}
		return buffer.length() == 0 ? null : buffer.toString();
	}

	private String getForegroundColor() {
		ISplashInfo info = fProduct.getSplashInfo();
		return info != null ? info.getForegroundColor() : null;
	}

	private String getProgressRect() {
		ISplashInfo info = fProduct.getSplashInfo();
		return info != null ? SplashInfo.getGeometryString(info.getProgressGeometry()) : null;
	}

	private String getMessageRect() {
		ISplashInfo info = fProduct.getSplashInfo();
		return info != null ? SplashInfo.getGeometryString(info.getMessageGeometry()) : null;
	}

	private String getPreferenceCustomization() {
		IPreferencesInfo info = fProduct.getPreferencesInfo();
		if (info != null) {
			String text = info.getPreferenceCustomizationPath();
			return text == null || text.length() == 0 ? null : getFullyQualifiedURL(text);
		}
		return null;
	}

	private String getApplicationCSS() {
		ICSSInfo info = fProduct.getCSSInfo();
		if (info != null) {
			String text = info.getFilePath();
			return text == null || text.length() == 0 ? null : getFullyQualifiedURL(text);
		}
		return null;
	}

	private void modifyExistingFile(IFile file, IProgressMonitor monitor) throws CoreException {
		IStatus status = PDEPlugin.getWorkspace().validateEdit(new IFile[] {file}, getShell());
		if (!status.isOK())
			throw new CoreException(Status.error(NLS.bind(PDEUIMessages.ProductDefinitionOperation_readOnly, fPluginId)));

		ModelModification mod = new ModelModification(file) {
			@Override
			protected void modifyModel(IBaseModel model, IProgressMonitor monitor) throws CoreException {
				if (!(model instanceof IPluginModelBase))
					return;
				IPluginExtension extension = findProductExtension((IPluginModelBase) model);
				if (extension == null)
					insertNewExtension((IPluginModelBase) model);
				else
					modifyExistingExtension(extension);
				// Update the splash handler.  Update plug-in model and copy files
				updateSplashHandler((IPluginModelBase) model, monitor);
				// Update splash progress.  Update plug-in model and copy files
				updateSplashProgress((IPluginModelBase) model, monitor);
			}
		};
		PDEModelUtility.modifyModel(mod, monitor);
	}

	private IPluginExtension findProductExtension(IPluginModelBase model) {
		IPluginExtension[] extensions = model.getPluginBase().getExtensions();
		for (IPluginExtension extension : extensions) {
			String point = extension.getPoint();
			String id = extension.getId();
			if (fProductId.equals(id) && "org.eclipse.core.runtime.products".equals(point)) { //$NON-NLS-1$
				return extension;
			}
		}
		return null;
	}

	private void insertNewExtension(IPluginModelBase model) throws CoreException {
		IPluginExtension extension = createExtension(model);
		model.getPluginBase().add(extension);
	}

	private void modifyExistingExtension(IPluginExtension extension) throws CoreException {
		if (extension.getChildCount() == 0) {
			insertNewProductElement(extension);
			return;
		}

		PluginElementNode element = (PluginElementNode) extension.getChildren()[0];

		if (!"product".equals(element.getName())) { //$NON-NLS-1$
			insertNewProductElement(extension);
			return;
		}

		element.setAttribute("application", fApplication); //$NON-NLS-1$
		element.setAttribute("name", fProduct.getName()); //$NON-NLS-1$
		synchronizeChild(element, IProductConstants.APP_NAME, fProduct.getName());

		synchronizeChild(element, IProductConstants.ABOUT_IMAGE, getAboutImage());
		synchronizeChild(element, IProductConstants.ABOUT_TEXT, getAboutText());
		synchronizeChild(element, IProductConstants.WINDOW_IMAGES, getWindowImagesString());
		synchronizeChild(element, IProductConstants.STARTUP_FOREGROUND_COLOR, getForegroundColor());
		synchronizeChild(element, IProductConstants.STARTUP_MESSAGE_RECT, getMessageRect());
		synchronizeChild(element, IProductConstants.STARTUP_PROGRESS_RECT, getProgressRect());
		synchronizeChild(element, IProductConstants.PREFERENCE_CUSTOMIZATION, getPreferenceCustomization());
		if (fProduct.getCSSInfo() != null) {
			synchronizeChild(element, APPLICATION_CSS, getApplicationCSS());
		}

	}

	private void synchronizeChild(IPluginElement element, String propertyName, String value) throws CoreException {
		IPluginElement child = null;
		IPluginObject[] children = element.getChildren();
		for (IPluginObject childObject : children) {
			IPluginElement candidate = (IPluginElement) childObject;
			if (candidate.getName().equals("property")) { //$NON-NLS-1$
				IPluginAttribute attr = candidate.getAttribute("name"); //$NON-NLS-1$
				if (attr != null && attr.getValue().equals(propertyName)) {
					child = candidate;
					break;
				}
			}
		}
		if (child != null && value == null)
			element.remove(child);

		if (value == null)
			return;

		if (child == null) {
			child = element.getModel().getFactory().createElement(element);
			child.setName("property"); //$NON-NLS-1$
			element.add(child);
		}
		child.setAttribute("value", value); //$NON-NLS-1$
		child.setAttribute("name", propertyName); //$NON-NLS-1$
	}

	private void insertNewProductElement(IPluginExtension extension) throws CoreException {
		IPluginElement element = createExtensionContent(extension);
		extension.add(element);
	}

}
