/*******************************************************************************
 * Copyright (c) 2013 Google, 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:
 * 	   Sergey Prigogin (Google) - initial API and implementation
 *******************************************************************************/
package org.eclipse.cdt.internal.ui.preferences;

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.Writer;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashSet;
import java.util.List;
import java.util.Map.Entry;
import java.util.Set;

import org.eclipse.core.filesystem.EFS;
import org.eclipse.core.filesystem.IFileStore;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Path;
import org.eclipse.jface.dialogs.MessageDialog;
import org.eclipse.jface.layout.PixelConverter;
import org.eclipse.jface.viewers.IFontProvider;
import org.eclipse.jface.viewers.ISelection;
import org.eclipse.jface.viewers.ITableLabelProvider;
import org.eclipse.jface.viewers.LabelProvider;
import org.eclipse.jface.window.Window;
import org.eclipse.osgi.util.NLS;
import org.eclipse.swt.SWT;
import org.eclipse.swt.events.SelectionEvent;
import org.eclipse.swt.events.SelectionListener;
import org.eclipse.swt.graphics.Font;
import org.eclipse.swt.graphics.Image;
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.Shell;
import org.eclipse.ui.PlatformUI;
import org.eclipse.ui.XMLMemento;

import com.ibm.icu.text.Collator;

import org.eclipse.cdt.internal.corext.codemanipulation.IncludeInfo;

import org.eclipse.cdt.internal.ui.ICHelpContextIds;
import org.eclipse.cdt.internal.ui.dialogs.ResizableStatusDialog;
import org.eclipse.cdt.internal.ui.dialogs.StatusInfo;
import org.eclipse.cdt.internal.ui.refactoring.includes.HeaderSubstitutionMap;
import org.eclipse.cdt.internal.ui.refactoring.includes.IncludeMap;
import org.eclipse.cdt.internal.ui.wizards.dialogfields.ComboDialogField;
import org.eclipse.cdt.internal.ui.wizards.dialogfields.DialogField;
import org.eclipse.cdt.internal.ui.wizards.dialogfields.IDialogFieldListener;
import org.eclipse.cdt.internal.ui.wizards.dialogfields.IListAdapter;
import org.eclipse.cdt.internal.ui.wizards.dialogfields.LayoutUtil;
import org.eclipse.cdt.internal.ui.wizards.dialogfields.ListDialogField;
import org.eclipse.cdt.internal.ui.wizards.dialogfields.Separator;
import org.eclipse.cdt.internal.ui.wizards.dialogfields.StringDialogField;

/**
 * Dialog for editing a header file substitution map.
 */
public class HeaderSubstitutionMapEditDialog extends ResizableStatusDialog {
	private static final String UTF_8 = "UTF-8"; //$NON-NLS-1$
	private static final String TAG_HEADER_SUBSTITUTION_MAP = "header_substitution_map"; //$NON-NLS-1$
	private static final Collator COLLATOR = Collator.getInstance();
	private static final Comparator<HeaderSubstitutionRule> SOURCE_COMPARATOR =
			new Comparator<HeaderSubstitutionRule>() {
		@Override
		public int compare(HeaderSubstitutionRule r1, HeaderSubstitutionRule r2) {
			return COLLATOR.compare(r1.getSource(), r2.getSource());
		}
	};

	private class HeaderSubstitutionListField extends ListDialogField<HeaderSubstitutionRule> {

		HeaderSubstitutionListField(IListAdapter<HeaderSubstitutionRule> adapter, String[] buttons) {
			super(adapter, buttons, new HeaderSubstitutionLabelProvider());
		}

		@Override
		protected boolean getManagedButtonState(ISelection sel, int index) {
			if (index == IDX_REMOVE) {
				return !sel.isEmpty();
			} else if (index == IDX_UP) {
				return !sel.isEmpty() && canMoveUp();
			} else if (index == IDX_DOWN) {
				return !sel.isEmpty() && canMoveDown();
			}
			return true;
		}

		@Override
		protected boolean managedButtonPressed(int index) {
			if (index == IDX_REMOVE) {
				remove();
			} else if (index == IDX_UP) {
				up();
			} else if (index == IDX_DOWN) {
				down();
			} else {
				return false;
			}
			return true;
		}

		@Override
		protected boolean canMoveUp() {
			if (!isOkToUse(fTableControl))
				return false;

			int[] indc= fTable.getTable().getSelectionIndices();
			for (int i= 0; i < indc.length; i++) {
				int index = indc[i];
				if (index == 0 ||
						SOURCE_COMPARATOR.compare(fElements.get(index), fElements.get(index - 1)) != 0) {
					return false;
				}
			}
			return true;
		}

