/*******************************************************************************
 * Copyright (c) 2011, 2020 IBM Corporation and others.
 * All rights reserved. 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.wst.sse.ui.internal.preferences.ui;

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.ProjectScope;
import org.eclipse.core.resources.ResourcesPlugin;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Platform;
import org.eclipse.core.runtime.Status;
import org.eclipse.core.runtime.jobs.Job;
import org.eclipse.core.runtime.preferences.DefaultScope;
import org.eclipse.core.runtime.preferences.IEclipsePreferences;
import org.eclipse.core.runtime.preferences.IPreferencesService;
import org.eclipse.core.runtime.preferences.IScopeContext;
import org.eclipse.jface.dialogs.IDialogSettings;
import org.eclipse.jface.resource.JFaceResources;
import org.eclipse.swt.SWT;
import org.eclipse.swt.events.SelectionEvent;
import org.eclipse.swt.events.SelectionListener;
import org.eclipse.swt.layout.GridData;
import org.eclipse.swt.layout.GridLayout;
import org.eclipse.swt.widgets.Combo;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Control;
import org.eclipse.swt.widgets.Label;
import org.eclipse.swt.widgets.MessageBox;
import org.eclipse.swt.widgets.Widget;
import org.eclipse.ui.forms.events.ExpansionAdapter;
import org.eclipse.ui.forms.events.ExpansionEvent;
import org.eclipse.ui.forms.widgets.ExpandableComposite;
import org.eclipse.wst.sse.core.internal.validate.ValidationMessage;
import org.eclipse.wst.sse.ui.internal.SSEUIMessages;
import org.eclipse.wst.validation.ValidationFramework;
import org.osgi.service.prefs.BackingStoreException;

/**
 * Based on org.eclipse.jdt.internal.ui.preferences.OptionsConfigurationBlock
 */
public abstract class AbstractValidationSettingsPage extends PropertyPreferencePage {

	private List<Combo> fCombos;
	protected List<ExpandableComposite> fExpandables;
	
	private SelectionListener fSelectionListener;
	
	private IPreferencesService fPreferencesService = null;
	
	private static final String SETTINGS_EXPANDED = "expanded"; //$NON-NLS-1$
	
	private ValidationFramework fValidation;
	
	private class ComboData {
		private String fKey;
		private int[] fSeverities;
		private int fIndex;
		int originalSeverity = -2;
		
		public ComboData(String key, int[] severities, int index) {
			fKey = key;
			fSeverities = severities;
			fIndex = index;
		}
		
		public String getKey() {
			return fKey;
		}
		
		public void setIndex(int index) {
			fIndex = index;
		}
		
		public int getIndex() {
			return fIndex;
		}
		
		/**
		 * Sets the severity index based on <code>severity</code>.
		 * If the severity doesn't exist, the index is set to -1.
		 * 
		 * @param severity the severity level
		 */
		public void setSeverity(int severity) {
			for (int i = 0; fSeverities != null && i < fSeverities.length; i++) {
				if (fSeverities[i] == severity) {
					fIndex = i;
					return;
				}
			}

			fIndex = -1;
		}
		
		public int getSeverity() {
			return (fIndex >= 0 && fSeverities != null && fIndex < fSeverities.length) ? fSeverities[fIndex] : -1;
		}
		
		boolean isChanged() {
			return fSeverities[fIndex] != originalSeverity;
		}
	}
	
	public AbstractValidationSettingsPage() {
		super();
		fCombos = new ArrayList<>();
		fExpandables = new ArrayList<>();
		fPreferencesService = Platform.getPreferencesService();
		fValidation = ValidationFramework.getDefault();
	}
	
	/**
	 * Creates a Combo widget in the composite <code>parent</code>. The data
	 * in the Combo is associated with <code>key</code>. The Combo data is
	 * generated based on the integer <code>values</code> where the index
	 * of <code>values</code> corresponds to the index of <code>valueLabels</code>
	 * 
	 * @param parent the composite to create the combo box in
	 * @param label the label to give the combo box
	 * @param key the unique key to identify the combo box
	 * @param values the values represented by the combo options
	 * @param valueLabels the calues displayed in the combo box
	 * @param indent how far to indent the combo box label
	 * 
	 * @return the generated combo box
	 */
	protected Combo addComboBox(Composite parent, String label, String key, int[] values, String[] valueLabels, int indent) {
		GridData gd= new GridData(GridData.FILL, GridData.CENTER, true, false, 2, 1);
		gd.horizontalIndent= indent;
				
		Label labelControl= new Label(parent, SWT.LEFT);
		labelControl.setFont(JFaceResources.getDialogFont());
		labelControl.setText(label);
		labelControl.setLayoutData(gd);
				
		Combo comboBox= newComboControl(parent, key, values, valueLabels);
		comboBox.setLayoutData(new GridData(GridData.HORIZONTAL_ALIGN_FILL));

		return comboBox;
	}
	
