/*
* Copyright (c) 2009 Nokia Corporation and/or its subsidiary(-ies).
* All rights reserved.
* This component and the accompanying materials are made available
* under the terms of the License "Eclipse Public License v1.0"
* which accompanies this distribution, and is available
* at the URL "http://www.eclipse.org/legal/epl-v10.html".
*
* Initial Contributors:
* Nokia Corporation - initial contribution.
*
* Contributors:
*
* Description: 
*
*/
package com.nokia.sdt.uidesigner.events;


import com.nokia.cpp.internal.api.utils.core.Logging;
import com.nokia.cpp.internal.api.utils.core.TextUtils;
import com.nokia.sdt.datamodel.IDesignerDataModel;
import com.nokia.sdt.datamodel.adapter.*;
import com.nokia.sdt.datamodel.util.ModelUtils;
import com.nokia.sdt.editor.IDesignerEditor;
import com.nokia.sdt.editor.IDesignerDataModelEditor;
import com.nokia.sdt.uidesigner.events.EventModel.CategoryItem;
import com.nokia.sdt.uidesigner.events.EventModel.EventItem;
import com.nokia.sdt.uidesigner.ui.UIDesignerPlugin;
import com.nokia.sdt.uidesigner.ui.command.DataModelCommandWrapper;
import com.nokia.sdt.uidesigner.ui.utils.Messages;
import com.nokia.sdt.uimodel.UIModelPlugin;

import org.eclipse.core.resources.IResource;
import org.eclipse.core.runtime.*;
import org.eclipse.emf.common.command.Command;
import org.eclipse.emf.ecore.EObject;
import org.eclipse.gef.ui.actions.ActionRegistry;
import org.eclipse.jface.action.*;
import org.eclipse.jface.operation.IRunnableWithProgress;
import org.eclipse.jface.viewers.*;
import org.eclipse.swt.SWT;
import org.eclipse.swt.events.*;
import org.eclipse.swt.graphics.Image;
import org.eclipse.swt.graphics.Rectangle;
import org.eclipse.swt.widgets.*;
import org.eclipse.ui.*;
import org.eclipse.ui.actions.ActionFactory;
import org.eclipse.ui.part.Page;
import org.eclipse.ui.progress.IWorkbenchSiteProgressService;

import java.lang.reflect.InvocationTargetException;
import java.util.HashSet;
import java.util.Set;

/**
 * Default implementation of the event editor UI
 */
public class EventPage extends Page implements IEventPage {

	private Shell parentShell;
	private EventView view;
	private EventTreeViewer viewer;
	private EventCellEditor cellEditor;
	EventModel model;
	private Action categoriesAction;
	private Action doubleClickAction;
	private boolean showCategories = true;
	String columnProperties[] = {"EventPage.0", "EventPage.1"}; //$NON-NLS-1$ //$NON-NLS-2$
	private IDesignerDataModelEditor designerDataModelEditor;
	private EventBindingListener eventBindingListener = new EventBindingListener();
	private IStatusLineManager statusLineManager;
	private boolean refreshModelPending;
	private final static String LAST_CONFIG = "EventPage.LastShowCategoriesSetting"; //$NON-NLS-1$
	private final static QualifiedName QUALIFIED_LAST_CONFIG = 
						new QualifiedName(UIDesignerPlugin.getDefault().toString(), LAST_CONFIG);
	
	public EventPage(EventView view) {
		this.view = view;
	}

	public void createControl(Composite parent) {
		parentShell = parent.getShell();
		initializeTreeViewer(parent);
		viewer.setContentProvider(new ViewContentProvider());
		viewer.setLabelProvider(new ViewLabelProvider());
		viewer.setSorter(new NameSorter());
		viewer.setInput(getSite());
		makeActions();
		hookContextMenu();
		hookDoubleClickAction();
		contributeToActionBars();
		hookTreeSelection();
		hookHelp();
	}
	
