/*******************************************************************************
 * Copyright (c) 2010, 2018 IBM Corporation and others.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License 2.0
 * which accompanies this distribution, and is available at
 * https://www.eclipse.org/legal/epl-2.0/
 *
 * SPDX-License-Identifier: EPL-2.0
 *
 * Contributors:
 *     IBM Corporation - initial API and implementation
 *******************************************************************************/
package org.eclipse.wst.sse.ui.preferences;

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

import org.eclipse.core.commands.Command;
import org.eclipse.core.commands.CommandManager;
import org.eclipse.core.commands.ParameterizedCommand;
import org.eclipse.core.commands.common.NotDefinedException;
import org.eclipse.core.commands.contexts.ContextManager;
import org.eclipse.core.runtime.Assert;
import org.eclipse.jface.bindings.BindingManager;
import org.eclipse.jface.bindings.Scheme;
import org.eclipse.jface.bindings.TriggerSequence;
import org.eclipse.jface.layout.PixelConverter;
import org.eclipse.jface.resource.ImageDescriptor;
import org.eclipse.jface.resource.JFaceResources;
import org.eclipse.jface.viewers.ArrayContentProvider;
import org.eclipse.jface.viewers.CheckStateChangedEvent;
import org.eclipse.jface.viewers.CheckboxTableViewer;
import org.eclipse.jface.viewers.ICheckStateListener;
import org.eclipse.jface.viewers.IStructuredSelection;
import org.eclipse.jface.viewers.ITableLabelProvider;
import org.eclipse.jface.viewers.LabelProvider;
import org.eclipse.jface.viewers.Viewer;
import org.eclipse.jface.viewers.ViewerComparator;
import org.eclipse.osgi.util.NLS;
import org.eclipse.swt.SWT;
import org.eclipse.swt.events.SelectionAdapter;
import org.eclipse.swt.events.SelectionEvent;
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.Button;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Control;
import org.eclipse.swt.widgets.Group;
import org.eclipse.swt.widgets.Label;
import org.eclipse.swt.widgets.Table;
import org.eclipse.swt.widgets.TableColumn;
import org.eclipse.ui.PlatformUI;
import org.eclipse.ui.commands.ICommandService;
import org.eclipse.ui.keys.IBindingService;
import org.eclipse.ui.texteditor.ITextEditorActionDefinitionIds;
import org.eclipse.wst.sse.ui.internal.Logger;
import org.eclipse.wst.sse.ui.internal.SSEUIMessages;
import org.eclipse.wst.sse.ui.internal.contentassist.CompletionProposalCategory;
import org.eclipse.wst.sse.ui.internal.contentassist.CompletionProposalComputerRegistry;
import org.eclipse.wst.sse.ui.internal.util.SWTUtil;


/**
 * <p>A helpful preference configuration block implementation for allowing a user to
 * set their preferences for which content assist categories to show on the default
 * content assist page as well as on their own separate page and page ordering for
 * a specific content type</p>
 * 
 * @base org.eclipse.jdt.internal.ui.preferences.CodeAssistAdvancedConfigurationBlock
 */
public final class CodeAssistCyclingConfigurationBlock  {

	/**
	 * <p>Used to compare categories based on their assigned page rank</p>
	 */
	private final Comparator fCategoryPageComparator = new Comparator() {
		private int getRank(Object o) {
			return ((ModelElement) o).getOwnPageRank();
		}

		public int compare(Object o1, Object o2) {
			int result = getRank(o1) - getRank(o2);
			if(result == 0) {
				result = ((ModelElement) o1).getId().compareTo(((ModelElement) o2).getId());
			}
			return result;
		}
	};
	
	/**
	 * <p>Used to compare categories based on their assigned default page rank</p>
	 */
	private final Comparator fCategoryDefaultPageComparator = new Comparator() {
		private int getRank(Object o) {
			return ((ModelElement) o).getDefaultPageRank();
		}

		public int compare(Object o1, Object o2) {
			int result = getRank(o1) - getRank(o2);
			if(result == 0) {
				result = ((ModelElement) o1).getId().compareTo(((ModelElement) o2).getId());
			}
			return result;
		}
	};

	/** the preference model for this block */
	private final PreferenceModel fModel;
	
