/*******************************************************************************
 *  Copyright (c) 2011 GitHub Inc.
 *  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:
 *    Kevin Sawicki (GitHub Inc.) - initial API and implementation
 *******************************************************************************/
package org.eclipse.egit.ui.internal.search;

import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.StringReader;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.regex.PatternSyntaxException;

import org.eclipse.core.resources.ResourcesPlugin;
import org.eclipse.egit.ui.Activator;
import org.eclipse.egit.ui.UIUtils;
import org.eclipse.egit.ui.internal.UIIcons;
import org.eclipse.egit.ui.internal.UIText;
import org.eclipse.egit.ui.internal.repository.RepositoriesViewContentProvider;
import org.eclipse.egit.ui.internal.repository.RepositoriesViewLabelProvider;
import org.eclipse.egit.ui.internal.repository.tree.RepositoryNode;
import org.eclipse.jface.dialogs.Dialog;
import org.eclipse.jface.dialogs.DialogPage;
import org.eclipse.jface.dialogs.IDialogSettings;
import org.eclipse.jface.fieldassist.ComboContentAdapter;
import org.eclipse.jface.layout.GridDataFactory;
import org.eclipse.jface.layout.GridLayoutFactory;
import org.eclipse.jface.resource.JFaceColors;
import org.eclipse.jface.text.FindReplaceDocumentAdapter;
import org.eclipse.jface.text.FindReplaceDocumentAdapterContentProposalProvider;
import org.eclipse.jface.text.ITextSelection;
import org.eclipse.jface.viewers.CheckStateChangedEvent;
import org.eclipse.jface.viewers.CheckboxTableViewer;
import org.eclipse.jface.viewers.DelegatingStyledCellLabelProvider;
import org.eclipse.jface.viewers.ICheckStateListener;
import org.eclipse.jface.viewers.ISelection;
import org.eclipse.search.ui.ISearchPage;
import org.eclipse.search.ui.ISearchPageContainer;
import org.eclipse.search.ui.ISearchQuery;
import org.eclipse.search.ui.NewSearchUI;
import org.eclipse.swt.SWT;
import org.eclipse.swt.custom.CLabel;
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.Image;
import org.eclipse.swt.layout.GridData;
import org.eclipse.swt.widgets.Button;
import org.eclipse.swt.widgets.Combo;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Group;
import org.eclipse.swt.widgets.Label;
import org.eclipse.swt.widgets.ToolBar;
import org.eclipse.swt.widgets.ToolItem;
import org.eclipse.ui.fieldassist.ContentAssistCommandAdapter;
import org.eclipse.ui.texteditor.ITextEditorActionDefinitionIds;

/**
 * Commit search dialog page class.
 */
public class CommitSearchPage extends DialogPage implements ISearchPage {

	/**
	 * ID
	 */
	public static final String ID = "org.eclipse.egit.ui.commitSearchPage"; //$NON-NLS-1$

	private static class SearchComposite extends Composite {

		/**
		 * @param parent
		 * @param style
		 */
		public SearchComposite(Composite parent, int style) {
			super(parent, style);
		}

		public void setLayoutData(Object layoutData) {
			// Prevent search dialog from overriding the locally set data
			if (getLayoutData() == null)
				super.setLayoutData(layoutData);
		}
	}

	private static final int HISTORY_SIZE = 12;

	// Dialog store id constants
	private static final String PAGE_NAME = "GitCommitSearchPage"; //$NON-NLS-1$

	private static final String STORE_HISTORY = "HISTORY"; //$NON-NLS-1$

	private static final String STORE_HISTORY_SIZE = "HISTORY_SIZE"; //$NON-NLS-1$

	private List<CommitSearchSettings> fPreviousSearchPatterns = new ArrayList<CommitSearchSettings>(
			HISTORY_SIZE);

	private boolean firstTime = true;

	private Combo patternCombo;

	private Button isCaseSensitiveButton;

	private Button isRegExButton;

	private Button searchTreeButton;

	private Button searchCommitButton;

	private Button searchParentsButton;

	private Button searchAuthorButton;

	private Button searchCommitterButton;

	private Button searchMessageButton;

	private Button searchAllBranchesButton;

	private CLabel statusLabel;

	private Group repositoryGroup;

	private CheckboxTableViewer repositoryViewer;

	private ISearchPageContainer container;

	private ContentAssistCommandAdapter patternFieldContentAssist;