	private void initializeTreeViewer(Composite parent) {
		viewer = new EventTreeViewer(parent, SWT.FULL_SELECTION | SWT.SINGLE
                | SWT.HIDE_SELECTION | SWT.V_SCROLL);	
		final Tree tree = viewer.getTree();
		tree.setLinesVisible(true);
		tree.setHeaderVisible(true);
				
		TreeColumn column = new TreeColumn(tree, 0);
		column.setText(Messages.getString(columnProperties[0]));
		column = new TreeColumn(tree, 1);
		column.setText(Messages.getString(columnProperties[1]));
		
		// install a cell editor for column 1. We never edit column 0
		cellEditor = new EventCellEditor(viewer.getTree());
		cellEditor.addListener(new ICellEditorListener() {

			public void applyEditorValue() {
				setErrorMessage(null);
			}

			public void cancelEditor() {
				setErrorMessage(null);
			}

			public void editorValueChanged(boolean oldValidState, boolean newValidState) {
				String message = null;
				if (!newValidState) {
					message = cellEditor.getErrorMessage();
				}
				setErrorMessage(message);
			}
			
		});
		
		CellEditor editors[] = new CellEditor[2];
		editors[1] = cellEditor;
		viewer.setCellEditors(editors);
		viewer.setColumnProperties(columnProperties);
		viewer.setCellModifier(new EventCellModifier(this));
   
		// listen for initial resize to set column widths
        tree.addControlListener(new ControlAdapter() {
            public void controlResized(ControlEvent e) {
                Rectangle area = tree.getClientArea();
                TreeColumn[] columns = tree.getColumns();
                if (area.width > 0) {
                    columns[0].setWidth(area.width * 40 / 100);
                    columns[1].setWidth(area.width - columns[0].getWidth() - 4);
                    tree.removeControlListener(this);
                }
            }
        });
	}
	
	private IDesignerEditor getDesignerEditor() {
		if (designerDataModelEditor != null)
			return (IDesignerEditor) designerDataModelEditor.getAdapter(IDesignerEditor.class);
		
		return null;
	}
	
	/**
	 * Tries to execute the command in the context of the
	 * editor's GEF command stack. If not accessible
	 * then it directly executes the command without undo support
	 */
	boolean executeEMFCommand(Command command) {
		boolean success = true;
		try {
			org.eclipse.gef.commands.CommandStack commandStack = null;
			IDesignerEditor designerEditor = getDesignerEditor();
			if (designerEditor != null) {
				commandStack = designerEditor.getEditDomain().getCommandStack();
			}
			
			if (commandStack != null) {
				DataModelCommandWrapper wrapper = new DataModelCommandWrapper();
				wrapper.setDataModelCommand(command);
				commandStack.execute(wrapper);
			}
			else {
				if (command.canExecute()) {
					command.execute();
				}
			}
		}
		catch (Exception x) {
			IStatus status = Logging.newSimpleStatus(0, x);
			UIDesignerPlugin.getDefault().log(x);
			Logging.showErrorDialog(Messages.getString("EventPage.2"), status.getMessage(), status); //$NON-NLS-1$
			success = false;
		}
		return success;
	}
	
	private void runAsync(Runnable r) {
		Display d = getSite().getShell().getDisplay();
		d.asyncExec(r);
	}
	
	private void hookTreeSelection() {
		viewer.addSelectionChangedListener( new ISelectionChangedListener() {

			/* We can't invoke editing from the selection event, we
			 * must do it asynchronously
			 */
			Object objToEdit;
			private Runnable runnable= new Runnable() {
				public void run() {
					if (objToEdit != null && !viewer.getControl().isDisposed()) {						
						Object tmp = objToEdit;
						objToEdit = null;
						cellEditor.setDescriptor(null);
						//viewer.editElement(tmp, 1);
						if (tmp instanceof EventModel.EventItem) {
							EventModel.EventItem item = (EventModel.EventItem) tmp;
							String description = item.descriptor.getDescription();
							EventPage.this.statusLineManager.setMessage(description);
							cellEditor.setDescriptor(item.descriptor);
							if (item.binding == null) {
								cellEditor.markEphemeral();
							}
						}
					
					}
				}
			};
		
			public void selectionChanged(SelectionChangedEvent event) {
				objToEdit = getSelectedEventItem(event.getSelection());
				if (objToEdit != null) {
					EventPage.this.runAsync(runnable);						
				}
			}
		});
	}
	
