/*******************************************************************************
 * Copyright (c) 2000, 2004 IBM Corporation and others.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Common Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/cpl-v10.html
 *
 * Contributors:
 *     IBM Corporation - initial API and implementation
 *******************************************************************************/
package net.sourceforge.phpdt.internal.ui.preferences;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

import net.sourceforge.phpdt.internal.ui.IJavaHelpContextIds;
import net.sourceforge.phpdt.internal.ui.dialogs.StatusDialog;
import net.sourceforge.phpdt.internal.ui.dialogs.StatusInfo;
import net.sourceforge.phpdt.internal.ui.text.IPHPPartitions;
import net.sourceforge.phpdt.internal.ui.text.template.preferences.TemplateVariableProcessor;
import net.sourceforge.phpdt.internal.ui.util.SWTUtil;
import net.sourceforge.phpdt.ui.IContextMenuConstants;
import net.sourceforge.phpdt.ui.PreferenceConstants;
import net.sourceforge.phpdt.ui.text.JavaTextTools;
import net.sourceforge.phpeclipse.PHPeclipsePlugin;
import net.sourceforge.phpeclipse.phpeditor.JavaSourceViewer;

import org.eclipse.jface.action.Action;
import org.eclipse.jface.action.GroupMarker;
import org.eclipse.jface.action.IAction;
import org.eclipse.jface.action.IMenuListener;
import org.eclipse.jface.action.IMenuManager;
import org.eclipse.jface.action.MenuManager;
import org.eclipse.jface.action.Separator;
import org.eclipse.jface.preference.IPreferenceStore;
import org.eclipse.jface.resource.JFaceResources;
import org.eclipse.jface.text.Document;
import org.eclipse.jface.text.IDocument;
import org.eclipse.jface.text.ITextListener;
import org.eclipse.jface.text.ITextOperationTarget;
import org.eclipse.jface.text.ITextViewer;
import org.eclipse.jface.text.TextEvent;
import org.eclipse.jface.text.source.ISourceViewer;
import org.eclipse.jface.text.source.SourceViewer;
import org.eclipse.jface.text.templates.ContextTypeRegistry;
import org.eclipse.jface.text.templates.Template;
import org.eclipse.jface.text.templates.TemplateContextType;
import org.eclipse.jface.text.templates.TemplateException;
import org.eclipse.jface.viewers.ISelectionChangedListener;
import org.eclipse.jface.viewers.SelectionChangedEvent;
import org.eclipse.swt.SWT;
import org.eclipse.swt.custom.StyledText;
import org.eclipse.swt.custom.VerifyKeyListener;
import org.eclipse.swt.events.FocusEvent;
import org.eclipse.swt.events.FocusListener;
import org.eclipse.swt.events.ModifyEvent;
import org.eclipse.swt.events.ModifyListener;
import org.eclipse.swt.events.SelectionEvent;
import org.eclipse.swt.events.SelectionListener;
import org.eclipse.swt.events.VerifyEvent;
import org.eclipse.swt.graphics.Font;
import org.eclipse.swt.layout.GridData;
import org.eclipse.swt.layout.GridLayout;
import org.eclipse.swt.widgets.Button;
import org.eclipse.swt.widgets.Combo;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Control;
import org.eclipse.swt.widgets.Label;
import org.eclipse.swt.widgets.Menu;
import org.eclipse.swt.widgets.Shell;
import org.eclipse.swt.widgets.Text;
import org.eclipse.swt.widgets.Widget;
import org.eclipse.ui.PlatformUI;
import org.eclipse.ui.texteditor.ITextEditorActionConstants;
import org.eclipse.ui.texteditor.IUpdate;

/**
 * Dialog to edit a template.
 */
public class EditTemplateDialog extends StatusDialog {

	private static class TextViewerAction extends Action implements IUpdate {

		private int fOperationCode = -1;

		private ITextOperationTarget fOperationTarget;

		/**
		 * Creates a new action.
		 * 
		 * @param viewer
		 *            the viewer
		 * @param operationCode
		 *            the opcode
		 */
		public TextViewerAction(ITextViewer viewer, int operationCode) {
			fOperationCode = operationCode;
			fOperationTarget = viewer.getTextOperationTarget();
			update();
		}

