/*******************************************************************************
 * Copyright (c) 2005 IBM Corporation and others.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 *
 * Contributors:
 *     IBM Corporation - initial API and implementation
 *******************************************************************************/
package org.eclipse.bpel.ui.properties;

import java.util.ArrayList;

import org.eclipse.bpel.common.ui.command.ICommandFramework;
import org.eclipse.bpel.common.ui.details.IDetailsAreaConstants;
import org.eclipse.bpel.common.ui.details.viewers.CComboViewer;
import org.eclipse.bpel.common.ui.editmodel.AbstractEditModelCommand;
import org.eclipse.bpel.common.ui.editmodel.EditModelCommandStack;
import org.eclipse.bpel.common.ui.flatui.FlatFormLayout;
import org.eclipse.bpel.model.Process;
import org.eclipse.bpel.model.adapters.IProperty;
import org.eclipse.bpel.ui.BPELEditor;
import org.eclipse.bpel.ui.BPELTabbedPropertySheetPage;
import org.eclipse.bpel.ui.actions.ShowPropertiesViewAction;
import org.eclipse.bpel.ui.adapters.AdapterNotification;
import org.eclipse.bpel.ui.adapters.IMarkerHolder;
import org.eclipse.bpel.ui.commands.CompoundCommand;
import org.eclipse.bpel.ui.proposal.providers.ModelContentProposalProvider;
import org.eclipse.bpel.ui.util.BPELUtil;
import org.eclipse.bpel.ui.util.MultiObjectAdapter;
import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IMarker;
import org.eclipse.emf.common.notify.Notification;
import org.eclipse.emf.ecore.EObject;
import org.eclipse.emf.ecore.resource.Resource;
import org.eclipse.gef.commands.Command;
import org.eclipse.jface.viewers.ISelection;
import org.eclipse.jface.viewers.IStructuredSelection;
import org.eclipse.jface.viewers.StructuredSelection;
import org.eclipse.swt.SWT;
import org.eclipse.swt.events.DisposeEvent;
import org.eclipse.swt.events.DisposeListener;
import org.eclipse.swt.layout.FillLayout;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Display;
import org.eclipse.ui.IFileEditorInput;
import org.eclipse.ui.IWorkbenchPart;
import org.eclipse.ui.internal.views.properties.tabbed.view.TabbedPropertyViewer;
import org.eclipse.ui.views.properties.tabbed.AbstractPropertySection;
import org.eclipse.ui.views.properties.tabbed.TabContents;
import org.eclipse.ui.views.properties.tabbed.TabbedPropertySheetPage;
import org.eclipse.ui.views.properties.tabbed.TabbedPropertySheetWidgetFactory;

/**
 * An abstract implementation which provides some adapter support and useful stuff.
 * This was based on the common implementation characteristics of bpel.ui properties
 * pages.
 * 
 * Implementors may subclass this class, or they could extend AbstractPropertySection directly.
 */ 
@SuppressWarnings("nls")
public abstract class BPELPropertySection extends AbstractPropertySection 	
{
	protected static final String EMPTY_STRING = ""; //$NON-NLS-1$
			
	protected static final IMarker EMPTY_MARKERS[] = new IMarker[] {};
		
	protected static final MultiObjectAdapter[] EMPTY_MULTI_OBJECT_ARRAY = new MultiObjectAdapter[0];
	
	/** Standard label width */
	public static final int STANDARD_LABEL_WIDTH_SM = 105;
	
	/** Standard label width - applying the 25% fudge factor */
	public static final int STANDARD_LABEL_WIDTH_AVG = STANDARD_LABEL_WIDTH_SM * 5/4;
	
	/** Standard label width - applying the 50% fudge factor */
	public static final int STANDARD_LABEL_WIDTH_LRG = STANDARD_LABEL_WIDTH_SM * 3/2;
	
	/** Standard button width */
	public static final int STANDARD_BUTTON_WIDTH = 60;
	
	/** Short button width */
	public static final int SHORT_BUTTON_WIDTH = 45;