	private ISearchQuery newQuery() {
		CommitSearchSettings settings = new CommitSearchSettings();
		settings.setTextPattern(patternCombo.getText());
		settings.setRegExSearch(isRegExButton.getSelection());
		settings.setCaseSensitive(isCaseSensitiveButton.getSelection());
		settings.setMatchAuthor(searchAuthorButton.getSelection());
		settings.setMatchCommitter(searchCommitterButton.getSelection());
		settings.setMatchMessage(searchMessageButton.getSelection());
		settings.setMatchCommit(searchCommitButton.getSelection());
		settings.setMatchTree(searchTreeButton.getSelection());
		settings.setMatchParents(searchParentsButton.getSelection());
		settings.setAllBranches(searchAllBranchesButton.getSelection());
		for (Object checked : repositoryViewer.getCheckedElements())
			settings.addRepository(((RepositoryNode) checked).getRepository()
					.getDirectory().getAbsolutePath());
		fPreviousSearchPatterns.add(0, settings);
		return new CommitSearchQuery(settings);
	}

	/**
	 * Perform search by running query in background
	 *
	 * @see org.eclipse.search.ui.ISearchPage#performAction()
	 */
	public boolean performAction() {
		NewSearchUI.runQueryInBackground(newQuery());
		return true;
	}

	private String[] getPreviousSearchPatterns() {
		int size = fPreviousSearchPatterns.size();
		String[] patterns = new String[size];
		for (int i = 0; i < size; i++)
			patterns[i] = fPreviousSearchPatterns.get(i).getTextPattern();
		return patterns;
	}

	public void setVisible(boolean visible) {
		if (visible && patternCombo != null) {
			if (firstTime) {
				firstTime = false;
				// Set item and text here to prevent page from resizing
				patternCombo.setItems(getPreviousSearchPatterns());
				if (!initializePatternControl()) {
					patternCombo.select(0);
					handleWidgetSelected();
				}
			}
			patternCombo.setFocus();
		}
		updateOKStatus();
		super.setVisible(visible);
	}

	private void updateOKStatus() {
		boolean status = validateRegex();
		if (status)
			status = validateScope();
		if (status)
			status = validateRepositories();
		getContainer().setPerformActionEnabled(status);
	}

	private boolean validateRepositories() {
		return this.repositoryViewer.getCheckedElements().length > 0;
	}

	private boolean validateScope() {
		return this.searchAuthorButton.getSelection()
				|| this.searchCommitterButton.getSelection()
				|| this.searchMessageButton.getSelection()
				|| this.searchCommitButton.getSelection()
				|| this.searchParentsButton.getSelection()
				|| this.searchTreeButton.getSelection();
	}

	public void createControl(Composite parent) {
		readConfiguration();

		initializeDialogUnits(parent);
		SearchComposite result = new SearchComposite(parent, SWT.NONE);
		result.setFont(parent.getFont());
		GridDataFactory.fillDefaults().grab(true, true).applyTo(result);
		GridLayoutFactory.swtDefaults().numColumns(2).equalWidth(false)
				.applyTo(result);
		addTextPatternControls(result);
		addScopeControls(result);
		addRepositoryControl(result);
		setControl(result);
		Dialog.applyDialogFont(result);
	}

	private boolean validateRegex() {
		if (isRegExButton.getSelection()) {
			try {
				PatternUtils.createPattern(patternCombo.getText(),
						isCaseSensitiveButton.getSelection(), true);
			} catch (PatternSyntaxException e) {
				String locMessage = e.getLocalizedMessage();
				int i = 0;
				while (i < locMessage.length()
						&& "\n\r".indexOf(locMessage.charAt(i)) == -1) //$NON-NLS-1$
					i++;
				statusMessage(true, locMessage.substring(0, i));
				return false;
			}
			statusMessage(false, ""); //$NON-NLS-1$
		} else {
			statusMessage(false, UIText.CommitSearchPage_ContainingTextHint);
		}
		return true;
	}

