/********************************************************************************
 * Copyright (c) 2015-2019 TU Darmstadt, Paderborn University This program and the accompanying materials are made available under the terms of the Eclipse Public License v. 2.0
 * which is available at http://www.eclipse.org/legal/epl-2.0. SPDX-License-Identifier: EPL-2.0
 ********************************************************************************/

package de.cognicrypt.staticanalyzer;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Iterator;
import java.util.List;
import org.eclipse.core.runtime.preferences.InstanceScope;
import org.eclipse.jface.dialogs.MessageDialog;
import org.eclipse.jface.preference.IPreferenceStore;
import org.eclipse.jface.viewers.CheckboxTableViewer;
import org.eclipse.jface.viewers.TableViewerColumn;
import org.eclipse.jface.window.Window;
import org.eclipse.swt.SWT;
import org.eclipse.swt.custom.CCombo;
import org.eclipse.swt.custom.TableEditor;
import org.eclipse.swt.events.SelectionAdapter;
import org.eclipse.swt.events.SelectionEvent;
import org.eclipse.swt.layout.GridLayout;
import org.eclipse.swt.widgets.Button;
import org.eclipse.swt.widgets.Combo;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Event;
import org.eclipse.swt.widgets.Group;
import org.eclipse.swt.widgets.Label;
import org.eclipse.swt.widgets.Listener;
import org.eclipse.swt.widgets.TableItem;
import org.eclipse.ui.IWorkbenchWindow;
import org.eclipse.ui.PlatformUI;
import org.osgi.service.prefs.BackingStoreException;
import org.osgi.service.prefs.Preferences;
import de.cognicrypt.core.Constants;
import de.cognicrypt.core.properties.PreferenceListener;
import de.cognicrypt.staticanalyzer.utilities.AddNewRulesetDialog;
import de.cognicrypt.staticanalyzer.utilities.ArtifactUtils;
import de.cognicrypt.staticanalyzer.utilities.Ruleset;
import de.cognicrypt.utils.CrySLUtils;
import de.cognicrypt.utils.UIUtils;

public class StaticAnalyzerPreferences extends PreferenceListener {

	private IPreferenceStore preferences = Activator.getDefault().getPreferenceStore();
	private Preferences rulePreferences = InstanceScope.INSTANCE.getNode(de.cognicrypt.core.Activator.PLUGIN_ID);

	private Button automatedAnalysisCheckBox;
	private Button providerDetectionCheckBox;
	private Button secureObjectsCheckBox;
	private Button analyseDependenciesCheckBox;
	private Button addNewRulesetButton, selectCustomRulesCheckBox;
	private CheckboxTableViewer table;

	private Combo CGSelection;
	private Combo forbidden;
	private Combo reqPred;
	private Combo constraint;
	private Combo neverType;
	private Combo incompleteOp;
	private Combo typestate;

	private List<Ruleset> listOfRulesets = new ArrayList<Ruleset>();

	@Override
	public void compileBasicPreferences(Composite parent) {
		createBasicContents(parent);
		performBasicDefaults();
		initializeBasicValues();
	}

	@Override
	public void compileAdvancedPreferences(Composite parent) {
		createAdvancedContents(parent);
		performAdvancedDefaults();
		initializeAdvancedValues();
	}

	private void initializeBasicValues() {
		automatedAnalysisCheckBox.setSelection(preferences.getBoolean(Constants.AUTOMATED_ANALYSIS));
		providerDetectionCheckBox.setSelection(preferences.getBoolean(Constants.PROVIDER_DETECTION_ANALYSIS));
		secureObjectsCheckBox.setSelection(preferences.getBoolean(Constants.SHOW_SECURE_OBJECTS));
		analyseDependenciesCheckBox.setSelection(preferences.getBoolean(Constants.ANALYSE_DEPENDENCIES));
		selectCustomRulesCheckBox.setSelection(preferences.getBoolean(Constants.SELECT_CUSTOM_RULES));
	}