	protected MultiObjectAdapter[] fAdapters;
	protected boolean isCreated;
	protected boolean isHidden;
	protected EObject fModelObject;
	
	protected TabbedPropertySheetWidgetFactory fWidgetFactory;
	
	
	protected BPELTabbedPropertySheetPage fTabbedPropertySheetPage;

	
		
	final protected ModelContentProposalProvider.ValueProvider inputValueProvider =  new ModelContentProposalProvider.ValueProvider () {
		@Override
		public Object value() {
			return getModel();
		}		
	};
	

	/**
	 * @see org.eclipse.ui.views.properties.tabbed.AbstractPropertySection#getWidgetFactory()
	 */
	@Override
	public TabbedPropertySheetWidgetFactory getWidgetFactory() {
		TabbedPropertySheetWidgetFactory wf = super.getWidgetFactory();
		wf.setBorderStyle(SWT.BORDER);
		return wf;
	}

	/**
	 * Brand new shiny BPELPropertySection
	 */
	
	public BPELPropertySection() {
		super();
		fAdapters = createAdapters();
	}

	/**
	 * Subclasses must override this to return DetailsAdapters.
	 * The default implementation returns an empty array.
	 */
	protected MultiObjectAdapter[] createAdapters() {
		return EMPTY_MULTI_OBJECT_ARRAY;
	}

	/**
	 * Removes all adapters from whatever they are attached to.
	 * Subclasses may override.
	 */
	protected void removeAllAdapters() {
		for (MultiObjectAdapter a : fAdapters) {
			a.removeFromAll();
		}
	}

	/**
	 * This implementation just hooks the first adapter on the input object.
	 * Subclasses may override.
	 */
	protected void addAllAdapters() {
		assert isCreated : "Not yet created!" ;
		
		if (fAdapters.length > 0) {
			if (getModel() != null) {
				fAdapters[0].addToObject(getModel());
			}
		}
	}

	/**
	 * Convenience method for removing and re-adding adapters.  This is a simple and general 
	 * way to react to model changes that grow or shrink the set of model objects we want our
	 * adapters to be on.  
	 */
	protected void refreshAdapters() {
		removeAllAdapters();
		addAllAdapters();
	}

	/**
	 * This method is intended to set the input object.  Subclasses may override this
	 * method to perform necessary cleanup before changing the input object, and/or
	 * perform initialization after changing the input object.
	 * 
	 * Subclasses may also override to change the policy of which object is used as
	 * the input for a particular properties section.
	 * 
	 * For example: a section for a custom activity, may wish to override this method
	 * to use the custom activity ExtensibilityElement as the "main" input object.
	 */
	@SuppressWarnings("unchecked")
	protected void basicSetInput(EObject newInput) {
		fModelObject = newInput;
	}
	
	@SuppressWarnings("unchecked")
	protected void restoreUserContextFromInput () {
		IProperty<String,Object> prop = BPELUtil.adapt(fModelObject, IProperty.class);
		if (prop != null) {
			restoreUserContext( prop.getProperty( getClass().getName() ) );			
		}
	}
	
	@SuppressWarnings("unchecked")
	protected void saveUserContextToInput () {
		IProperty<String,Object> prop = BPELUtil.adapt(fModelObject, IProperty.class);
		if (prop != null) {
			prop.setProperty(getClass().getName(), getUserContext() );			
		}		
	}
	
	
	/**
	 * @see org.eclipse.ui.views.properties.tabbed.AbstractPropertySection#setInput(org.eclipse.ui.IWorkbenchPart, org.eclipse.jface.viewers.ISelection)
	 */
	@Override
	public final void setInput(IWorkbenchPart part, ISelection selection) {
		
		super.setInput(part, selection);
		
		if ((selection instanceof IStructuredSelection) == false) {
			return ;
		}
		
		Object model = ((IStructuredSelection)selection).getFirstElement();
		if (model == fModelObject) {
			return;
		}
				
		removeAllAdapters();
		
		super.setInput(part, selection);
		
	    basicSetInput((EObject)model);
	    
		// Careful: don't assume input == newInput.
		// There are basicSetInput() hacks that violate that assumption  =)
		// TODO: is this comment related to the custom activities?
		addAllAdapters();		
	}

	
	/**
	 * @see org.eclipse.ui.views.properties.tabbed.AbstractPropertySection#aboutToBeHidden()
	 */
	@Override
	public void aboutToBeHidden() {
		isHidden = true;
	}

