/*******************************************************************************
 * Copyright (c) 2005, 2007 BEA Systems, Inc. and others.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 *
 * Contributors:
 *     BEA Systems Inc. - initial API and implementation
 *******************************************************************************/

package org.eclipse.jdt.apt.ui.internal.preferences;

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

import org.eclipse.core.resources.IFolder;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.ProjectScope;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.preferences.IEclipsePreferences;
import org.eclipse.core.runtime.preferences.IScopeContext;
import org.eclipse.core.runtime.preferences.InstanceScope;
import org.eclipse.jdt.apt.core.internal.AptPlugin;
import org.eclipse.jdt.apt.core.util.AptConfig;
import org.eclipse.jdt.apt.core.util.AptPreferenceConstants;
import org.eclipse.jdt.core.IJavaProject;
import org.eclipse.jdt.core.JavaCore;
import org.eclipse.jdt.internal.ui.dialogs.StatusInfo;
import org.eclipse.jdt.internal.ui.util.PixelConverter;
import org.eclipse.jdt.internal.ui.wizards.IStatusChangeListener;
import org.eclipse.jdt.internal.ui.wizards.dialogfields.DialogField;
import org.eclipse.jdt.internal.ui.wizards.dialogfields.IDialogFieldListener;
import org.eclipse.jdt.internal.ui.wizards.dialogfields.IListAdapter;
import org.eclipse.jdt.internal.ui.wizards.dialogfields.LayoutUtil;
import org.eclipse.jdt.internal.ui.wizards.dialogfields.ListDialogField;
import org.eclipse.jdt.internal.ui.wizards.dialogfields.SelectionButtonDialogField;
import org.eclipse.jdt.internal.ui.wizards.dialogfields.StringDialogField;
import org.eclipse.jface.dialogs.Dialog;
import org.eclipse.jface.viewers.ITableLabelProvider;
import org.eclipse.jface.viewers.LabelProvider;
import org.eclipse.jface.viewers.Viewer;
import org.eclipse.jface.viewers.ViewerComparator;
import org.eclipse.jface.window.Window;
import org.eclipse.swt.SWT;
import org.eclipse.swt.graphics.Image;
import org.eclipse.swt.layout.GridData;
import org.eclipse.swt.layout.GridLayout;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Control;
import org.eclipse.swt.widgets.Label;
import org.eclipse.ui.preferences.IWorkbenchPreferenceContainer;
import org.osgi.service.prefs.BackingStoreException;

/**
 * Preference pane for most APT (Java annotation processing) settings.
 * see org.eclipse.jdt.ui.internal.preferences.TodoTaskConfigurationBlock
 * for the conceptual source of some of this code.
 * <p>
 * 
 */
public class AptConfigurationBlock extends BaseConfigurationBlock {
		
	private static final Key KEY_APTENABLED= getKey(AptPlugin.PLUGIN_ID, AptPreferenceConstants.APT_ENABLED);
	private static final Key KEY_RECONCILEENABLED= getKey(AptPlugin.PLUGIN_ID, AptPreferenceConstants.APT_RECONCILEENABLED);
	private static final Key KEY_GENSRCDIR= getKey(AptPlugin.PLUGIN_ID, AptPreferenceConstants.APT_GENSRCDIR);
	
	private static Key[] getAllKeys() {
		return new Key[] {
				KEY_APTENABLED, KEY_RECONCILEENABLED, KEY_GENSRCDIR
		};	
	}
	
	private static final int IDX_ADD= 0;
	private static final int IDX_EDIT= 1;
	private static final int IDX_REMOVE= 2;
	
	private final IJavaProject fJProj;

	private SelectionButtonDialogField fAptEnabledField;
	private SelectionButtonDialogField fReconcileEnabledField;
	private StringDialogField fGenSrcDirField;
	private ListDialogField fProcessorOptionsField;
	
	private PixelConverter fPixelConverter;
	private Composite fBlockControl;
	
	private Map<String, String> fOriginalProcOptions; // cache of saved values
	private String fOriginalGenSrcDir;
	private boolean fOriginalAptEnabled;
	private boolean fOriginalReconcileEnabled;
	
	// used to distinguish actual changes from re-setting of same value - see useProjectSpecificSettings()
	private boolean fPerProjSettingsEnabled; 
	
	/**
	 * Event handler for Processor Options list control.
	 */
	private class ProcessorOptionsAdapter implements IListAdapter, IDialogFieldListener {
		
		public void customButtonPressed(ListDialogField field, int index) {
			switch (index) {
			case IDX_ADD:
				editOrAddProcessorOption(null);
				break;
			case IDX_EDIT:
				tryToEdit(field);
				break;
			}
		}