	private void hookHelp() {
		viewer.getControl().addHelpListener(new HelpListener() {
			public void helpRequested(HelpEvent e) {
				EventModel.EventItem item = getSelectedEventItem(viewer.getSelection());
				if (item != null) {
					String helpKey = item.descriptor.getHelpKey();
					if (TextUtils.strlen(helpKey) > 0) {
                        getSite().getWorkbenchWindow().getWorkbench()
						.getHelpSystem().displayHelp(helpKey);
					}
				}
			}
		});
	}
	
	private EventModel.EventItem getSelectedEventItem(ISelection selection) {
		EventModel.EventItem result = null;
		if (selection instanceof IStructuredSelection) {
			IStructuredSelection ss = (IStructuredSelection) selection;
			Object obj = ss.getFirstElement();
			if (obj instanceof EventModel.EventItem) {
				result = (EventItem) obj;
			}
		}
		return result;
	}

	public Control getControl() {
		Control result = null;
		if (viewer != null) {
			result = viewer.getControl();
		}
		return result;
	}

	public void setFocus() {
		if (viewer != null) {
			viewer.getControl().setFocus();
		}
	}
		
	/*
	 * Tries to convert the object into an IComponentInstance
	 */
	private static IComponentInstance convertToInstance(Object obj)  {
		IComponentInstance result = null;
		if (obj instanceof IComponentInstance) {
			result = (IComponentInstance) obj;
		}
		else if (obj instanceof EObject) {
			result = ModelUtils.getComponentInstance((EObject)obj);
		}
		else if (obj instanceof IAdaptable) {
			IAdaptable adaptable = (IAdaptable) obj;
			result = (IComponentInstance) adaptable.getAdapter(IComponentInstance.class);
		}
		return result;
	}

	private IComponentInstance[] getInstancesFromSelection(ISelection selection) {
		IComponentInstance[] result = null;
		if (selection instanceof IStructuredSelection) {
			IStructuredSelection ss = (IStructuredSelection) selection;
	
			// We must be able to get an IComponentInstance for each
			// object in the selection
			Object[] objects = ss.toArray();
			
			boolean selectionContainsNonInstance = false;
			// the same object can be selected in two parts of the editor, so use a set
			Set<IComponentInstance> instances = new HashSet<IComponentInstance>();
			for (int i = 0; i < objects.length; i++) {
				IComponentInstance ci = convertToInstance(objects[i]);
				if (ci != null) {
					instances.add(ci);
				}
				else {
					selectionContainsNonInstance = true;
					break;
				}
			}
			
			if (!selectionContainsNonInstance && (instances.size() > 0)) {
				result = (IComponentInstance[]) 
						instances.toArray(new IComponentInstance[instances.size()]);
			}
		}
		return result;	
	}

	public void selectionChanged(IWorkbenchPart part, ISelection selection) {
		// the selection has changed in some other workbench part
		if (designerDataModelEditor == null) {
			return; // don't show anything if this is the default page
		}
			
		if (viewer != null) {
			EventModel newModel = null;
			IComponentInstance[] instances = getInstancesFromSelection(selection);
			if (instances != null) {
				newModel = new EventModel(instances);
			}
			setModel(newModel, true);
		}
	}
	