	/**
	 * <code>{@link Map}<{@link ImageDescriptor}, {@link Image}></code>
	 */
	private final Map fImages= new HashMap();

	/** table viewer to configure which categories are displayed on the default page */
	private CheckboxTableViewer fDefaultPageViewer;
	
	/**
	 * table viewer to configure which categories are displayed
	 * on their own page, as well as their ordering
	 */
	private CheckboxTableViewer fOwnPageViewer;
	
	/** categories pages sort order up button */
	private Button fPageOrderUpButton;
	
	/** categories pages sort order down button */
	private Button fPageOrderDownButton;
	
	/** categories default page sort order up button */
	private Button fDefaultPageOrderUpButton;
	
	/** categories default page  sort order down button */
	private Button fDefaultPageOrderDownButton;
	
	/** The content type ID this configuration block is for */
	private String fContentTypeID;
	
	/** The writable categories configuration */
	private ICompletionProposalCategoriesConfigurationWriter fConfigurationWriter;

	/**
	 * <p>Creates a new content assist preference block for the given content type
	 * using the given configuration writer</p>
	 * 
	 * @param contentTypeID content type this content assist preference block is for
	 * @param configurationWriter {@link ICompletionProposalCategoriesConfigurationWriter} used
	 * to read and write the user preferences 
	 */
	public CodeAssistCyclingConfigurationBlock(String contentTypeID, ICompletionProposalCategoriesConfigurationWriter configurationWriter) {
		this.fContentTypeID = contentTypeID;
		this.fConfigurationWriter = configurationWriter;
		
		List categories = CompletionProposalComputerRegistry.getDefault().getProposalCategories(this.fContentTypeID);
		this.fModel = new PreferenceModel(categories);
	}
	
	/**
	 * <p>Saves the user configuration</p>
	 * @return <code>true</code> if store was successful, <code>false</code> otherwise
	 */
	public boolean storeValues() {
		return this.fConfigurationWriter.saveConfiguration();
	}
	
	/**
	 * <p>Loads the preference defaults</p>
	 */
	public void performDefaults() {
		this.fConfigurationWriter.loadDefaults();
		this.initializeValues();
		this.fModel.performDefaults();
	}
	
	/**
	 * <p>Disposes of this preference block</p>
	 */
	public void dispose() {
		for (Iterator it= fImages.values().iterator(); it.hasNext();) {
			Image image= (Image) it.next();
			image.dispose();
		}
	}

	/**
	 * <p>Creates the contents of this configuration block using a {@link Group} if
	 * the given <code>groupTitle</code> is not <code>null</code> else creates it
	 * as a composite and in either case adds itself to the given parent</p>
	 * 
	 * @param parent {@link Composite} parent to add this configuration block to
	 * @param groupTitle Title to use for the configuration block group, if
	 * <code>null</code> then a {@link Composite} will be used instead of a
	 * {@link Group}
	 * 
	 * @return the created configuration block that has already been added to the parent
	 */
	public Control createContents(Composite parent, String groupTitle) {
		Composite container;
		if(groupTitle != null) {
			container = new Group(parent, SWT.NULL);
			((Group)container).setText(groupTitle);
		} else {
			container = new Composite(parent, SWT.NULL);
		}
		int columns= 2;
		GridLayout layout= new GridLayout(columns, false);
		container.setLayout(layout);
		
		GridData data = new GridData(GridData.FILL);
		data.horizontalIndent = 0;
		data.verticalAlignment = GridData.FILL;
		data.horizontalAlignment = GridData.FILL;
		data.grabExcessHorizontalSpace = true;
		container.setLayoutData(data);

		createDefaultPageLabel(container, columns);
		createDefaultPageSection(container);

		createFiller(container, columns);

		createOwnPageLabel(container, columns);
        createOwnPageSection(container);

        createFiller(container, columns);

		if (fModel.pageElements.size() > 0) {
			fDefaultPageViewer.getTable().select(0);
			fOwnPageViewer.getTable().select(0);
			handleOwnPageTableSelection();
			handleDefaultPageTableSelection();
		}
		
		return container;
	}
	