		@SuppressWarnings("unchecked")
		public void selectionChanged(ListDialogField field) {
			List selectedElements= field.getSelectedElements();
			field.enableButton(IDX_EDIT, canEdit(field, selectedElements));
		}
			
		public void doubleClicked(ListDialogField field) {
			tryToEdit(field);
		}

		public void dialogFieldChanged(DialogField field) {
			updateModel(field);
		}			

		@SuppressWarnings("unchecked")
		private boolean canEdit(DialogField field, List selectedElements) {
			if (!field.isEnabled())
				return false;
			return selectedElements.size() == 1;
		}
		
		private void tryToEdit(ListDialogField field) {
			List<ProcessorOption> selection= getListSelection();
			if (canEdit(field, selection)) {
				editOrAddProcessorOption(selection.get(0));
			}
		}
	}
	
	/**
	 * An entry in the Processor Options list control.
	 */
	public static class ProcessorOption {
		public String key;
		public String value;
	}

	/**
	 * Sorts items in the Processor Options list control.
	 */
	private static class ProcessorOptionSorter extends ViewerComparator {
		@SuppressWarnings("unchecked") // getComparator() returns a raw Comparator rather than a Comparator<T>
		public int compare(Viewer viewer, Object e1, Object e2) {
			return getComparator().compare(((ProcessorOption) e1).key, ((ProcessorOption) e2).key);
		}
	}
	
	/**
	 * Controls display of items in the Processor Options list control.
	 */
	private class ProcessorOptionsLabelProvider extends LabelProvider implements ITableLabelProvider {
		
		/* (non-Javadoc)
		 * @see org.eclipse.jface.viewers.ITableLabelProvider#getColumnImage(java.lang.Object, int)
		 */
		public Image getColumnImage(Object element, int columnIndex) {
			return null;
		}
		
		/* (non-Javadoc)
		 * @see org.eclipse.jface.viewers.ITableLabelProvider#getColumnText(java.lang.Object, int)
		 */
		public String getColumnText(Object element, int columnIndex) {
			ProcessorOption o = (ProcessorOption) element;
			if (columnIndex == 0) {
				return o.key;
			}
			else if (columnIndex == 1) {
				return o.value;
			}
			else {
				return ""; //$NON-NLS-1$
			}
		}
	}

	public AptConfigurationBlock(IStatusChangeListener context, IProject project, IWorkbenchPreferenceContainer container) {
		super(context, project, getAllKeys(), container);
		
		fJProj = JavaCore.create(project);
		
		UpdateAdapter adapter= new UpdateAdapter();
		
		if (fJProj != null) {
			fAptEnabledField= new SelectionButtonDialogField(SWT.CHECK);
			fAptEnabledField.setDialogFieldListener(adapter);
			fAptEnabledField.setLabelText(Messages.AptConfigurationBlock_enable);
		}
		else {
			fAptEnabledField = null;
		}
		
		fReconcileEnabledField= new SelectionButtonDialogField(SWT.CHECK);
		fReconcileEnabledField.setDialogFieldListener(adapter);
		fReconcileEnabledField.setLabelText(Messages.AptConfigurationBlock_enableReconcileProcessing);
		
		fGenSrcDirField = new StringDialogField();
		fGenSrcDirField.setDialogFieldListener(adapter);
		fGenSrcDirField.setLabelText(Messages.AptConfigurationBlock_generatedSrcDir);
		
		String[] buttons= new String[] {
			Messages.AptConfigurationBlock_add,
			Messages.AptConfigurationBlock_edit,
			Messages.AptConfigurationBlock_remove
		};
		ProcessorOptionsAdapter optionsAdapter = new ProcessorOptionsAdapter();
		fProcessorOptionsField = new ListDialogField(optionsAdapter, buttons, new ProcessorOptionsLabelProvider());
		fProcessorOptionsField.setDialogFieldListener(optionsAdapter);
		fProcessorOptionsField.setRemoveButtonIndex(IDX_REMOVE);
		String[] columnHeaders= new String[] {
			Messages.AptConfigurationBlock_key,
			Messages.AptConfigurationBlock_value
		};
		fProcessorOptionsField.setTableColumns(new ListDialogField.ColumnsDescription(columnHeaders, true));
		fProcessorOptionsField.setViewerComparator(new ProcessorOptionSorter());
		fProcessorOptionsField.setLabelText(Messages.AptConfigurationBlock_options);
		
		updateControls();
		
		if (fProcessorOptionsField.getSize() > 0) {
			fProcessorOptionsField.selectFirstElement();
		} else {
			fProcessorOptionsField.enableButton(IDX_EDIT, false);
		}
		
	}
	
