/*******************************************************************************
 * Copyright (c) 2000, 2008 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.wst.jsdt.internal.ui.refactoring;

import java.lang.reflect.InvocationTargetException;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;

import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.SubProgressMonitor;
import org.eclipse.jface.dialogs.Dialog;
import org.eclipse.jface.operation.IRunnableWithProgress;
import org.eclipse.jface.viewers.IColorProvider;
import org.eclipse.jface.viewers.ISelectionChangedListener;
import org.eclipse.jface.viewers.IStructuredSelection;
import org.eclipse.jface.viewers.LabelProviderChangedEvent;
import org.eclipse.jface.viewers.SelectionChangedEvent;
import org.eclipse.jface.viewers.TreeViewer;
import org.eclipse.jface.wizard.IWizardPage;
import org.eclipse.ltk.ui.refactoring.RefactoringWizard;
import org.eclipse.ltk.ui.refactoring.UserInputWizardPage;
import org.eclipse.swt.SWT;
import org.eclipse.swt.graphics.Color;
import org.eclipse.swt.graphics.GC;
import org.eclipse.swt.graphics.Image;
import org.eclipse.swt.layout.GridData;
import org.eclipse.swt.layout.GridLayout;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Label;
import org.eclipse.swt.widgets.TreeItem;
import org.eclipse.ui.PlatformUI;
import org.eclipse.wst.jsdt.core.dom.ITypeBinding;
import org.eclipse.wst.jsdt.internal.corext.refactoring.structure.ChangeTypeRefactoring;
import org.eclipse.wst.jsdt.internal.corext.util.Messages;
import org.eclipse.wst.jsdt.internal.ui.IJavaHelpContextIds;
import org.eclipse.wst.jsdt.internal.ui.JavaScriptPlugin;
import org.eclipse.wst.jsdt.internal.ui.viewsupport.BindingLabelProvider;


/**
 * @author tip
 */
public class ChangeTypeWizard extends RefactoringWizard {

	private ChangeTypeRefactoring fCT;

	public ChangeTypeWizard(ChangeTypeRefactoring ref) {
		super(ref, DIALOG_BASED_USER_INTERFACE);
		setDefaultPageTitle(RefactoringMessages.ChangeTypeWizard_title); 
		fCT= ref;
	}

	/* non java-doc
	 * @see RefactoringWizard#addUserInputPages
	 */ 
	protected void addUserInputPages(){
		addPage(new ChangeTypeInputPage());
	}
	
	// For debugging
	static String print(Collection/*<ITypeBinding>*/ types){
		if (types.isEmpty())
			return "{ }"; //$NON-NLS-1$
		String result = "{ "; //$NON-NLS-1$
		for (Iterator it=types.iterator(); it.hasNext(); ){
			ITypeBinding type= (ITypeBinding)it.next();
			result += type.getQualifiedName();
			if (it.hasNext()){
				result += ", ";  //$NON-NLS-1$
			} else {
				result += " }"; //$NON-NLS-1$
			}
		}
		return result;
	}
	
	
	/**
	 * A JavaScriptElementLabelProvider that supports graying out of invalid types.
	 */
	private class ChangeTypeLabelProvider extends BindingLabelProvider 
										  implements IColorProvider {
		
		private Color fGrayColor;
		private HashMap/*<Image color, Image gray>*/ fGrayImages;
		
		public ChangeTypeLabelProvider(){
			fGrayColor= Display.getCurrent().getSystemColor(SWT.COLOR_WIDGET_NORMAL_SHADOW);
			fGrayImages= new HashMap();
		}
		
		private Collection/*<ITypeBinding>*/ fInvalidTypes;
		
		public void grayOut(Collection/*<ITypeBinding>*/ invalidTypes){
			fInvalidTypes= invalidTypes; 
			/*
			 * Invalidate all labels. Invalidating only invalid types doesn't
			 * work since there can be multiple nodes in the tree that
			 * correspond to the same invalid IType. The TreeViewer only updates
			 * the label of one of these ITypes and leaves the others in their
			 * old state.
			 */
			fireLabelProviderChanged(new LabelProviderChangedEvent(this));
		}

		/* (non-Javadoc)
		 * @see org.eclipse.jface.viewers.IColorProvider#getForeground(java.lang.Object)
		 */
		public Color getForeground(Object element) {
			if (isInvalid(element))
				return fGrayColor;
			else
				return null;
		}
		
		private boolean isInvalid(Object element) {
			if (fInvalidTypes == null)
				return false; // initially, everything is enabled
			else
				return fInvalidTypes.contains(element);
		}
		
		/* (non-Javadoc)
		 * @see org.eclipse.jface.viewers.IColorProvider#getBackground(java.lang.Object)
		 */
		public Color getBackground(Object element) {
			return null;
		}
		
		/*
		 * @see org.eclipse.jface.viewers.ILabelProvider#getImage(java.lang.Object)
		 */
		public Image getImage(Object element) {
			Image image= super.getImage(element);
			if (isInvalid(element) && image != null) {
				Image grayImage= (Image) fGrayImages.get(image);
				if (grayImage == null) {
					grayImage= new Image(Display.getCurrent(), image, SWT.IMAGE_GRAY);
					fGrayImages.put(image, grayImage);
				}
				return grayImage;
			} else {
				return image;
			}
		}
		
		public void dispose() {
			for (Iterator iter= fGrayImages.values().iterator(); iter.hasNext();) {
				Image image= (Image) iter.next();
				image.dispose();
			}
			fGrayImages.clear();
			super.dispose();
		}
	}
	