	/**
	 * <p>Initialize the values of the configuration block</p>
	 */
	public void initializeValues() {
		updateCheckedState();
		fDefaultPageViewer.refresh();
		fOwnPageViewer.refresh();
		handleOwnPageTableSelection();
		handleDefaultPageTableSelection();
	}
	
	private void createDefaultPageSection(Composite composite) {
		createDefaultPageViewer(composite);
		createDefaultPageButtonList(composite);
	}

	private void createDefaultPageLabel(Composite composite, int h_span) {
	    final ICommandService commandSvc= PlatformUI.getWorkbench().getAdapter(ICommandService.class);
		final Command command= commandSvc.getCommand(ITextEditorActionDefinitionIds.CONTENT_ASSIST_PROPOSALS);
		ParameterizedCommand pCmd= new ParameterizedCommand(command, null);
		String key= getKeyboardShortcut(pCmd);
		if (key == null) {
			key= SSEUIMessages.CodeAssistAdvancedConfigurationBlock_no_shortcut;
		}

		PixelConverter pixelConverter= new PixelConverter(composite);
		int width= pixelConverter.convertWidthInCharsToPixels(40);

		Label label= new Label(composite, SWT.NONE | SWT.WRAP);
		label.setText(NLS.bind(SSEUIMessages.CodeAssistAdvancedConfigurationBlock_page_description, new Object[] { key }));
		GridData gd= new GridData(GridData.FILL, GridData.FILL, true, false, h_span, 1);
		gd.widthHint= width;
		label.setLayoutData(gd);

		createFiller(composite, h_span);

		label= new Label(composite, SWT.NONE | SWT.WRAP);
		label.setText(SSEUIMessages.CodeAssistAdvancedConfigurationBlock_default_table_description);
		gd= new GridData(GridData.FILL, GridData.FILL, true, false, h_span, 1);
		gd.widthHint= width;
		label.setLayoutData(gd);
    }

	private void createDefaultPageViewer(Composite composite) {
		fDefaultPageViewer= CheckboxTableViewer.newCheckList(composite, SWT.SINGLE | SWT.BORDER);
		Table table= fDefaultPageViewer.getTable();
		table.setHeaderVisible(true);
		table.setLinesVisible(false);
		table.setLayoutData(new GridData(GridData.FILL, GridData.BEGINNING, false, false, 1, 1));

		TableColumn nameColumn= new TableColumn(table, SWT.NONE);
		nameColumn.setText(SSEUIMessages.CodeAssistAdvancedConfigurationBlock_default_table_category_column_title);
		nameColumn.setResizable(false);

		fDefaultPageViewer.addCheckStateListener(new ICheckStateListener() {
			public void checkStateChanged(CheckStateChangedEvent event) {
				boolean checked= event.getChecked();
				ModelElement element= (ModelElement) event.getElement();
				element.setShouldDisplayOnDefaultPage(checked);
			}
		});

		fDefaultPageViewer.setContentProvider(new ArrayContentProvider());

		DefaultPageTableLabelProvider labelProvider= new DefaultPageTableLabelProvider();
		fDefaultPageViewer.setLabelProvider(labelProvider);
		fDefaultPageViewer.setInput(fModel.defaultPageElements);
		fDefaultPageViewer.setComparator(new ModelViewerComparator(fCategoryDefaultPageComparator));

		final int ICON_AND_CHECKBOX_WITH= 50;
		final int HEADER_MARGIN= 20;
		int minNameWidth= computeWidth(table, nameColumn.getText()) + HEADER_MARGIN;
		for (int i= 0; i < fModel.defaultPageElements.size(); i++) {
			minNameWidth= Math.max(minNameWidth, computeWidth(table, labelProvider.getColumnText(fModel.defaultPageElements.get(i), 0)) + ICON_AND_CHECKBOX_WITH);
		}

		nameColumn.setWidth(minNameWidth);
		
		table.addSelectionListener(new SelectionAdapter() {
			public void widgetSelected(SelectionEvent e) {
				handleDefaultPageTableSelection();
			}
		});
	}
	
