/*******************************************************************************
 * Copyright (c) 2000, 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.jdt.internal.debug.ui.jres;

import java.io.File;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.Iterator;
import java.util.List;

import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.ListenerList;
import org.eclipse.core.runtime.Status;
import org.eclipse.debug.internal.ui.SWTFactory;
import org.eclipse.jdt.debug.ui.IJavaDebugUIConstants;
import org.eclipse.jdt.internal.debug.ui.JDIDebugUIPlugin;
import org.eclipse.jdt.internal.debug.ui.actions.ControlAccessibleListener;
import org.eclipse.jdt.launching.IVMInstall;
import org.eclipse.jdt.launching.IVMInstallType;
import org.eclipse.jdt.launching.JavaRuntime;
import org.eclipse.jdt.launching.VMStandin;
import org.eclipse.jdt.launching.environments.IExecutionEnvironment;
import org.eclipse.jdt.launching.environments.IExecutionEnvironmentsManager;
import org.eclipse.jface.util.IPropertyChangeListener;
import org.eclipse.jface.util.PropertyChangeEvent;
import org.eclipse.osgi.util.NLS;
import org.eclipse.swt.SWT;
import org.eclipse.swt.events.SelectionAdapter;
import org.eclipse.swt.events.SelectionEvent;
import org.eclipse.swt.layout.GridData;
import org.eclipse.swt.widgets.Button;
import org.eclipse.swt.widgets.Combo;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Control;
import org.eclipse.swt.widgets.Event;
import org.eclipse.swt.widgets.Group;
import org.eclipse.swt.widgets.Listener;
import org.eclipse.swt.widgets.Shell;

/**
 * A composite that displays installed JREs in a combo box, with a 'manage...'
 * button to modify installed JREs.
 * <p>
 * This block implements ISelectionProvider - it sends selection change events
 * when the checked JRE in the table changes, or when the "use default" button
 * check state changes.
 * </p>
 */
public class JREsComboBlock {

	public static final String PROPERTY_JRE = "PROPERTY_JRE"; //$NON-NLS-1$

	/**
	 * This block's control
	 */
	private Composite fControl;

	/**
	 * VMs being displayed
	 */
	private List<Object> fVMs = new ArrayList<>();

	/**
	 * The main control
	 */
	private Combo fCombo;

	// Action buttons
	private Button fManageButton;

	/**
	 * JRE change listeners
	 */
	private ListenerList<IPropertyChangeListener> fListeners = new ListenerList<>();

	/**
	 * Whether the default JRE should be in first position (if <code>false</code>, it becomes last).
	 */
	private boolean fDefaultFirst;

	/**
	 * Default JRE descriptor or <code>null</code> if none.
	 */
	private JREDescriptor fDefaultDescriptor = null;

	/**
	 * Specific JRE descriptor or <code>null</code> if none.
	 */
	private JREDescriptor fSpecificDescriptor = null;

	/**
	 * Default JRE radio button or <code>null</code> if none
	 */
	private Button fDefaultButton = null;

	/**
	 * Selected JRE radio button
	 */
	private Button fSpecificButton = null;

	/**
	 * The title used for the JRE block
	 */
	private String fTitle = null;

	/**
	 * Selected JRE profile radio button
	 */
	private Button fEnvironmentsButton = null;

	/**
	 * Combo box of JRE profiles
	 */
	private Combo fEnvironmentsCombo = null;

	private Button fManageEnvironmentsButton = null;

	// a path to an unavailable JRE
	private IPath fErrorPath;

	/**
	 * List of execution environments
	 */
	private List<Object> fEnvironments = new ArrayList<>();

	private IStatus fStatus = OK_STATUS;

	private static IStatus OK_STATUS = new Status(IStatus.OK, JDIDebugUIPlugin.getUniqueIdentifier(), 0, "", null); //$NON-NLS-1$

	/**
	 * Creates a JREs combo block.
	 *
	 * @param defaultFirst whether the default JRE should be in first position (if <code>false</code>, it becomes last)
	 */
	public JREsComboBlock(boolean defaultFirst) {
		fDefaultFirst= defaultFirst;
	}

	public void addPropertyChangeListener(IPropertyChangeListener listener) {
		fListeners.add(listener);
	}