	/**
	 * Creates a combo box and associates the combo data with the
	 * combo box.
	 * 
	 * @param composite the composite to create the combo box in
	 * @param key the unique key to identify the combo box
	 * @param values the values represented by the combo options
	 * @param valueLabels the values displayed in the combo box
	 * 
	 * @return the generated combo box
	 */
	protected Combo newComboControl(Composite composite, String key, int[] values, String[] valueLabels) {
		ComboData data = new ComboData(key, values, -1);
		
		Combo comboBox= new Combo(composite, SWT.READ_ONLY);
		comboBox.setItems(valueLabels);
		comboBox.setData(data);
		comboBox.addSelectionListener(getSelectionListener());
		comboBox.setFont(JFaceResources.getDialogFont());
			
		makeScrollableCompositeAware(comboBox);
		
		int severity = -1;
		if (key != null) {
			severity = fPreferencesService.getInt(getPreferenceNodeQualifier(), key, ValidationMessage.WARNING, createPreferenceScopes());
		}

		if (severity == ValidationMessage.ERROR || severity == ValidationMessage.WARNING || severity == ValidationMessage.IGNORE || severity == ValidationMessage.INFORMATION) {
			data.setSeverity(severity);
			data.originalSeverity = severity;
		}
		
		if(data.getIndex() >= 0) {
			comboBox.select(data.getIndex());
		}
		
		fCombos.add(comboBox);
		return comboBox;
	}
	
	protected SelectionListener getSelectionListener() {
		if (fSelectionListener == null) {
			fSelectionListener= new SelectionListener() {
				public void widgetDefaultSelected(SelectionEvent e) {}
	
				public void widgetSelected(SelectionEvent e) {
					controlChanged(e.widget);
				}
			};
		}
		return fSelectionListener;
	}
	
	protected void controlChanged(Widget widget) {
		ComboData data= (ComboData) widget.getData();
		if (widget instanceof Combo) {
			data.setIndex(((Combo)widget).getSelectionIndex());
		} else {
			return;
		}
	}

	/*
	 * (non-Javadoc)
	 * @see org.eclipse.wst.sse.ui.internal.preferences.ui.AbstractSettingsPage#storeValues()
	 */
	protected void storeValues() {
		if(fCombos == null || fCombos.size() == 0)
			return;
		
		Iterator<Combo> it = fCombos.iterator();
		
		IScopeContext[] contexts = createPreferenceScopes();

		while(it.hasNext()) {
			ComboData data = (ComboData) it.next().getData();
			if(data.getKey() != null) {
				contexts[0].getNode(getPreferenceNodeQualifier()).putInt(data.getKey(), data.getSeverity());
				data.originalSeverity = data.getSeverity();
			}
		}
		
		for(int i = 0; i < contexts.length; i++) {
			try {
				contexts[i].getNode(getPreferenceNodeQualifier()).flush();
			}
			catch (BackingStoreException e) {
				
			}
		}
	}
	
	protected ExpandableComposite getParentExpandableComposite(Control control) {
		Control parent= control.getParent();
		while (!(parent instanceof ExpandableComposite) && parent != null) {
			parent= parent.getParent();
		}
		if (parent instanceof ExpandableComposite) {
			return (ExpandableComposite) parent;
		}
		return null;
	}
	
	protected ExpandableComposite createStyleSection(Composite parent, String label, int nColumns) {
		ExpandableComposite excomposite= new ExpandableComposite(parent, SWT.NONE, ExpandableComposite.TWISTIE | ExpandableComposite.CLIENT_INDENT);
		excomposite.setText(label);
		excomposite.setExpanded(false);
		excomposite.setFont(JFaceResources.getFontRegistry().getBold(JFaceResources.DIALOG_FONT));
		excomposite.setLayoutData(new GridData(GridData.FILL, GridData.FILL, true, false, nColumns, 1));
		excomposite.addExpansionListener(new ExpansionAdapter() {
			public void expansionStateChanged(ExpansionEvent e) {
				expandedStateChanged((ExpandableComposite) e.getSource());
			}
		});
		fExpandables.add(excomposite);
		makeScrollableCompositeAware(excomposite);
		return excomposite;
	}
	
	protected Composite createStyleSectionWithContentComposite(Composite parent, String label, int nColumns) {
		ExpandableComposite excomposite = new ExpandableComposite(parent, SWT.NONE, ExpandableComposite.TWISTIE | ExpandableComposite.CLIENT_INDENT);
		excomposite.setText(label);
		excomposite.setExpanded(false);
		excomposite.setFont(JFaceResources.getFontRegistry().getBold(JFaceResources.DIALOG_FONT));
		excomposite.setLayoutData(new GridData(GridData.FILL, GridData.FILL, true, false, nColumns, 1));
		excomposite.addExpansionListener(new ExpansionAdapter() {
			public void expansionStateChanged(ExpansionEvent e) {
				expandedStateChanged((ExpandableComposite) e.getSource());
			}
		});
		fExpandables.add(excomposite);
		makeScrollableCompositeAware(excomposite);

		Composite inner = new Composite(excomposite, SWT.NONE);
		inner.setFont(excomposite.getFont());
		inner.setLayout(new GridLayout(nColumns, false));
		excomposite.setClient(inner);
		return inner;
	}
	