	private class ChangeTypeInputPage extends UserInputWizardPage{

		public static final String PAGE_NAME= "ChangeTypeInputPage";//$NON-NLS-1$
		private final  String MESSAGE= RefactoringMessages.ChangeTypeInputPage_Select_Type; 
		private ChangeTypeLabelProvider fLabelProvider;
		private TreeViewer fTreeViewer;
		private boolean fTreeUpdated= false;
		
		public ChangeTypeInputPage() {
			super(PAGE_NAME);
			setMessage(MESSAGE);
		}
		
		private class ValidTypesTask implements Runnable {
			private Collection/*<ITypeBinding>*/ fInvalidTypes;
			private Collection/*<ITypeBinding>*/ fValidTypes;
			public void run() {
				IRunnableWithProgress runnable= new IRunnableWithProgress() {
					public void run(IProgressMonitor pm) {
						pm.beginTask(RefactoringMessages.ChangeTypeWizard_analyzing, 1000); 
						ChangeTypeRefactoring ct= (ChangeTypeRefactoring)ChangeTypeWizard.this.getRefactoring();
						fInvalidTypes = new HashSet();
						fInvalidTypes.addAll(fCT.getAllSuperTypes(ct.getOriginalType()));
						fValidTypes= ct.computeValidTypes(new SubProgressMonitor(pm, 950));
						fInvalidTypes.add(ct.getOriginalType());
						fInvalidTypes.removeAll(fValidTypes);
						pm.worked(50);
						pm.done();
					}
				};
				boolean internalError= false;
				try {
					getWizard().getContainer().run(true, true, runnable);
				} catch (InvocationTargetException e) {
					internalError= true;
					JavaScriptPlugin.log(e);
					ChangeTypeInputPage.this.setErrorMessage(RefactoringMessages.ChangeTypeWizard_internalError); 
				} catch (InterruptedException e) {
					ChangeTypeInputPage.this.setMessage(RefactoringMessages.ChangeTypeWizard_computationInterrupted); 
				}
													
				fLabelProvider.grayOut(fInvalidTypes);
				
				if (internalError) {
					setPageComplete(false);
				} else if (fValidTypes == null || fValidTypes.size() == 0){
					ChangeTypeInputPage.this.setErrorMessage(RefactoringMessages.ChangeTypeWizard_declCannotBeChanged); 
					setPageComplete(false);
				} else {
					TreeItem selection= getInitialSelection(fValidTypes);
					fTreeViewer.getTree().setSelection(new TreeItem[]{ selection });
					setPageComplete(true);
					ChangeTypeInputPage.this.setMessage(""); //$NON-NLS-1$
				}
			}			
		}
		
		private TreeItem getInitialSelection(Collection/*<ITypeBinding>*/ types) {
			
			// first, find a most general valid type (there may be more than one)
			ITypeBinding type= (ITypeBinding)types.iterator().next();
			for (Iterator it= types.iterator(); it.hasNext(); ){
				ITypeBinding other= (ITypeBinding)it.next();
				if (getGeneralizeTypeRefactoring().isSubTypeOf(type, other)){
					type= other;
				}
			}
			
			// now find a corresponding TreeItem (there may be more than one)		
			return findItem(fTreeViewer.getTree().getItems(), type);
		}
		