	private void performBasicDefaults() {
		preferences.setDefault(Constants.RULE_SELECTION, 0);
		preferences.setDefault(Constants.AUTOMATED_ANALYSIS, true);
		preferences.setDefault(Constants.PROVIDER_DETECTION_ANALYSIS, false);
		preferences.setDefault(Constants.SHOW_SECURE_OBJECTS, false);
		preferences.setDefault(Constants.ANALYSE_DEPENDENCIES, true);
		preferences.setDefault(Constants.CALL_GRAPH_SELECTION, 0);
	}

	/***
	 * This method creates a row for each of the rule set with a drop-down list of versions passed.
	 * 
	 * @param ruleset rule set to be added
	 */
	private void createRulesTableRow(Ruleset ruleset) {

		TableEditor editor = new TableEditor(table.getTable());
		TableItem rulesRow = new TableItem(table.getTable(), SWT.NONE);
		rulesRow.setText(0, ruleset.getFolderName());
		editor.grabHorizontal = true;
		editor.setEditor(ruleset.getVersions(), rulesRow, 1);
		rulesRow.setText(2, ruleset.getUrl());
		rulesRow.setChecked(ruleset.isChecked());
		ruleset.setRulesRow(rulesRow);
	}

	/**
	 * This method fetches the list of rule sets which are stored in preference file
	 * 
	 * @return list of rule sets
	 */
	private List<Ruleset> getRulesetsFromPrefs() {
		List<Ruleset> ruleSets = new ArrayList<Ruleset>();

		try {
			String[] listOfNodes = rulePreferences.childrenNames();

			for (String currentNode : listOfNodes) {
				Ruleset loadedRuleset = new Ruleset(currentNode);
				Preferences subPref = rulePreferences.node(currentNode);
				String[] keys = subPref.keys();
				for (String key : keys) {
					switch (key) {
						case "FolderName":
							loadedRuleset.setFolderName(subPref.get(key, ""));
							break;
						case "CheckboxState":
							loadedRuleset.setChecked(subPref.getBoolean(key, false));
							break;
						case "SelectedVersion":
							loadedRuleset.setSelectedVersion(subPref.get(key, ""));
							break;
						case "Url":
							loadedRuleset.setUrl(subPref.get(key, ""));
							break;
						default:
							break;
					}
				}
				ruleSets.add(loadedRuleset);
			}
		}
		catch (BackingStoreException e) {
			Activator.getDefault().logError(e);
		}
		return ruleSets;
	}

	/***
	 * This method creates a table with check boxes for the CrySL rule sets.
	 */
	private void createRulesTable() {
		TableViewerColumn rulesColumn = new TableViewerColumn(table, SWT.FILL);
		TableViewerColumn versionsColumn = new TableViewerColumn(table, SWT.FILL);
		TableViewerColumn rulesURL = new TableViewerColumn(table, SWT.FILL);
		rulesColumn.getColumn().setText(Constants.TABLE_HEADER_RULES);
		versionsColumn.getColumn().setText(Constants.TABLE_HEADER_VERSION);
		rulesURL.getColumn().setText(Constants.TABLE_HEADER_URL);
		rulesColumn.getColumn().setWidth(200);
		versionsColumn.getColumn().setWidth(100);
		rulesURL.getColumn().setWidth(200);

		listOfRulesets = getRulesetsFromPrefs();

		for (Iterator<Ruleset> itr = listOfRulesets.iterator(); itr.hasNext();) {
			Ruleset ruleset = (Ruleset) itr.next();
			ruleset.setVersions(new CCombo(table.getTable(), SWT.NONE));
			String[] items = CrySLUtils.getRuleVersions(ruleset.getFolderName());
			if (items != null) {
				ruleset.getVersions().setItems(items);
				ruleset.getVersions().setItems(CrySLUtils.getRuleVersions(ruleset.getFolderName()));
				ruleset.setSelectedVersion(
						(ruleset.getSelectedVersion().length() > 0) ? ruleset.getSelectedVersion() : ruleset.getVersions().getItem(ruleset.getVersions().getItemCount() - 1));
				ruleset.getVersions().select(ruleset.getVersions().indexOf(ruleset.getSelectedVersion()));
			}
			createRulesTableRow(ruleset);
			ruleset.getVersions().addSelectionListener(new SelectionAdapter() {
				@Override
				public void widgetSelected(SelectionEvent e) {
					super.widgetSelected(e);

					ruleset.setSelectedVersion(ruleset.getVersions().getItem(ruleset.getVersions().getSelectionIndex()));
				}
			});
		}
	}

