/*******************************************************************************
 * Copyright (c) 2004, 2010 TimeSys 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:
 *     TimeSys Corporation - Initial implementation
 *******************************************************************************/
package org.eclipse.cdt.internal.ui.preferences;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;

import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.ProjectScope;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.ListenerList;
import org.eclipse.core.runtime.Platform;
import org.eclipse.core.runtime.content.IContentType;
import org.eclipse.core.runtime.content.IContentTypeManager;
import org.eclipse.core.runtime.content.IContentTypeSettings;
import org.eclipse.core.runtime.preferences.IScopeContext;
import org.eclipse.jface.dialogs.ErrorDialog;
import org.eclipse.jface.dialogs.MessageDialog;
import org.eclipse.jface.layout.PixelConverter;
import org.eclipse.jface.viewers.ColumnWeightData;
import org.eclipse.jface.viewers.ILabelProvider;
import org.eclipse.jface.viewers.ILabelProviderListener;
import org.eclipse.jface.viewers.ISelectionChangedListener;
import org.eclipse.jface.viewers.IStructuredContentProvider;
import org.eclipse.jface.viewers.IStructuredSelection;
import org.eclipse.jface.viewers.ITableLabelProvider;
import org.eclipse.jface.viewers.SelectionChangedEvent;
import org.eclipse.jface.viewers.TableLayout;
import org.eclipse.jface.viewers.TableViewer;
import org.eclipse.jface.viewers.Viewer;
import org.eclipse.jface.viewers.ViewerSorter;
import org.eclipse.jface.window.Window;
import org.eclipse.swt.SWT;
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.Event;
import org.eclipse.swt.widgets.Listener;
import org.eclipse.swt.widgets.Table;
import org.eclipse.swt.widgets.TableColumn;

import org.eclipse.cdt.core.model.CoreModel;

import org.eclipse.cdt.internal.ui.util.Messages;
import org.eclipse.cdt.internal.ui.util.SWTUtil;

/*
 * Preference block that encapsulates the controls used
 * for displaying/editing CDT file type associations
 */
public class CFileTypesPreferenceBlock {
	private static final int 	COL_PATTERN		= 0;
	private static final int 	COL_DESCRIPTION	= 1;
	private static final int 	COL_STATUS	= 2;
	
	private ArrayList<CFileTypeAssociation>	fAddAssoc;
	private ArrayList<CFileTypeAssociation>	fRemoveAssoc;
	private boolean		fDirty = false;
	private IProject	fInput;
	private IContentType[] fContentTypes;

	private TableViewer 		fAssocViewer;
	private Button				fBtnNew;
	private Button				fBtnRemove;

	private class AssocSorter extends ViewerSorter {
		@Override
		public int category(Object element) {
			if (element instanceof CFileTypeAssociation) {
				CFileTypeAssociation assoc = (CFileTypeAssociation) element;
				if (assoc.isExtSpec()) {
					return 10;
				}
				return 20;
			}
			return 30;
		}
	}
	
	private class AssocContentProvider implements IStructuredContentProvider {
		CFileTypeAssociation[] assocs;
		
		@Override
		public Object[] getElements(Object inputElement) {
			return assocs;
		}

		@Override
		public void dispose() {
			assocs = null;
		}

		@Override
		public void inputChanged(Viewer viewer, Object oldInput, Object newInput) {
			if (newInput instanceof CFileTypeAssociation[]) {	
				assocs = (CFileTypeAssociation[]) newInput;
			}
		}
	}
	
	private class AssocLabelProvider implements ILabelProvider, ITableLabelProvider {
		private ListenerList listeners = new ListenerList();
		
		@Override
		public Image getColumnImage(Object element, int columnIndex) {
			if (element instanceof CFileTypeAssociation) {
				if (COL_PATTERN == columnIndex) {
					return null;
				}
			}
			return null;
		}