	/**
	 * <p>Create the Up and Down buttons for the default page viewer</p>
	 * @param parent {@link Composite} to add the button list to 
	 */
	private void createDefaultPageButtonList(Composite parent) {
		Composite composite= new Composite(parent, SWT.NONE);
		composite.setLayoutData(new GridData(SWT.BEGINNING, SWT.BEGINNING, false, false));

		GridLayout layout= new GridLayout();
		layout.marginWidth= 0;
		layout.marginHeight= 0;
		composite.setLayout(layout);

		fDefaultPageOrderUpButton= new Button(composite, SWT.PUSH | SWT.CENTER);
		fDefaultPageOrderUpButton.setText(SSEUIMessages.CodeAssistAdvancedConfigurationBlock_Up);
		fDefaultPageOrderUpButton.addSelectionListener(new SelectionAdapter() {
        	public void widgetSelected(SelectionEvent e) {
        		int index = fDefaultPageViewer.getTable().getSelectionIndex();
        		if (index != -1) {
        			fModel.moveDefaultPageCategoryUp(index);
        			fDefaultPageViewer.refresh();
        			handleDefaultPageTableSelection();
        		}
        	}
        });
		fDefaultPageOrderUpButton.setLayoutData(new GridData());
        
        SWTUtil.setButtonDimensionHint(fDefaultPageOrderUpButton);

        fDefaultPageOrderDownButton= new Button(composite, SWT.PUSH | SWT.CENTER);
        fDefaultPageOrderDownButton.setText(SSEUIMessages.CodeAssistAdvancedConfigurationBlock_Down);
        fDefaultPageOrderDownButton.addSelectionListener(new SelectionAdapter() {
        	public void widgetSelected(SelectionEvent e) {
        		int index= fDefaultPageViewer.getTable().getSelectionIndex();
        		if (index != -1) {
        			fModel.moveDefaultPageCategoryDown(index);
        			fDefaultPageViewer.refresh();
        			handleDefaultPageTableSelection();
        		}
        	}
        });
        fDefaultPageOrderDownButton.setLayoutData(new GridData());
        SWTUtil.setButtonDimensionHint(fDefaultPageOrderDownButton);
	}

	private void createFiller(Composite composite, int h_span) {
	    Label filler= new Label(composite, SWT.NONE);
		filler.setVisible(false);
		filler.setLayoutData(new GridData(SWT.FILL, SWT.FILL, false, false, h_span, 1));
    }

	private void createOwnPageLabel(Composite composite, int h_span) {
		PixelConverter pixelConverter= new PixelConverter(composite);
		int width= pixelConverter.convertWidthInCharsToPixels(40);

		Label label= new Label(composite, SWT.NONE | SWT.WRAP);
		label.setText(SSEUIMessages.CodeAssistAdvancedConfigurationBlock_separate_table_description);
		GridData gd= new GridData(GridData.FILL, GridData.FILL, false, false, h_span, 1);
		gd.widthHint= width;
		label.setLayoutData(gd);
	}

	private void createOwnPageSection(Composite composite) {
		createOwnPageViewer(composite);
		createOwnPageButtonList(composite);
	}

	private void createOwnPageViewer(Composite composite) {
		fOwnPageViewer= CheckboxTableViewer.newCheckList(composite, SWT.SINGLE | SWT.BORDER);
		Table table= fOwnPageViewer.getTable();
		table.setHeaderVisible(true);
		table.setLinesVisible(false);
		table.setLayoutData(new GridData(GridData.FILL, GridData.BEGINNING, true, false, 1, 1));

		TableColumn nameColumn= new TableColumn(table, SWT.NONE);
		nameColumn.setText(SSEUIMessages.CodeAssistAdvancedConfigurationBlock_separate_table_category_column_title);
		nameColumn.setResizable(false);

		fOwnPageViewer.setContentProvider(new ArrayContentProvider());

		ITableLabelProvider labelProvider= new OwnPageTableLabelProvider();
		fOwnPageViewer.setLabelProvider(labelProvider);
		fOwnPageViewer.setInput(fModel.pageElements);
		fOwnPageViewer.setComparator(new ModelViewerComparator(fCategoryPageComparator));

		final int ICON_AND_CHECKBOX_WITH= 50;
		final int HEADER_MARGIN= 20;
		int minNameWidth= computeWidth(table, nameColumn.getText()) + HEADER_MARGIN;
		for (int i= 0; i < fModel.pageElements.size(); i++) {
			minNameWidth= Math.max(minNameWidth, computeWidth(table, labelProvider.getColumnText(fModel.pageElements.get(i), 0)) + ICON_AND_CHECKBOX_WITH);
		}

		nameColumn.setWidth(minNameWidth);

		fOwnPageViewer.addCheckStateListener(new ICheckStateListener() {
			public void checkStateChanged(CheckStateChangedEvent event) {
				boolean checked= event.getChecked();
				ModelElement element= (ModelElement) event.getElement();
				element.setShouldDisplayOnOwnPage(checked);
			}
		});

		table.addSelectionListener(new SelectionAdapter() {
			public void widgetSelected(SelectionEvent e) {
				handleOwnPageTableSelection();
			}
		});

	}