	/**
	 * @see org.eclipse.ui.views.properties.tabbed.AbstractPropertySection#aboutToBeShown()
	 */
	@Override
	public void aboutToBeShown() {
		isHidden = false;
	}

	@SuppressWarnings("unchecked")
	protected <T extends EObject> T getModel() {
		return (T) fModelObject;
	}

	protected final <T extends EObject> T getInput() {
		return getModel();
	}
	
	
	/**
	 * Refresh the given CComboViewer, and also make sure selectedObject is selected in it.
	 */
	protected void refreshCCombo(CComboViewer viewer, Object selectedObject) {
		viewer.refresh();
		
		if (selectedObject == null) {
			viewer.setSelectionNoNotify ( StructuredSelection.EMPTY ,false );
		} else {
			viewer.setSelectionNoNotify (new StructuredSelection(selectedObject), false);
		}
	}

	/**
	 * Create the controls.
	 * 
	 * @see org.eclipse.ui.views.properties.tabbed.AbstractPropertySection#createControls(org.eclipse.swt.widgets.Composite, org.eclipse.ui.views.properties.tabbed.TabbedPropertySheetPage)
	 */
	
	@Override
	public void createControls (final Composite parent, TabbedPropertySheetPage aTabbedPropertySheetPage) {
		
		super.createControls(parent, aTabbedPropertySheetPage);
		
		this.fTabbedPropertySheetPage = (BPELTabbedPropertySheetPage)aTabbedPropertySheetPage;
		this.fWidgetFactory = getWidgetFactory();
		
		assert !isCreated : "Not yet created!";
		
		Composite marginComposite = fWidgetFactory.createComposite(parent); 
		FillLayout fillLayout = new FillLayout();
		fillLayout.marginWidth = IDetailsAreaConstants.HMARGIN;
		fillLayout.marginHeight = IDetailsAreaConstants.VMARGIN/2;
		marginComposite.setLayout(fillLayout);
		createClient(marginComposite);
		isHidden = true;
		isCreated = true;
			
		parent.addDisposeListener(new DisposeListener() {
			public void widgetDisposed(DisposeEvent e) {
				dispose();
			}
			
		});
		
	}
	
	/**
	 * Subclasses should override this to create the child controls of the section.
	 */
	protected abstract void createClient(Composite parent);

	/**
	 * @see org.eclipse.ui.views.properties.tabbed.AbstractPropertySection#dispose()
	 */
	@Override
	public void dispose() {		
		if (isCreated) {
			// TODO HACK: this shouldn't really be here!  But where should it be??
			getCommandFramework().applyCurrentChange();
			removeAllAdapters();
		}
		isCreated = false;
	}

	/**
	 * @see org.eclipse.ui.views.properties.tabbed.AbstractPropertySection#refresh()
	 */
	@Override
	public void refresh() {
		super.refresh();
		updateStatusLabels();
	}

	/**
	 * Subclasses must override this method in order to refresh the status labels 
	 */
	protected void updateStatusLabels() {
	}

	/**
	 * Gets all IMarker according to the passed input model.
	 * @see IMarkerHolder.getMarkers 
	 */
	
	protected IMarker[] getMarkers (Object input) {
		
		IMarkerHolder markerHolder = BPELUtil.adapt(input, IMarkerHolder.class);
		if (markerHolder != null) {
			ArrayList<IMarker> filteredMarkers = new ArrayList<IMarker>(4);			
			for(IMarker m : markerHolder.getMarkers(input)) {
				if (isValidMarker(m)) {
					filteredMarkers.add(m);
				}
			}
			if (filteredMarkers.size() > 0) {
				return filteredMarkers.toArray(EMPTY_MARKERS);
			}
		}
		return EMPTY_MARKERS;
	}
	
	
	protected boolean markersHaveChanged  ( Notification n ) {		
		int eventGroup = n.getEventType() / 100; 
		return eventGroup == AdapterNotification.NOTIFICATION_MARKERS_CHANGED_GROUP ;
	}
	
	
	protected void updateMarkers ( ) {
		
	}