		@Override
		public String getColumnText(Object element, int columnIndex) {
			if (element instanceof CFileTypeAssociation) {
				CFileTypeAssociation assoc = (CFileTypeAssociation) element;
				switch (columnIndex) {
					case COL_PATTERN:
						return assoc.getPattern();
					
					case COL_DESCRIPTION:
						return assoc.getDescription();
						
					case COL_STATUS:
						if (assoc.isUserDefined()) {
							return PreferencesMessages.CFileTypesPreferencePage_userDefined; 
						} else if (assoc.isPredefined()) {
							return PreferencesMessages.CFileTypesPreferencePage_preDefined; 
						}
						return new String();
				}
			}
			return element.toString();
		}

		@Override
		public void addListener(ILabelProviderListener listener) {
			listeners.add(listener);
		}

		@Override
		public void dispose() {
			listeners.clear();
			listeners = null;
		}

		@Override
		public boolean isLabelProperty(Object element, String property) {
			return false;
		}

		@Override
		public void removeListener(ILabelProviderListener listener) {
			listeners.remove(listener);
		}

		@Override
		public Image getImage(Object element) {
			return getColumnImage(element, 0);
		}

		@Override
		public String getText(Object element) {
			return getColumnText(element, 0);
		}
	}

	public CFileTypesPreferenceBlock() {
		this(null);
	}

	public CFileTypesPreferenceBlock(IProject input) {
		fAddAssoc = new ArrayList<CFileTypeAssociation>();
		fRemoveAssoc = new ArrayList<CFileTypeAssociation>();
		fInput = input;
		setDirty(false);
	}

	public Control createControl(Composite parent) {
		Composite 	control 		= new Composite(parent, SWT.NONE);
		GridLayout	controlLayout 	= new GridLayout(2, false);

		controlLayout.marginHeight = 0;
		controlLayout.marginWidth  = 0;

		control.setLayout(controlLayout);
		control.setLayoutData(new GridData(GridData.FILL_BOTH | GridData.GRAB_VERTICAL));
		
		// Create the table viewer for file associations
		
		Composite 	tablePane 		= new Composite(control, SWT.NONE);
		GridLayout	tablePaneLayout = new GridLayout();
		GridData	gridData		= new GridData(GridData.FILL_BOTH);
		
		tablePaneLayout.marginHeight = 0;
		tablePaneLayout.marginWidth  = 0;
		
		tablePane.setLayout(tablePaneLayout);
		tablePane.setLayoutData(gridData);

		Table table = new Table(tablePane, SWT.MULTI | SWT.V_SCROLL | SWT.BORDER | SWT.FULL_SELECTION);
		
		TableLayout 	tblLayout	= new TableLayout();
		TableColumn 	col			= null;
		
		gridData = new GridData(GridData.FILL_BOTH);
		gridData.grabExcessHorizontalSpace = true;
		gridData.grabExcessVerticalSpace = true;
		gridData.heightHint = SWTUtil.getTableHeightHint(table, 25);
		gridData.widthHint = new PixelConverter(parent).convertWidthInCharsToPixels(60);
		
		tblLayout.addColumnData(new ColumnWeightData(20));
		tblLayout.addColumnData(new ColumnWeightData(60));
		tblLayout.addColumnData(new ColumnWeightData(20));
		
		table.setLayout(tblLayout);
		table.setLayoutData(gridData);
		table.setHeaderVisible(true);
		table.setLinesVisible(true);		
		
		col = new TableColumn(table, SWT.LEFT);
		col.setText(PreferencesMessages.CFileTypesPreferencePage_colTitlePattern); 
		
		col = new TableColumn(table, SWT.LEFT);
		col.setText(PreferencesMessages.CFileTypesPreferencePage_colTitleDescription); 
		
		col = new TableColumn(table, SWT.LEFT);
		col.setText(PreferencesMessages.CFileTypesPreferencePage_colTitleStatus); 

		// Create the button pane

		Composite	buttonPane			= new Composite(control, SWT.NONE);
		GridLayout	buttonPaneLayout	= new GridLayout();
		
		buttonPaneLayout.marginHeight = 0;
		buttonPaneLayout.marginWidth  = 0;
		
		buttonPane.setLayout(buttonPaneLayout);
		buttonPane.setLayoutData(new GridData(GridData.VERTICAL_ALIGN_BEGINNING));

		// New button
		
		fBtnNew		= new Button(buttonPane, SWT.PUSH);
		fBtnNew.setText(PreferencesMessages.CFileTypesPreferenceBlock_New___);  
		
		gridData	= new GridData(GridData.FILL_HORIZONTAL);
		gridData.widthHint	= SWTUtil.getButtonWidthHint(fBtnNew);
		fBtnNew.setLayoutData(gridData);
		
		fBtnNew.addListener(SWT.Selection, new Listener() {
			@Override
			public void handleEvent(Event e) {
				handleAdd();
			}
		});

		// Remove button
		
		fBtnRemove 	= new Button(buttonPane, SWT.PUSH);
		fBtnRemove.setText(PreferencesMessages.CFileTypesPreferenceBlock_Remove);  
		
		gridData	= new GridData(GridData.FILL_HORIZONTAL);
		gridData.widthHint	= SWTUtil.getButtonWidthHint(fBtnRemove);
		fBtnRemove.setLayoutData(gridData);
		
		fBtnRemove.addListener(SWT.Selection, new Listener() {
			@Override
			public void handleEvent(Event e) {
				handleRemove();
			}
		});

		// Hook up the viewer
		
		fAssocViewer = new TableViewer(table);
		
		fAssocViewer.setSorter(new AssocSorter());
		fAssocViewer.setContentProvider(new AssocContentProvider());
		fAssocViewer.setLabelProvider(new AssocLabelProvider());
		fAssocViewer.setInput(getCFileTypeAssociations());

		fAssocViewer.addSelectionChangedListener(new ISelectionChangedListener() {
			@Override
			public void selectionChanged(SelectionChangedEvent event) {
				handleSelectionChanged();
			}
		});
		
		handleSelectionChanged();
		
		return control;
	}