	/***
	 * This method modifies the rule set table by adding a new rule set entry
	 * 
	 * @param newRuleset The new rule set which is added to the table
	 */
	private void modifyRulesTable(Ruleset newRuleset) {
		newRuleset.setVersions(new CCombo(table.getTable(), SWT.NONE));
		newRuleset.getVersions().setItems(CrySLUtils.getRuleVersions(newRuleset.getFolderName()));
		newRuleset.setSelectedVersion(newRuleset.getVersions().getItem(newRuleset.getVersions().getItemCount() - 1));
		newRuleset.getVersions().select(newRuleset.getVersions().getItemCount() - 1);
		createRulesTableRow(newRuleset);
		newRuleset.getVersions().addSelectionListener(new SelectionAdapter() {
			@Override
			public void widgetSelected(SelectionEvent e) {
				super.widgetSelected(e);
				newRuleset.setSelectedVersion(newRuleset.getVersions().getItem(newRuleset.getVersions().getSelectionIndex()));
			}
		});
	}

	/***
	 * This method creates the UI for the preference page.
	 * 
	 * @param parent Instance of the eclipse preference window on which UI widgets for CogniCrypt are added.
	 */
	private void createBasicContents(Composite parent) {
		final Group staticAnalysisGroup = UIUtils.addHeaderGroup(parent, "Analysis");

		final Composite source = new Composite(staticAnalysisGroup, SWT.FILL);
		source.setLayout(new GridLayout(3, true));
		final Label ruleSource = new Label(source, SWT.NONE);
		ruleSource.setText("Source of CrySL rules: ");

		table = CheckboxTableViewer.newCheckList(staticAnalysisGroup, SWT.CHECK);
		table.getTable().setHeaderVisible(true);
		table.getTable().setLinesVisible(true);
		createRulesTable();
		table.getTable().addSelectionListener(new SelectionAdapter() {
			@Override
			public void widgetSelected(SelectionEvent e) {
				super.widgetSelected(e);
				if (e.detail == SWT.CHECK) {
					TableItem item = (TableItem) e.item;

					for (Iterator<Ruleset> itr = listOfRulesets.iterator(); itr.hasNext();) {
						Ruleset ruleset = (Ruleset) itr.next();
						if (item.getText(0) == ruleset.getFolderName())
							ruleset.setChecked(item.getChecked());
					}
				}
			}
		});

		selectCustomRulesCheckBox = new Button(staticAnalysisGroup, SWT.CHECK);
		selectCustomRulesCheckBox.setText("Select custom rules");
		selectCustomRulesCheckBox.addListener(SWT.Selection, new Listener() {
			@Override
			public void handleEvent(Event e) {
				preferences.setValue(Constants.SELECT_CUSTOM_RULES,selectCustomRulesCheckBox.getSelection());
			}
		});

		automatedAnalysisCheckBox = new Button(staticAnalysisGroup, SWT.CHECK);
		automatedAnalysisCheckBox.setText("Enable automated analysis when saving");

		providerDetectionCheckBox = new Button(staticAnalysisGroup, SWT.CHECK);
		providerDetectionCheckBox.setText("Enable provider detection analysis");

		secureObjectsCheckBox = new Button(staticAnalysisGroup, SWT.CHECK);
		secureObjectsCheckBox.setText("Show secure objects");

		analyseDependenciesCheckBox = new Button(staticAnalysisGroup, SWT.CHECK);
		analyseDependenciesCheckBox.setText("Include dependencies to projects analysis");
		analyseDependenciesCheckBox.setSelection(preferences.getBoolean(Constants.ANALYSE_DEPENDENCIES));

		addNewRulesetButton = new Button(staticAnalysisGroup, SWT.PUSH);
		addNewRulesetButton.setText("Add ruleset");
		addNewRulesetButton.addListener(SWT.Selection, new Listener() {

			@Override
			public void handleEvent(Event e) {
				addNewRuleset();
			}
		});
	}