	class ViewContentProvider implements IStructuredContentProvider, 
										   ITreeContentProvider {

		public void inputChanged(Viewer v, Object oldInput, Object newInput) {
		}
		public void dispose() {
		}
		public Object[] getElements(Object parent) {
			Object[] result = null;
			if (model != null) {
				if (parent.equals(getSite())) {
					if (showCategories) {
						result = model.getCategoryItems();
					}
					else {
						result = model.getEventItems();
					}
				}
				else if (parent instanceof EventModel.CategoryItem) {
					EventModel.CategoryItem item = (CategoryItem) parent;
					result = item.events;
				}
			}
			
			if (result == null)
				result = new Object[0];
			return result;
		}
		
		public Object getParent(Object child) {
			Object result = null;
			if (showCategories && model != null && child instanceof EventModel.EventItem) {
				result = model.findCategoryItem(((EventModel.EventItem)child));
			}
			return result;
		}
		public Object [] getChildren(Object parent) {
			Object result[] = null;
			if (parent instanceof EventModel.CategoryItem) {
				EventModel.CategoryItem item = (CategoryItem) parent;
				result = item.events;
			}
			return result;
		}
		public boolean hasChildren(Object parent) {
			boolean result = false;
			if (parent instanceof EventModel.CategoryItem) {
				EventModel.CategoryItem item = (EventModel.CategoryItem) parent;
				result = item.events != null;
			}
			return result;
		}
	}

	class ViewLabelProvider extends LabelProvider implements ITableLabelProvider {
	
		public String getColumnText(Object element, int columnIndex) {
			String result = null;
			if (element instanceof EventModel.CategoryItem) {
				if (columnIndex == 0) {
					EventModel.CategoryItem item = (EventModel.CategoryItem) element;
					result = item.category;
				}
			}
			else if (element instanceof EventModel.EventItem) {
				EventModel.EventItem item = (EventModel.EventItem) element;
				if (columnIndex == 0) {
					result = item.descriptor.getDisplayText();
				}
				else if (item.binding != null){
					result = item.binding.getHandlerName();
				}
			}
			return result;
		}
		
		public String getText(Object element) {
			// getText is called for sorting
			return getColumnText(element, 0);
		}

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

	class NameSorter extends ViewerSorter {
		
	}

	public void associateDesignerEditor(IDesignerDataModelEditor editor) {
		this.designerDataModelEditor = editor;
		IDesignerEditor designerEditor = getDesignerEditor();
		boolean savedShowCategories = (designerEditor != null) && getSavedShowCategories(designerEditor);
		showCategories(savedShowCategories);
	}

	private void hookContextMenu() {
		MenuManager menuMgr = new MenuManager("#PopupMenu"); //$NON-NLS-1$
		menuMgr.setRemoveAllWhenShown(true);
		menuMgr.addMenuListener(new IMenuListener() {
			public void menuAboutToShow(IMenuManager manager) {
				EventPage.this.fillContextMenu(manager);
			}
		});
		Menu menu = menuMgr.createContextMenu(viewer.getControl());
		viewer.getControl().setMenu(menu);
	//	getSite().registerContextMenu(menuMgr, viewer);
	}

	private void contributeToActionBars() {
		IActionBars bars = getSite().getActionBars();
		fillLocalPullDown(bars.getMenuManager());
		fillLocalToolBar(bars.getToolBarManager());
		
		statusLineManager = bars.getStatusLineManager();
		
		IDesignerEditor designerEditor = getDesignerEditor();
		if (designerEditor != null) {
			// Add the editor's revert, undo, and redo.
			ActionRegistry registry = designerEditor.getActionRegistry();
			IAction action = registry.getAction(ActionFactory.REVERT.getId());
			bars.setGlobalActionHandler(action.getId(), action);
			action = registry.getAction(ActionFactory.UNDO.getId());
			bars.setGlobalActionHandler(action.getId(), action);
			action = registry.getAction(ActionFactory.REDO.getId());
			bars.setGlobalActionHandler(action.getId(), action);
			bars.updateActionBars();
		}
	}

	private void fillLocalPullDown(IMenuManager manager) {
		manager.add(categoriesAction);
	}