	protected final void expandedStateChanged(ExpandableComposite expandable) {
		ScrolledPageContent parentScrolledComposite= getParentScrolledComposite(expandable);
		if (parentScrolledComposite != null) {
			parentScrolledComposite.reflow(true);
		}
	}
	
	protected void makeScrollableCompositeAware(Control control) {
		ScrolledPageContent parentScrolledComposite= getParentScrolledComposite(control);
		if (parentScrolledComposite != null) {
			parentScrolledComposite.adaptChild(control);
		}
	}
	
	protected ScrolledPageContent getParentScrolledComposite(Control control) {
		Control parent= control.getParent();
		while (!(parent instanceof ScrolledPageContent) && parent != null) {
			parent= parent.getParent();
		}
		if (parent instanceof ScrolledPageContent) {
			return (ScrolledPageContent) parent;
		}
		return null;
	}
	
	protected void storeSectionExpansionStates(IDialogSettings section) {
		for(int i = 0; i < fExpandables.size(); i++) {
			ExpandableComposite comp = fExpandables.get(i);
			section.put(SETTINGS_EXPANDED + String.valueOf(i), comp.isExpanded());
		}
	}
	
	protected void restoreSectionExpansionStates(IDialogSettings settings) {
		for (int i= 0; i < fExpandables.size(); i++) {
			ExpandableComposite excomposite= fExpandables.get(i);
			if (settings == null) {
				excomposite.setExpanded(i == 0); // only expand the first node by default
			} else {
				excomposite.setExpanded(settings.getBoolean(SETTINGS_EXPANDED + String.valueOf(i)));
			}
		}
	}
	
	protected void resetSeverities() {
		IEclipsePreferences defaultContext = new DefaultScope().getNode(getPreferenceNodeQualifier());
		for(int i = 0; i < fCombos.size(); i++) {
			ComboData data = (ComboData)fCombos.get(i).getData();
			int severity = defaultContext.getInt(data.getKey(), ValidationMessage.WARNING);
			data.setSeverity(severity);
			fCombos.get(i).select(data.getIndex());
		}
	}
	
	protected boolean shouldRevalidateOnSettingsChange() {
		Iterator<Combo> it = fCombos.iterator();

		while (it.hasNext()) {
			ComboData data = (ComboData) it.next().getData();
			if (data.isChanged())
				return true;
		}
		return false;
	}
	
	public boolean performOk() {
		if(super.performOk() && shouldRevalidateOnSettingsChange()) {
			MessageBox mb = new MessageBox(this.getShell(), SWT.APPLICATION_MODAL | SWT.YES | SWT.NO | SWT.CANCEL | SWT.ICON_INFORMATION | SWT.RIGHT);
			mb.setText(SSEUIMessages.Validation_Title);
			/* Choose which message to use based on if its project or workspace settings */
			String msg = (getProject() == null) ? SSEUIMessages.Validation_Workspace : SSEUIMessages.Validation_Project;
			mb.setMessage(msg);
			switch(mb.open()) {
				case SWT.CANCEL:
					return false;
				case SWT.YES:
					storeValues();
					ValidateJob job = new ValidateJob(SSEUIMessages.Validation_jobName);
					job.schedule();
				case SWT.NO:
					storeValues();
				default:
					return true;
			}
		}
		return true;
	}
	
	/**
	 * Performs validation after validation preferences have been modified.
	 */
	private class ValidateJob extends Job {
		
		public ValidateJob(String name) {
			super(name);
		}

		protected IStatus run(IProgressMonitor monitor) {
			IStatus status = Status.OK_STATUS;
			try {
				IProject[] projects = null;
				/* Changed preferences for a single project, only validate it */
				if(getProject() != null)
					projects = new IProject[] {getProject()};
				/* Workspace-wide preferences changed */
				else {
					/* Get all of the projects in the workspace */
					projects = ResourcesPlugin.getWorkspace().getRoot().getProjects();
					IEclipsePreferences prefs = null;
					List<IProject> projectList = new ArrayList<>();
					
					/* Filter out projects that use project-specific settings or have been closed */
					for(int i = 0; i < projects.length; i++) {
						prefs = new ProjectScope(projects[i]).getNode(getPreferenceNodeQualifier());
						if(projects[i].isAccessible() && !prefs.getBoolean(getProjectSettingsKey(), false))
							projectList.add(projects[i]);
					}
					projects = projectList.toArray(new IProject[projectList.size()]);
				}
				fValidation.validate(projects, true, false, monitor);
			}
			catch (CoreException ce) {
				status = Status.CANCEL_STATUS;
			}
			
			return status;
		}
		
	}
	
}