	protected void addNewRuleset() {
		IWorkbenchWindow window = PlatformUI.getWorkbench().getActiveWorkbenchWindow();
		AddNewRulesetDialog dialog = new AddNewRulesetDialog(window.getShell());
		dialog.create();
		if (dialog.open() == Window.OK) {
			if (ifExists(dialog.getRulesetUrl())) {
				MessageDialog.openError(window.getShell(), "Duplicate Ruleset", "You are trying to add an existing ruleset!");
				return;
			} else {
				if (ArtifactUtils.downloadRulesets(dialog.getRulesetUrl())) {
					Activator.getDefault().logInfo("Rulesets updated.");
				}
				Ruleset newRuleset = new Ruleset(dialog.getRulesetUrl());
				modifyRulesTable(newRuleset);
				listOfRulesets.add(newRuleset);
			}
		}
	}

	boolean ifExists(String url) {
		List<Ruleset> existingRulesets = getRulesetsFromPrefs();
		for (Ruleset ruleset : existingRulesets) {
			if (ruleset.getUrl().equals(url)) {
				return true;
			}
		}
		return false;
	}

	private void initializeAdvancedValues() {
		int currentCG = preferences.getInt(Constants.CALL_GRAPH_SELECTION);
		CGSelection.select(currentCG > -1 ? currentCG : preferences.getDefaultInt(Constants.CALL_GRAPH_SELECTION));

		int errorType = preferences.getInt(Constants.FORBIDDEN_METHOD_MARKER_TYPE);
		forbidden.select(errorType > -1 ? errorType : preferences.getDefaultInt(Constants.FORBIDDEN_METHOD_MARKER_TYPE));

		errorType = preferences.getInt(Constants.CONSTRAINT_ERROR_MARKER_TYPE);
		constraint.select(errorType > -1 ? errorType : preferences.getDefaultInt(Constants.CONSTRAINT_ERROR_MARKER_TYPE));

		errorType = preferences.getInt(Constants.INCOMPLETE_OPERATION_MARKER_TYPE);
		incompleteOp.select(errorType > -1 ? errorType : preferences.getDefaultInt(Constants.INCOMPLETE_OPERATION_MARKER_TYPE));

		errorType = preferences.getInt(Constants.TYPESTATE_ERROR_MARKER_TYPE);
		typestate.select(errorType > -1 ? errorType : preferences.getDefaultInt(Constants.TYPESTATE_ERROR_MARKER_TYPE));

		errorType = preferences.getInt(Constants.NEVER_TYPEOF_MARKER_TYPE);
		neverType.select(errorType > -1 ? errorType : preferences.getDefaultInt(Constants.NEVER_TYPEOF_MARKER_TYPE));

		errorType = preferences.getInt(Constants.REQUIRED_PREDICATE_MARKER_TYPE);
		reqPred.select(errorType > -1 ? errorType : preferences.getDefaultInt(Constants.REQUIRED_PREDICATE_MARKER_TYPE));
	}

	private void performAdvancedDefaults() {
		preferences.setDefault(Constants.FORBIDDEN_METHOD_MARKER_TYPE, 0);
		preferences.setDefault(Constants.TYPESTATE_ERROR_MARKER_TYPE, 0);
		preferences.setDefault(Constants.INCOMPLETE_OPERATION_MARKER_TYPE, 0);
		preferences.setDefault(Constants.NEVER_TYPEOF_MARKER_TYPE, 0);
		preferences.setDefault(Constants.REQUIRED_PREDICATE_MARKER_TYPE, 0);
		preferences.setDefault(Constants.CONSTRAINT_ERROR_MARKER_TYPE, 0);

		preferences.setDefault(Constants.PREDICATE_CONTRADICTION_MARKER_TYPE, 0);
		preferences.setDefault(Constants.IMPRECISE_VALUE_EXTRACTION_MARKER_TYPE, 1);
	}