	private void fillContextMenu(IMenuManager manager) {
		manager.add(categoriesAction);
		// Other plug-ins can contribute there actions here
		manager.add(new Separator(IWorkbenchActionConstants.MB_ADDITIONS));
	}
	
	private void fillLocalToolBar(IToolBarManager manager) {
		manager.add(categoriesAction);
	}

	private void makeActions() {
		categoriesAction = new CategoriesAction(this);
		categoriesAction.setChecked(isShowingCategories());
	
		// The behavior of double-click is:
		// - create a binding if none exists
		// - navigate to binding
		doubleClickAction = new Action() {
			public void run() {
				EventModel.EventItem item = getSelectedEventItem(viewer.getSelection());
				if (item != null) {
					viewer.cancelEditing();
					IEventBinding binding = item.binding;
					boolean isNewBinding = false;
					if (binding == null) {
						IComponentInstance instances[] = model.getInstances();
						IDesignerDataModel model = instances[0].getDesignerDataModel();
						isNewBinding = true;
						Command command = EventCommands.createEventBindingCommand(
								instances, item.descriptor, null);
						if (command != null) {
							executeEMFCommand(command);
							binding = EventCommands.getEventBindingFromCommandResult(command);
							
							boolean doSave = EventCommands.userConfirmationToSaveModelDialog(
											parentShell, model);
							if (!doSave || !saveDataModel()) {
								binding = null;
							}
						}
					}
					if (binding != null) {
						EventCommands.navigateToHandlerCode(EventPage.this, binding, isNewBinding);
					}
				}
			}
		};
		cellEditor.setDoubleClickAction(doubleClickAction);
	}

	private void hookDoubleClickAction() {
		viewer.addDoubleClickListener(new IDoubleClickListener() {
			public void doubleClick(DoubleClickEvent event) {
				doubleClickAction.run();
			}
		});
	}

	public boolean isShowingCategories() {
		return showCategories;
	}

	public void showCategories(boolean show) {
		if (show != this.showCategories) {
			this.showCategories = show;
			refresh();
			saveShowCategories(getDesignerEditor(), show);
		}
	}
	
	public static void saveShowCategories(IDesignerEditor designerEditor, boolean show) {
		UIModelPlugin.getDefault().getPreferenceStore().setValue(LAST_CONFIG, show);
		// save in the resource
		if (designerEditor != null) {
			IResource resource = designerEditor.getDataModelResource();
			try {
				resource.setPersistentProperty(QUALIFIED_LAST_CONFIG, String.valueOf(show));
			} 
			catch (CoreException e) {} // don't do anything if this fails
		}
	}
	
	public static boolean getSavedShowCategories(IDesignerEditor designerEditor) {
		String savedStateString = null;
		
		// first try the resource
		IResource resource = designerEditor.getDataModelResource();
		try {
			savedStateString = resource.getPersistentProperty(QUALIFIED_LAST_CONFIG);
		} 
		catch (CoreException e) {} // don't do anything if this fails

		// then try the plugin setting
		if ((savedStateString == null) || (savedStateString.length() == 0))
			savedStateString = UIModelPlugin.getDefault().getPreferenceStore().getString(LAST_CONFIG);

		// if still no saved setting, then default to true
		if ((savedStateString == null) || (savedStateString.length() == 0))
			return true;
			
		return Boolean.parseBoolean(savedStateString);
	}
	
	void refresh() {
		if (viewer != null) {
			viewer.refresh();
			viewer.expandAll();
		}
	}
	
	public void dispose() {
		setModel(null, false);
		super.dispose();
	}

	void refreshModel(IComponentInstance[] instances) {
		if (model != null) {
			EventModel newModel = new EventModel(model.getInstances());
			setModel(newModel, true);
		}
	}
	
