content: /**
   *  Copyright (c) 2013-2016 Angelo ZERR.
   *  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:
   *  Angelo Zerr <angelo.zerr@gmail.com> - initial API and implementation
   */
  package tern.eclipse.ide.linter.ui.properties;
  
  import java.io.File;
  import java.util.Collection;
  
  import org.eclipse.core.resources.IProject;
  import org.eclipse.core.resources.IResource;
  import org.eclipse.core.resources.ResourcesPlugin;
  import org.eclipse.jface.dialogs.Dialog;
  import org.eclipse.jface.dialogs.IDialogSettings;
  import org.eclipse.jface.layout.TreeColumnLayout;
  import org.eclipse.jface.preference.PreferencePage;
  import org.eclipse.jface.viewers.ColumnWeightData;
  import org.eclipse.jface.viewers.ISelectionChangedListener;
  import org.eclipse.jface.viewers.IStructuredSelection;
  import org.eclipse.jface.viewers.SelectionChangedEvent;
  import org.eclipse.jface.viewers.TreeColumnViewerLabelProvider;
  import org.eclipse.jface.viewers.TreeViewer;
  import org.eclipse.jface.viewers.TreeViewerColumn;
  import org.eclipse.jface.window.Window;
  import org.eclipse.swt.SWT;
  import org.eclipse.swt.custom.SashForm;
  import org.eclipse.swt.custom.StackLayout;
  import org.eclipse.swt.events.ModifyEvent;
  import org.eclipse.swt.events.ModifyListener;
  import org.eclipse.swt.events.SelectionAdapter;
  import org.eclipse.swt.events.SelectionEvent;
  import org.eclipse.swt.graphics.Font;
  import org.eclipse.swt.layout.GridData;
  import org.eclipse.swt.layout.GridLayout;
  import org.eclipse.swt.widgets.Button;
  import org.eclipse.swt.widgets.Composite;
  import org.eclipse.swt.widgets.Control;
  import org.eclipse.swt.widgets.FileDialog;
  import org.eclipse.swt.widgets.Label;
  import org.eclipse.swt.widgets.Shell;
  import org.eclipse.swt.widgets.Text;
  import org.eclipse.swt.widgets.Tree;
  
  import com.eclipsesource.json.JsonObject;
  import com.eclipsesource.json.JsonValue;
  
  import tern.TernException;
  import tern.eclipse.ide.core.IWorkingCopy;
  import tern.eclipse.ide.core.IWorkingCopyListener;
  import tern.eclipse.ide.linter.core.ITernLinterConfig;
  import tern.eclipse.ide.linter.core.ITernLinterOption;
  import tern.eclipse.ide.linter.core.TernLinterCorePlugin;
  import tern.eclipse.ide.linter.internal.ui.TernLinterUIMessages;
  import tern.eclipse.ide.linter.internal.ui.TernLinterUIPlugin;
  import tern.eclipse.ide.linter.internal.ui.Trace;
  import tern.eclipse.ide.linter.ui.viewers.LinterConfigContentProvider;
  import tern.eclipse.ide.linter.ui.viewers.LinterConfigLabelProvider;
  import tern.eclipse.ide.linter.ui.viewers.LinterOptionEditingSupport;
  import tern.eclipse.ide.ui.controls.AbstractTreeBlock;
  import tern.eclipse.ide.ui.dialogs.OpenResourceDialog;
  import tern.server.ITernModule;
  import tern.server.ITernModuleConfigurable;
  import tern.utils.StringUtils;
  
  /**
   * Block which hosts the Tree of the Tern linter options.
   * 
   */
  public class TernLinterOptionsBlock extends AbstractTreeBlock implements
  		IWorkingCopyListener {
  
  	// JSON field for linter config.
  	private static final String CONFIG_FILE_FIELD = "configFile";
  	private static final String CONFIG_FIELD = "config";
  	
  	private final String linterId;
  	private String linterConfigFilename;
  
  	private final IWorkingCopy workingCopy;
  	private final PreferencePage preferencePage;
  	
  	private TreeViewer treeViewer;
  
  	private TernLinterOptionsPanel optionsPanel;
  	private Button enableCheckbox;
  	private Button useConfigFilesCheckbox;
  
  	// Use config
  	private Text linterConfigFileText;
  	private Button projectBrowserButton;
  	private Button workspaceBrowserButton;
  	private Button filesystemBrowserButton;
  	
  	// Composite panel stack.
  	private Composite contentPanel;
  	private Composite configPage;
  	private Composite configFilePage;
  
  	public TernLinterOptionsBlock(String linterId, IWorkingCopy workingCopy, PreferencePage preferencePage) {
  		this.linterId = linterId;
  		this.workingCopy = workingCopy;
  		this.preferencePage = preferencePage;
  		this.linterConfigFilename = TernLinterCorePlugin.getDefault()
  				.getTernLinterConfigurationsManager().getFilename(linterId);
  		workingCopy.addWorkingCopyListener(this);
  	}
  
  	public Control createControl(Composite ancestor) {
  
  		Composite parent = new Composite(ancestor, SWT.NONE);
  		GridLayout layout = new GridLayout();
  		layout.numColumns = 1;
  		layout.marginHeight = 0;
  		layout.marginWidth = 0;
  		parent.setLayout(layout);
  		Font font = ancestor.getFont();
  		parent.setFont(font);
  
  		createHeader(parent);
  		createSeparator(parent);
  		this.contentPanel = createBody(parent);
  
  		// Display page
  		displayPage();
  
  		Dialog.applyDialogFont(parent);
  		return parent;
  	}
  
  	protected void createSeparator(Composite parent) {
  		Label line = new Label(parent, SWT.SEPARATOR | SWT.HORIZONTAL);
  		line.setLayoutData(new GridData(GridData.HORIZONTAL_ALIGN_FILL));
  	}
  
  	private void createHeader(Composite parent) {
  		Composite header = new Composite(parent, SWT.NONE);
  		GridData data = new GridData(GridData.FILL_HORIZONTAL);
  		data.horizontalSpan = 2;
  		header.setLayoutData(data);
  		header.setLayout(new GridLayout(6, false));
  
  		// Create "Enable" checkbox
  		enableCheckbox = new Button(header, SWT.CHECK);
  		enableCheckbox
  				.setText(TernLinterUIMessages.TernLinterOptionsBlock_enable);
  		enableCheckbox.addSelectionListener(new SelectionAdapter() {
  			@Override
  			public void widgetSelected(SelectionEvent event) {
  				boolean checked = enableCheckbox.getSelection();
  				// Update UI
  				updateEnabled();
  				try {
  					// update working copy
  					ITernModule module = workingCopy.getTernModule(linterId);
  					if (module != null) {
  						if (checked) {
  							workingCopy.getCheckedModules().add(module);
  						} else {
  							workingCopy.getCheckedModules().remove(module);
  						}
  					}
  				} catch (TernException e) {
  					Trace.trace(Trace.SEVERE,
  							"Error while updating working copy", e);
  				}
  
  			}
  		});
  
  		if (canUseConfigFiles()) {
  			// Create "Use config files" checkbox
  			useConfigFilesCheckbox = new Button(header, SWT.CHECK);
  			useConfigFilesCheckbox
  					.setText(TernLinterUIMessages.TernLinterOptionsBlock_useConfigFiles);
  			useConfigFilesCheckbox.addSelectionListener(new SelectionAdapter() {
  				@Override
  				public void widgetSelected(SelectionEvent e) {
  					displayPage();
  				}
  			});
  		}
  
  		// Update "Enable" checkbox according if the linter exists in the
  		// .tern-project.
  		enableCheckbox.setSelection(hasLinter());
  
  	}
  
  	protected boolean canUseConfigFiles() {
  		return !StringUtils.isEmpty(linterConfigFilename);
  	}
  
  	private void displayPage() {
  		((StackLayout) contentPanel.getLayout()).topControl = isUseConfigFiles() ? configFilePage
  				: configPage;
  		contentPanel.layout();
  	}
  
  	private void updateEnabled() {
  		Boolean checked = enableCheckbox.getSelection();
  		if (useConfigFilesCheckbox != null) {
  			useConfigFilesCheckbox.setEnabled(checked);
  		}
  		if (linterConfigFileText != null) {
  			linterConfigFileText.setEnabled(checked);
  			projectBrowserButton.setEnabled(checked);
  			workspaceBrowserButton.setEnabled(checked);
  			filesystemBrowserButton.setEnabled(checked);
  		}
  		if (optionsPanel != null) {
  			optionsPanel.updateEnabled(checked);
  		}
  		treeViewer.getTree().setEnabled(checked);
  	}
  
  	protected Composite createBody(Composite parent) {
  		Composite contentPanel = new Composite(parent, SWT.NONE);
  		GridData data = new GridData(GridData.FILL_HORIZONTAL);
  		data.heightHint = 400;
  		contentPanel.setLayoutData(data);
  		StackLayout layout = new StackLayout();
  		contentPanel.setLayout(layout);
  		this.configPage = createConfigPage(contentPanel);
  		if (canUseConfigFiles()) {
  			this.configFilePage = createConfigFilePage(contentPanel);
  		}
  		return contentPanel;
  	}
  
  	// -------------------- Config Page
  
  	/**
  	 * 
  	 * @param parent
  	 */
  	protected Composite createConfigPage(Composite parent) {
  		SashForm sashForm = new SashForm(parent, SWT.HORIZONTAL | SWT.SMOOTH);
  		GridData data = new GridData(GridData.FILL_HORIZONTAL);
  		sashForm.setLayoutData(data);
  		createOptionsMaster(sashForm);
  		createOptionsDetails(sashForm);
  		return sashForm;
  	}
  
  	/**
  	 * Create tree of tern linter config options.
  	 * 
  	 * @param ancestor
  	 */
  	private void createOptionsMaster(Composite ancestor) {
  		Composite parent = new Composite(ancestor, SWT.NONE);
  		parent.setLayoutData(new GridData(GridData.FILL_BOTH));
  		TreeColumnLayout layout = new TreeColumnLayout();
  		parent.setLayout(layout);
  		Font font = ancestor.getFont();
  		parent.setFont(font);
  
  		// Create Tree
  		treeViewer = new TreeViewer(parent, SWT.BORDER | SWT.FULL_SELECTION
  				| SWT.V_SCROLL | SWT.MULTI);
  		Tree tree = treeViewer.getTree();
  		tree.setHeaderVisible(false);
  		tree.setLinesVisible(true);
  
  		GridData data = new GridData(GridData.FILL_BOTH);
  		data.minimumHeight= 400;
  		tree.setLayoutData(data);
  
  		treeViewer
  				.setContentProvider(LinterConfigContentProvider.getInstance());
  
  		// Create label column
  		TreeViewerColumn labelColumnViewer = new TreeViewerColumn(treeViewer,
  				SWT.LEFT);
  		labelColumnViewer.setLabelProvider(new TreeColumnViewerLabelProvider(
  				LinterConfigLabelProvider.getInstance()));
  		layout.setColumnData(labelColumnViewer.getColumn(),
  				new ColumnWeightData(1, 150));
  
  		// Create value column
  		TreeViewerColumn valueColumnViewer = new TreeViewerColumn(treeViewer,
  				SWT.LEFT);
  		valueColumnViewer.setLabelProvider(new TreeColumnViewerLabelProvider(
  				LinterConfigLabelProvider.getInstance()));
  		valueColumnViewer.setEditingSupport(new LinterOptionEditingSupport(
  				valueColumnViewer.getViewer()));
  		layout.setColumnData(valueColumnViewer.getColumn(),
  				new ColumnWeightData(1, 100));
  
  		// Add tree selection listener to refresh the detail information of teh
  		// selected option
  		treeViewer.addSelectionChangedListener(new ISelectionChangedListener() {
  
  			@Override
  			public void selectionChanged(SelectionChangedEvent e) {
  				if (!e.getSelection().isEmpty()) {
  					ITernLinterOption option = (ITernLinterOption) ((IStructuredSelection) e
  							.getSelection()).getFirstElement();
  					refreshOption(option);
  				} else {
  					refreshOption(null);
  				}
  			}
  		});
  		restoreColumnSettings();
  	}
  
  	protected void refreshOption(ITernLinterOption option) {
  		optionsPanel.refresh(option);
  	}
  
  	private void createOptionsDetails(Composite parent) {
  		this.optionsPanel = new TernLinterOptionsPanel(parent, null);
  	}
  
  	// -------------------- Config File Page
  
  	/**
  	 * 
  	 * @param parent
  	 */
  	protected Composite createConfigFilePage(Composite parent) {
  		Composite page = new Composite(parent, SWT.NONE);
  		page.setLayoutData(new GridData(GridData.FILL_BOTH));
  		page.setLayout(new GridLayout(3, false));
  		
  		createConfigFileText(page);		
  		createBrowseButtons(page);
  		
  		return page;
  	}
  
  	protected void createConfigFileText(Composite parent) {
  		Composite container = new Composite(parent, SWT.NONE);
  		container.setLayout(new GridLayout(2, false));
  		GridData data = new GridData(GridData.FILL_BOTH);
  		data.verticalAlignment = SWT.BEGINNING; 
  		container.setLayoutData(data);
  		
  		Label linterConfigFileLabel = new Label(container, SWT.NONE);
  		linterConfigFileLabel.setText(new StringBuilder(linterConfigFilename)
  				.append(":").toString());
  		linterConfigFileText = new Text(container, SWT.BORDER);
  		linterConfigFileText.addModifyListener(new ModifyListener() {
  
  			@Override
  			public void modifyText(ModifyEvent e) {
  				validate();
  			}			
  		});
  		linterConfigFileText.setLayoutData(new GridData(
  				GridData.FILL_HORIZONTAL));
  	}
  
  	private void createBrowseButtons(Composite parent) {
  		Composite container = new Composite(parent, SWT.NONE);
  		container.setLayout(new GridLayout(1, true));
  		container.setLayoutData(new GridData(GridData.FILL_VERTICAL));
  		
  		final Shell shell = parent.getShell();
  		final IProject project = workingCopy
  				.getProject().getProject();
  		
  		// Browse files config of Project
  		projectBrowserButton = new Button(container, SWT.NONE);
  		projectBrowserButton.setText(TernLinterUIMessages.Button_browse_project);
  		projectBrowserButton.addSelectionListener(new SelectionAdapter() {
  			@Override
  			public void widgetSelected(SelectionEvent e) {
  				OpenResourceDialog dialog = new OpenResourceDialog(
  						shell, false, project, IResource.FILE);
  				dialog.setInitialPattern(linterConfigFilename);
  				if (dialog.open() != Window.OK) {
  					return;
  				}
  				IResource result = (IResource) dialog.getFirstResult();
  				if (result != null
  						&& linterConfigFilename.equals(result.getName())) {
  					linterConfigFileText.setText(result
  							.getProjectRelativePath().toString());
  				}
  			}
  		});
  		projectBrowserButton.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
  		
  		// Browse files config of Workspace
  		workspaceBrowserButton = new Button(container, SWT.NONE);
  		workspaceBrowserButton.setText(TernLinterUIMessages.Button_browse_workspace);
  		workspaceBrowserButton.addSelectionListener(new SelectionAdapter() {
  			@Override
  			public void widgetSelected(SelectionEvent e) {
  				OpenResourceDialog dialog = new OpenResourceDialog(
  						shell, false, ResourcesPlugin.getWorkspace().getRoot(), IResource.FILE);
  				dialog.setInitialPattern(linterConfigFilename);
  				if (dialog.open() != Window.OK) {
  					return;
  				}
  				IResource result = (IResource) dialog.getFirstResult();
  				if (result != null
  						&& linterConfigFilename.equals(result.getName())) {					
  					linterConfigFileText.setText(result.getFullPath().makeRelativeTo(project.getFullPath()).toString());
  				}
  			}
  		});
  		
  		// Browse files config of File System
  		filesystemBrowserButton = new Button(container, SWT.NONE);
  		filesystemBrowserButton.setText(TernLinterUIMessages.Button_browse_filesystem);
  		filesystemBrowserButton.addSelectionListener(new SelectionAdapter() {
  			@Override
  			public void widgetSelected(SelectionEvent e) {
  				FileDialog dialog = new FileDialog(shell, SWT.NONE);
  				dialog.setFileName(linterConfigFileText.getText());
  				dialog.setFileName(linterConfigFilename);
  				String file = dialog.open();
  				if (file != null) {
  					file = file.trim();
  					if (file.length() > 0) {
  						linterConfigFileText.setText(file);
  					}
  				}
  			}
  		});
  	}
  
  	public void setLinterConfig(ITernLinterConfig config) throws TernException {
  		// load value for each option from the .tern-project
  		if (workingCopy.hasCheckedTernModule(linterId)) {
  			ITernModuleConfigurable module = (ITernModuleConfigurable) workingCopy
  					.getTernModule(linterId);
  			JsonObject jsonOptions = module.getOptionsObject();
  			if (jsonOptions != null) {
  				JsonValue jsonConfig = jsonOptions.get(CONFIG_FIELD);
  				if (jsonConfig != null && jsonConfig.isObject()) {
  					// It's a config options
  					updateConfig((JsonObject) jsonOptions.get(CONFIG_FIELD),
  							config.getOptions());
  				} else {
  					JsonValue jsonConfigFile = jsonOptions
  							.get(CONFIG_FILE_FIELD);
  					if (canUseConfigFiles() && jsonConfigFile != null
  							&& jsonConfigFile.isString()) {
  						// it's a config file
  						setConfigFile(jsonConfigFile.asString());
  					}
  				}
  			}
  		}
  		treeViewer.setInput(config);
  		treeViewer.expandAll();
  		updateEnabled();
  	}
  
  	private void setConfigFile(String confiFile) {
  		linterConfigFileText.setText(confiFile);
  		useConfigFilesCheckbox.setSelection(true);
  		displayPage();
  	}
  
  	private void updateConfig(JsonObject jsonOptions,
  			Collection<ITernLinterOption> options) {
  		for (ITernLinterOption option : options) {
  			if (option.isCategoryType()) {
  				updateConfig(jsonOptions, option.getOptions());
  			} else {
  				String name = option.getId();
  				JsonValue jsonValue = jsonOptions.get(name);
  				if (jsonValue != null) {
  					if (jsonValue.isBoolean()) {
  						option.setValue(jsonValue.asBoolean());
  					} else if (jsonValue.isNumber()) {
  						option.setValue(jsonValue.asLong());
  					} else if (jsonValue.isString()) {
  						option.setValue(jsonValue.asString());
  					}
  				}
  			}
  		}
  	}
  
  	@Override
  	protected Tree getTree() {
  		return treeViewer.getTree();
  	}
  
  	@Override
  	protected IDialogSettings getDialogSettings() {
  		return TernLinterUIPlugin.getDefault().getDialogSettings();
  	}
  
  	@Override
  	protected String getQualifier() {
  		return TernLinterUIPlugin.PLUGIN_ID;
  	}
  
  	private boolean hasLinter() {
  		return workingCopy.hasCheckedTernModule(linterId);
  	}
  
  	@Override
  	public void moduleSelectionChanged(ITernModule module, boolean selected) {
  		if (linterId.equals(module.getName())) {
  			enableCheckbox.setSelection(selected);
  			updateEnabled();
  		}
  	}
  
  	@Override
  	public void dispose() {
  		super.dispose();
  		workingCopy.removeWorkingCopyListener(this);
  	}
  
  	/**
  	 * Update the .tern-project :
  	 * 
  	 * <ul>
  	 * <li>add tern linter in the .tern-project by updating JSON options.</li>
  	 * <li>or remove the tern linter</li>
  	 * </ul>
  	 * 
  	 * @throws TernException
  	 */
  	public void updateTenProject() throws TernException {
  		// To be sure that update of checked modules doesn't fire event.
  		workingCopy.removeWorkingCopyListener(this);
  		ITernModuleConfigurable module = (ITernModuleConfigurable) workingCopy
  				.getTernModule(linterId);
  		if (enableCheckbox.getSelection()) {
  			// add tern linter + options in the .tern-project
  			ITernLinterConfig config = (ITernLinterConfig) treeViewer
  					.getInput();
  			JsonObject jsonOptions = new JsonObject();
  			if (isUseConfigFiles()) {
  				// configFile : save the referenced file path linter config (eg
  				// : .jshintrc, eslint.json) in the .tern-project.
  				String configFile = linterConfigFileText.getText();
  				jsonOptions.add(CONFIG_FILE_FIELD, configFile);
  			} else {
  				// config : save the linter options inside the .tern-project
  				JsonObject jsonConfig = new JsonObject();
  				jsonOptions.add(CONFIG_FIELD, jsonConfig);
  				updateJSONOptions(config.getOptions(), jsonConfig);
  			}
  			module.setOptions(jsonOptions);
  		} else {
  			// remove the tern linter from the .tern-project.
  			workingCopy.getCheckedModules().remove(module);
  		}
  	}
  
  	private void updateJSONOptions(Collection<ITernLinterOption> options,
  			JsonObject json) {
  		for (ITernLinterOption option : options) {
  			if (option.isCategoryType()) {
  				updateJSONOptions(option.getOptions(), json);
  			} else if (option.hasValue()) {
  				if (option.isBooleanType()) {
  					json.add(option.getId(), option.getBooleanValue());
  				} else if (option.isNumberType()) {
  					json.add(option.getId(), option.getNumberValue());
  				} else {
  					json.add(option.getId(), option.getStringValue());
  				}
  			}
  		}
  	}
  
  	private boolean isUseConfigFiles() {
  		return useConfigFilesCheckbox != null
  				&& useConfigFilesCheckbox.getSelection();
  	}
  	
  	private void validate() {
  		preferencePage.setErrorMessage(null);
  		if (linterConfigFileText != null) {
  			String fileConfig = linterConfigFileText.getText();
  			// Validate if file config is valid.
  			if (!isProjectFile(fileConfig) && !(new File(fileConfig).exists())) {
  				preferencePage.setErrorMessage(fileConfig + " is not a valid path.");
  			}		
  		}
  	}
  	
  	private boolean isProjectFile(String fileConfig) {
  		try {
  			IProject project = workingCopy
  					.getProject().getProject();
  			return project.getFile(fileConfig).exists();
  		}
  		catch(Throwable e) {
  			return false;
  		}
  	}
  }
  