	public void removePropertyChangeListener(IPropertyChangeListener listener) {
		fListeners.remove(listener);
	}

	private void firePropertyChange() {
		PropertyChangeEvent event = new PropertyChangeEvent(this, PROPERTY_JRE, null, getPath());
		for (IPropertyChangeListener listener : fListeners) {
			listener.propertyChange(event);
		}
	}

	/**
	 * Creates this block's control in the given control.
	 *
	 * @param anscestor containing control
	 */
	public void createControl(Composite ancestor) {
		fControl = SWTFactory.createComposite(ancestor, 1, 1, GridData.FILL_BOTH);
		if (fTitle == null) {
			fTitle = JREMessages.JREsComboBlock_3;
		}
		Group group = SWTFactory.createGroup(fControl, fTitle, 1, 1, GridData.FILL_HORIZONTAL);
		Composite comp = SWTFactory.createComposite(group, group.getFont(), 3, 1, GridData.FILL_BOTH, 0, 0);

		if (fDefaultFirst) {
			createDefaultJREControls(comp);
		}
		createEEControls(comp);
		createAlternateJREControls(comp);
		if (!fDefaultFirst) {
			createDefaultJREControls(comp);
		}
	}

	private void createEEControls(Composite comp) {
		fEnvironmentsButton = SWTFactory.createRadioButton(comp, JREMessages.JREsComboBlock_4);
		fEnvironmentsButton.addSelectionListener(new SelectionAdapter() {
			@Override
			public void widgetSelected(SelectionEvent e) {
				if (fEnvironmentsButton.getSelection()) {
					fCombo.setEnabled(false);
					if (fEnvironmentsCombo.getText().length() == 0 && !fEnvironments.isEmpty()) {
						fEnvironmentsCombo.select(0);
					}
					fEnvironmentsCombo.setEnabled(true);
					if (fEnvironments.isEmpty()) {
						setError(JREMessages.JREsComboBlock_5);
					} else {
						setStatus(OK_STATUS);
					}
					firePropertyChange();
				}
			}
		});

		fEnvironmentsCombo = SWTFactory.createCombo(comp, SWT.DROP_DOWN | SWT.READ_ONLY, 1, null);
		fEnvironmentsCombo.addSelectionListener(new SelectionAdapter() {
			@Override
			public void widgetSelected(SelectionEvent e) {
				setPath(JavaRuntime.newJREContainerPath(getEnvironment()));
				firePropertyChange();
			}
		});

		fManageEnvironmentsButton = SWTFactory.createPushButton(comp, JREMessages.JREsComboBlock_14, null);
		fManageEnvironmentsButton.addListener(SWT.Selection, new Listener() {
			@Override
			public void handleEvent(Event event) {
				showPrefPage(ExecutionEnvironmentsPreferencePage.ID);
			}
		});

		fillWithWorkspaceProfiles();
	}

	private void createAlternateJREControls(Composite comp) {
		String text = JREMessages.JREsComboBlock_1;
		if (fSpecificDescriptor != null) {
			text = fSpecificDescriptor.getDescription();
		}
		fSpecificButton = SWTFactory.createRadioButton(comp, text, 1);
		fSpecificButton.addSelectionListener(new SelectionAdapter() {
			@Override
			public void widgetSelected(SelectionEvent e) {
				if (fSpecificButton.getSelection()) {
					fCombo.setEnabled(true);
					if (fCombo.getText().length() == 0 && !fVMs.isEmpty()) {
						fCombo.select(0);
					}
					if (fVMs.isEmpty()) {
						setError(JREMessages.JREsComboBlock_0);
					} else {
						setStatus(OK_STATUS);
					}
					fEnvironmentsCombo.setEnabled(false);
					firePropertyChange();
				}
			}
		});
		fCombo = SWTFactory.createCombo(comp, SWT.DROP_DOWN | SWT.READ_ONLY, 1, null);
		ControlAccessibleListener.addListener(fCombo, fSpecificButton.getText());
		fCombo.addSelectionListener(new SelectionAdapter() {
			@Override
			public void widgetSelected(SelectionEvent e) {
				setStatus(OK_STATUS);
				firePropertyChange();
			}
		});

		fManageButton = SWTFactory.createPushButton(comp, JREMessages.JREsComboBlock_2, null);
		fManageButton.addListener(SWT.Selection, new Listener() {
			@Override
			public void handleEvent(Event event) {
				showPrefPage(JREsPreferencePage.ID);
			}
		});
		fillWithWorkspaceJREs();
	}