		/**
		 * Updates the enabled state of the action. Fires a property change if
		 * the enabled state changes.
		 * 
		 * @see Action#firePropertyChange(String, Object, Object)
		 */
		public void update() {

			boolean wasEnabled = isEnabled();
			boolean isEnabled = (fOperationTarget != null && fOperationTarget
					.canDoOperation(fOperationCode));
			setEnabled(isEnabled);

			if (wasEnabled != isEnabled) {
				firePropertyChange(ENABLED, wasEnabled ? Boolean.TRUE
						: Boolean.FALSE, isEnabled ? Boolean.TRUE
						: Boolean.FALSE);
			}
		}

		/**
		 * @see Action#run()
		 */
		public void run() {
			if (fOperationCode != -1 && fOperationTarget != null) {
				fOperationTarget.doOperation(fOperationCode);
			}
		}
	}

	private final Template fTemplate;

	private Text fNameText;

	private Text fDescriptionText;

	private Combo fContextCombo;

	private SourceViewer fPatternEditor;

	private Button fInsertVariableButton;

	private boolean fIsNameModifiable;

	private StatusInfo fValidationStatus;

	private boolean fSuppressError = true; // #4354

	private Map fGlobalActions = new HashMap(10);

	private List fSelectionActions = new ArrayList(3);

	private String[][] fContextTypes;

	private ContextTypeRegistry fContextTypeRegistry;

	private final TemplateVariableProcessor fTemplateProcessor = new TemplateVariableProcessor();

	/**
	 * Creates a new dialog.
	 * 
	 * @param parent
	 *            the shell parent of the dialog
	 * @param template
	 *            the template to edit
	 * @param edit
	 *            whether this is a new template or an existing being edited
	 * @param isNameModifiable
	 *            whether the name of the template may be modified
	 * @param registry
	 *            the context type registry to use
	 */
	public EditTemplateDialog(Shell parent, Template template, boolean edit,
			boolean isNameModifiable, ContextTypeRegistry registry) {
		super(parent);

		setShellStyle(getShellStyle() | SWT.MAX | SWT.RESIZE);

		String title = edit ? PreferencesMessages
				.getString("EditTemplateDialog.title.edit") //$NON-NLS-1$
				: PreferencesMessages.getString("EditTemplateDialog.title.new"); //$NON-NLS-1$
		setTitle(title);

		fTemplate = template;
		fIsNameModifiable = isNameModifiable;

		// XXX workaround for bug 63313 - disabling prefix until fixed.
		// String delim= new Document().getLegalLineDelimiters()[0];

		List contexts = new ArrayList();
		for (Iterator it = registry.contextTypes(); it.hasNext();) {
			TemplateContextType type = (TemplateContextType) it.next();
			// if (type.getId().equals("javadoc")) //$NON-NLS-1$
			// contexts.add(new String[] { type.getId(), type.getName(), "/**" +
			// delim }); //$NON-NLS-1$
			// else
			contexts.add(new String[] { type.getId(), type.getName(), "" }); //$NON-NLS-1$
		}
		fContextTypes = (String[][]) contexts.toArray(new String[contexts
				.size()][]);

		fValidationStatus = new StatusInfo();

		fContextTypeRegistry = registry;

		TemplateContextType type = fContextTypeRegistry.getContextType(template
				.getContextTypeId());
		fTemplateProcessor.setContextType(type);
	}

	/*
	 * @see net.sourceforge.phpdt.internal.ui.dialogs.StatusDialog#create()
	 */
	public void create() {
		super.create();
		// update initial ok button to be disabled for new templates
		boolean valid = fNameText == null
				|| fNameText.getText().trim().length() != 0;
		if (!valid) {
			StatusInfo status = new StatusInfo();
			status.setError(PreferencesMessages
					.getString("EditTemplateDialog.error.noname")); //$NON-NLS-1$
			updateButtonsEnableState(status);
		}
	}