	/**
	 * <p>Create the Up and Down buttons for the own page viewer</p>
	 * @param parent {@link Composite} to add the button list to 
	 */
	private void createOwnPageButtonList(Composite parent) {
		Composite composite= new Composite(parent, SWT.NONE);
		composite.setLayoutData(new GridData(SWT.BEGINNING, SWT.BEGINNING, false, false));

		GridLayout layout= new GridLayout();
		layout.marginWidth= 0;
		layout.marginHeight= 0;
		composite.setLayout(layout);

		fPageOrderUpButton= new Button(composite, SWT.PUSH | SWT.CENTER);
        fPageOrderUpButton.setText(SSEUIMessages.CodeAssistAdvancedConfigurationBlock_PagesUp);
        fPageOrderUpButton.addSelectionListener(new SelectionAdapter() {
        	public void widgetSelected(SelectionEvent e) {
        		int index = fOwnPageViewer.getTable().getSelectionIndex();
        		if (index != -1) {
        			fModel.movePageUp(index);
        			fOwnPageViewer.refresh();
        			handleOwnPageTableSelection();
        		}
        	}
        });
        fPageOrderUpButton.setLayoutData(new GridData());
        
        SWTUtil.setButtonDimensionHint(fPageOrderUpButton);

        fPageOrderDownButton= new Button(composite, SWT.PUSH | SWT.CENTER);
        fPageOrderDownButton.setText(SSEUIMessages.CodeAssistAdvancedConfigurationBlock_PagesDown);
        fPageOrderDownButton.addSelectionListener(new SelectionAdapter() {
        	public void widgetSelected(SelectionEvent e) {
        		int index= fOwnPageViewer.getTable().getSelectionIndex();
        		if (index != -1) {
        			fModel.movePageDown(index);
        			fOwnPageViewer.refresh();
        			handleOwnPageTableSelection();
        		}
        	}
        });
        fPageOrderDownButton.setLayoutData(new GridData());
        SWTUtil.setButtonDimensionHint(fPageOrderDownButton);
	}

	/**
	 * <p>Update the enablement of the Up and Down buttons for the own page table viewer</p>
	 */
	private void handleOwnPageTableSelection() {
		ModelElement item= (ModelElement) ((IStructuredSelection) fOwnPageViewer.getSelection()).getFirstElement();
		if (item != null) {
			int index= fOwnPageViewer.getTable().getSelectionIndex();
			fPageOrderUpButton.setEnabled(index > 0);
			fPageOrderDownButton.setEnabled(index < fModel.pageElements.size() - 1);
		} else {
			fPageOrderUpButton.setEnabled(false);
			fPageOrderDownButton.setEnabled(false);
		}
	}
	
	/**
	 * <p>Update the enablement of the Up and Down buttons for the default page table viewer</p>
	 */
	private void handleDefaultPageTableSelection() {
		ModelElement item= (ModelElement) ((IStructuredSelection) fDefaultPageViewer.getSelection()).getFirstElement();
		if (item != null) {
			int index = fDefaultPageViewer.getTable().getSelectionIndex();
			fDefaultPageOrderUpButton.setEnabled(index > 0);
			fDefaultPageOrderDownButton.setEnabled(index < fModel.defaultPageElements.size() - 1);
		} else {
			fDefaultPageOrderUpButton.setEnabled(false);
			fDefaultPageOrderDownButton.setEnabled(false);
		}
	}