		@Override
		protected boolean canMoveDown() {
			if (!isOkToUse(fTableControl))
				return false;

			int k= fElements.size() - 1;
			int[] indc= fTable.getTable().getSelectionIndices();
			for (int i= 0; i < indc.length; i++) {
				int index = indc[i];
				if (index == k ||
						SOURCE_COMPARATOR.compare(fElements.get(index), fElements.get(index + 1)) != 0) {
					return false;
				}
			}
			return true;
		}

	}

	private class HeaderSubstitutionLabelProvider extends LabelProvider implements ITableLabelProvider, IFontProvider {

		public HeaderSubstitutionLabelProvider() {
		}

		@Override
		public Image getImage(Object element) {
			return null;
		}

		@Override
		public String getText(Object element) {
			return getColumnText(element, 0);
		}

		@Override
		public Image getColumnImage(Object element, int columnIndex) {
			return null;
		}

		@Override
		public String getColumnText(Object element, int columnIndex) {
			HeaderSubstitutionRule substitution = (HeaderSubstitutionRule) element;
			return columnIndex == 0 ? substitution.getSource() : substitution.getTarget();
		}

		@Override
		public Font getFont(Object element) {
			return null;
		}
	}

	public class ListAdapter implements IListAdapter<HeaderSubstitutionRule>, IDialogFieldListener {
		private boolean canEdit(List<HeaderSubstitutionRule> selectedElements) {
			return selectedElements.size() == 1;
		}

		@Override
		public void customButtonPressed(ListDialogField<HeaderSubstitutionRule> field, int index) {
			onButtonPressed(field, index);
		}

		@Override
		public void selectionChanged(ListDialogField<HeaderSubstitutionRule> field) {
			List<HeaderSubstitutionRule> selectedElements = field.getSelectedElements();
			field.enableButton(IDX_EDIT, canEdit(selectedElements));
		}

		@Override
		public void doubleClicked(ListDialogField<HeaderSubstitutionRule> field) {
			if (canEdit(field.getSelectedElements())) {
				onButtonPressed(field, IDX_EDIT);
			}
		}

		@Override
		public void dialogFieldChanged(DialogField field) {
			updateButtonState();
		}
	}

	private static abstract class ButtonSelectionListener implements SelectionListener {
		@Override
		public void widgetDefaultSelected(SelectionEvent e) {
		}
	}

	private static final int IDX_ADD = 0;
	private static final int IDX_EDIT = 1;
	private static final int IDX_REMOVE = 2;
	private static final int IDX_UP = 3;
	private static final int IDX_DOWN = 4;

	private final StringDialogField fNameField;
	private final ComboDialogField fAppliesToField;
	private final HeaderSubstitutionListField fUnconditionalSubstitutionsField;
	private final HeaderSubstitutionListField fOptionalSubstitutionsField;

	private final Set<String> fExistingNames;
	private final boolean fNewMap;