	/*
	 * @see Dialog#createDialogArea(Composite)
	 */
	protected Control createDialogArea(Composite ancestor) {
		Composite parent = new Composite(ancestor, SWT.NONE);
		GridLayout layout = new GridLayout();
		layout.numColumns = 2;
		parent.setLayout(layout);
		parent.setLayoutData(new GridData(GridData.FILL_BOTH));

		ModifyListener listener = new ModifyListener() {
			public void modifyText(ModifyEvent e) {
				doTextWidgetChanged(e.widget);
			}
		};

		if (fIsNameModifiable) {
			createLabel(parent, PreferencesMessages
					.getString("EditTemplateDialog.name")); //$NON-NLS-1$

			Composite composite = new Composite(parent, SWT.NONE);
			composite.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
			layout = new GridLayout();
			layout.numColumns = 3;
			layout.marginWidth = 0;
			layout.marginHeight = 0;
			composite.setLayout(layout);

			fNameText = createText(composite);
			fNameText.addFocusListener(new FocusListener() {

				public void focusGained(FocusEvent e) {
				}

				public void focusLost(FocusEvent e) {
					if (fSuppressError) {
						fSuppressError = false;
						updateButtons();
					}
				}
			});

			createLabel(composite, PreferencesMessages
					.getString("EditTemplateDialog.context")); //$NON-NLS-1$
			fContextCombo = new Combo(composite, SWT.READ_ONLY);

			for (int i = 0; i < fContextTypes.length; i++) {
				fContextCombo.add(fContextTypes[i][1]);
			}

			fContextCombo.addModifyListener(listener);
		}

		createLabel(parent, PreferencesMessages
				.getString("EditTemplateDialog.description")); //$NON-NLS-1$

		int descFlags = fIsNameModifiable ? SWT.BORDER : SWT.BORDER
				| SWT.READ_ONLY;
		fDescriptionText = new Text(parent, descFlags);
		fDescriptionText.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));

		fDescriptionText.addModifyListener(listener);

		Label patternLabel = createLabel(parent, PreferencesMessages
				.getString("EditTemplateDialog.pattern")); //$NON-NLS-1$
		patternLabel.setLayoutData(new GridData(
				GridData.VERTICAL_ALIGN_BEGINNING));
		fPatternEditor = createEditor(parent);

		Label filler = new Label(parent, SWT.NONE);
		filler.setLayoutData(new GridData());

		Composite composite = new Composite(parent, SWT.NONE);
		layout = new GridLayout();
		layout.marginWidth = 0;
		layout.marginHeight = 0;
		composite.setLayout(layout);
		composite.setLayoutData(new GridData());

		fInsertVariableButton = new Button(composite, SWT.NONE);
		fInsertVariableButton
				.setLayoutData(getButtonGridData(fInsertVariableButton));
		fInsertVariableButton.setText(PreferencesMessages
				.getString("EditTemplateDialog.insert.variable")); //$NON-NLS-1$
		fInsertVariableButton.addSelectionListener(new SelectionListener() {
			public void widgetSelected(SelectionEvent e) {
				fPatternEditor.getTextWidget().setFocus();
				fPatternEditor
						.doOperation(ISourceViewer.CONTENTASSIST_PROPOSALS);
			}

			public void widgetDefaultSelected(SelectionEvent e) {
			}
		});

		fDescriptionText.setText(fTemplate.getDescription());
		if (fIsNameModifiable) {
			fNameText.setText(fTemplate.getName());
			fNameText.addModifyListener(listener);
			fContextCombo.select(getIndex(fTemplate.getContextTypeId()));
		} else {
			fPatternEditor.getControl().setFocus();
		}
		initializeActions();

		applyDialogFont(parent);
		return composite;
	}

	protected void doTextWidgetChanged(Widget w) {
		if (w == fNameText) {
			fSuppressError = false;
			String name = fNameText.getText();
			fTemplate.setName(name);
			updateButtons();
		} else if (w == fContextCombo) {
			String name = fContextCombo.getText();
			String contextId = getContextId(name);
			fTemplate.setContextTypeId(contextId);
			fTemplateProcessor.setContextType(fContextTypeRegistry
					.getContextType(contextId));
		} else if (w == fDescriptionText) {
			String desc = fDescriptionText.getText();
			fTemplate.setDescription(desc);
		}
	}

	private String getContextId(String name) {
		if (name == null)
			return name;

		for (int i = 0; i < fContextTypes.length; i++) {
			if (name.equals(fContextTypes[i][1])) {
				return fContextTypes[i][0];
			}
		}
		return name;
	}

	protected void doSourceChanged(IDocument document) {
		String text = document.get();
		String prefix = getPrefix();
		fTemplate.setPattern(text.substring(prefix.length(), text.length()));
		fValidationStatus.setOK();
		TemplateContextType contextType = fContextTypeRegistry
				.getContextType(fTemplate.getContextTypeId());
		if (contextType != null) {
			try {
				contextType.validate(text);
			} catch (TemplateException e) {
				fValidationStatus.setError(e.getLocalizedMessage());
			}
		}

		updateUndoAction();
		updateButtons();
	}

	private static GridData getButtonGridData(Button button) {
		GridData data = new GridData(GridData.FILL_HORIZONTAL);
		data.heightHint = SWTUtil.getButtonHeightHint(button);

		return data;
	}

	private static Label createLabel(Composite parent, String name) {
		Label label = new Label(parent, SWT.NULL);
		label.setText(name);
		label.setLayoutData(new GridData());

		return label;
	}

	private static Text createText(Composite parent) {
		Text text = new Text(parent, SWT.BORDER);
		text.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));

		return text;
	}

	private SourceViewer createEditor(Composite parent) {
		String prefix = getPrefix();
		IDocument document = new Document(prefix + fTemplate.getPattern());
		JavaTextTools tools = PHPeclipsePlugin.getDefault().getJavaTextTools();
		tools.setupJavaDocumentPartitioner(document,
				IPHPPartitions.PHP_PARTITIONING);
		IPreferenceStore store = PHPeclipsePlugin.getDefault()
				.getCombinedPreferenceStore();
		SourceViewer viewer = new JavaSourceViewer(parent, null, null, false,
				SWT.BORDER | SWT.V_SCROLL | SWT.H_SCROLL, store);
		TemplateEditorSourceViewerConfiguration configuration = new TemplateEditorSourceViewerConfiguration(
				tools.getColorManager(), store, null, fTemplateProcessor);
		viewer.configure(configuration);
		viewer.setEditable(true);
		// XXX workaround for bug 63313 - disabling prefix until fixed.
		// viewer.setDocument(document, prefix.length(), document.getLength() -
		// prefix.length());
		viewer.setDocument(document);

		Font font = JFaceResources
				.getFont(PreferenceConstants.EDITOR_TEXT_FONT);
		viewer.getTextWidget().setFont(font);
		new JavaSourcePreviewerUpdater(viewer, configuration, store);

		int nLines = document.getNumberOfLines();
		if (nLines < 5) {
			nLines = 5;
		} else if (nLines > 12) {
			nLines = 12;
		}

		Control control = viewer.getControl();
		GridData data = new GridData(GridData.FILL_BOTH);
		data.widthHint = convertWidthInCharsToPixels(80);
		data.heightHint = convertHeightInCharsToPixels(nLines);
		control.setLayoutData(data);

		viewer.addTextListener(new ITextListener() {
			public void textChanged(TextEvent event) {
				if (event.getDocumentEvent() != null)
					doSourceChanged(event.getDocumentEvent().getDocument());
			}
		});

		viewer.addSelectionChangedListener(new ISelectionChangedListener() {
			public void selectionChanged(SelectionChangedEvent event) {
				updateSelectionDependentActions();
			}
		});

		viewer.prependVerifyKeyListener(new VerifyKeyListener() {
			public void verifyKey(VerifyEvent event) {
				handleVerifyKeyPressed(event);
			}
		});

		return viewer;
	}

	private String getPrefix() {
		String prefix;
		int idx = getIndex(fTemplate.getContextTypeId());
		if (idx != -1)
			prefix = fContextTypes[idx][2];
		else
			prefix = ""; //$NON-NLS-1$

		return prefix;
	}

	private void handleVerifyKeyPressed(VerifyEvent event) {
		if (!event.doit)
			return;

		if (event.stateMask != SWT.MOD1)
			return;

		switch (event.character) {
		case ' ':
			fPatternEditor.doOperation(ISourceViewer.CONTENTASSIST_PROPOSALS);
			event.doit = false;
			break;

		// CTRL-Z
		case 'z' - 'a' + 1:
			fPatternEditor.doOperation(ITextOperationTarget.UNDO);
			event.doit = false;
			break;
		}
	}

	private void initializeActions() {
		TextViewerAction action = new TextViewerAction(fPatternEditor,
				SourceViewer.UNDO);
		action
				.setText(PreferencesMessages
						.getString("EditTemplateDialog.undo")); //$NON-NLS-1$
		fGlobalActions.put(ITextEditorActionConstants.UNDO, action);

		action = new TextViewerAction(fPatternEditor, SourceViewer.CUT);
		action.setText(PreferencesMessages.getString("EditTemplateDialog.cut")); //$NON-NLS-1$
		fGlobalActions.put(ITextEditorActionConstants.CUT, action);

		action = new TextViewerAction(fPatternEditor, SourceViewer.COPY);
		action
				.setText(PreferencesMessages
						.getString("EditTemplateDialog.copy")); //$NON-NLS-1$
		fGlobalActions.put(ITextEditorActionConstants.COPY, action);

		action = new TextViewerAction(fPatternEditor, SourceViewer.PASTE);
		action.setText(PreferencesMessages
				.getString("EditTemplateDialog.paste")); //$NON-NLS-1$
		fGlobalActions.put(ITextEditorActionConstants.PASTE, action);

		action = new TextViewerAction(fPatternEditor, SourceViewer.SELECT_ALL);
		action.setText(PreferencesMessages
				.getString("EditTemplateDialog.select.all")); //$NON-NLS-1$
		fGlobalActions.put(ITextEditorActionConstants.SELECT_ALL, action);

		action = new TextViewerAction(fPatternEditor,
				SourceViewer.CONTENTASSIST_PROPOSALS);
		action.setText(PreferencesMessages
				.getString("EditTemplateDialog.content.assist")); //$NON-NLS-1$
		fGlobalActions.put("ContentAssistProposal", action); //$NON-NLS-1$

		fSelectionActions.add(ITextEditorActionConstants.CUT);
		fSelectionActions.add(ITextEditorActionConstants.COPY);
		fSelectionActions.add(ITextEditorActionConstants.PASTE);

		// create context menu
		MenuManager manager = new MenuManager(null, null);
		manager.setRemoveAllWhenShown(true);
		manager.addMenuListener(new IMenuListener() {
			public void menuAboutToShow(IMenuManager mgr) {
				fillContextMenu(mgr);
			}
		});

		StyledText text = fPatternEditor.getTextWidget();
		Menu menu = manager.createContextMenu(text);
		text.setMenu(menu);
	}

	private void fillContextMenu(IMenuManager menu) {
		menu.add(new GroupMarker(ITextEditorActionConstants.GROUP_UNDO));
		menu.appendToGroup(ITextEditorActionConstants.GROUP_UNDO,
				(IAction) fGlobalActions.get(ITextEditorActionConstants.UNDO));

		menu.add(new Separator(ITextEditorActionConstants.GROUP_EDIT));
		menu.appendToGroup(ITextEditorActionConstants.GROUP_EDIT,
				(IAction) fGlobalActions.get(ITextEditorActionConstants.CUT));
		menu.appendToGroup(ITextEditorActionConstants.GROUP_EDIT,
				(IAction) fGlobalActions.get(ITextEditorActionConstants.COPY));
		menu.appendToGroup(ITextEditorActionConstants.GROUP_EDIT,
				(IAction) fGlobalActions.get(ITextEditorActionConstants.PASTE));
		menu.appendToGroup(ITextEditorActionConstants.GROUP_EDIT,
				(IAction) fGlobalActions
						.get(ITextEditorActionConstants.SELECT_ALL));

		menu.add(new Separator(IContextMenuConstants.GROUP_GENERATE));
		menu.appendToGroup(IContextMenuConstants.GROUP_GENERATE,
				(IAction) fGlobalActions.get("ContentAssistProposal")); //$NON-NLS-1$
	}

	protected void updateSelectionDependentActions() {
		Iterator iterator = fSelectionActions.iterator();
		while (iterator.hasNext())
			updateAction((String) iterator.next());
	}

	protected void updateUndoAction() {
		IAction action = (IAction) fGlobalActions
				.get(ITextEditorActionConstants.UNDO);
		if (action instanceof IUpdate)
			((IUpdate) action).update();
	}

	protected void updateAction(String actionId) {
		IAction action = (IAction) fGlobalActions.get(actionId);
		if (action instanceof IUpdate)
			((IUpdate) action).update();
	}

	private int getIndex(String contextid) {

		if (contextid == null)
			return -1;

		for (int i = 0; i < fContextTypes.length; i++) {
			if (contextid.equals(fContextTypes[i][0])) {
				return i;
			}
		}
		return -1;
	}

	protected void okPressed() {
		super.okPressed();
	}

	private void updateButtons() {
		StatusInfo status;

		boolean valid = fNameText == null
				|| fNameText.getText().trim().length() != 0;
		if (!valid) {
			status = new StatusInfo();
			if (!fSuppressError) {
				status.setError(PreferencesMessages
						.getString("EditTemplateDialog.error.noname")); //$NON-NLS-1$
			}
		} else {
			status = fValidationStatus;
		}
		updateStatus(status);
	}

	/*
	 * @see org.eclipse.jface.window.Window#configureShell(Shell)
	 */
	protected void configureShell(Shell newShell) {
		super.configureShell(newShell);
		PlatformUI.getWorkbench().getHelpSystem().setHelp(newShell,
				IJavaHelpContextIds.EDIT_TEMPLATE_DIALOG);
	}

}