	private void updateCheckedState() {
		/* does not matter which set of elements we use here
		 * because order does not matter in this case
		 */
		final int size= fModel.pageElements.size();
		List defaultChecked= new ArrayList(size);
		List separateChecked= new ArrayList(size);

		for (Iterator it= fModel.pageElements.iterator(); it.hasNext();) {
			ModelElement element= (ModelElement) it.next();
			if (element.shouldDisplayOnDefaultPage())
				defaultChecked.add(element);
			if (element.shouldDisplayOnOwnPage())
				separateChecked.add(element);
		}

		fDefaultPageViewer.setCheckedElements(defaultChecked.toArray(new Object[defaultChecked.size()]));
		fOwnPageViewer.setCheckedElements(separateChecked.toArray(new Object[separateChecked.size()]));
	}

	private int computeWidth(Control control, String name) {
		if (name == null)
			return 0;
		GC gc= new GC(control);
		try {
			gc.setFont(JFaceResources.getDialogFont());
			return gc.stringExtent(name).x + 10;
		} finally {
			gc.dispose();
		}
	}

	private static BindingManager fgLocalBindingManager;
	static {
		fgLocalBindingManager= new BindingManager(new ContextManager(), new CommandManager());
		final IBindingService bindingService= PlatformUI.getWorkbench().getService(IBindingService.class);
		final Scheme[] definedSchemes= bindingService.getDefinedSchemes();
		if (definedSchemes != null) {
			try {
				for (int i = 0; i < definedSchemes.length; i++) {
					final Scheme scheme= definedSchemes[i];
					final Scheme copy= fgLocalBindingManager.getScheme(scheme.getId());
					copy.define(scheme.getName(), scheme.getDescription(), scheme.getParentId());
				}
			} catch (final NotDefinedException e) {
				Logger.logException(e);
			}
		}
		fgLocalBindingManager.setLocale(bindingService.getLocale());
		fgLocalBindingManager.setPlatform(bindingService.getPlatform());
	}

	private static String getKeyboardShortcut(ParameterizedCommand command) {
		IBindingService bindingService= PlatformUI.getWorkbench().getAdapter(IBindingService.class);
		fgLocalBindingManager.setBindings(bindingService.getBindings());
		try {
			Scheme activeScheme= bindingService.getActiveScheme();
			if (activeScheme != null)
				fgLocalBindingManager.setActiveScheme(activeScheme);
		} catch (NotDefinedException e) {
			Logger.logException(e);
		}

		TriggerSequence[] bindings= fgLocalBindingManager.getActiveBindingsDisregardingContextFor(command);
		if (bindings.length > 0)
			return bindings[0].format();
		return null;
	}

	/**
	 * <p>Gets and image based on an image descriptor, and stores the image so it
	 * does not have to be created more then once</p>
	 * 
	 * @param imgDesc {@link ImageDescriptor} to get the {@link Image} for
	 * @return {@link Image} created from the {@link ImageDescriptor}, or stored
	 * {@link Image} associated with the given {@link ImageDescriptor} if an
	 * {@link Image} had already been created for the given {@link ImageDescriptor}
	 */
	private Image getImage(ImageDescriptor imgDesc) {
		if (imgDesc == null)
			return null;

		Image img= (Image) fImages.get(imgDesc);
		if (img == null) {
			img= imgDesc.createImage(false);
			fImages.put(imgDesc, img);
		}
		return img;
	}

	/**
	 * <p>Label provider for the table for configuring which categories should be displayed on the
	 * default assist page</p>
	 */
	private final class DefaultPageTableLabelProvider extends LabelProvider implements ITableLabelProvider {
		/**
		 * @see org.eclipse.jface.viewers.ITableLabelProvider#getColumnImage(java.lang.Object, int)
		 */
		public Image getColumnImage(Object element, int columnIndex) {
			if (columnIndex == 0)
				return ((ModelElement) element).getImage();
			return null;
		}

		/**
		 * @see org.eclipse.jface.viewers.ITableLabelProvider#getColumnText(java.lang.Object, int)
		 */
		public String getColumnText(Object element, int columnIndex) {
			switch (columnIndex) {
	            case 0:
	            	return ((ModelElement) element).getName();
	            default:
	            	Assert.isTrue(false);
	            	return null;
            }
		}

		/**
		 * @see org.eclipse.jface.viewers.LabelProvider#getText(java.lang.Object)
		 */
		public String getText(Object element) {
		    return getColumnText(element, 0); // needed to make the sorter work
		}
	}

