/*******************************************************************************
 * Copyright (c) 2000, 2012 IBM Corporation and others.
 * Copyright (C) 2009, Tor Arne Vestbø <torarnv@gmail.com>
 * Copyright (C) 2010, Mathias Kinzler <mathias.kinzler@sap.com>
 *
 * 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
 *******************************************************************************/
package org.eclipse.egit.ui.internal.preferences;

import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.Observable;
import java.util.Observer;

import org.eclipse.core.resources.IResource;
import org.eclipse.egit.ui.Activator;
import org.eclipse.egit.ui.UIPreferences;
import org.eclipse.egit.ui.internal.GitLabelProvider;
import org.eclipse.egit.ui.internal.SWTUtils;
import org.eclipse.egit.ui.internal.UIText;
import org.eclipse.egit.ui.internal.decorators.GitLightweightDecorator.DecorationHelper;
import org.eclipse.egit.ui.internal.decorators.DecorationResult;
import org.eclipse.egit.ui.internal.decorators.IDecoratableResource;
import org.eclipse.egit.ui.internal.decorators.IDecoratableResource.Staged;
import org.eclipse.egit.ui.internal.synchronize.mapping.GitChangeSetLabelProvider;
import org.eclipse.jface.dialogs.Dialog;
import org.eclipse.jface.preference.IPersistentPreferenceStore;
import org.eclipse.jface.preference.IPreferenceStore;
import org.eclipse.jface.preference.PreferencePage;
import org.eclipse.jface.preference.PreferenceStore;
import org.eclipse.jface.resource.ImageDescriptor;
import org.eclipse.jface.resource.JFaceResources;
import org.eclipse.jface.resource.LocalResourceManager;
import org.eclipse.jface.resource.ResourceManager;
import org.eclipse.jface.util.IPropertyChangeListener;
import org.eclipse.jface.util.PropertyChangeEvent;
import org.eclipse.jface.viewers.ArrayContentProvider;
import org.eclipse.jface.viewers.DecorationOverlayIcon;
import org.eclipse.jface.viewers.IDecoration;
import org.eclipse.jface.viewers.ILabelProvider;
import org.eclipse.jface.viewers.IStructuredContentProvider;
import org.eclipse.jface.viewers.ITreeContentProvider;
import org.eclipse.jface.viewers.LabelProvider;
import org.eclipse.jface.viewers.TreeViewer;
import org.eclipse.jface.viewers.Viewer;
import org.eclipse.jface.window.Window;
import org.eclipse.jgit.lib.RepositoryState;
import org.eclipse.swt.SWT;
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.events.SelectionListener;
import org.eclipse.swt.graphics.Image;
import org.eclipse.swt.layout.GridData;
import org.eclipse.swt.widgets.Button;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Control;
import org.eclipse.swt.widgets.Label;
import org.eclipse.swt.widgets.TabFolder;
import org.eclipse.swt.widgets.TabItem;
import org.eclipse.swt.widgets.Text;
import org.eclipse.swt.widgets.TreeItem;
import org.eclipse.ui.ISharedImages;
import org.eclipse.ui.IWorkbench;
import org.eclipse.ui.IWorkbenchPreferencePage;
import org.eclipse.ui.PlatformUI;
import org.eclipse.ui.dialogs.ListSelectionDialog;
import org.eclipse.ui.ide.IDE.SharedImages;
import org.eclipse.ui.preferences.IWorkbenchPreferenceContainer;

/**
 * Preference page for customizing Git label decorations
 */