	private void addScopeControls(Composite parent) {
		SelectionAdapter statusAdapter = new SelectionAdapter() {

			public void widgetSelected(SelectionEvent e) {
				updateOKStatus();
			}

		};

		Group scopeArea = new Group(parent, SWT.NONE);
		scopeArea.setText(UIText.CommitSearchPage_Scope);
		GridDataFactory.fillDefaults().grab(true, false).span(2, 1)
				.applyTo(scopeArea);
		GridLayoutFactory.swtDefaults().numColumns(3).applyTo(scopeArea);

		this.searchMessageButton = new Button(scopeArea, SWT.CHECK);
		this.searchMessageButton.setText(UIText.CommitSearchPage_Message);
		this.searchMessageButton.setSelection(true);
		this.searchMessageButton.addSelectionListener(statusAdapter);

		this.searchAuthorButton = new Button(scopeArea, SWT.CHECK);
		this.searchAuthorButton.setText(UIText.CommitSearchPage_Author);
		this.searchAuthorButton.setSelection(true);
		this.searchAuthorButton.addSelectionListener(statusAdapter);

		this.searchCommitterButton = new Button(scopeArea, SWT.CHECK);
		this.searchCommitterButton.setText(UIText.CommitSearchPage_Committer);
		this.searchCommitterButton.setSelection(true);
		this.searchCommitterButton.addSelectionListener(statusAdapter);

		this.searchCommitButton = new Button(scopeArea, SWT.CHECK);
		this.searchCommitButton.setText(UIText.CommitSearchPage_CommitId);
		this.searchCommitButton.setSelection(true);
		this.searchCommitButton.addSelectionListener(statusAdapter);

		this.searchTreeButton = new Button(scopeArea, SWT.CHECK);
		this.searchTreeButton.setText(UIText.CommitSearchPage_TreeId);
		this.searchTreeButton.setSelection(true);
		this.searchTreeButton.addSelectionListener(statusAdapter);

		this.searchParentsButton = new Button(scopeArea, SWT.CHECK);
		this.searchParentsButton.setText(UIText.CommitSearchPage_ParentIds);
		this.searchParentsButton.setSelection(true);
		this.searchParentsButton.addSelectionListener(statusAdapter);
	}

	private void addRepositoryControl(Composite parent) {
		repositoryGroup = new Group(parent, SWT.NONE);
		repositoryGroup.setBackgroundMode(SWT.INHERIT_DEFAULT);
		GridDataFactory.fillDefaults().grab(true, true).span(2, 1)
				.applyTo(repositoryGroup);
		GridLayoutFactory.swtDefaults().numColumns(2).applyTo(repositoryGroup);

		this.repositoryViewer = CheckboxTableViewer.newCheckList(
				repositoryGroup, SWT.SINGLE | SWT.V_SCROLL | SWT.H_SCROLL
						| SWT.BORDER);
		this.repositoryViewer
				.setLabelProvider(new DelegatingStyledCellLabelProvider(
						new RepositoriesViewLabelProvider()));
		this.repositoryViewer
				.setContentProvider(new RepositoriesViewContentProvider());
		this.repositoryViewer
				.setInput(ResourcesPlugin.getWorkspace().getRoot());
		this.repositoryViewer.addCheckStateListener(new ICheckStateListener() {

			public void checkStateChanged(CheckStateChangedEvent event) {
				updateOKStatus();
				repositoryGroup.setText(getRepositoryText());
			}
		});
		GridDataFactory.fillDefaults().grab(true, true).hint(SWT.DEFAULT, 40)
				.applyTo(this.repositoryViewer.getControl());

		ToolBar checkBar = new ToolBar(repositoryGroup, SWT.FLAT | SWT.VERTICAL);
		GridDataFactory.swtDefaults().align(SWT.BEGINNING, SWT.TOP)
				.grab(false, true).applyTo(checkBar);
		ToolItem checkItem = new ToolItem(checkBar, SWT.PUSH);
		checkItem.setToolTipText(UIText.CommitSearchPage_CheckAll);
		Image checkImage = UIIcons.CHECK_ALL.createImage();
		UIUtils.hookDisposal(checkItem, checkImage);
		checkItem.setImage(checkImage);
		checkItem.addSelectionListener(new SelectionAdapter() {

			public void widgetSelected(SelectionEvent e) {
				repositoryViewer.setAllChecked(true);
				repositoryGroup.setText(getRepositoryText());
				updateOKStatus();
			}

		});
		ToolItem uncheckItem = new ToolItem(checkBar, SWT.PUSH);
		uncheckItem.setToolTipText(UIText.CommitSearchPage_UncheckAll);
		Image uncheckImage = UIIcons.UNCHECK_ALL.createImage();
		UIUtils.hookDisposal(uncheckItem, uncheckImage);
		uncheckItem.setImage(uncheckImage);
		uncheckItem.addSelectionListener(new SelectionAdapter() {

			public void widgetSelected(SelectionEvent e) {
				repositoryViewer.setAllChecked(false);
				repositoryGroup.setText(getRepositoryText());
				updateOKStatus();
			}

		});

		this.searchAllBranchesButton = new Button(repositoryGroup, SWT.CHECK);
		this.searchAllBranchesButton
				.setText(UIText.CommitSearchPage_SearchAllBranches);
		GridDataFactory.swtDefaults().grab(true, false).span(2, 1)
				.applyTo(this.searchAllBranchesButton);

		repositoryGroup.setText(getRepositoryText());
	}