	protected CompoundCommand makeCompound ( Command command ) {
		
		if (command == null) {
			return null;
		}
		if (command instanceof CompoundCommand) {
			return (CompoundCommand) command;
		}
		CompoundCommand cc = new CompoundCommand ();
		cc.add(command);
		return cc;
	}
	
	
	protected void runCommand ( Command command ) {
		getCommandFramework().execute( wrapInShowContextCommand(command) );
	}
	
	protected ICommandFramework getCommandFramework() {
		BPELEditor editor = getBPELEditor();
		if (editor != null) {
			return editor.getCommandFramework();
		}
		return null;
	}

	/**
	 * @return the BPELEditor
	 */
	
	public BPELEditor getBPELEditor() {
		if (fTabbedPropertySheetPage != null) {
			return fTabbedPropertySheetPage.getEditor();
		}
		return null;
	}

	/**
	 * @return the BPEL process
	 */
	public Process getProcess()  {
		return getBPELEditor().getProcess();
	}

	protected EditController createEditController ( ) {
		return new EditController ( getCommandFramework() ) {
			@Override
			public Command createApplyCommand() {				
				return wrapInShowContextCommand( super.createApplyCommand() );
			}			
		};
	}
	
	
	/**
	 * Convenience accessor with default policy (this is overridden in certain subclasses).
	 */
	protected Command wrapInShowContextCommand(final Command inner) {
		return wrapInShowContextCommand(inner, this);
	}

	/**
	 * Create a command that wraps the command passed in the show/restore context commands.
	 * 
	 * @param inner the inner command to be run.
	 * @param section the BPEL property section
	 * @return the command new wrapped command.
	 */
	
	protected Command wrapInShowContextCommand(final Command inner, BPELPropertySection section) {
		
		/**
		 * Sometimes we have property sections inside property sections.
		 *  
		 * The owners section's input needs to be saved, because it is used to restore
		 * the selection later on in the "wrapping" command. The "inner" section's input
		 * may not be visibly selectable. For example, consider "Variable" property sheet.
		 * A separate section is used for the "From" part of Variable.  
		 */
		
		final Object previousInput = section.getInput(); 
		
		final TabbedPropertyViewer viewer = getTabbedPropertySheetPage().getTabbedPropertyViewer();
		final int tabIndex = viewer.getSelectionIndex();
		final int sectionIndex = getTabbedPropertySheetPage().getCurrentTab().getSectionIndex(section);
		
		if (!inner.canExecute()) {
			System.out.println("WARNING: unexecutable command passed to wrapInShowContextCommand():"); //$NON-NLS-1$
			System.out.println("    "+inner.getDebugLabel()); //$NON-NLS-1$
		}
		
		return new AbstractEditModelCommand() {
			Object beforeContext, afterContext;
			
			@Override
			public String getLabel() {
				return inner.getLabel();
			}
			
			@Override
			public void setLabel(String label) {
				inner.setLabel(label);
			}
			
			@Override
			public String getDebugLabel() {
				return "ShowContext wrapper:[" + inner.getDebugLabel() + "]"; //$NON-NLS-1$ //$NON-NLS-2$
			}
			
			@Override
			public boolean canExecute() {
				return inner.canExecute();
			}
			
			@Override
			public void execute() {
				BPELPropertySection aSection = getSection(sectionIndex); 
				beforeContext = (aSection==null)? null : aSection.getUserContext();
				inner.execute();
				afterContext = (aSection==null)? null : aSection.getUserContext();
			}
			
			@Override
			public boolean canUndo() {
				return inner.canUndo();
			}
			
			@Override
			public void undo() {
				inner.undo();
				showPropertiesTab();
				BPELPropertySection aSection = getSection(sectionIndex);
				if (aSection != null) {
					aSection.restoreUserContext(beforeContext);
				}
			}
			
			@Override
			public void redo() {
				inner.redo();
				showPropertiesTab();
				BPELPropertySection aSection = getSection(sectionIndex);
				if (aSection != null) {
					aSection.restoreUserContext(afterContext);
				}
			}
			
			@Override
			public void dispose() {
				inner.dispose();
			}
			
			protected BPELPropertySection getSection (int index) {
				 TabContents tab = getTabbedPropertySheetPage().getCurrentTab();
				 if (tab != null) {
					 return (BPELPropertySection) tab.getSectionAtIndex(sectionIndex);
				 }
				 return null;
			}
			
			protected void showPropertiesTab() {
				// TODO: Try to avoid selecting the model object all
				// the time, as it could cause unnecessary flashing.
				getBPELEditor().selectModelObject(previousInput);
				
				if (tabIndex != viewer.getSelectionIndex()) {
					Object selectedTab = viewer.getElementAt(tabIndex);
					if (selectedTab != null) {
						viewer.setSelection(new StructuredSelection(selectedTab));
					}
				}
			}
			
			// TODO: THIS IS A HACK.. these helpers might belong somewhere else.
			@Override
			public Resource[] getResources() { 
				return EditModelCommandStack.getResources(inner); 
			}
			
			@Override
			public Resource[] getModifiedResources() { 
				return EditModelCommandStack.getModifiedResources(inner); 
			}
			
		};
	}