	private void createDefaultJREControls(Composite comp) {
		if (fDefaultDescriptor != null) {
			fDefaultButton = SWTFactory.createRadioButton(comp, fDefaultDescriptor.getDescription(), 3);
			fDefaultButton.addSelectionListener(new SelectionAdapter() {
				@Override
				public void widgetSelected(SelectionEvent e) {
					if (fDefaultButton.getSelection()) {
						setUseDefaultJRE();
						setStatus(OK_STATUS);
						firePropertyChange();
					}
				}
			});
		}
	}

	/**
	 * Opens the given preference page, and updates when closed.
	 *
	 * @param id pref page id
	 * @param page pref page
	 */
	private void showPrefPage(String id/*, IPreferencePage page*/) {
		IVMInstall prevJRE = getJRE();
		IExecutionEnvironment prevEnv = getEnvironment();
		JDIDebugUIPlugin.showPreferencePage(id);
		fillWithWorkspaceJREs();
		fillWithWorkspaceProfiles();
		restoreCombo(fVMs, prevJRE, fCombo);
		restoreCombo(fEnvironments, prevEnv, fEnvironmentsCombo);
		// update text
		setDefaultJREDescriptor(fDefaultDescriptor);
		if (isDefaultJRE()) {
			// reset in case default has changed
			setUseDefaultJRE();
		}
		setPath(getPath());
		firePropertyChange();
	}

	private void restoreCombo(List<Object> elements, Object element, Combo combo) {
		int index = -1;
		if (element != null) {
			index = elements.indexOf(element);
		}
		if (index >= 0) {
			combo.select(index);
		} else {
			combo.select(0);
		}
	}

	/**
	 * Returns this block's control
	 *
	 * @return control
	 */
	public Control getControl() {
		return fControl;
	}

	/**
	 * Sets the JREs to be displayed in this block
	 *
	 * @param vms JREs to be displayed
	 */
	protected void setJREs(List<VMStandin> jres) {
		fVMs.clear();
		fVMs.addAll(jres);
		// sort by name
		Collections.sort(fVMs, new Comparator<Object>() {
			@Override
			public int compare(Object o1, Object o2) {
				IVMInstall left = (IVMInstall)o1;
				IVMInstall right = (IVMInstall)o2;
				return left.getName().compareToIgnoreCase(right.getName());
			}

			@Override
			public boolean equals(Object obj) {
				return obj == this;
			}
		});
		// now make an array of names
		String[] names = new String[fVMs.size()];
		Iterator<Object> iter = fVMs.iterator();
		int i = 0;
		while (iter.hasNext()) {
			IVMInstall vm = (IVMInstall)iter.next();
			names[i] = vm.getName();
			i++;
		}
		fCombo.setItems(names);
		fCombo.setVisibleItemCount(Math.min(names.length, 20));
	}

	protected Shell getShell() {
		return getControl().getShell();
	}

	/**
	 * Selects a specific JRE based on type/name.
	 *
	 * @param vm JRE or <code>null</code>
	 */
	private void selectJRE(IVMInstall vm) {
		fSpecificButton.setSelection(true);
		fDefaultButton.setSelection(false);
		fEnvironmentsButton.setSelection(false);
		fCombo.setEnabled(true);
		fEnvironmentsCombo.setEnabled(false);
		if (vm != null) {
			int index = fVMs.indexOf(vm);
			if (index >= 0) {
				fCombo.select(index);
			}
		}
		firePropertyChange();
	}

	/**
	 * Selects a JRE based environment.
	 *
	 * @param env environment or <code>null</code>
	 */
	private void selectEnvironment(IExecutionEnvironment env) {
		fSpecificButton.setSelection(false);
		fDefaultButton.setSelection(false);
		fCombo.setEnabled(false);
		fEnvironmentsButton.setSelection(true);
		fEnvironmentsCombo.setEnabled(true);
		if (env != null) {
			int index = fEnvironments.indexOf(env);
			if (index >= 0) {
				fEnvironmentsCombo.select(index);
			}
		}
		firePropertyChange();
	}