	/* 
	 * At workspace level, don't ask for a rebuild.
	 */
	@Override
	protected String[] getFullBuildDialogStrings(boolean workspaceSettings) {
		if (workspaceSettings)
			return null;
		// if the only thing that changed was the reconcile setting, return null: a rebuild is not necessary
		if (fOriginalGenSrcDir.equals(fGenSrcDirField.getText())) {
			if (fOriginalAptEnabled == fAptEnabledField.isSelected()) {
				if (!procOptionsChanged()) {
					return null;
				}
			}
		}
		return super.getFullBuildDialogStrings(workspaceSettings);
	}

	/*
	 * Helper to eliminate unchecked-conversion warning
	 */
	@SuppressWarnings("unchecked")
	private List<ProcessorOption> getListElements() {
		return fProcessorOptionsField.getElements();
	}
	
	/*
	 * Helper to eliminate unchecked-conversion warning
	 */
	@SuppressWarnings("unchecked")
	private List<ProcessorOption> getListSelection() {
		return fProcessorOptionsField.getSelectedElements();
	}
	
	private void editOrAddProcessorOption(ProcessorOption original) {
		ProcessorOptionInputDialog dialog= new ProcessorOptionInputDialog(getShell(), original, getListElements());
		if (dialog.open() == Window.OK) {
			if (original != null) {
				fProcessorOptionsField.replaceElement(original, dialog.getResult());
			} else {
				fProcessorOptionsField.addElement(dialog.getResult());
			}
		}
	}
	
	@Override
	protected Control createContents(Composite parent) {
		setShell(parent.getShell());
		
		fPixelConverter= new PixelConverter(parent);
		int indent= fPixelConverter.convertWidthInCharsToPixels(4);
		
		fBlockControl = new Composite(parent, SWT.NONE);
		fBlockControl.setFont(parent.getFont());
		
		GridLayout layout= new GridLayout();
		layout.numColumns= 2;
		layout.marginWidth= 0;
		layout.marginHeight= 0;
		
		fBlockControl.setLayout(layout);
		
		DialogField[] fields = fAptEnabledField != null ? 
				new DialogField[] {
					fAptEnabledField,
					fReconcileEnabledField,
					fGenSrcDirField,
					fProcessorOptionsField,
				} :
				new DialogField[] {
					fReconcileEnabledField,
					fGenSrcDirField,
					fProcessorOptionsField,
				};
		LayoutUtil.doDefaultLayout(fBlockControl, fields, true, SWT.DEFAULT, SWT.DEFAULT);
		LayoutUtil.setHorizontalGrabbing(fProcessorOptionsField.getListControl(null));
		
		GridData reconcileGD= (GridData)fReconcileEnabledField.getSelectionButton(parent).getLayoutData();
		reconcileGD.horizontalIndent = indent;
		fReconcileEnabledField.getSelectionButton(parent).setLayoutData(reconcileGD);
		
		Label description= new Label(fBlockControl, SWT.WRAP);
		description.setText(Messages.AptConfigurationBlock_classpathAddedAutomaticallyNote); 
		GridData gdLabel= new GridData(GridData.HORIZONTAL_ALIGN_FILL);
		gdLabel.horizontalSpan= 2;
		gdLabel.widthHint= fPixelConverter.convertWidthInCharsToPixels(60);
		description.setLayoutData(gdLabel);
		
		Dialog.applyDialogFont(fBlockControl);
		
		validateSettings(null, null, null);
		
		return fBlockControl;
	}
	
	@Override
	protected void cacheOriginalValues() {
		super.cacheOriginalValues();
		fOriginalProcOptions= AptConfig.getRawProcessorOptions(fJProj);
		fOriginalGenSrcDir = AptConfig.getGenSrcDir(fJProj);
		fOriginalAptEnabled = AptConfig.isEnabled(fJProj);
		fOriginalReconcileEnabled = AptConfig.shouldProcessDuringReconcile(fJProj);
		fPerProjSettingsEnabled = hasProjectSpecificOptionsNoCache(fProject);
	}

	protected void initContents() {
		loadProcessorOptions(fJProj);
	}