	private void createAdvancedContents(Composite parent) {
		final Group staticAnalysisGroup = UIUtils.addHeaderGroup(parent, "Analysis");

		final Composite callGraphContainer = new Composite(staticAnalysisGroup, SWT.None);
		callGraphContainer.setLayout(new GridLayout(2, true));
		final Label label1 = new Label(callGraphContainer, SWT.SHADOW_IN);
		label1.setText("Call-graph construction algorithm");

		CGSelection = new Combo(callGraphContainer, SWT.DROP_DOWN | SWT.READ_ONLY);
		CGSelection.setItems(Arrays.stream(Constants.CG.values()).map(Enum::name).toArray(String[]::new));

		final Group errorTypeGroup = new Group(staticAnalysisGroup, SWT.SHADOW_IN);
		errorTypeGroup.setText("Error-Warning Types");
		errorTypeGroup.setLayout(new GridLayout(1, true));

		// ConstraintError
		final Composite constraintContainer = new Composite(errorTypeGroup, SWT.None);
		constraintContainer.setLayout(new GridLayout(2, true));
		final Label constraintLabel = new Label(constraintContainer, SWT.None);
		constraintLabel.setText("Incorrect Parameter Problem:");
		constraint = new Combo(constraintContainer, SWT.DROP_DOWN | SWT.READ_ONLY);
		constraint.setItems(Arrays.stream(Constants.Severities.values()).map(Enum::name).toArray(String[]::new));

		// TypestateError
		final Composite typestateContainer = new Composite(errorTypeGroup, SWT.None);
		typestateContainer.setLayout(new GridLayout(2, true));
		final Label typestateLabel = new Label(typestateContainer, SWT.None);
		typestateLabel.setText("Incorrect Method Call Problem:");
		typestate = new Combo(typestateContainer, SWT.DROP_DOWN | SWT.READ_ONLY);
		typestate.setItems(Arrays.stream(Constants.Severities.values()).map(Enum::name).toArray(String[]::new));

		// IncompleteOperationError
		final Composite incompleteContainer = new Composite(errorTypeGroup, SWT.None);
		incompleteContainer.setLayout(new GridLayout(2, true));
		final Label incompleteLabel = new Label(incompleteContainer, SWT.None);
		incompleteLabel.setText("Missing Method Call Problem:");
		incompleteOp = new Combo(incompleteContainer, SWT.DROP_DOWN | SWT.READ_ONLY);
		incompleteOp.setItems(Arrays.stream(Constants.Severities.values()).map(Enum::name).toArray(String[]::new));

		final Composite forbiddenContainer = new Composite(errorTypeGroup, SWT.None);
		forbiddenContainer.setLayout(new GridLayout(2, true));
		final Label forbiddenLabel = new Label(forbiddenContainer, SWT.None);
		forbiddenLabel.setText("Forbidden Method Problem:");
		forbidden = new Combo(forbiddenContainer, SWT.DROP_DOWN | SWT.READ_ONLY);
		forbidden.setItems(Arrays.stream(Constants.Severities.values()).map(Enum::name).toArray(String[]::new));

		// RequiredPredicateError
		final Composite reqPredContainer = new Composite(errorTypeGroup, SWT.None);
		reqPredContainer.setLayout(new GridLayout(2, true));
		final Label reqPredLabel = new Label(reqPredContainer, SWT.None);
		reqPredLabel.setText("Insecure Class Composition Problem:");
		reqPred = new Combo(reqPredContainer, SWT.DROP_DOWN | SWT.READ_ONLY);
		reqPred.setItems(Arrays.stream(Constants.Severities.values()).map(Enum::name).toArray(String[]::new));

		// NeverTypeOfError
		final Composite neverTypeContainer = new Composite(errorTypeGroup, SWT.None);
		neverTypeContainer.setLayout(new GridLayout(2, true));
		final Label neverTypeLabel = new Label(neverTypeContainer, SWT.None);
		neverTypeLabel.setText("Wrong Type Problem:");
		neverType = new Combo(neverTypeContainer, SWT.DROP_DOWN | SWT.READ_ONLY);
		neverType.setItems(Arrays.stream(Constants.Severities.values()).map(Enum::name).toArray(String[]::new));
	}