	/**
	 * Returns the selected JRE or <code>null</code> if none.
	 *
	 * @return the selected JRE or <code>null</code> if none
	 */
	public IVMInstall getJRE() {
		int index = fCombo.getSelectionIndex();
		if (index >= 0) {
			return (IVMInstall)fVMs.get(index);
		}
		return null;
	}

	/**
	 * Returns the selected Environment or <code>null</code> if none.
	 *
	 * @return the selected Environment or <code>null</code> if none
	 */
	private IExecutionEnvironment getEnvironment() {
		int index = fEnvironmentsCombo.getSelectionIndex();
		if (index >= 0) {
			return (IExecutionEnvironment)fEnvironments.get(index);
		}
		return null;
	}

	/**
	 * Populates the JRE table with existing JREs defined in the workspace.
	 */
	protected void fillWithWorkspaceJREs() {
		// fill with JREs
		List<VMStandin> standins = new ArrayList<>();
		IVMInstallType[] types = JavaRuntime.getVMInstallTypes();
		for (int i = 0; i < types.length; i++) {
			IVMInstallType type = types[i];
			IVMInstall[] installs = type.getVMInstalls();
			for (int j = 0; j < installs.length; j++) {
				IVMInstall install = installs[j];
				standins.add(new VMStandin(install));
			}
		}
		setJREs(standins);
	}

	/**
	 * Populates the JRE profile combo with profiles defined in the workspace.
	 */
	protected void fillWithWorkspaceProfiles() {
		fEnvironments.clear();
		IExecutionEnvironment[] environments = JavaRuntime.getExecutionEnvironmentsManager().getExecutionEnvironments();
		for (int i = 0; i < environments.length; i++) {
			fEnvironments.add(environments[i]);
		}

		String[] names = new String[fEnvironments.size()];
		Iterator<Object> iter = fEnvironments.iterator();
		int i = 0;
		while (iter.hasNext()) {
			IExecutionEnvironment env = (IExecutionEnvironment)iter.next();
			IPath path = JavaRuntime.newJREContainerPath(env);
			IVMInstall install = JavaRuntime.getVMInstall(path);
			if (install != null) {
				names[i] = NLS.bind(JREMessages.JREsComboBlock_15, new String[]{env.getId(), install.getName()});
			} else {
				names[i] = NLS.bind(JREMessages.JREsComboBlock_16, new String[]{env.getId()});
			}
			i++;
		}
		fEnvironmentsCombo.setItems(names);
		fEnvironmentsCombo.setVisibleItemCount(Math.min(names.length, 20));
	}

	/**
	 * Sets the Default JRE Descriptor for this block.
	 *
	 * @param descriptor default JRE descriptor
	 */
	public void setDefaultJREDescriptor(JREDescriptor descriptor) {
		fDefaultDescriptor = descriptor;
		setButtonTextFromDescriptor(fDefaultButton, descriptor);
	}

	private void setButtonTextFromDescriptor(Button button, JREDescriptor descriptor) {
		if (button != null) {
			//update the description & JRE in case it has changed
			String currentText = button.getText();
			String newText = descriptor.getDescription();
			if (!newText.equals(currentText)) {
				button.setText(newText);
				fControl.layout();
			}
		}
	}

	/**
	 * Sets the specific JRE Descriptor for this block.
	 *
	 * @param descriptor specific JRE descriptor
	 */
	public void setSpecificJREDescriptor(JREDescriptor descriptor) {
		fSpecificDescriptor = descriptor;
		setButtonTextFromDescriptor(fSpecificButton, descriptor);
	}

	/**
	 * Returns whether the 'use default JRE' button is checked.
	 *
	 * @return whether the 'use default JRE' button is checked
	 */
	public boolean isDefaultJRE() {
		if (fDefaultButton != null) {
			return fDefaultButton.getSelection();
		}
		return false;
	}

	/**
	 * Sets this control to use the 'default' JRE.
	 */
	private void setUseDefaultJRE() {
		if (fDefaultDescriptor != null) {
			fDefaultButton.setSelection(true);
			fSpecificButton.setSelection(false);
			fEnvironmentsButton.setSelection(false);
			fCombo.setEnabled(false);
			fEnvironmentsCombo.setEnabled(false);
			firePropertyChange();
		}
	}