	@Override
	protected void saveSettings() {
		List<ProcessorOption> elements;
		boolean isProjSpecificDisabled = (fJProj != null) && !fBlockControl.isEnabled();
		if (isProjSpecificDisabled) {
			// We're in a project properties pane but the entire configuration 
			// block control is disabled.  That means the per-project settings checkbox 
			// is unchecked.  To save that state, we'll clear the proc options map.
			elements = Collections.<ProcessorOption>emptyList();
		}
		else {
			elements = getListElements();
		}
		saveProcessorOptions(elements);
		super.saveSettings();
		if (null != fAptProject) {
			if (isProjSpecificDisabled) { // compare against workspace defaults
				if (!fOriginalGenSrcDir.equals(AptConfig.getGenSrcDir(null))) {
					fAptProject.preferenceChanged(AptPreferenceConstants.APT_GENSRCDIR);
				}
				if (fOriginalAptEnabled != AptConfig.isEnabled(null)) {
					fAptProject.preferenceChanged(AptPreferenceConstants.APT_ENABLED);
					// make JDT "processingEnabled" setting track APT "enabled" setting.
					setJDTProcessAnnotationsSetting(fAptEnabledField.isSelected());
				}
				if (fOriginalReconcileEnabled != AptConfig.shouldProcessDuringReconcile(null)) {
					fAptProject.preferenceChanged(AptPreferenceConstants.APT_RECONCILEENABLED);
				}
			}
			else { // compare against current settings
				if (!fOriginalGenSrcDir.equals(fGenSrcDirField.getText()))
					fAptProject.preferenceChanged(AptPreferenceConstants.APT_GENSRCDIR);
				boolean isAptEnabled = fAptEnabledField.isSelected();
				if (fOriginalAptEnabled != isAptEnabled) {
					fAptProject.preferenceChanged(AptPreferenceConstants.APT_ENABLED);
					// make JDT "processingEnabled" setting track APT "enabled" setting.
					setJDTProcessAnnotationsSetting(isAptEnabled);
				}
				if (fOriginalReconcileEnabled != fReconcileEnabledField.isSelected())
					fAptProject.preferenceChanged(AptPreferenceConstants.APT_RECONCILEENABLED);
			}
		}
	}
	
	/**
	 * Set the org.eclipse.jdt.core.compiler.processAnnotations setting.  
	 * In Eclipse 3.3, this value replaces org.eclipse.jdt.apt.aptEnabled,
	 * but we continue to set both values in order to ensure backward
	 * compatibility with prior versions.
	 * the aptEnabled setting.
	 * @param enable
	 */
	private void setJDTProcessAnnotationsSetting(boolean enable) {
		IScopeContext context = (null != fJProj) ? 
				new ProjectScope(fJProj.getProject()) : new InstanceScope();
		IEclipsePreferences node = context.getNode(JavaCore.PLUGIN_ID);
		final String value = enable ? AptPreferenceConstants.ENABLED : AptPreferenceConstants.DISABLED;
		node.put(AptPreferenceConstants.APT_PROCESSANNOTATIONS, value);
		try {
			node.flush();
		}
		catch (BackingStoreException e){
			AptPlugin.log(e, "Failed to save preference: " + AptPreferenceConstants.APT_PROCESSANNOTATIONS); //$NON-NLS-1$
		}
	}
	
	/**
	 * Check whether any processor options have changed.
	 * @return true if they did.
	 */
	private boolean procOptionsChanged() {
		Map<String, String> savedProcOptions = new HashMap<String, String>(fOriginalProcOptions);
		for (ProcessorOption o : getListElements()) {
			final String savedVal = savedProcOptions.get(o.key);
			if (savedVal != null && savedVal.equals(o.value)) {
				savedProcOptions.remove(o.key);
			}
			else {
				// found an unsaved option in the list
				return true;
			}
		}
		if (!savedProcOptions.isEmpty()) {
			// found a saved option that has been removed
			return true;
		}
		return false;
	}

	/**
	 * Check whether any processor options have changed, as well as
	 * any of the settings tracked in the "normal" way (as Keys).
	 */
	@Override
	protected boolean settingsChanged(IScopeContext currContext) {
		if (procOptionsChanged())
			return true;
		else
			return super.settingsChanged(currContext);
	}

	/**
	 * Call after updating key values, to warn user if new values are invalid.
	 * @param changedKey may be null, e.g. if called from createContents.
	 * @param oldValue may be null
	 * @param newValue may be null
	 */
	@Override
	protected void validateSettings(Key changedKey, String oldValue, String newValue) {
		IStatus status = null;
		
		status = validateGenSrcDir();
		if (status.getSeverity() == IStatus.OK) {
			status = validateProcessorOptions();
		}

		fContext.statusChanged(status);
	}	
	