public class GitDecoratorPreferencePage extends PreferencePage implements
		IWorkbenchPreferencePage {

	private GeneralTab generalTab;

	private TextDecorationTab textDecorationTab;

	private IconDecorationTab iconDecorationTab;

	private OtherDecorationTab otherDecorationTab;

	private Preview navigatorPreview;

	private Preview changeSetPreview;

	private boolean tabsInitialized;

	private static final Collection PREVIEW_FILESYSTEM_ROOT;

	private static final Map<String, String> FILE_AND_FOLDER_BINDINGS;

	private static final Map<String, String> PROJECT_BINDINGS;

	private static final Map<String, String> CHANGESET_LABEL_BINDINGS;

	private static IPropertyChangeListener themeListener;

	static {
		final PreviewResource project = new PreviewResource(
				"Project", IResource.PROJECT, "repository" + '|' + RepositoryState.MERGING.getDescription(), "master", "↑2 ↓1", true, false, true, Staged.NOT_STAGED, false, false); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$
		final ArrayList<PreviewResource> children = new ArrayList<PreviewResource>();

		children
				.add(new PreviewResource(
						"folder", IResource.FOLDER, "repository", null, null, true, false, true, Staged.NOT_STAGED, false, false)); //$NON-NLS-1$ //$NON-NLS-2$
		children
				.add(new PreviewResource(
						"tracked.txt", IResource.FILE, "repository", null, null, true, false, false, Staged.NOT_STAGED, false, false)); //$NON-NLS-1$ //$NON-NLS-2$
		children
				.add(new PreviewResource(
						"untracked.txt", IResource.FILE, "repository", null, null, false, false, false, Staged.NOT_STAGED, false, false)); //$NON-NLS-1$ //$NON-NLS-2$
		children
				.add(new PreviewResource(
						"ignored.txt", IResource.FILE, "repository", null, null, false, true, false, Staged.NOT_STAGED, false, false)); //$NON-NLS-1$ //$NON-NLS-2$
		children
				.add(new PreviewResource(
						"dirty.txt", IResource.FILE, "repository", null, null, true, false, true, Staged.NOT_STAGED, false, false)); //$NON-NLS-1$ //$NON-NLS-2$
		children
				.add(new PreviewResource(
						"staged.txt", IResource.FILE, "repository", null, null, true, false, false, Staged.MODIFIED, false, false)); //$NON-NLS-1$ //$NON-NLS-2$
		children
				.add(new PreviewResource(
						"partially-staged.txt", IResource.FILE, "repository", null, null, true, false, true, Staged.MODIFIED, false, false)); //$NON-NLS-1$ //$NON-NLS-2$
		children
				.add(new PreviewResource(
						"added.txt", IResource.FILE, "repository", null, null, true, false, false, Staged.ADDED, false, false)); //$NON-NLS-1$ //$NON-NLS-2$
		children
				.add(new PreviewResource(
						"removed.txt", IResource.FILE, "repository", null, null, true, false, false, Staged.REMOVED, false, false)); //$NON-NLS-1$ //$NON-NLS-2$
		children
				.add(new PreviewResource(
						"conflict.txt", IResource.FILE, "repository", null, null, true, false, true, Staged.NOT_STAGED, true, false)); //$NON-NLS-1$ //$NON-NLS-2$
		children
				.add(new PreviewResource(
						"assume-valid.txt", IResource.FILE, "repository", null, null, true, false, false, Staged.NOT_STAGED, false, true)); //$NON-NLS-1$ //$NON-NLS-2$
		project.children = children;
		PREVIEW_FILESYSTEM_ROOT = Collections.singleton(project);

		FILE_AND_FOLDER_BINDINGS = new HashMap<String, String>();
		FILE_AND_FOLDER_BINDINGS.put(DecorationHelper.BINDING_RESOURCE_NAME,
				UIText.DecoratorPreferencesPage_bindingResourceName);
		FILE_AND_FOLDER_BINDINGS.put(DecorationHelper.BINDING_DIRTY_FLAG,
				UIText.DecoratorPreferencesPage_bindingDirtyFlag);
		FILE_AND_FOLDER_BINDINGS.put(DecorationHelper.BINDING_STAGED_FLAG,
				UIText.DecoratorPreferencesPage_bindingStagedFlag);

		PROJECT_BINDINGS = new HashMap<String, String>();
		PROJECT_BINDINGS.put(DecorationHelper.BINDING_RESOURCE_NAME,
				UIText.DecoratorPreferencesPage_bindingResourceName);
		PROJECT_BINDINGS.put(DecorationHelper.BINDING_DIRTY_FLAG,
				UIText.DecoratorPreferencesPage_bindingDirtyFlag);
		PROJECT_BINDINGS.put(DecorationHelper.BINDING_STAGED_FLAG,
				UIText.DecoratorPreferencesPage_bindingStagedFlag);
		PROJECT_BINDINGS.put(DecorationHelper.BINDING_REPOSITORY_NAME,
				UIText.GitDecoratorPreferencePage_bindingRepositoryNameFlag);
		PROJECT_BINDINGS.put(DecorationHelper.BINDING_BRANCH_NAME,
				UIText.DecoratorPreferencesPage_bindingBranchName);
		PROJECT_BINDINGS.put(DecorationHelper.BINDING_BRANCH_STATUS,
				UIText.DecoratorPreferencesPage_bindingBranchStatus);


		CHANGESET_LABEL_BINDINGS = new HashMap<String, String>();
		CHANGESET_LABEL_BINDINGS.put(removeBraces(GitChangeSetLabelProvider.BINDING_CHANGESET_AUTHOR),
				UIText.DecoratorPreferencesPage_bindingChangeSetAuthor);
		CHANGESET_LABEL_BINDINGS.put(removeBraces(GitChangeSetLabelProvider.BINDING_CHANGESET_DATE),
				UIText.DecoratorPreferencesPage_bindingChangeSetDate);
		CHANGESET_LABEL_BINDINGS.put(removeBraces(GitChangeSetLabelProvider.BINDING_CHANGESET_COMMITTER),
				UIText.DecoratorPreferencesPage_bindingChangeSetCommitter);
		CHANGESET_LABEL_BINDINGS.put(removeBraces(GitChangeSetLabelProvider.BINDING_CHANGESET_SHORT_MESSAGE),
				UIText.DecoratorPreferencesPage_bindingChangeSetShortMessage);
	}

	private static String removeBraces(String string) {
		return string.replaceAll("[}{]", ""); //$NON-NLS-1$ //$NON-NLS-2$
	}


	/**
	 * @see PreferencePage#createContents(Composite)
	 */
	protected Control createContents(Composite parent) {

		Composite composite = SWTUtils.createHVFillComposite(parent,
				SWTUtils.MARGINS_NONE);

		SWTUtils.createLabel(composite, UIText.DecoratorPreferencesPage_description);

		Composite folderComposite = SWTUtils.createHFillComposite(composite,
				SWTUtils.MARGINS_NONE);

		TabFolder tabFolder = new TabFolder(folderComposite, SWT.NONE);
		tabFolder.setLayoutData(SWTUtils.createHVFillGridData());

		tabFolder.addSelectionListener(new SelectionAdapter() {

			public void widgetSelected(SelectionEvent e) {
				if (navigatorPreview != null && changeSetPreview != null) {
					if (UIText.DecoratorPreferencesPage_otherDecorations.equals(e.item.getData())) {
						navigatorPreview.hide();
						changeSetPreview.show();
					} else {
						changeSetPreview.hide();
						navigatorPreview.show();
					}
				}
			}

		});

		changeSetPreview = new ChangeSetPreview(composite);
		navigatorPreview = new NavigatorPreview(composite);

		generalTab = new GeneralTab(tabFolder);
		textDecorationTab = new TextDecorationTab(tabFolder);
		iconDecorationTab = new IconDecorationTab(tabFolder);
		otherDecorationTab = new OtherDecorationTab(tabFolder);

		initializeValues();

		changeSetPreview.hide();

		changeSetPreview.refresh();
		navigatorPreview.refresh();

		generalTab.addObserver(navigatorPreview);
		textDecorationTab.addObserver(navigatorPreview);
		iconDecorationTab.addObserver(navigatorPreview);

		otherDecorationTab.addObserver(changeSetPreview);

		// TODO: Add help text for this preference page

		themeListener = new IPropertyChangeListener() {
			public void propertyChange(PropertyChangeEvent event) {
				navigatorPreview.refresh();
				changeSetPreview.refresh();
			}
		};
		PlatformUI.getWorkbench().getThemeManager().addPropertyChangeListener(
				themeListener);

		Dialog.applyDialogFont(parent);

		return tabFolder;
	}

	/**
	 * Wrapper class for TabItems used in the preference page TabFolder
	 *
	 * The reason for this wrapper is mainly that the class TabItem is not
	 * supposed to be subclassed.
	 *
	 * When controls in the tab change it will emit update() to any registered
	 * observers. This is currently used for updating the decoration preview.
	 */
	private abstract class Tab extends Observable {
		public abstract void initializeValues(IPreferenceStore store);

		public abstract void performDefaults(IPreferenceStore store);

		public abstract void performOk(IPreferenceStore store);
	}

	/**
	 * Tab page for general preferences related to decoration
	 */
	private class GeneralTab extends Tab implements SelectionListener {

		private Button recomputeAncestorDecorations;

		public GeneralTab(TabFolder parent) {
			Composite composite = SWTUtils.createHVFillComposite(parent,
					SWTUtils.MARGINS_DEFAULT, 1);

			recomputeAncestorDecorations = SWTUtils
					.createCheckBox(
							composite,
							UIText.DecoratorPreferencesPage_recomputeAncestorDecorations);
			recomputeAncestorDecorations
					.setToolTipText(UIText.DecoratorPreferencesPage_recomputeAncestorDecorationsTooltip);

			SWTUtils.createPreferenceLink(
					(IWorkbenchPreferenceContainer) getContainer(), composite,
					"org.eclipse.ui.preferencePages.Decorators", //$NON-NLS-1$
					UIText.DecoratorPreferencesPage_labelDecorationsLink);

			SWTUtils.createPreferenceLink(
					(IWorkbenchPreferenceContainer) getContainer(), composite,
					"org.eclipse.ui.preferencePages.ColorsAndFonts", //$NON-NLS-1$
					UIText.DecoratorPreferencesPage_colorsAndFontsLink);

			recomputeAncestorDecorations.addSelectionListener(this);

			final TabItem tabItem = new TabItem(parent, SWT.NONE);
			tabItem.setText(UIText.DecoratorPreferencesPage_generalTabFolder);
			tabItem.setControl(composite);
		}

		public void initializeValues(IPreferenceStore store) {
			recomputeAncestorDecorations.setSelection(store
					.getBoolean(UIPreferences.DECORATOR_RECOMPUTE_ANCESTORS));
		}

		public void performDefaults(IPreferenceStore store) {
			recomputeAncestorDecorations
					.setSelection(store
							.getDefaultBoolean(UIPreferences.DECORATOR_RECOMPUTE_ANCESTORS));
		}

		public void performOk(IPreferenceStore store) {
			store.setValue(UIPreferences.DECORATOR_RECOMPUTE_ANCESTORS,
					recomputeAncestorDecorations.getSelection());
		}

		public void widgetSelected(SelectionEvent e) {
			setChanged();
			notifyObservers();
		}

		public void widgetDefaultSelected(SelectionEvent e) {
			// Not interesting for us
		}

	}

	/**
	 * The tab page for text-decoration preferences
	 */
	private class TextDecorationTab extends Tab implements ModifyListener {

		private final FormatEditor fileTextFormat;

		private final FormatEditor folderTextFormat;

		private final FormatEditor projectTextFormat;

		public TextDecorationTab(TabFolder parent) {
			Composite composite = SWTUtils.createHVFillComposite(parent,
					SWTUtils.MARGINS_DEFAULT, 3);

			fileTextFormat = new FormatEditor(composite,
					UIText.DecoratorPreferencesPage_fileFormatLabel,
					UIText.DecoratorPreferencesPage_addVariablesAction,
					FILE_AND_FOLDER_BINDINGS,
					UIPreferences.DECORATOR_FILETEXT_DECORATION);
			folderTextFormat = new FormatEditor(composite,
					UIText.DecoratorPreferencesPage_folderFormatLabel,
					UIText.DecoratorPreferencesPage_addVariablesAction2,
					FILE_AND_FOLDER_BINDINGS,
					UIPreferences.DECORATOR_FOLDERTEXT_DECORATION);
			projectTextFormat = new FormatEditor(composite,
					UIText.DecoratorPreferencesPage_projectFormatLabel,
					UIText.DecoratorPreferencesPage_addVariablesAction3,
					PROJECT_BINDINGS,
					UIPreferences.DECORATOR_PROJECTTEXT_DECORATION);

			fileTextFormat.addModifyListener(this);
			folderTextFormat.addModifyListener(this);
			projectTextFormat.addModifyListener(this);

			final TabItem tabItem = new TabItem(parent, SWT.NONE);
			tabItem.setText(UIText.DecoratorPreferencesPage_textLabel);
			tabItem.setControl(composite);
		}

		public void initializeValues(IPreferenceStore store) {
			fileTextFormat.initializeValue(store);
			folderTextFormat.initializeValue(store);
			projectTextFormat.initializeValue(store);
		}

		public void performDefaults(IPreferenceStore store) {
			fileTextFormat.performDefaults(store);
			folderTextFormat.performDefaults(store);
			projectTextFormat.performDefaults(store);
		}

		public void performOk(IPreferenceStore store) {
			fileTextFormat.performOk(store);
			folderTextFormat.performOk(store);
			projectTextFormat.performOk(store);
		}

		public void modifyText(ModifyEvent e) {
			setChanged();
			notifyObservers();
		}

	}

	private class OtherDecorationTab extends Tab implements ModifyListener {

		private final FormatEditor changeSetLabelFormat;

		private final Text dateFormat;

		private final Label dateFormatPreview;

		private final Date exampleDate = new Date();

		private boolean formatValid;

		public OtherDecorationTab(TabFolder parent) {
			Composite composite = SWTUtils.createHVFillComposite(parent,
					SWTUtils.MARGINS_DEFAULT, 3);

			changeSetLabelFormat = new FormatEditor(composite,
					UIText.DecoratorPreferencesPage_changeSetLabelFormat,
					UIText.DecoratorPreferencesPage_addVariablesAction3,
					CHANGESET_LABEL_BINDINGS,
					UIPreferences.SYNC_VIEW_CHANGESET_LABEL_FORMAT);

			final TabItem tabItem = new TabItem(parent, SWT.NONE);

			Label dfLabel = SWTUtils.createLabel(composite, UIText.DecoratorPreferencesPage_dateFormat);
			dfLabel.setLayoutData(SWTUtils.createGridData(SWT.DEFAULT,
					SWT.DEFAULT, false, false));
			dateFormat = SWTUtils.createText(composite, 2);

			Label dpLabel = SWTUtils.createLabel(composite, UIText.DecoratorPreferencesPage_dateFormatPreview);
			dpLabel.setLayoutData(SWTUtils.createGridData(SWT.DEFAULT,
					SWT.DEFAULT, false, false));
			dateFormatPreview = SWTUtils.createLabel(composite, null, 2);

			tabItem.setText(UIText.DecoratorPreferencesPage_otherDecorations);
			tabItem.setControl(composite);
			tabItem.setData(UIText.DecoratorPreferencesPage_otherDecorations);

			changeSetLabelFormat.addModifyListener(this);
			dateFormat.addModifyListener(this);
		}

		private void updateDateFormatPreview() {
			SimpleDateFormat sdf;
			try {
				sdf = new SimpleDateFormat(dateFormat.getText());
				dateFormatPreview.setText(sdf.format(exampleDate));
				formatValid = true;
			} catch (Exception ex) {
				dateFormatPreview.setText(UIText.DecoratorPreferencesPage_wrongDateFormat);
				formatValid = false;
			}
		}

		public void initializeValues(IPreferenceStore store) {
			changeSetLabelFormat.initializeValue(store);
			dateFormat.setText(store.getString(UIPreferences.DATE_FORMAT));
		}

		public void performDefaults(IPreferenceStore store) {
			changeSetLabelFormat.performDefaults(store);
			dateFormat.setText(store.getDefaultString(UIPreferences.DATE_FORMAT));
		}

		public void performOk(IPreferenceStore store) {
			changeSetLabelFormat.performOk(store);

			if (formatValid) {
				store.setValue(UIPreferences.DATE_FORMAT, dateFormat.getText());
			}
		}

		public void modifyText(ModifyEvent e) {
			updateDateFormatPreview();
			setChanged();
			notifyObservers();
		}
	}

	private static final class FormatEditor extends SelectionAdapter {
		private final Text text;

		private final Map bindings;

		private final String key;

		public FormatEditor(Composite composite, String title,
				String buttonText, Map bindings, String key) {

			this.key = key;
			this.bindings = bindings;

			final Label label = SWTUtils.createLabel(composite, title);
			label.setLayoutData(SWTUtils.createGridData(SWT.DEFAULT,
					SWT.DEFAULT, false, false));

			text = SWTUtils.createText(composite);

			final Button button = new Button(composite, SWT.NONE);
			button.setText(buttonText);
			button.setLayoutData(new GridData());

			button.addSelectionListener(this);
		}

		public void addModifyListener(ModifyListener listener) {
			text.addModifyListener(listener);
		}

		public void widgetSelected(SelectionEvent e) {
			final ILabelProvider labelProvider = new LabelProvider() {
				public String getText(Object element) {
					return ((Map.Entry) element).getKey()
					+ " - " + ((Map.Entry) element).getValue(); //$NON-NLS-1$
				}
			};

			final IStructuredContentProvider contentsProvider = ArrayContentProvider.getInstance();

			final ListSelectionDialog dialog = new ListSelectionDialog(text
					.getShell(), bindings.entrySet(), contentsProvider,
					labelProvider,
					UIText.DecoratorPreferencesPage_selectVariablesToAdd);
			dialog.setHelpAvailable(false);
			dialog
			.setTitle(UIText.DecoratorPreferencesPage_addVariablesTitle);
			if (dialog.open() != Window.OK)
				return;

			Object[] result = dialog.getResult();

			for (int i = 0; i < result.length; i++) {
				text.insert("{" + ((Map.Entry) result[i]).getKey() + "}"); //$NON-NLS-1$ //$NON-NLS-2$
			}
		}

		public void performOk(IPreferenceStore store) {
			store.setValue(key, text.getText());
		}

		public void performDefaults(IPreferenceStore store) {
			store.setToDefault(key);
			text.setText(store.getDefaultString(key));
		}

		public void initializeValue(IPreferenceStore store) {
			text.setText(store.getString(key));
		}
	}

	/**
	 * Tab page for icon-related preferences
	 */
	private class IconDecorationTab extends Tab implements SelectionListener {

		private Button showTracked;

		private Button showUntracked;

		private Button showStaged;

		private Button showConflicts;

		private Button showAssumeValid;

		private Button showDirty;

		public IconDecorationTab(TabFolder parent) {
			Composite composite = SWTUtils.createHVFillComposite(parent,
					SWTUtils.MARGINS_DEFAULT, 2);

			showTracked = SWTUtils.createCheckBox(composite,
					UIText.DecoratorPreferencesPage_iconsShowTracked);
			showUntracked = SWTUtils.createCheckBox(composite,
					UIText.DecoratorPreferencesPage_iconsShowUntracked);
			showStaged = SWTUtils.createCheckBox(composite,
					UIText.DecoratorPreferencesPage_iconsShowStaged);
			showConflicts = SWTUtils.createCheckBox(composite,
					UIText.DecoratorPreferencesPage_iconsShowConflicts);
			showAssumeValid = SWTUtils.createCheckBox(composite,
					UIText.DecoratorPreferencesPage_iconsShowAssumeValid);
			showDirty = SWTUtils.createCheckBox(composite,
					UIText.GitDecoratorPreferencePage_iconsShowDirty);

			showTracked.addSelectionListener(this);
			showUntracked.addSelectionListener(this);
			showStaged.addSelectionListener(this);
			showConflicts.addSelectionListener(this);
			showAssumeValid.addSelectionListener(this);
			showDirty.addSelectionListener(this);

			final TabItem tabItem = new TabItem(parent, SWT.NONE);
			tabItem.setText(UIText.DecoratorPreferencesPage_iconLabel);
			tabItem.setControl(composite);
		}

		public void initializeValues(IPreferenceStore store) {
			showTracked.setSelection(store
					.getBoolean(UIPreferences.DECORATOR_SHOW_TRACKED_ICON));
			showUntracked.setSelection(store
					.getBoolean(UIPreferences.DECORATOR_SHOW_UNTRACKED_ICON));
			showStaged.setSelection(store
					.getBoolean(UIPreferences.DECORATOR_SHOW_STAGED_ICON));
			showConflicts.setSelection(store
					.getBoolean(UIPreferences.DECORATOR_SHOW_CONFLICTS_ICON));
			showAssumeValid
					.setSelection(store
							.getBoolean(UIPreferences.DECORATOR_SHOW_ASSUME_VALID_ICON));
			showDirty.setSelection(store
					.getBoolean(UIPreferences.DECORATOR_SHOW_DIRTY_ICON));
		}

		public void performDefaults(IPreferenceStore store) {
			showTracked
					.setSelection(store
							.getDefaultBoolean(UIPreferences.DECORATOR_SHOW_TRACKED_ICON));
			showUntracked
					.setSelection(store
							.getDefaultBoolean(UIPreferences.DECORATOR_SHOW_UNTRACKED_ICON));
			showStaged
					.setSelection(store
							.getDefaultBoolean(UIPreferences.DECORATOR_SHOW_STAGED_ICON));
			showConflicts
					.setSelection(store
							.getDefaultBoolean(UIPreferences.DECORATOR_SHOW_CONFLICTS_ICON));
			showAssumeValid
					.setSelection(store
							.getDefaultBoolean(UIPreferences.DECORATOR_SHOW_ASSUME_VALID_ICON));
			showDirty
					.setSelection(store
							.getDefaultBoolean(UIPreferences.DECORATOR_SHOW_DIRTY_ICON));
		}

		public void performOk(IPreferenceStore store) {
			store.setValue(UIPreferences.DECORATOR_SHOW_TRACKED_ICON,
					showTracked.getSelection());
			store.setValue(UIPreferences.DECORATOR_SHOW_UNTRACKED_ICON,
					showUntracked.getSelection());
			store.setValue(UIPreferences.DECORATOR_SHOW_STAGED_ICON, showStaged
					.getSelection());
			store.setValue(UIPreferences.DECORATOR_SHOW_CONFLICTS_ICON,
					showConflicts.getSelection());
			store.setValue(UIPreferences.DECORATOR_SHOW_ASSUME_VALID_ICON,
					showAssumeValid.getSelection());
			store.setValue(UIPreferences.DECORATOR_SHOW_DIRTY_ICON,
					showDirty.getSelection());
		}

		public void widgetSelected(SelectionEvent e) {
			setChanged();
			notifyObservers();
		}

		public void widgetDefaultSelected(SelectionEvent e) {
			// Not interesting for us
		}
	}

	/**
	 * Initializes states of the controls from the preference store.
	 */
	private void initializeValues() {
		final IPreferenceStore store = getPreferenceStore();
		generalTab.initializeValues(store);
		textDecorationTab.initializeValues(store);
		iconDecorationTab.initializeValues(store);
		otherDecorationTab.initializeValues(store);
		setValid(true);
		tabsInitialized = true;
	}

	/**
	 * @see IWorkbenchPreferencePage#init(IWorkbench)
	 */
	public void init(IWorkbench workbench) {
		// No-op
	}

	/**
	 * OK was clicked. Store the preferences to the plugin store
	 *
	 * @return whether it is okay to close the preference page
	 */
	public boolean performOk() {
		IPreferenceStore store = getPreferenceStore();
		final boolean okToClose = performOk(store);
		if (store.needsSaving()) {
			try {
				((IPersistentPreferenceStore)store).save();
				Activator.broadcastPropertyChange(new PropertyChangeEvent(this,
						Activator.DECORATORS_CHANGED, null, null));
			} catch (IOException e) {
				Activator.handleError(e.getMessage(), e, true);
			}
		}
		return okToClose;
	}

	/**
	 * Store the preferences to the given preference store
	 *
	 * @param store
	 *            the preference store to store the preferences to
	 *
	 * @return whether it operation succeeded
	 */
	private boolean performOk(IPreferenceStore store) {
		generalTab.performOk(store);
		textDecorationTab.performOk(store);
		iconDecorationTab.performOk(store);
		otherDecorationTab.performOk(store);
		return true;
	}

	/**
	 * Defaults was clicked. Restore the Git decoration preferences to their
	 * default values
	 */
	protected void performDefaults() {
		IPreferenceStore store = getPreferenceStore();
		generalTab.performDefaults(store);
		textDecorationTab.performDefaults(store);
		iconDecorationTab.performDefaults(store);
		otherDecorationTab.performDefaults(store);
		super.performDefaults();
		navigatorPreview.refresh();
		changeSetPreview.refresh();
	}

	/**
	 * Returns the preference store that belongs to our plugin.
	 *
	 * This is important because we want to store our preferences separately
	 * from the desktop.
	 *
	 * @return the preference store for this plugin
	 */
	protected IPreferenceStore doGetPreferenceStore() {
		return Activator.getDefault().getPreferenceStore();
	}

	/*
	 * (non-Javadoc)
	 *
	 * @see org.eclipse.jface.dialogs.DialogPage#dispose()
	 */
	public void dispose() {
		PlatformUI.getWorkbench().getThemeManager()
				.removePropertyChangeListener(themeListener);
		super.dispose();
	}

	private abstract class Preview
			implements Observer {

		protected PreferenceStore store = new PreferenceStore();
		protected final TreeViewer fViewer;
		private Composite composite;
		private Composite parent;

		protected final ResourceManager fImageCache = new LocalResourceManager(
				JFaceResources.getResources());

		public Preview(Composite parent) {
			this.parent = parent;
			composite = SWTUtils.createHVFillComposite(parent, SWTUtils.MARGINS_NONE);

			SWTUtils.createLabel(composite, UIText.DecoratorPreferencesPage_preview);

			fViewer = new TreeViewer(composite);
			fViewer.getControl().setLayoutData(SWTUtils.createHVFillGridData());
		}

		public void update(Observable o, Object arg) {
			refresh();
		}

		public abstract void refresh();

		public void dispose() {
			fImageCache.dispose();
		}

		public void hide() {
			((GridData)composite.getLayoutData()).exclude = true;	// ignore by layout
			composite.setVisible(false);
			composite.layout();
			parent.layout();
		}

		public void show() {
			((GridData)composite.getLayoutData()).exclude = false;	// ignore by layout
			composite.setVisible(true);
			composite.layout();
			parent.layout();
		}
	}

	private class ChangeSetPreview extends Preview implements
			ITreeContentProvider {

		public ChangeSetPreview(Composite composite) {
			super(composite);
			fViewer.setContentProvider(this);
			fViewer.setLabelProvider(new GitLabelProvider() {

				@Override
				public Image getImage(Object element) {
					if (element instanceof GitModelCommitMockup)
						return getChangesetIcon();

					return super.getImage(element);
				}

				public String getText(Object element) {
					if (element instanceof GitModelCommitMockup) {
						String format = store.getString(UIPreferences.SYNC_VIEW_CHANGESET_LABEL_FORMAT);
						String dateFormat = store.getString(UIPreferences.DATE_FORMAT);
						return ((GitModelCommitMockup)element).getMokeupText(format, dateFormat);
					}
					return super.getText(element);
				}
			});
			fViewer.setContentProvider(this);
			fViewer.setInput(new GitModelCommitMockup());
		}

		public Object[] getChildren(Object parentElement) {
			return new Object[0];
		}

		public Object getParent(Object element) {
			return null;
		}

		public boolean hasChildren(Object element) {
			return false;
		}

		public Object[] getElements(Object inputElement) {
			return new Object[] { inputElement };
		}

		public void inputChanged(Viewer viewer, Object oldInput, Object newInput) {
			// No-op
		}

		public void refresh() {
			store = new PreferenceStore();
			performOk(store);
			fViewer.refresh(true);
		}
	}


	/**
	 * NavigatorPreview control for showing how changes in the dialog will affect
	 * decoration
	 */
	private class NavigatorPreview extends Preview
			implements ITreeContentProvider {

		private DecorationHelper fHelper;

		public NavigatorPreview(Composite composite) {
			super(composite);
			// Has to happen before the tree control is constructed
			reloadDecorationHelper();

			fViewer.setContentProvider(this);
			fViewer.setLabelProvider(new ResLabelProvider());
			fViewer.setInput(PREVIEW_FILESYSTEM_ROOT);
			fViewer.expandAll();
			fHelper = new DecorationHelper(new PreferenceStore());
		}

		private void reloadDecorationHelper() {
			store = new PreferenceStore();
			if (tabsInitialized)
				performOk(store);

			fHelper = new DecorationHelper(store);
		}

		public void refresh() {
			reloadDecorationHelper();
			fViewer.refresh(true);
			setColorsAndFonts(fViewer.getTree().getItems());
		}

		private void setColorsAndFonts(TreeItem[] items) {
			for (int i = 0; i < items.length; i++) {
				DecorationResult decoration = getDecoration(items[i].getData());
				items[i].setBackground(decoration.getBackgroundColor());
				items[i].setForeground(decoration.getForegroundColor());
				items[i].setFont(decoration.getFont());
				setColorsAndFonts(items[i].getItems());
			}
		}

		public Object[] getChildren(Object parentElement) {
			return ((PreviewResource) parentElement).children.toArray();
		}

		public Object getParent(Object element) {
			return null;
		}

		public boolean hasChildren(Object element) {
			return !((PreviewResource) element).children.isEmpty();
		}

		public Object[] getElements(Object inputElement) {
			return ((Collection) inputElement).toArray();
		}

		public void inputChanged(Viewer viewer, Object oldInput, Object newInput) {
			// No-op
		}

		private DecorationResult getDecoration(Object element) {
			DecorationResult decoration = new DecorationResult();
			fHelper.decorate(decoration, (PreviewResource) element);
			return decoration;
		}

		private class ResLabelProvider extends LabelProvider {

			public String getText(Object element) {
				final DecorationResult decoration = getDecoration(element);
				final StringBuilder buffer = new StringBuilder();
				final String prefix = decoration.getPrefix();
				if (prefix != null)
					buffer.append(prefix);
				buffer.append(((PreviewResource) element).getName());
				final String suffix = decoration.getSuffix();
				if (suffix != null)
					buffer.append(suffix);
				return buffer.toString();
			}

			public Image getImage(Object element) {
				final String s;
				switch (((PreviewResource) element).type) {
				case IResource.PROJECT:
					s = SharedImages.IMG_OBJ_PROJECT;
					break;
				case IResource.FOLDER:
					s = ISharedImages.IMG_OBJ_FOLDER;
					break;
				default:
					s = ISharedImages.IMG_OBJ_FILE;
					break;
				}
				final Image baseImage = PlatformUI.getWorkbench().getSharedImages()
						.getImage(s);
				final ImageDescriptor overlay = getDecoration(element).getOverlay();
				if (overlay == null)
					return baseImage;
				try {
					return fImageCache.createImage(new DecorationOverlayIcon(
							baseImage, overlay, IDecoration.BOTTOM_RIGHT));
				} catch (Exception e) {
					Activator.logError(e.getMessage(), e);
				}

				return null;
			}
		}
	}

	private static class GitModelCommitMockup {

		private static final String message = "Commit message text"; //$NON-NLS-1$
		private static final String author = "Author Name"; //$NON-NLS-1$
		private static final Date date = new Date();
		private static final String committer = "Committer Name";  //$NON-NLS-1$

		public String getMokeupText(String format, String dateFormat) {
			SimpleDateFormat sdf = new SimpleDateFormat(dateFormat);

			Map<String, String> bindings = new HashMap<String, String>();
			bindings.put(GitChangeSetLabelProvider.BINDING_CHANGESET_DATE, sdf.format(date));
			bindings.put(GitChangeSetLabelProvider.BINDING_CHANGESET_AUTHOR, author);
			bindings.put(GitChangeSetLabelProvider.BINDING_CHANGESET_COMMITTER, committer);
			bindings.put(GitChangeSetLabelProvider.BINDING_CHANGESET_SHORT_MESSAGE, message);

			return GitChangeSetLabelProvider.formatName(format, bindings);
		}
	}

	private static class PreviewResource implements IDecoratableResource {
		private final String name;

		private final String repositoryName;

		private final String branch;

		private final String branchStatus;

		private final int type;

		private Collection children;

		private boolean tracked;

		private boolean ignored;

		private boolean dirty;

		private boolean conflicts;

		private Staged staged;

		private boolean assumeValid;

		public PreviewResource(String name, int type, String repositoryName, String branch,
				String branchStatus, boolean tracked, boolean ignored, boolean dirty, Staged staged,
				boolean conflicts, boolean assumeValid) {

			this.name = name;
			this.repositoryName = repositoryName;
			this.branch = branch;
			this.branchStatus = branchStatus;
			this.type = type;
			this.children = Collections.EMPTY_LIST;
			this.tracked = tracked;
			this.ignored = ignored;
			this.dirty = dirty;
			this.staged = staged;
			this.conflicts = conflicts;
			this.assumeValid = assumeValid;
		}

		public String getName() {
			return name;
		}

		public String getRepositoryName() {
			return repositoryName;
		}

		public int getType() {
			return type;
		}

		public String getBranch() {
			return branch;
		}

		public String getBranchStatus() {
			return branchStatus;
		}

		public boolean isTracked() {
			return tracked;
		}

		public boolean isIgnored() {
			return ignored;
		}

		public boolean isDirty() {
			return dirty;
		}

		public Staged staged() {
			return staged;
		}

		public boolean hasConflicts() {
			return conflicts;
		}

		public boolean isAssumeValid() {
			return assumeValid;
		}
	}
}