	/**
	 * Sets the title used for this JRE block
	 *
	 * @param title title for this JRE block
	 */
	public void setTitle(String title) {
		fTitle = title;
	}

	/**
	 * Refresh the default JRE description.
	 */
	public void refresh() {
		setDefaultJREDescriptor(fDefaultDescriptor);
	}

	/**
	 * Returns a classpath container path identifying the selected JRE.
	 *
	 * @return classpath container path or <code>null</code>
	 * @since 3.2
	 */
	public IPath getPath() {
		if (!getStatus().isOK() && fErrorPath != null) {
			return fErrorPath;
		}
		if (fEnvironmentsButton.getSelection()) {
			int index = fEnvironmentsCombo.getSelectionIndex();
			if (index >= 0) {
				IExecutionEnvironment env = (IExecutionEnvironment) fEnvironments.get(index);
				return JavaRuntime.newJREContainerPath(env);
			}
			return null;
		}
		if (fSpecificButton.getSelection()) {
			int index = fCombo.getSelectionIndex();
			if (index >= 0) {
				IVMInstall vm = (IVMInstall) fVMs.get(index);
				return JavaRuntime.newJREContainerPath(vm);
			}
			return null;
		}
		return JavaRuntime.newDefaultJREContainerPath();
	}

	/**
	 * Sets the selection based on the given container path and returns
	 * a status indicating if the selection was successful.
	 *
	 * @param containerPath
	 * @return status
	 */
	public void setPath(IPath containerPath) {
		fErrorPath = null;
		setStatus(OK_STATUS);
		if (JavaRuntime.newDefaultJREContainerPath().equals(containerPath)) {
			setUseDefaultJRE();
		} else {
			String envId = JavaRuntime.getExecutionEnvironmentId(containerPath);
			if (envId != null) {
				IExecutionEnvironmentsManager manager = JavaRuntime.getExecutionEnvironmentsManager();
				IExecutionEnvironment environment = manager.getEnvironment(envId);
				if (environment == null) {
					fErrorPath = containerPath;
					selectEnvironment(environment);
					setError(NLS.bind(JREMessages.JREsComboBlock_6, new String[]{envId}));
				} else {
					selectEnvironment(environment);
					IVMInstall[] installs = environment.getCompatibleVMs();
					if (installs.length == 0) {
						setError(NLS.bind(JREMessages.JREsComboBlock_7, new String[]{environment.getId()}));
					}
				}
			} else {
				IVMInstall install = JavaRuntime.getVMInstall(containerPath);
				if (install == null) {
					selectJRE(install);
					fErrorPath = containerPath;
					String installTypeId = JavaRuntime.getVMInstallTypeId(containerPath);
					if (installTypeId == null) {
						setError(JREMessages.JREsComboBlock_8);
					} else {
						IVMInstallType installType = JavaRuntime.getVMInstallType(installTypeId);
						if (installType == null) {
							setError(NLS.bind(JREMessages.JREsComboBlock_9, new String[]{installTypeId}));
						} else {
							String installName = JavaRuntime.getVMInstallName(containerPath);
							if (installName == null) {
								setError(NLS.bind(JREMessages.JREsComboBlock_10, new String[]{installType.getName()}));
							} else {
								setError(NLS.bind(JREMessages.JREsComboBlock_11, new String[]{installName, installType.getName()}));
							}
						}
					}
				} else {
					selectJRE(install);
					File location = install.getInstallLocation();
					if (location == null) {
						setError(JREMessages.JREsComboBlock_12);
					} else if (!location.exists()) {
						setError(JREMessages.JREsComboBlock_13);
					}
				}
			}
		}
	}

	private void setError(String message) {
		setStatus(new Status(IStatus.ERROR, JDIDebugUIPlugin.getUniqueIdentifier(),
				IJavaDebugUIConstants.INTERNAL_ERROR, message, null));
	}

	/**
	 * Returns the status of the JRE selection.
	 *
	 * @return status
	 */
	public IStatus getStatus() {
		return fStatus;
	}

	private void setStatus(IStatus status) {
		fStatus = status;
	}
}
