/*******************************************************************************
 * Copyright (c) 2007, 2009 Wind River Systems 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:
 *     Wind River Systems - initial API and implementation
 *******************************************************************************/
package org.eclipse.cdt.dsf.ui.viewmodel.properties;

import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;

import org.eclipse.cdt.dsf.concurrent.ImmediateExecutor;
import org.eclipse.cdt.dsf.concurrent.ThreadSafe;
import org.eclipse.cdt.dsf.internal.ui.DsfUIPlugin;
import org.eclipse.cdt.dsf.ui.concurrent.ViewerDataRequestMonitor;
import org.eclipse.core.runtime.IAdaptable;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Status;
import org.eclipse.debug.internal.ui.viewers.model.provisional.IElementLabelProvider;
import org.eclipse.debug.internal.ui.viewers.model.provisional.ILabelUpdate;

/**
 * A configurable label provider which uses element's property label provider 
 * to set element's label attributes.
 * <p>
 * When this provider is registered for an element it calculates the properties
 * that need to be retrieved based on view's active columns, and then it calls the
 * element's property provider to retrieve those properties.  After the property
 * values are retrieved, they are processed in order to produce correct label text, 
 * images, fonts, and colors, for the given element.
 * 
 * @since 2.0 - Renamed from PropertyBasedLabelProvider 
 */