	/**
	 * Validate "generated source directory" setting.  It must be a valid
	 * pathname relative to a project, and must not be a source directory.
	 * @return true if current field value is valid
	 */
	private IStatus validateGenSrcDir() {
		String dirName = fGenSrcDirField.getText();
		if (!AptConfig.validateGenSrcDir(fJProj, dirName)) {
			return new StatusInfo(IStatus.ERROR, Messages.AptConfigurationBlock_genSrcDirMustBeValidRelativePath);
		}
		if (fJProj != null && !dirName.equals(fOriginalGenSrcDir)) {
			IFolder folder = fJProj.getProject().getFolder( dirName );
			if (folder != null && folder.exists() && !folder.isDerived()) {
				return new StatusInfo(IStatus.WARNING, Messages.AptConfigurationBlock_warningContentsMayBeDeleted);
			}
		}
		return new StatusInfo();
	}

	/**
	 * Validate the currently set processor options.  We do this by
	 * looking at the table contents rather than the packed string,
	 * just because it's easier.
	 * @return a StatusInfo containing a warning if appropriate.
	 */
	private IStatus validateProcessorOptions() {
		List<ProcessorOption> elements = getListElements();
		for (ProcessorOption o : elements) {
			if (AptConfig.isAutomaticProcessorOption(o.key)) {
				return new StatusInfo(IStatus.WARNING, 
						Messages.AptConfigurationBlock_warningIgnoredOptions + ": " + o.key); //$NON-NLS-1$
			}
		}
		return new StatusInfo();
	}
	
	/**
	 * Update the UI based on the values presently stored in the keys.
	 */
	@Override
	protected void updateControls() {
		if (fAptEnabledField != null) {
			boolean aptEnabled= Boolean.valueOf(getValue(KEY_APTENABLED)).booleanValue();
			fAptEnabledField.setSelection(aptEnabled);
		}
		boolean reconcileEnabled= Boolean.valueOf(getValue(KEY_RECONCILEENABLED)).booleanValue();
		fReconcileEnabledField.setSelection(reconcileEnabled);
		String str= getValue(KEY_GENSRCDIR);
		fGenSrcDirField.setText(str == null ? "" : str); //$NON-NLS-1$
	}	
	
	/**
	 * Update the values stored in the keys based on the UI.
	 */
	protected final void updateModel(DialogField field) {
		
		if (fAptEnabledField != null && field == fAptEnabledField) {
			String newVal = String.valueOf(fAptEnabledField.isSelected());
			setValue(KEY_APTENABLED, newVal);
		} else if (field == fGenSrcDirField) {
			String newVal = fGenSrcDirField.getText();
			setValue(KEY_GENSRCDIR, newVal);
		} else if (field == fReconcileEnabledField) {
			String newVal = String.valueOf(fReconcileEnabledField.isSelected());
			setValue(KEY_RECONCILEENABLED, newVal);
		}
		validateSettings(null, null, null); // params are ignored
	}

	/**
	 * Bugzilla 136498: when project-specific settings are enabled, force APT to be enabled.
	 */
	@Override
	public void useProjectSpecificSettings(boolean enable) {
		super.useProjectSpecificSettings(enable);
		if (enable ^ fPerProjSettingsEnabled) {
			fAptEnabledField.setSelection(enable);
			fPerProjSettingsEnabled = enable;
		}
	}

	/**
	 * Save the contents of the options list.
	 */
	private void saveProcessorOptions(List<ProcessorOption> elements) {
		Map<String, String> map = new LinkedHashMap<String, String>(elements.size());
		for (ProcessorOption o : elements) {
			map.put(o.key, (o.value.length() > 0) ? o.value : null);
		}
		AptConfig.setProcessorOptions(map, fJProj);
	}

	/**
	 * Set the processor options list contents
	 */
	private void loadProcessorOptions(IJavaProject jproj) {
		List<ProcessorOption> options= new ArrayList<ProcessorOption>();
		Map<String, String> parsedOptions = AptConfig.getRawProcessorOptions(jproj);
		for (Map.Entry<String, String> entry : parsedOptions.entrySet()) {
			ProcessorOption o = new ProcessorOption();
			o.key = entry.getKey();
			if (o.key == null || o.key.length() < 1) {
				// Don't allow defective entries
				continue;
			}
			o.value = (entry.getValue() == null) ? "" : entry.getValue(); //$NON-NLS-1$
			options.add(o);
		}
		fProcessorOptionsField.setElements(options);
	}

	@Override
	public void performDefaults() {
		fPerProjSettingsEnabled = false;
		if (fJProj != null) {
			// If project-specific, load workspace settings
			loadProcessorOptions(null);
		}
		else {
			// If workspace, load "factory default," which is empty.
			fProcessorOptionsField.removeAllElements();
		}
		super.performDefaults();
	}

}