	/**
	 * Creates a composite with a flat border around it.
	 */
	protected Composite createBorderComposite(Composite parent) {
	    return BPELUtil.createBorderComposite(parent, fWidgetFactory);
	}
	
	/**
	 * NOTE: use this method, NOT the method in TabbedPropertySheetWidgetFactory,
	 * whose semantics were inexplicably changed.
	 *
	 * 	TODO: We need a new/better story for layouts and borders ??
	 */
	protected Composite createFlatFormComposite(Composite parent) {
		Composite result = fWidgetFactory.createFlatFormComposite(parent);
		FlatFormLayout formLayout = new FlatFormLayout();
		formLayout.marginWidth = formLayout.marginHeight = 0;		
		result.setLayout(formLayout);
		return result;
	}
	
	/**
	 * @return the BPEL Tabbed Property Sheet page.
	 */
	
	public BPELTabbedPropertySheetPage getTabbedPropertySheetPage() {
		return fTabbedPropertySheetPage;
	}
	
	
	/**
	 * @return the IFile that the editor is editing.
	 */
	
	public IFile getBPELFile() {
		return ((IFileEditorInput) getBPELEditor().getEditorInput()).getFile();
	}

	/** 
	 * Returns a token indicating which widget should have focus.  Note that the token can't
	 * be tied to this particular instance of the section; after the section is destroyed
	 * and re-created, the token must still be valid.
	 * @return the user context 
	 */
	public Object getUserContext() {
		return null;
	}

	/** 
	 * Accepts a token created by getUserContext() and gives focus to the widget represented
	 * by the token.
	 * @param userContext the user context to restore.
	 */
	public void restoreUserContext(Object userContext) {
	}
	
	/**
	 * Shows the given marker.
	 * @param marker
	 */
	public void gotoMarker (IMarker marker) {
		
	}
	
	/**
	 * Returns true if this section knows how to show the given marker.
	 * 
	 * @param marker the marker to be checked. 
	 * @return true if so, false otherwise ... 
	 */
	
	public boolean isValidMarker (IMarker marker) {
		return true;
	}

	
	/**
	 * Return the Context names that allows us to point markers correctly at this 
	 * section.
	 * 
	 * @return an array of context names 
	 */
	
	public String[] getContextNames () {
		return new String[] {}; 
	}
	
	
	/**
	 * Given a model object, selects it in the BPEL Editor and makes sure the
	 * properties pages are also shown for it.
	 */
	protected void selectModelObject(final EObject target) {
		Runnable runnable = new Runnable() {
			public void run() {
				getBPELEditor().selectModelObject(target);
                new ShowPropertiesViewAction().run();
			}
		};
		Display.getDefault().asyncExec(runnable);
	}
}