	public void setEnabled(boolean enabled) {
		fAssocViewer.getTable().setEnabled(enabled);
		fBtnNew.setEnabled(enabled);
		fBtnRemove.setEnabled(enabled);
		setDirty(enabled);
	}
	
	public void setInput(IProject input) {
		fAddAssoc.clear();
		fRemoveAssoc.clear();
		fInput = input;
		if (null != fAssocViewer) {
			fAssocViewer.setInput(getCFileTypeAssociations());
		}
		setDirty(true);
	}

	private void setDirty(boolean dirty) {
		fDirty = dirty;
	}
	
	public boolean performOk() {
		boolean changed = fDirty;
		
		if (fDirty) {
			CFileTypeAssociation[] add = fAddAssoc.toArray(new CFileTypeAssociation[fAddAssoc.size()]);
			CFileTypeAssociation[] rem = fRemoveAssoc.toArray(new CFileTypeAssociation[fRemoveAssoc.size()]);
			
			changed = add.length > 0 || rem.length > 0;
			adjustAssociations(add, rem);
	
			fAddAssoc.clear();
			fRemoveAssoc.clear();

			setDirty(false);
		}		
		return changed;
	}
	
	private CFileTypeAssociation[] getCFileTypeAssociations() {
		ArrayList<CFileTypeAssociation> list = new ArrayList<CFileTypeAssociation>();
		if (fInput == null) {
			fillWithUserDefinedCFileTypeAssociations(list);
			fillWithPredefinedCFileTypeAssociations(list);
		} else {
			fillWithProjectCFileTypeAssociations(list, fInput);
		}
		CFileTypeAssociation[] assocs = new CFileTypeAssociation[list.size()];
		list.toArray(assocs);
		return assocs;
	}

	protected void adjustAssociations(CFileTypeAssociation[] add, CFileTypeAssociation[] rem) {
		IScopeContext context = null;
		if (fInput != null) {
			context = new ProjectScope(fInput);
		}
		removeAssociations(rem, context);
		addAssociations(add, context);
	}