	public HeaderSubstitutionMapEditDialog(Shell parent, HeaderSubstitutionMap map,
			List<HeaderSubstitutionMap> existingEntries) {
		super(parent);

		fExistingNames = new HashSet<String>();
		for (HeaderSubstitutionMap exising : existingEntries) {
			if (!exising.equals(map)) {
				fExistingNames.add(exising.getName());
			}
		}

		if (map == null) {
			fNewMap = true;
			setTitle(PreferencesMessages.HeaderSubstitutionMapEditDialog_new_title);
		} else {
			fNewMap = false;
			setTitle(PreferencesMessages.HeaderSubstitutionMapEditDialog_edit_title);
		}

		ListAdapter adapter = new ListAdapter();

		fNameField = new StringDialogField();
		fNameField.setLabelText(PreferencesMessages.HeaderSubstitutionMapEditDialog_name);
		fNameField.setDialogFieldListener(adapter);

		String[] items = new String[] {
			PreferencesMessages.HeaderSubstitutionMapEditDialog_c_and_cpp,
			PreferencesMessages.HeaderSubstitutionMapEditDialog_cpp_only,
		};

		fAppliesToField = new ComboDialogField(SWT.READ_ONLY);
		fAppliesToField.setLabelText(PreferencesMessages.HeaderSubstitutionMapEditDialog_applies_to);
		fAppliesToField.setItems(items);

		String[] buttons = new String[] {
				PreferencesMessages.HeaderSubstitutionMapEditDialog_add_button,
				PreferencesMessages.HeaderSubstitutionMapEditDialog_edit_button,
				PreferencesMessages.HeaderSubstitutionMapEditDialog_remove_button,
			};
		fUnconditionalSubstitutionsField = new HeaderSubstitutionListField(adapter, buttons);
		fUnconditionalSubstitutionsField.setLabelText(PreferencesMessages.HeaderSubstitutionMapEditDialog_required_substitution);
		fUnconditionalSubstitutionsField.setDialogFieldListener(adapter);

		final String[] columnsHeaders = new String[] {
				PreferencesMessages.HeaderSubstitutionMapEditDialog_header,
				PreferencesMessages.HeaderSubstitutionMapEditDialog_replacement,
			};
		fUnconditionalSubstitutionsField.setTableColumns(new ListDialogField.ColumnsDescription(columnsHeaders, true));

		buttons = new String[] {
				PreferencesMessages.HeaderSubstitutionMapEditDialog_add_button2,
				PreferencesMessages.HeaderSubstitutionMapEditDialog_edit_button2,
				PreferencesMessages.HeaderSubstitutionMapEditDialog_remove_button2,
				PreferencesMessages.HeaderSubstitutionMapEditDialog_up_button,
				PreferencesMessages.HeaderSubstitutionMapEditDialog_down_button,
			};
		fOptionalSubstitutionsField = new HeaderSubstitutionListField(adapter, buttons);
		fOptionalSubstitutionsField.setLabelText(PreferencesMessages.HeaderSubstitutionMapEditDialog_optional_substitution);
		fOptionalSubstitutionsField.setDialogFieldListener(adapter);
		fOptionalSubstitutionsField.enableButton(IDX_EDIT, false);

		fOptionalSubstitutionsField.setTableColumns(new ListDialogField.ColumnsDescription(columnsHeaders, true));

		updateFromMap(map);

		adapter.selectionChanged(fUnconditionalSubstitutionsField);
		adapter.selectionChanged(fOptionalSubstitutionsField);
	}

	private void updateFromMap(HeaderSubstitutionMap map) {
		fNameField.setText(map != null ? map.getName() : createUniqueName());

		fAppliesToField.selectItem(map != null && map.isCppOnly() ? 1 : 0);

		if (map != null) {
			List<HeaderSubstitutionRule> substitutionRules = getSubstitutionRules(map.getUnconditionalSubstitutionMap());
			fUnconditionalSubstitutionsField.setElements(substitutionRules);
			substitutionRules = getSubstitutionRules(map.getOptionalSubstitutionMap());
			fOptionalSubstitutionsField.setElements(substitutionRules);
		}
	}

	private String createUniqueName() {
		for (int i = 1; ; i++) {
			String name = NLS.bind(PreferencesMessages.HeaderSubstitutionMapEditDialog_default_map_name, i);
			if (!fExistingNames.contains(name))
				return name;
		}
	}

	private List<HeaderSubstitutionRule> getSubstitutionRules(IncludeMap map) {
		ArrayList<HeaderSubstitutionRule> result = new ArrayList<HeaderSubstitutionRule>();
		for (Entry<IncludeInfo, List<IncludeInfo>> entry : map.getMap().entrySet()) {
			String source = stripQuotes(entry.getKey().toString());
			for (IncludeInfo target : entry.getValue()) {
				boolean unconditional = map.isUnconditionalSubstitution();
				HeaderSubstitutionRule rule =
						new HeaderSubstitutionRule(source, stripQuotes(target.toString()), unconditional);
				result.add(rule);
			}

		}
		Collections.sort(result, SOURCE_COMPARATOR);
		return result;
	}

	private String stripQuotes(String str) {
		if (str.length() > 2 && str.charAt(0) == '"' && str.charAt(str.length() - 1) == '"')
			return str.substring(1, str.length() - 1);
		return str;
	}

	public HeaderSubstitutionMap getResult() {
		HeaderSubstitutionMap map = createEmptyMap();
		for (HeaderSubstitutionRule substitution : fUnconditionalSubstitutionsField.getElements()) {
			map.addMapping(substitution.getSource(), substitution.getTarget(), true);
		}
		for (HeaderSubstitutionRule substitution : fOptionalSubstitutionsField.getElements()) {
			map.addMapping(substitution.getSource(), substitution.getTarget(), false);
		}
		return map;
	}

	private HeaderSubstitutionMap createEmptyMap() {
		HeaderSubstitutionMap map = new HeaderSubstitutionMap(fAppliesToField.getSelectionIndex() != 0);
		map.setName(fNameField.getText().trim());
		return map;
	}