	void asynchRefreshModel() {
		Runnable r = new Runnable() {
			public void run() {
				EventPage.this.refreshModelPending = false;
				if (!viewer.getControl().isDisposed()) {
					if (model != null) {
						// attempt to preserve selected event
						String selectedEventID = null;
						EventModel.EventItem currSelected = getSelectedEventItem(viewer.getSelection());
						if (currSelected != null) {
							selectedEventID = currSelected.descriptor.getId();
						}

						refreshModel(model.getInstances());
						
						if (selectedEventID != null) {
							EventItem item = model.findEventItem(selectedEventID);
							if (item != null) {
								selectItem(item);
							}
						}

					}
				}
			}
		};
		// This is meant to avoid multiple refreshes queued on the
		// event thread (to avoid flashing). Thread safety is not needed
		if (!refreshModelPending) {
			refreshModelPending = true;
			runAsync(r);
		}
	}
	
	/**
	 * Tell Eclipse the content changed. In the default presentation
	 * this temporarily bolds the view's title tab.
	 */
	private void markViewChanged() {
		IWorkbenchPartSite site = view.getSite();
		IWorkbenchSiteProgressService wsps = 
			(IWorkbenchSiteProgressService)site.getAdapter(IWorkbenchSiteProgressService.class);
		if (wsps != null) {
			wsps.warnOfContentChange();
	 	}
	}
	
	private void setModel(EventModel newModel, boolean refresh) {
		IComponentInstance instances[];
		if (model != null) {
			instances = model.getInstances();
			for (int i = 0; i < instances.length; i++) {
				IComponentInstance instance = instances[i];
				instance.removeEventBindingListener(eventBindingListener);
			}
		}
		this.model = newModel;
		if (model != null) {
			instances = model.getInstances();
			for (int i = 0; i < instances.length; i++) {
				IComponentInstance instance = instances[i];
				instance.addEventBindingListener(eventBindingListener);
			}
		}
		if (refresh) {
			refresh();
		}
	}

	public void deactivateCellEditor() {
		viewer.cancelEditing();
	}
	
	boolean cellEditorIsDirty() {
		// we only ever edit column 1
		return viewer.getCellEditors()[1].isDirty();
	}
	
	void setErrorMessage(String message) {
		if (statusLineManager != null) {
			statusLineManager.setErrorMessage(message);
		}
	}
		
	// All view updating is driven by these events
	class EventBindingListener implements IEventBindingListener {
		public void bindingAdded(EObject instance, IEventBinding eventBinding) {
			asynchRefreshModel();
			markViewChanged();			
		}
		
		public void bindingRemoved(EObject instance, IEventBinding eventBinding) {
			asynchRefreshModel();			
		}
	}

	public Shell getParentShell() {
		return parentShell;
	}
	
	boolean saveDataModel() {
		/*
		UIJob job = new UIJob(Messages.getString("EventPage.3")) { //$NON-NLS-1$
			public IStatus runInUIThread(IProgressMonitor monitor) {
				designerEditor.doSave(monitor);
				return Logging.newStatus(UIDesignerPlugin.getDefault(), IStatus.OK, null);
			}
		};
		job.setUser(true);
		job.schedule();
		return true;*/
		IRunnableWithProgress runnable = new IRunnableWithProgress() {
			public void run(IProgressMonitor monitor) throws InvocationTargetException, InterruptedException {
				designerDataModelEditor.doSave(monitor);
			}
		};
		//ProgressMonitorDialog dialog = new ProgressMonitorDialog(PlatformUI.getWorkbench().getActiveWorkbenchWindow().getShell());
		//dialog.run(false, true, runnable);
		try {
			PlatformUI.getWorkbench().getProgressService().runInUI(PlatformUI.getWorkbench().getActiveWorkbenchWindow(), runnable, null);
			return true;
		} catch (InvocationTargetException e) {
			UIDesignerPlugin.getDefault().log(e);
			return false;
		} catch (InterruptedException e) {
			return false;
		}
	}
	
	public void selectItem(EventItem item) {
		viewer.setSelection(new StructuredSelection(item));
	}
}