	final protected void addAssociations(CFileTypeAssociation[] add, IScopeContext context) {
		for (int i = 0; i < add.length; ++i) {
			CFileTypeAssociation assoc = add[i];
			String spec = assoc.getSpec();
			IContentType contentType = assoc.getContentType();
			int type = IContentType.FILE_NAME_SPEC;
			if (assoc.isExtSpec()) {
				type = IContentType.FILE_EXTENSION_SPEC;
			}			
			addAssociation(context, contentType, spec, type);
		}
	}

	protected void addAssociation(IScopeContext context, IContentType contentType, String spec, int type) {
		try {
			IContentTypeSettings settings = contentType.getSettings(context);
			settings.addFileSpec(spec, type);
		} catch (CoreException e) {
			// ignore ??
		}
	}

	protected void removeAssociations(CFileTypeAssociation[] rem, IScopeContext context) {
		for (int i = 0; i < rem.length; ++i) {
			CFileTypeAssociation assoc = rem[i];
			IContentType contentType = assoc.getContentType();
			String spec = assoc.getSpec();
			int type = IContentType.FILE_NAME_SPEC;
			if (assoc.isExtSpec()) {
				type = IContentType.FILE_EXTENSION_SPEC;
			}
			removeAssociation(context, contentType, spec, type);
		}
	}

	protected void removeAssociation(IScopeContext context, IContentType contentType, String spec, int type) {
		try {
			IContentTypeSettings settings = contentType.getSettings(context);
			settings.removeFileSpec(spec, type);
		} catch (CoreException e) {
			// ignore ??
		}
	}

	public IContentType[] getRegistedContentTypes() {
		if (fContentTypes == null) {
			String[] ids = CoreModel.getRegistedContentTypeIds();
			IContentTypeManager manager = Platform.getContentTypeManager();
			IContentType[] ctypes = new IContentType[ids.length];
			for (int i = 0; i < ids.length; i++) {
				ctypes[i] = manager.getContentType(ids[i]);
			}
			fContentTypes = ctypes;
		}
		return fContentTypes;
	}

	private void fillWithUserDefinedCFileTypeAssociations(ArrayList<CFileTypeAssociation> list) {
		IContentType[] ctypes = getRegistedContentTypes();
		fillWithCFileTypeAssociations(ctypes, null, IContentType.IGNORE_PRE_DEFINED | IContentType.FILE_EXTENSION_SPEC, list);
		fillWithCFileTypeAssociations(ctypes, null, IContentType.IGNORE_PRE_DEFINED | IContentType.FILE_NAME_SPEC, list);
	}

	private void fillWithPredefinedCFileTypeAssociations(ArrayList<CFileTypeAssociation> list) {
		IContentType[] ctypes = getRegistedContentTypes();
		fillWithCFileTypeAssociations(ctypes, null, IContentType.IGNORE_USER_DEFINED | IContentType.FILE_EXTENSION_SPEC, list);
		fillWithCFileTypeAssociations(ctypes, null, IContentType.IGNORE_USER_DEFINED | IContentType.FILE_NAME_SPEC, list);
	}

	private void fillWithProjectCFileTypeAssociations(ArrayList<CFileTypeAssociation> list, IProject project) {
		IContentType[] ctypes = getRegistedContentTypes();
		IScopeContext context = new ProjectScope(project);
		fillWithCFileTypeAssociations(ctypes, context, IContentType.IGNORE_PRE_DEFINED | IContentType.FILE_EXTENSION_SPEC, list);
		fillWithCFileTypeAssociations(ctypes, context, IContentType.IGNORE_PRE_DEFINED | IContentType.FILE_NAME_SPEC, list);
	}

	private void fillWithCFileTypeAssociations(IContentType[] ctypes, IScopeContext context, int type, ArrayList<CFileTypeAssociation> list) {
		for (IContentType ctype : ctypes) {
			try {
				IContentTypeSettings setting = ctype.getSettings(context);
				String[] specs = setting.getFileSpecs(type);
				for (String spec : specs) {
					CFileTypeAssociation assoc = new CFileTypeAssociation(spec, type, ctype);
					list.add(assoc);
				}
			} catch (CoreException e) {
				// skip over it.
			}
		}
	}