@ThreadSafe
public class PropertiesBasedLabelProvider 
    implements IElementLabelProvider
{
    public static final String ID_COLUMN_NO_COLUMNS = "ID_COLUMN_NO_COLUMNS"; //$NON-NLS-1$
    
    /**
     * Attribute information for each column by column ID.
     */
    private Map<String, LabelColumnInfo> fColumnInfos = Collections.synchronizedMap(new HashMap<String,LabelColumnInfo>());
    
    private IPropertiesUpdateListener[] fListeners = new IPropertiesUpdateListener[0];
    
    /**
     * Standard constructor.  A property based label constructor does not 
     * initialize column attribute information {@link #setColumnInfo(String, LabelColumnInfo)} 
     * must be called to configure each column.
     */
    public PropertiesBasedLabelProvider() {
    }
    
    /**
     * Sets the given column info object for the given column ID.  This column
     * info will be used to generate the label when the given column is visibile.
     * 
     * @param columnId Column ID that the given column info is being registered for.
     * @param info Column 'info' object containing column attributes.
     * @return The previous column info object configured for this ID.
     */
    public LabelColumnInfo setColumnInfo(String columnId, LabelColumnInfo info) {
        LabelColumnInfo oldInfo = fColumnInfos.put(columnId, info);
        return oldInfo;
    }

    /**
     * Returns the given column info object for the given column ID.  
     * @param columnId Column ID to retrieve the column info for.
     * 
     * @param columnId Column ID that the given column info is being registered for.
     * @@return Column 'info' object containing column attributes.
     */
    public LabelColumnInfo getColumnInfo(String columnId) {
        return fColumnInfos.get(columnId);
    }

    /**
     * Adds a listener for properties updates generated by this label provider.
     * 
     * @since 2.2
     */
    public void addPropertiesUpdateListener(IPropertiesUpdateListener listener) {
        synchronized(this) {
            if (!Arrays.asList(fListeners).contains(listener)) {
                IPropertiesUpdateListener[] newListeners = new IPropertiesUpdateListener[fListeners.length + 1];
                System.arraycopy(fListeners, 0, newListeners, 0, fListeners.length);
                newListeners[fListeners.length] = listener;
                fListeners = newListeners;
            }
        }
    }

    /**
     * Removes a listener for properties updates generated by this label provider.
     * 
     * @since 2.2
     */
    public void removePropertiesUpdateListener(IPropertiesUpdateListener listener) {
        synchronized(this) {
            int listenerIdx = Arrays.asList(fListeners).indexOf(listener);
            
            if (listenerIdx != -1) {
                IPropertiesUpdateListener[] newListeners = new IPropertiesUpdateListener[fListeners.length - 1];
                System.arraycopy(fListeners, 0, newListeners, 0, listenerIdx);
                System.arraycopy(fListeners, listenerIdx + 1, newListeners, listenerIdx, newListeners.length - listenerIdx);
                fListeners = newListeners;
            }
        }
    }

	/**
	 * In addition to guarantees on [labelUpdates] declared by
	 * {@link IElementLabelProvider}, we further require/assume that all the
	 * model elements referenced by [labelUpdates] adapt to the same
	 * {@link IElementPropertiesProvider}.
	 * 
	 * @see org.eclipse.debug.internal.ui.viewers.model.provisional.IElementLabelProvider#update(org.eclipse.debug.internal.ui.viewers.model.provisional.ILabelUpdate[])
	 */
    @Override
	public void update(final ILabelUpdate[] labelUpdates) {
        IElementPropertiesProvider propertiesProvider = getElementPropertiesProvider(labelUpdates[0].getElement());
        if (propertiesProvider == null) {
            for (ILabelUpdate update : labelUpdates) {
                update.setStatus(new Status(IStatus.ERROR, DsfUIPlugin.PLUGIN_ID, "Properties-based label provider " + this + " failed to generate a label, no properties provider registered for element: " + labelUpdates[0].getElement()));  //$NON-NLS-1$ //$NON-NLS-2$
                update.done();
            }
            return;
        }
        
		// We are guaranteed that all the provided updates are for the same
		// presentation context. Thus we can safely assume they request the same
		// columns
        String[] columnIds = labelUpdates[0].getColumnIds();
        
        Set<String> propertyNames = calcPropertyNamesForColumns(columnIds);
        
        // Call the properties provider.  Create a request monitor for each label update.
        // We can use an immediate executor for the request monitor because the label provider
        // is thread safe.
        final IPropertiesUpdate[] propertiesUpdates = new IPropertiesUpdate[labelUpdates.length];
        for (int i = 0; i < labelUpdates.length; i++) {
            final int idx = i;
            propertiesUpdates[idx] = new VMPropertiesUpdate(
                propertyNames, labelUpdates[idx],  
                new ViewerDataRequestMonitor<Map<String, Object>>(ImmediateExecutor.getInstance(), labelUpdates[idx]) {
                    @Override
                    protected void handleCompleted() {
                        notifyPropertiesUpdateCompleted(propertiesUpdates[idx]);
                        updateLabel(labelUpdates[idx], getStatus(), getData());
                    }
                });
        }
        notifyPropertiesUpdatesStarted(propertiesUpdates);
        propertiesProvider.update(propertiesUpdates);
    }

    private void notifyPropertiesUpdatesStarted(IPropertiesUpdate[] updates) {
        IPropertiesUpdateListener[] listeners = null;
        synchronized(this) {
            listeners = fListeners;
        }
        for (IPropertiesUpdateListener listener : listeners) {
            listener.propertiesUpdatesStarted(updates);
        }
    }

    private void notifyPropertiesUpdateCompleted(IPropertiesUpdate update) {
        IPropertiesUpdateListener[] listeners = null;
        synchronized(this) {
            listeners = fListeners;
        }
        for (IPropertiesUpdateListener listener : listeners) {
            listener.propertiesUpdateCompleted(update);
        }
    }

    /**
     * Calculates the names of properties that have to be retrieved from the property
     * provider to generate the labels for given columns.
     * @param columnIds Column IDs to check.
     * @return Array of property names.
     */
    private Set<String> calcPropertyNamesForColumns(String[] columnIds) {
        Set<String> propertyNames = new HashSet<String>();
        if (columnIds == null) {
            LabelColumnInfo columnInfo = getColumnInfo(ID_COLUMN_NO_COLUMNS);
            if (columnInfo != null) {
                for (String propertyName : columnInfo.getPropertyNames()) {
                    propertyNames.add(propertyName);
                }
            } 
        } else {
            for (String columnId : columnIds) {
                LabelColumnInfo info = getColumnInfo(columnId);
                if (info != null) {
                    String[] infoPropertyNames = info.getPropertyNames();
                    for (int i = 0; i < infoPropertyNames.length; i++) {
                        propertyNames.add(infoPropertyNames[i]);
                    }
                }
            }
        }
        return propertyNames;
    }
    
    /**
     * Updates the label information based on given map of properties.
     * 
     * @param update Label update to write to.
     * @param status Result of the properties update
     * @param properties Properties retrieved from the element properties provider.
     * 
     * @since 2.0
     */
    protected void updateLabel(ILabelUpdate update, IStatus status, Map<String, Object> properties) {
        if (update.getColumnIds() == null) {
            LabelColumnInfo info = getColumnInfo(ID_COLUMN_NO_COLUMNS);
            if (info != null) {
                info.updateColumn(update, 0, status, properties);
            }
        } else {
            String[] columnIds = update.getColumnIds();
            
            for (int i = 0; i < columnIds.length; i++) {
                LabelColumnInfo info = getColumnInfo(columnIds[i]);
                if (info != null) {
                    info.updateColumn(update, i, status, properties);
                }
            }       
        }
        
        update.done();
    }

    private IElementPropertiesProvider getElementPropertiesProvider(Object element) {
        if (element instanceof IAdaptable) {
            return (IElementPropertiesProvider)((IAdaptable)element).getAdapter(IElementPropertiesProvider.class);
        }
        return null;
    }
}