		private TreeItem findItem(TreeItem[] items, ITypeBinding type){
			for (int i=0; i < items.length; i++){
				if (items[i].getData().equals(type)) return items[i];
			}
			for (int i=0; i < items.length; i++){
				TreeItem item= findItem(items[i].getItems(), type);
				if (item != null) return item;
			}
			return null;
		}
		
		
		public void createControl(Composite parent) {
			Composite composite= new Composite(parent, SWT.NONE);
			setControl(composite);
			composite.setLayout(new GridLayout());
			composite.setLayoutData(new GridData());
			
			Label label= new Label(composite, SWT.NONE);
			label.setText(Messages.format(
					RefactoringMessages.ChangeTypeWizard_pleaseChooseType, 
					((ChangeTypeRefactoring) getRefactoring()).getTarget()));
			label.setLayoutData(new GridData());
			
			addTreeComponent(composite);			
			Dialog.applyDialogFont(composite);
			PlatformUI.getWorkbench().getHelpSystem().setHelp(getControl(), IJavaHelpContextIds.CHANGE_TYPE_WIZARD_PAGE);
		}
		
		/**
		 * Tree-viewer that shows the allowable types in a tree view.
		 */
		private void addTreeComponent(Composite parent) {
			fTreeViewer= new TreeViewer(parent, SWT.SINGLE | SWT.BORDER | SWT.V_SCROLL | SWT.H_SCROLL);
			GridData gd= new GridData(GridData.FILL_BOTH);
			gd.grabExcessHorizontalSpace= true;
			gd.grabExcessVerticalSpace= true;
			GC gc= null;
			try {
				gc= new GC(parent); 
				gc.setFont(gc.getFont()); 
				gd.heightHint= Dialog.convertHeightInCharsToPixels(gc.getFontMetrics(), 6); // 6 characters tall
			} finally {
				if (gc != null) {
					gc.dispose();
					gc= null;
				}
			}
			fTreeViewer.getTree().setLayoutData(gd);
			
			fTreeViewer.setContentProvider(new ChangeTypeContentProvider(((ChangeTypeRefactoring)getRefactoring())));
			fLabelProvider= new ChangeTypeLabelProvider(); 
			fTreeViewer.setLabelProvider(fLabelProvider);
			ISelectionChangedListener listener= new ISelectionChangedListener(){
				public void selectionChanged(SelectionChangedEvent event) {
					IStructuredSelection selection= (IStructuredSelection)event.getSelection();
					typeSelected((ITypeBinding)selection.getFirstElement());
				}
			};
			fTreeViewer.addSelectionChangedListener(listener);
			fTreeViewer.setInput(new ChangeTypeContentProvider.RootType(getGeneralizeTypeRefactoring().getOriginalType()));
			fTreeViewer.expandToLevel(10);
		}

		private void typeSelected(ITypeBinding type) {
			boolean isValid= getGeneralizeTypeRefactoring().getValidTypes().contains(type);
			ChangeTypeInputPage.this.setPageComplete(isValid);
			if (isValid) {
				ChangeTypeInputPage.this.setMessage(""); //$NON-NLS-1$
			} else {
				if (getGeneralizeTypeRefactoring().getOriginalType().equals(type)) {
					ChangeTypeInputPage.this.setMessage(Messages.format(
						RefactoringMessages.ChangeTypeWizard_with_itself, type.getName())); 
					
				} else {
					ChangeTypeInputPage.this.setMessage(Messages.format(
						RefactoringMessages.ChangeTypeWizard_grayed_types,  
						new Object[] {type.getName(), getGeneralizeTypeRefactoring().getOriginalType().getName()}));
				}
			}
		}
		
		private ChangeTypeRefactoring getGeneralizeTypeRefactoring(){
			return (ChangeTypeRefactoring)getRefactoring();
		}
		/*
		 * @see org.eclipse.jface.wizard.IWizardPage#getNextPage()
		 */
		public IWizardPage getNextPage() {
			initializeRefactoring();
			return super.getNextPage();
		}

		private ITypeBinding getSelectedType() {
			IStructuredSelection ss= (IStructuredSelection)fTreeViewer.getSelection();
			return (ITypeBinding)ss.getFirstElement();
		}

		/*
		 * @see org.eclipse.wst.jsdt.internal.ui.refactoring.RefactoringWizardPage#performFinish()
		 */
		public boolean performFinish(){
			initializeRefactoring();
			return super.performFinish();
		}

		private void initializeRefactoring() {
			getGeneralizeTypeRefactoring().setSelectedType(getSelectedType());
		}
	
		/*
		 * @see org.eclipse.jface.dialogs.IDialogPage#dispose()
		 */
		public void dispose() {
			fTreeViewer= null;
			super.dispose();
		}
	
		/* (non-Javadoc)
		 * @see org.eclipse.jface.dialogs.IDialogPage#setVisible(boolean)
		 */
		public void setVisible(boolean visible) {
			super.setVisible(visible);
			if (visible && fTreeViewer != null)
				fTreeViewer.getTree().setFocus();
				if (!fTreeUpdated){
					fTreeViewer.getTree().getDisplay().asyncExec(new ValidTypesTask());
					fTreeUpdated= true;
				}
		}
	}	
}