	private CFileTypeAssociation createAssociation(String pattern, IContentType contentType) {
		int type = IContentType.FILE_NAME_SPEC;
		if (pattern.startsWith("*.")) { //$NON-NLS-1$
			pattern = pattern.substring(2);
			type = IContentType.FILE_EXTENSION_SPEC;
		}
		return new CFileTypeAssociation(pattern, type | IContentType.IGNORE_PRE_DEFINED, contentType);
	}

	protected void handleSelectionChanged() {
		IStructuredSelection sel = getSelection();
		if (sel.isEmpty()) {
			fBtnRemove.setEnabled(false);
		} else {
			boolean enabled = true;
			List<?> elements = sel.toList();
			for (Object element : elements) {
				CFileTypeAssociation assoc = (CFileTypeAssociation) element;
				if (assoc.isPredefined())
					enabled = false;
			}
			fBtnRemove.setEnabled(enabled);
		}
	}

	final protected void handleAdd() {
		CFileTypeAssociation assoc = null;

		CFileTypeDialog dlg = new CFileTypeDialog(fBtnNew.getParent().getShell());
		
		if (Window.OK == dlg.open()) {
			assoc = createAssociation(dlg.getPattern(), dlg.getContentType());
			if (handleAdd(assoc)) {
				fAssocViewer.add(assoc);
				setDirty(true);
			}
		}
	}
	
	private boolean handleAdd(CFileTypeAssociation assoc) {
		// assoc is marked to be added.
		if (containsIgnoreCaseOfSpec(fAddAssoc, assoc)) {
			reportDuplicateAssociation(assoc);
			return false;
		}			
		// assoc exists, but is marked to be removed.
		if (containsIgnoreCaseOfSpec(fRemoveAssoc, assoc)) {
			if (!fRemoveAssoc.remove(assoc)) {
				fAddAssoc.add(assoc);
			}
			return true;
		}

		// analyze current settings
		IContentTypeSettings settings;
		if (fInput == null) {
			settings= assoc.getContentType();
		} else {
			try {
				settings= assoc.getContentType().getSettings(new ProjectScope(fInput));
			} catch (CoreException e) {
				ErrorDialog.openError(fBtnNew.getParent().getShell(),
						PreferencesMessages.CFileTypesPreferenceBlock_addAssociationError_title, 
						null, e.getStatus());
				return false;
			}
		}
		String newSpec= assoc.getSpec();
		String[] specs= settings.getFileSpecs(assoc.getFileSpecType());
		for (String spec : specs) {
			if (spec.equalsIgnoreCase(newSpec)) {
				reportDuplicateAssociation(assoc);
				return false;
			}				
		}
		fAddAssoc.add(assoc);
		return true;
	}	
	
	private boolean containsIgnoreCaseOfSpec(Collection<CFileTypeAssociation> collection, CFileTypeAssociation assoc) {
		for (CFileTypeAssociation existing : collection) {
			if (assoc.equalsIgnoreCaseOfSpec(existing)) {
				return true;
			}
		}
		return false;
	}

	private void reportDuplicateAssociation(CFileTypeAssociation assoc) {
		MessageDialog.openError(fBtnNew.getParent().getShell(),
				PreferencesMessages.CFileTypesPreferenceBlock_addAssociationError_title, 
				Messages.format(PreferencesMessages.CFileTypesPreferenceBlock_addAssociationErrorMessage,  
						assoc.getPattern(), assoc.getContentType().getName()));
	}

	final protected void handleRemove() {
		IStructuredSelection sel = getSelection();
		if ((null != sel) && (!sel.isEmpty())) {
			for (Iterator<?> iter = sel.iterator(); iter.hasNext();) {
				CFileTypeAssociation assoc = (CFileTypeAssociation) iter.next();
				handleRemove(assoc);
				fAssocViewer.remove(assoc);
				setDirty(true);
			}
		}
	}

	private void handleRemove(CFileTypeAssociation assoc) {
		if (!fAddAssoc.remove(assoc)) {
			fRemoveAssoc.add(assoc);
		}
	}

	private IStructuredSelection getSelection() {
		return (IStructuredSelection) fAssocViewer.getSelection();
	}
}