	/**
	 * <p>Label provider for the table for configuring which categories should be displayed on their
	 * own content assist page</p>
	 */
	private final class OwnPageTableLabelProvider extends LabelProvider implements ITableLabelProvider {
		/**
		 * @see org.eclipse.jface.viewers.ITableLabelProvider#getColumnImage(java.lang.Object, int)
		 */
		public Image getColumnImage(Object element, int columnIndex) {
			if (columnIndex == 0)
				return ((ModelElement) element).getImage();
			return null;
		}

		/**
		 * @see org.eclipse.jface.viewers.ITableLabelProvider#getColumnText(java.lang.Object, int)
		 */
		public String getColumnText(Object element, int columnIndex) {
			switch (columnIndex) {
				case 0:
					return ((ModelElement) element).getName();
				default:
					Assert.isTrue(false);
				return null;
			}
		}
	}
	
	/**
	 * <p>PreferenceModel used to read and write the user preferences
	 * using the {@link ICompletionProposalCategoriesConfigurationWriter}</p>
	 */
	private final class PreferenceModel {
		/** private modifiable page element list */
		private final List fPageElements;
		
		/** public unmodifiable page element list */
		final List pageElements;
		
		/** private modifiable default page element list */
		private final List fDefaultPageElements;
		
		/** public unmodifiable default page element list */
		final List defaultPageElements;

		/**
		 * <p>Create the preference model for the given categories</p>
		 * 
		 * @param categories <code>{@link List}<{@link CompletionProposalCategory}></code>
		 */
		public PreferenceModel(List categories) {
			//need separate lists because they will be ordered differently
			this.fPageElements = new ArrayList();
			this.fDefaultPageElements = new ArrayList();
			for (Iterator it= categories.iterator(); it.hasNext();) {
				CompletionProposalCategory category= (CompletionProposalCategory) it.next();
				if (category.hasComputers()) {
					ModelElement elem = new ModelElement(category);
					fPageElements.add(elem);
					fDefaultPageElements.add(elem);
				}
			}
			//sort the lists
			this.performDefaults();
			
			pageElements = Collections.unmodifiableList(fPageElements);
			defaultPageElements = Collections.unmodifiableList(fDefaultPageElements);
		}

		/**
		 * <p>Move the model element specified by the given index
		 * up in the content assist page order</p>
		 * 
		 * @param elementIndex the index of the model element to move up
		 */
        public void movePageUp(int elementIndex) {
        	Object removed = fPageElements.remove(elementIndex);
        	fPageElements.add(elementIndex-1, removed);
        	
        	fConfigurationWriter.setPageOrder(getPageOrderedCategoryIDs());
        }
        
        /**
		 * <p>Move the model element specified by the given index
		 * down in the content assist page order</p>
		 * 
		 * @param elementIndex the index of the model element to move up
		 */
        public void movePageDown(int elementIndex) {
        	Object removed = fPageElements.remove(elementIndex);
        	fPageElements.add(elementIndex+1, removed);
        	
        	fConfigurationWriter.setPageOrder(getPageOrderedCategoryIDs());
        }
        
        /**
		 * <p>Move the model element specified by the given index
		 * up in the content assist page order</p>
		 * 
		 * @param elementIndex the index of the model element to move up
		 */
        public void moveDefaultPageCategoryUp(int elementIndex) {
        	Object removed = fDefaultPageElements.remove(elementIndex);
        	fDefaultPageElements.add(elementIndex-1, removed);
        	
        	fConfigurationWriter.setDefaultPageOrder(getDefaultPageOrderedCategoryIDs());
        }
        
        /**
		 * <p>Move the model element specified by the given index
		 * down in the content assist page order</p>
		 * 
		 * @param elementIndex the index of the model element to move up
		 */
        public void moveDefaultPageCategoryDown(int elementIndex) {
        	Object removed = fDefaultPageElements.remove(elementIndex);
        	fDefaultPageElements.add(elementIndex+1, removed);
        	
        	fConfigurationWriter.setDefaultPageOrder(getDefaultPageOrderedCategoryIDs());
        }
        