	@Override
	protected Control createDialogArea(Composite parent) {
		Composite composite = (Composite) super.createDialogArea(parent);

		PixelConverter conv = new PixelConverter(composite);

		Composite inner = new Composite(composite, SWT.NONE);
		inner.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true));
		GridLayout layout = new GridLayout();
		layout.marginHeight = 0;
		layout.marginWidth = 0;
		layout.numColumns = 4;
		inner.setLayout(layout);

		fNameField.doFillIntoGrid(inner, 3);
		if (fNewMap)
			fNameField.getTextControl(null).selectAll();
		Button button = new Button(inner, SWT.PUSH);
		button.setText(PreferencesMessages.HeaderSubstitutionMapEditDialog_import_button);
		button.setLayoutData(new GridData(GridData.FILL, GridData.CENTER, false, false));
		button.addSelectionListener(new ButtonSelectionListener() {
			@Override
			public void widgetSelected(SelectionEvent e) {
				importFromFile();
			}
		});

		fAppliesToField.doFillIntoGrid(inner, 3);
		button = new Button(inner, SWT.PUSH);
		button.setText(PreferencesMessages.HeaderSubstitutionMapEditDialog_export_button);
		button.setLayoutData(new GridData(GridData.FILL, GridData.CENTER, false, false));
		button.addSelectionListener(new ButtonSelectionListener() {
			@Override
			public void widgetSelected(SelectionEvent e) {
				exportToFile();
			}
		});

		Separator separator = new Separator(SWT.NONE);
		separator.doFillIntoGrid(inner, 4, conv.convertHeightInCharsToPixels(1));

	    int minHeight = convertHeightInCharsToPixels(12);

		fUnconditionalSubstitutionsField.doFillIntoGrid(inner, 4);
		LayoutUtil.setHeightHint(fUnconditionalSubstitutionsField.getListControl(null), minHeight);

		fOptionalSubstitutionsField.doFillIntoGrid(inner, 4);
	    LayoutUtil.setHeightHint(fOptionalSubstitutionsField.getListControl(null), minHeight);

	    applyDialogFont(composite);

		return composite;
	}

	private void importFromFile() {
		FileDialog dialog= new FileDialog(getShell(), SWT.OPEN);
		dialog.setText(PreferencesMessages.HeaderSubstitutionMapEditDialog_import_title);
		// TODO(sprigogin): Add import from .imp files
		// (see http://code.google.com/p/include-what-you-use/wiki/IWYUMappings)
		dialog.setFilterExtensions(new String[] { "*.xml" });  //$NON-NLS-1$
		String path= dialog.open();

		if (path == null)
			return;

		try {
			IFileStore fileStore = EFS.getLocalFileSystem().getStore(Path.fromOSString(path));
			InputStream stream = fileStore.openInputStream(EFS.NONE, null);
			InputStreamReader reader = new InputStreamReader(new BufferedInputStream(stream), UTF_8);
			try {
				HeaderSubstitutionMap map = HeaderSubstitutionMap.fromSerializedMemento(reader);
				updateFromMap(map);
			} finally {
				try {
					reader.close();
				} catch (IOException e) {
				}
			}
		} catch (IOException e) {
			String title= PreferencesMessages.HeaderSubstitutionMapEditDialog_import_title;
			String message= e.getLocalizedMessage();
			if (message != null) {
				message= NLS.bind(PreferencesMessages.HeaderSubstitutionMapEditDialog_error_parse_message, message);
			} else {
				message= NLS.bind(PreferencesMessages.HeaderSubstitutionMapEditDialog_error_read_message, path);
			}
			MessageDialog.openError(getShell(), title, message);
		} catch (CoreException e) {
			MessageDialog.openError(getShell(),
					PreferencesMessages.HeaderSubstitutionMapEditDialog_import_title,
					e.getLocalizedMessage());
		}

		updateButtonState();
	}

	private void exportToFile() {
		HeaderSubstitutionMap map = getResult();

		FileDialog dialog= new FileDialog(getShell(), SWT.SAVE);
		dialog.setText(PreferencesMessages.HeaderSubstitutionMapEditDialog_export_title);
		dialog.setFilterExtensions(new String[] { "*.xml" });  //$NON-NLS-1$
		dialog.setFileName(map.getName() + ".xml"); //$NON-NLS-1$
		dialog.setOverwrite(true);
		String path= dialog.open();

		if (path == null)
			return;

		try {
			IFileStore fileStore = EFS.getLocalFileSystem().getStore(Path.fromOSString(path));
			OutputStream stream = fileStore.openOutputStream(EFS.OVERWRITE, null);

			XMLMemento memento = XMLMemento.createWriteRoot(TAG_HEADER_SUBSTITUTION_MAP);
			map.saveToMemento(memento);
			Writer writer = new OutputStreamWriter(new BufferedOutputStream(stream), UTF_8);
			try {
				memento.save(writer);
			} finally {
				try {
					writer.close();
				} catch (IOException e) {
				}
			}
		} catch (IOException e) {
			MessageDialog.openError(getShell(),
					PreferencesMessages.HeaderSubstitutionMapEditDialog_export_title,
					PreferencesMessages.HeaderSubstitutionMapEditDialog_error_write_message);
		} catch (CoreException e) {
			MessageDialog.openError(getShell(),
					PreferencesMessages.HeaderSubstitutionMapEditDialog_export_title,
					e.getLocalizedMessage());
		}
	}

	@Override
	protected void configureShell(Shell newShell) {
		super.configureShell(newShell);
		PlatformUI.getWorkbench().getHelpSystem().setHelp(newShell, ICHelpContextIds.HEADER_SUBSTITUTION_MAP_EDIT_DIALOG);
	}

	private void onButtonPressed(ListDialogField<HeaderSubstitutionRule> field, int buttonId) {
		HeaderSubstitutionRule oldRule = null;
		if (buttonId == IDX_ADD) {
			oldRule = new HeaderSubstitutionRule("", "", field == fUnconditionalSubstitutionsField); //$NON-NLS-1$ //$NON-NLS-2$
		} else {
			oldRule = field.getSelectedElements().get(0);
		}

		switch (buttonId) {
		case IDX_ADD:
		case IDX_EDIT:
			HeaderSubstitutionEditDialog dialog = new HeaderSubstitutionEditDialog(getShell(), oldRule);
			if (dialog.open() != Window.OK)
				break;

			HeaderSubstitutionRule newRule = dialog.getResult();
			ListDialogField<HeaderSubstitutionRule> newField = newRule.isUnconditionalSubstitution() ?
					fUnconditionalSubstitutionsField : fOptionalSubstitutionsField;
			if (buttonId == IDX_ADD) {
				newField.addElement(newRule);
			} else {
				if (newField == field) {
					field.replaceElement(oldRule, newRule);
				} else {
					field.removeElement(oldRule);
					newField.addElement(newRule);
				}
			}
			// Restore order.
			List<HeaderSubstitutionRule> elements = newField.getElements();
			Collections.sort(elements, SOURCE_COMPARATOR);
			newField.setElements(elements);

			// There can be no more than one unconditional substitution for any header file.
			// The unconditional and optional substitutions are mutually exclusive.
			for (HeaderSubstitutionRule rule : fUnconditionalSubstitutionsField.getElements()) {
				if (rule != newRule && rule.getSource().equals(newRule.getSource())) {
					fUnconditionalSubstitutionsField.removeElement(rule);
				}
			}
			if (newRule.isUnconditionalSubstitution()) {
				List<HeaderSubstitutionRule> rulesToDelete = null;
				for (HeaderSubstitutionRule rule : fOptionalSubstitutionsField.getElements()) {
					if (rule.getSource().equals(newRule.getSource())) {
						if (rulesToDelete == null)
							rulesToDelete = new ArrayList<HeaderSubstitutionRule>();
						rulesToDelete.add(rule);
					}
				}
				if (rulesToDelete != null)
					fOptionalSubstitutionsField.removeElements(rulesToDelete);
			}
			break;
		}

		updateButtonState();
	}

	private void updateButtonState() {
		IStatus status = StatusInfo.OK_STATUS;
		String name = fNameField.getText().trim();
		if (name.isEmpty()) {
			status = new StatusInfo(IStatus.WARNING, PreferencesMessages.HeaderSubstitutionMapEditDialog_enter_name);
		} else if (fExistingNames.contains(name)) {
			status = new StatusInfo(IStatus.WARNING, PreferencesMessages.HeaderSubstitutionMapEditDialog_duplicate_name);
		} else if (fUnconditionalSubstitutionsField.getElements().isEmpty() &&
				fOptionalSubstitutionsField.getElements().isEmpty()) {
			status = new StatusInfo(IStatus.WARNING, PreferencesMessages.HeaderSubstitutionMapEditDialog_map_is_empty);
		}
		updateStatus(status);
	}

	@Override
	protected void updateButtonsEnableState(IStatus status) {
		// OK button is disabled unless the status is OK.
		super.updateButtonsEnableState(status.isOK() ? status : new StatusInfo(IStatus.ERROR, null));
	}
}