	/***
	 * This method assigns default values for each of the preference options and is invoked when Restore defaults is clicked.
	 */
	@Override
	public void setDefaultValues() {
		selectCustomRulesCheckBox.setSelection(preferences.getDefaultBoolean(Constants.SELECT_CUSTOM_RULES));
		automatedAnalysisCheckBox.setSelection(preferences.getDefaultBoolean(Constants.AUTOMATED_ANALYSIS));
		providerDetectionCheckBox.setSelection(preferences.getDefaultBoolean(Constants.PROVIDER_DETECTION_ANALYSIS));
		secureObjectsCheckBox.setSelection(preferences.getDefaultBoolean(Constants.SHOW_SECURE_OBJECTS));
		analyseDependenciesCheckBox.setSelection(preferences.getDefaultBoolean(Constants.ANALYSE_DEPENDENCIES));

		for (Iterator<Ruleset> itr = listOfRulesets.iterator(); itr.hasNext();) {
			Ruleset ruleset = (Ruleset) itr.next();
			ruleset.getVersions().select(ruleset.getVersions().getItemCount() - 1);
			if (ruleset.getFolderName().equals("JavaCryptographicArchitecture"))
				ruleset.getRulesRow().setChecked(true);
			else
				ruleset.getRulesRow().setChecked(false);
			ruleset.setSelectedVersion(ruleset.getVersions().getItem(ruleset.getVersions().getItemCount() - 1));
			ruleset.setChecked(ruleset.getRulesRow().getChecked());
		}

		CGSelection.select(preferences.getDefaultInt(Constants.CALL_GRAPH_SELECTION));

		forbidden.select(preferences.getDefaultInt(Constants.FORBIDDEN_METHOD_MARKER_TYPE));
		constraint.select(preferences.getDefaultInt(Constants.CONSTRAINT_ERROR_MARKER_TYPE));
		incompleteOp.select(preferences.getDefaultInt(Constants.INCOMPLETE_OPERATION_MARKER_TYPE));
		typestate.select(preferences.getDefaultInt(Constants.TYPESTATE_ERROR_MARKER_TYPE));
		neverType.select(preferences.getDefaultInt(Constants.NEVER_TYPEOF_MARKER_TYPE));
		reqPred.select(preferences.getDefaultInt(Constants.REQUIRED_PREDICATE_MARKER_TYPE));
	}

	@Override
	protected void storeValues() {
		preferences.setValue(Constants.AUTOMATED_ANALYSIS, automatedAnalysisCheckBox.getSelection());
		preferences.setValue(Constants.PROVIDER_DETECTION_ANALYSIS, providerDetectionCheckBox.getSelection());
		preferences.setValue(Constants.SHOW_SECURE_OBJECTS, secureObjectsCheckBox.getSelection());
		preferences.setValue(Constants.ANALYSE_DEPENDENCIES, analyseDependenciesCheckBox.getSelection());
		preferences.setValue(Constants.CALL_GRAPH_SELECTION, CGSelection.getSelectionIndex());
		preferences.setValue(Constants.FORBIDDEN_METHOD_MARKER_TYPE, forbidden.getSelectionIndex());
		preferences.setValue(Constants.CONSTRAINT_ERROR_MARKER_TYPE, constraint.getSelectionIndex());
		preferences.setValue(Constants.INCOMPLETE_OPERATION_MARKER_TYPE, incompleteOp.getSelectionIndex());
		preferences.setValue(Constants.NEVER_TYPEOF_MARKER_TYPE, neverType.getSelectionIndex());
		preferences.setValue(Constants.REQUIRED_PREDICATE_MARKER_TYPE, reqPred.getSelectionIndex());
		preferences.setValue(Constants.TYPESTATE_ERROR_MARKER_TYPE, typestate.getSelectionIndex());

		for (Iterator<Ruleset> itr = listOfRulesets.iterator(); itr.hasNext();) {
			Ruleset ruleset = (Ruleset) itr.next();

			Preferences subPref = rulePreferences.node(ruleset.getFolderName());
			subPref.putBoolean("CheckboxState", ruleset.isChecked());
			subPref.put("FolderName", ruleset.getFolderName());
			subPref.put("SelectedVersion", ruleset.getSelectedVersion());
			subPref.put("Url", ruleset.getUrl());
		}
	}

}