	private String getRepositoryText() {
		return MessageFormat.format(UIText.CommitSearchPage_Repositories,
				Integer.valueOf(repositoryViewer.getCheckedElements().length),
				Integer.valueOf(repositoryViewer.getTable().getItemCount()));
	}

	private void addTextPatternControls(Composite group) {
		// Info text
		Label label = new Label(group, SWT.NONE);
		label.setText(UIText.CommitSearchPage_ContainingText);
		label.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, false, false, 2,
				1));
		label.setFont(group.getFont());

		// Pattern combo
		patternCombo = new Combo(group, SWT.SINGLE | SWT.BORDER);
		patternCombo.addSelectionListener(new SelectionAdapter() {
			public void widgetSelected(SelectionEvent e) {
				handleWidgetSelected();
				updateOKStatus();
			}
		});
		// add some listeners for regex syntax checking
		patternCombo.addModifyListener(new ModifyListener() {
			public void modifyText(ModifyEvent e) {
				updateOKStatus();
			}
		});
		patternCombo.setFont(group.getFont());
		GridData data = new GridData(GridData.FILL, GridData.FILL, true, false,
				1, 1);
		data.widthHint = convertWidthInCharsToPixels(50);
		patternCombo.setLayoutData(data);

		ComboContentAdapter contentAdapter = new ComboContentAdapter();
		FindReplaceDocumentAdapterContentProposalProvider findProposer = new FindReplaceDocumentAdapterContentProposalProvider(
				true);
		patternFieldContentAssist = new ContentAssistCommandAdapter(
				patternCombo, contentAdapter, findProposer,
				ITextEditorActionDefinitionIds.CONTENT_ASSIST_PROPOSALS,
				new char[0], true);
		patternFieldContentAssist.setEnabled(false);

		isCaseSensitiveButton = new Button(group, SWT.CHECK);
		isCaseSensitiveButton.setText(UIText.CommitSearchPage_CaseSensitive);
		isCaseSensitiveButton.setLayoutData(new GridData(SWT.FILL, SWT.CENTER,
				false, false, 1, 1));
		isCaseSensitiveButton.setFont(group.getFont());

		// Text line which explains the special characters
		statusLabel = new CLabel(group, SWT.LEAD);
		statusLabel.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true,
				false, 1, 1));
		statusLabel.setFont(group.getFont());
		statusLabel.setAlignment(SWT.LEFT);
		statusLabel.setText(UIText.CommitSearchPage_ContainingTextHint);

		// RegEx checkbox
		isRegExButton = new Button(group, SWT.CHECK);
		isRegExButton.setText(UIText.CommitSearchPage_RegularExpression);
		isRegExButton.addSelectionListener(new SelectionAdapter() {
			public void widgetSelected(SelectionEvent e) {
				updateOKStatus();

				patternFieldContentAssist.setEnabled(isRegExButton
						.getSelection());
			}
		});
		isRegExButton.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, false,
				false, 1, 1));
		isRegExButton.setFont(group.getFont());
	}

	private void handleWidgetSelected() {
		int selectionIndex = patternCombo.getSelectionIndex();
		if (selectionIndex < 0
				|| selectionIndex >= fPreviousSearchPatterns.size()) {
			repositoryViewer.setAllChecked(true);
			repositoryGroup.setText(getRepositoryText());
			return;
		}

		CommitSearchSettings settings = fPreviousSearchPatterns
				.get(selectionIndex);
		if (!patternCombo.getText().equals(settings.getTextPattern()))
			return;

		isCaseSensitiveButton.setSelection(settings.isCaseSensitive());
		isRegExButton.setSelection(settings.isRegExSearch());
		patternCombo.setText(settings.getTextPattern());
		patternFieldContentAssist.setEnabled(settings.isRegExSearch());

		searchAuthorButton.setSelection(settings.isMatchAuthor());
		searchCommitButton.setSelection(settings.isMatchCommit());
		searchCommitterButton.setSelection(settings.isMatchCommitter());
		searchMessageButton.setSelection(settings.isMatchMessage());
		searchParentsButton.setSelection(settings.isMatchParents());
		searchTreeButton.setSelection(settings.isMatchTree());
		searchAllBranchesButton.setSelection(settings.isAllBranches());

		List<RepositoryNode> repositories = new LinkedList<RepositoryNode>();
		for (String path : settings.getRepositories()) {
			File file = new File(path);
			if (file.exists())
				try {
					RepositoryNode node = new RepositoryNode(null,
							org.eclipse.egit.core.Activator.getDefault()
									.getRepositoryCache()
									.lookupRepository(file));
					repositories.add(node);
				} catch (IOException ignore) {
					// Ignore and don't check
				}
		}
		repositoryViewer.setCheckedElements(repositories.toArray());
		repositoryGroup.setText(getRepositoryText());
	}

	private boolean initializePatternControl() {
		ISelection selection = getSelection();
		if (selection instanceof ITextSelection && !selection.isEmpty()) {
			String text = ((ITextSelection) selection).getText();
			if (text != null) {
				if (isRegExButton.getSelection())
					patternCombo.setText(FindReplaceDocumentAdapter
							.escapeForRegExPattern(text));
				else
					patternCombo.setText(insertEscapeChars(text));

				return true;
			}
		}
		return false;
	}

	private String insertEscapeChars(String text) {
		if (text == null || text.length() == 0)
			return ""; //$NON-NLS-1$
		StringBuilder sbIn = new StringBuilder(text);
		BufferedReader reader = new BufferedReader(new StringReader(text));
		int lengthOfFirstLine = 0;
		try {
			String l = reader.readLine();
			if (l != null)
				lengthOfFirstLine = l.length();
		} catch (IOException ex) {
			return ""; //$NON-NLS-1$
		}
		StringBuilder sbOut = new StringBuilder(lengthOfFirstLine + 5);
		int i = 0;
		while (i < lengthOfFirstLine) {
			char ch = sbIn.charAt(i);
			if (ch == '*' || ch == '?' || ch == '\\')
				sbOut.append('\\');
			sbOut.append(ch);
			i++;
		}
		return sbOut.toString();
	}

	/**
	 * Sets the search page's container.
	 *
	 * @param container
	 *            the container to set
	 */
	public void setContainer(ISearchPageContainer container) {
		this.container = container;
	}

	private ISearchPageContainer getContainer() {
		return this.container;
	}

	private ISelection getSelection() {
		return this.container.getSelection();
	}

	public void dispose() {
		writeConfiguration();
		super.dispose();
	}

	/**
	 * Returns the page settings for this Text search page.
	 *
	 * @return the page settings to be used
	 */
	private IDialogSettings getDialogSettings() {
		IDialogSettings dialogSettings = Activator.getDefault()
				.getDialogSettings();
		IDialogSettings section = dialogSettings.getSection(PAGE_NAME);
		if (section == null)
			section = dialogSettings.addNewSection(PAGE_NAME);
		return section;
	}

	/**
	 * Initializes itself from the stored page settings.
	 */
	private void readConfiguration() {
		IDialogSettings settings = getDialogSettings();
		try {
			int historySize = settings.getInt(STORE_HISTORY_SIZE);
			for (int i = 0; i < historySize; i++) {
				IDialogSettings histSettings = settings
						.getSection(STORE_HISTORY + i);
				if (histSettings != null)
					fPreviousSearchPatterns.add(CommitSearchSettings
							.create(histSettings));
			}
		} catch (NumberFormatException ignored) {
			// Ignored
		}
	}

	/**
	 * Stores it current configuration in the dialog store.
	 */
	private void writeConfiguration() {
		IDialogSettings s = getDialogSettings();
		int historySize = Math
				.min(fPreviousSearchPatterns.size(), HISTORY_SIZE);
		s.put(STORE_HISTORY_SIZE, historySize);
		for (int i = 0; i < historySize; i++)
			fPreviousSearchPatterns.get(i).store(
					s.addNewSection(STORE_HISTORY + i));
	}

	private void statusMessage(boolean error, String message) {
		statusLabel.setText(message);
		if (error)
			statusLabel.setForeground(JFaceColors.getErrorText(statusLabel
					.getDisplay()));
		else
			statusLabel.setForeground(null);
	}

}