        /**
         * @return <code>{@link List}<{@link String}></code> -
         * List of category IDs by page order
         */
        private List getPageOrderedCategoryIDs() {
        	List ordered = new ArrayList(pageElements.size());
        	for(int i = 0; i < pageElements.size(); ++i) {
        		ordered.add(((ModelElement)pageElements.get(i)).getId());
        	}
        	
        	return ordered;
        }
        
        /**
         * @return <code>{@link List}<{@link String}></code> -
         * List of category IDs by default page order
         */
        private List getDefaultPageOrderedCategoryIDs() {
        	List ordered = new ArrayList(defaultPageElements.size());
        	for(int i = 0; i < defaultPageElements.size(); ++i) {
        		ordered.add(((ModelElement)defaultPageElements.get(i)).getId());
        	}
        	
        	return ordered;
        }
        
        /**
         * <p>need to re-sort the lists after performing defaults</p>
         */
        public void performDefaults() {
        	Collections.sort(fPageElements, fCategoryPageComparator);
			Collections.sort(fDefaultPageElements, fCategoryDefaultPageComparator);
        }
	}
	
	/**
	 * <p>Wraps a {@link CompletionProposalCategory} for use in the {@link PreferenceModel}</p>
	 */
	private final class ModelElement {
		/** The wrapped category */
		private final CompletionProposalCategory fCategory;

		/**
		 * <p>Create a new model element wrapping the given category</p>
		 * 
		 * @param category {@link CompletionProposalCategory} to be wrapped by this model element
		 * for use in the {@link PreferenceModel}
		 */
		ModelElement(CompletionProposalCategory category) {
			fCategory= category;
		}
		
		/**
		 * @return {@link Image} associated with the wrapped category
		 */
		Image getImage() {
			return CodeAssistCyclingConfigurationBlock.this.getImage(fCategory.getImageDescriptor());
		}
		
		/**
		 * @return name of the wrapped category
		 */
		String getName() {
			return fCategory.getDisplayName();
		}
		
		String getId() {
			return fCategory.getId();
		}
		
		/**
		 * @return <code>true</code> if the wrapped category should be displayed on the
		 * default content assist page, <code>false</code> otherwise
		 */
		boolean shouldDisplayOnDefaultPage() {
			return fConfigurationWriter.shouldDisplayOnDefaultPage(this.getId());
		}
		
		/**
		 * @param included <code>true</code> if the wrapped category should be displayed on the
		 * default content assist page, <code>false</code> otherwise
		 */
		void setShouldDisplayOnDefaultPage(boolean included) {
			fConfigurationWriter.setShouldDisplayOnDefaultPage(this.getId(), included);
		}
		
		/**
		 * @return <code>true</code> if the wrapped category should be displayed on the
		 * its own content assist page, <code>false</code> otherwise
		 */
		boolean shouldDisplayOnOwnPage() {
			return fConfigurationWriter.shouldDisplayOnOwnPage(this.getId());
		}
		
		/**
		 * @param shouldDisplay <code>true</code> if the wrapped category should be displayed on the
		 * its own content assist page, <code>false</code> otherwise
		 */
		void setShouldDisplayOnOwnPage(boolean shouldDisplay) {
			fConfigurationWriter.setShouldDisplayOnOwnPage(this.getId(), shouldDisplay);
		}
		
		/**
		 * @return the wrapped categories content assist page sort rank compared to the
		 * other categories
		 */
		int getOwnPageRank() {
			return fConfigurationWriter.getPageSortOrder(this.getId());
		}
		
		/**
		 * @return the wrapped categories content assist page sort rank compared to the
		 * other categories
		 */
		int getDefaultPageRank() {
			return fConfigurationWriter.getDefaultPageSortOrder(this.getId());
		}
	}
	
	private class ModelViewerComparator extends ViewerComparator {
		private Comparator c;
		/**
		 * 
		 */
		public ModelViewerComparator(Comparator comparator) {
			super(comparator);
			this.c = comparator;
		}
		
		/**
		 * @see org.eclipse.jface.viewers.ViewerComparator#compare(org.eclipse.jface.viewers.Viewer, java.lang.Object, java.lang.Object)
		 */
		public int compare(Viewer viewer, Object e1, Object e2) {
			return c.compare(e1, e2);
		}
	}
}
