/*******************************************************************************
 * 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 
 *  	Sebastian Davids <sdavids@gmx.de> - Fix for bug 19346 - Dialog
 * 		font should be activated and used by other components.
 *******************************************************************************/
package org.eclipse.ui.internal.dialogs;

import java.io.IOException;
import java.net.URL;
import java.util.HashMap;
import java.util.Map;

import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Path;
import org.eclipse.core.runtime.Platform;
import org.eclipse.core.runtime.Status;
import org.eclipse.core.runtime.jobs.ISchedulingRule;
import org.eclipse.core.runtime.jobs.Job;

import org.eclipse.swt.SWT;
import org.eclipse.swt.events.SelectionAdapter;
import org.eclipse.swt.events.SelectionEvent;
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.Label;
import org.eclipse.swt.widgets.Shell;
import org.eclipse.swt.widgets.TableColumn;

import org.eclipse.jface.dialogs.DialogTray;
import org.eclipse.jface.dialogs.IDialogConstants;
import org.eclipse.jface.internal.ConfigureColumnsDialog;
import org.eclipse.jface.viewers.ArrayContentProvider;
import org.eclipse.jface.viewers.IBaseLabelProvider;
import org.eclipse.jface.viewers.ISelectionChangedListener;
import org.eclipse.jface.viewers.IStructuredSelection;
import org.eclipse.jface.viewers.ITableLabelProvider;
import org.eclipse.jface.viewers.LabelProvider;
import org.eclipse.jface.viewers.LabelProviderChangedEvent;
import org.eclipse.jface.viewers.SelectionChangedEvent;
import org.eclipse.jface.viewers.TableViewer;
import org.eclipse.jface.viewers.Viewer;
import org.eclipse.jface.viewers.ViewerComparator;

import org.eclipse.ui.PlatformUI;
import org.eclipse.ui.internal.IWorkbenchGraphicConstants;
import org.eclipse.ui.internal.IWorkbenchHelpContextIds;
import org.eclipse.ui.internal.WorkbenchImages;
import org.eclipse.ui.internal.WorkbenchMessages;
import org.eclipse.ui.internal.WorkbenchPlugin;
import org.eclipse.ui.internal.about.AboutBundleData;
import org.eclipse.ui.internal.misc.StatusUtil;
import org.eclipse.ui.internal.util.BundleUtility;
import org.eclipse.ui.progress.UIJob;
import org.eclipse.ui.statushandlers.StatusManager;

import org.eclipse.osgi.util.NLS;
import org.osgi.framework.Bundle;

/**
 * Displays information about the product plugins.
 *
 * PRIVATE
 *	this class is internal to the ide
 */
public class AboutPluginsDialog extends ProductInfoDialog {

	public class BundleTableLabelProvider extends LabelProvider implements ITableLabelProvider  {
		
		/**
	     * Scheduling rule for resolve jobs.
	     */
	    private ISchedulingRule resolveRule = new ISchedulingRule() {

			public boolean contains(ISchedulingRule rule) {
				return rule == this;
			}

			public boolean isConflicting(ISchedulingRule rule) {
				return rule == this;
			}};
			
		private Job sortingJob= new UIJob(getShell().getDisplay(), AboutPluginsDialog.class.getName()) {
			{
				setRule(resolveRule);
				setSystem(true);
				setPriority(Job.DECORATE);
			}
			
			/* (non-Javadoc)
			 * @see org.eclipse.ui.progress.UIJob#runInUIThread(org.eclipse.core.runtime.IProgressMonitor)
			 */
			public IStatus runInUIThread(IProgressMonitor monitor) {
				fireLabelProviderChanged(new LabelProviderChangedEvent(
						BundleTableLabelProvider.this));
				return Status.OK_STATUS;
			}
		};
			
		/* (non-Javadoc)
		 * @see org.eclipse.jface.viewers.ITableLabelProvider#getColumnImage(java.lang.Object, int)
		 */
		public Image getColumnImage(Object element, int columnIndex) {
			if (columnIndex == 0) {
				if (element instanceof AboutBundleData) {
					final AboutBundleData data = (AboutBundleData) element;
					if (data.isSignedDetermined()) {
						return WorkbenchImages
								.getImage(data.isSigned() ? IWorkbenchGraphicConstants.IMG_OBJ_SIGNED_YES
										: IWorkbenchGraphicConstants.IMG_OBJ_SIGNED_NO);
					} 
					Job resolveJob = new Job(data.getId()){ 

						protected IStatus run(IProgressMonitor monitor) {
							
							data.isSigned();
							Shell dialogShell = getShell();
							if (dialogShell == null || dialogShell.isDisposed())
								return Status.OK_STATUS;
								
							dialogShell.getDisplay().asyncExec(new Runnable() {

								public void run() {
									fireLabelProviderChanged(new LabelProviderChangedEvent(
											BundleTableLabelProvider.this, data));
								}
							});

							sortingJob.schedule();

							return Status.OK_STATUS;
						}
					}; 
					resolveJob.setRule(resolveRule);
					resolveJob.setSystem(true);
					resolveJob.schedule();
					return WorkbenchImages
							.getImage(IWorkbenchGraphicConstants.IMG_OBJ_SIGNED_UNKNOWN);
				}
			}
			return null;
		}

		/* (non-Javadoc)
		 * @see org.eclipse.jface.viewers.ITableLabelProvider#getColumnText(java.lang.Object, int)
		 */
		public String getColumnText(Object element, int columnIndex) {
			if (element instanceof AboutBundleData) {
				AboutBundleData data = (AboutBundleData) element;
				switch (columnIndex) {
				case 1:
					return data.getProviderName();
				case 2:
					return data.getName();
				case 3:
					return data.getVersion();
				case 4:
					return data.getId();
				}
			}
			return ""; //$NON-NLS-1$
		}		
	}
	
    /**
     * Table height in dialog units (value 200).
     */
    private static final int TABLE_HEIGHT = 200;

	private static final IPath baseNLPath = new Path("$nl$"); //$NON-NLS-1$

    private static final String PLUGININFO = "about.html"; //$NON-NLS-1$

    private final static int MORE_ID = IDialogConstants.CLIENT_ID + 1;
    private final static int SIGNING_ID = MORE_ID + 1;
    private final static int COLUMNS_ID = MORE_ID + 2;
    
    private static final int PLUGIN_NAME_COLUMN_INDEX = 2;

    private TableViewer vendorInfo;

    private Button moreInfo, signingInfo;

    private String title;

    private String message;
    
    private String helpContextId;

    private String columnTitles[] = {
    		WorkbenchMessages.AboutPluginsDialog_signed,
            WorkbenchMessages.AboutPluginsDialog_provider,
            WorkbenchMessages.AboutPluginsDialog_pluginName,
            WorkbenchMessages.AboutPluginsDialog_version, 
            WorkbenchMessages.AboutPluginsDialog_pluginId,
            
    };

    private String productName;

    private AboutBundleData[] bundleInfos;
    
    /**
     * Constructor for AboutPluginsDialog.
     * 
     * @param parentShell the parent shell
     * @param productName the product name
     */
    public AboutPluginsDialog(Shell parentShell, String productName) {
        this(parentShell, productName, WorkbenchPlugin.getDefault()
                .getBundles(), null, null, IWorkbenchHelpContextIds.ABOUT_PLUGINS_DIALOG);
        WorkbenchPlugin.class.getSigners();
    }

    /**
     * Constructor for AboutPluginsDialog.
     * 
     * @param parentShell 
     * 			  the parent shell
     * @param productName
     *            must not be null
     * @param bundles
     *            must not be null
     * @param title 
     *            the title
     * @param message 
     * 			  the message
     * @param helpContextId 
     *            the help context id
     */
    public AboutPluginsDialog(Shell parentShell, String productName,
            Bundle[] bundles, String title, String message, String helpContextId) {
        super(parentShell);
        this.title = title;
        this.message = message;
        this.helpContextId = helpContextId;
        this.productName = productName;
        
        // create a data object for each bundle, remove duplicates, and include only resolved bundles (bug 65548)
        Map map = new HashMap();
        for (int i = 0; i < bundles.length; ++i) {
            AboutBundleData data = new AboutBundleData(bundles[i]);
            if (BundleUtility.isReady(data.getState()) && !map.containsKey(data.getVersionedId())) {
				map.put(data.getVersionedId(), data);
			}
        }
        bundleInfos = (AboutBundleData[]) map.values().toArray(
                new AboutBundleData[0]);
    }

    /*
     * (non-Javadoc) Method declared on Dialog.
     */
    protected void buttonPressed(int buttonId) {
        switch (buttonId) {
        case MORE_ID:
            handleMoreInfoPressed();
            break;
        case SIGNING_ID:
        	handleSigningInfoPressed();
        	break;
        case COLUMNS_ID:
        	handleColumnsPressed();
        	break;
        default:
            super.buttonPressed(buttonId);
            break;
        }
    }

    /**
	 * 
	 */
	private void handleColumnsPressed() {
		ConfigureColumnsDialog d = new ConfigureColumnsDialog(this, vendorInfo.getTable());
		d.open();
	}

	/**
	 */
	private void handleSigningInfoPressed() {
		DialogTray existingTray = getTray();
		if (existingTray instanceof BundleSigningTray) {
			// hide
			getButton(SIGNING_ID).setText(WorkbenchMessages.AboutPluginsDialog_signingInfo_show); 
			closeTray();
		}
		else {
			//show
			getButton(SIGNING_ID).setText(WorkbenchMessages.AboutPluginsDialog_signingInfo_hide);
			if (existingTray != null)
				closeTray();
			AboutBundleData bundleInfo = (AboutBundleData) ((IStructuredSelection) vendorInfo
					.getSelection()).getFirstElement();
			BundleSigningTray tray = new BundleSigningTray(this);
			tray.setData(bundleInfo);
			openTray(tray);
		}
		
	}

	/*
     * (non-Javadoc) Method declared on Window.
     */
    protected void configureShell(Shell newShell) {
        super.configureShell(newShell);
        
        //signImage = new Image( this.getParentShell().getDisplay(), AboutPluginsDialog.class.getResourceAsStream("Signed.gif")); //$NON-NLS-1$
        
        if (title == null && productName != null) {
			title = NLS.bind(WorkbenchMessages.AboutPluginsDialog_shellTitle, productName);
		}

        if (title != null) {
			newShell.setText(title);
		}

        PlatformUI.getWorkbench().getHelpSystem().setHelp(newShell,
				helpContextId);
    }
    
    /**
     * Add buttons to the dialog's button bar.
     * 
     * Subclasses should override.
     * 
     * @param parent
     *            the button bar composite
     */
    protected void createButtonsForButtonBar(Composite parent) {
        parent.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));

        moreInfo = createButton(parent, MORE_ID, WorkbenchMessages.AboutPluginsDialog_moreInfo, false); 
        moreInfo.setEnabled(false);
        
        signingInfo = createButton(parent, SIGNING_ID, WorkbenchMessages.AboutPluginsDialog_signingInfo_show, false);
        signingInfo.setEnabled(false);

        createButton(parent, COLUMNS_ID, WorkbenchMessages.AboutPluginsDialog_columns, false);

        Label l = new Label(parent, SWT.NONE);
        l.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
        GridLayout layout = (GridLayout) parent.getLayout();
        layout.numColumns++;
        layout.makeColumnsEqualWidth = false;

        createButton(parent, IDialogConstants.OK_ID, IDialogConstants.OK_LABEL,
                true);
    }

    /**
     * Create the contents of the dialog (above the button bar).
     * 
     * Subclasses should overide.
     * 
     * @param parent
     *            the parent composite to contain the dialog area
     * @return the dialog area control
     */
    protected Control createDialogArea(Composite parent) {
        Composite outer = (Composite) super.createDialogArea(parent);

        if (message != null) {
            Label label = new Label(outer, SWT.NONE);
            label.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
            label.setFont(parent.getFont());
            label.setText(message);
        }

        createTable(outer);

        return outer;
    }

    /**
     * Create the table part of the dialog.
     * 
     * @param parent
     *            the parent composite to contain the dialog area
     */
    protected void createTable(Composite parent) {
        vendorInfo = new TableViewer(parent, SWT.H_SCROLL | SWT.V_SCROLL | SWT.SINGLE
                | SWT.FULL_SELECTION | SWT.BORDER);
        vendorInfo.setUseHashlookup(true);
        vendorInfo.getTable().setHeaderVisible(true);
        vendorInfo.getTable().setLinesVisible(true);
        vendorInfo.getTable().setFont(parent.getFont());
        vendorInfo.addSelectionChangedListener(new ISelectionChangedListener() {

			public void selectionChanged(SelectionChangedEvent event) {
				// enable if there is an item selected and that
                // item has additional info
				IStructuredSelection selection = (IStructuredSelection) event.getSelection();
				if (selection.getFirstElement() instanceof AboutBundleData) {
					AboutBundleData selected = (AboutBundleData) selection.getFirstElement(); 
					moreInfo.setEnabled(selectionHasInfo(selected));
					signingInfo.setEnabled(true);
					if (getTray() instanceof BundleSigningTray) {
						((BundleSigningTray)getTray()).setData(selected);
					}
				}
				else {
					moreInfo.setEnabled(false);
					signingInfo.setEnabled(false);
				}
			}});
        
        final TableComparator comparator = new TableComparator();
        vendorInfo.setComparator(comparator);
        int[] columnWidths = {
        		convertHorizontalDLUsToPixels(30), //signature
        		convertHorizontalDLUsToPixels(120),
                convertHorizontalDLUsToPixels(120),
                convertHorizontalDLUsToPixels(70),
                convertHorizontalDLUsToPixels(130),
                };

        
        // create table headers
        for (int i = 0; i < columnTitles.length; i++) {
            TableColumn column = new TableColumn(vendorInfo.getTable(), SWT.NULL);
            if (i == PLUGIN_NAME_COLUMN_INDEX) { // prime initial sorting
            	updateTableSorting(i);
            }	
            column.setWidth(columnWidths[i]);
            column.setText(columnTitles[i]);
            final int columnIndex = i;
            column.addSelectionListener(new SelectionAdapter() {
                public void widgetSelected(SelectionEvent e) {
                    updateTableSorting(columnIndex);
                }
            });
        }
                
        vendorInfo.setContentProvider(new ArrayContentProvider());        
        vendorInfo.setLabelProvider(new BundleTableLabelProvider());
       
        GridData gridData = new GridData(GridData.FILL, GridData.FILL, true,
                true);
        gridData.heightHint = convertVerticalDLUsToPixels(TABLE_HEIGHT);
        vendorInfo.getTable().setLayoutData(gridData);
        
        vendorInfo.setInput(bundleInfos);
    }

	/**
	 * Update the sort information on both the comparator and the table.
	 * 
	 * @param columnIndex
	 *            the index to sort by
	 * @since 3.4
	 */
	private void updateTableSorting(final int columnIndex) {
		TableComparator comparator = (TableComparator) vendorInfo
				.getComparator();
		// toggle direction if it's the same column
		if (columnIndex == comparator.getSortColumn()) {
			comparator.setAscending(!comparator.isAscending());
		}
		comparator.setSortColumn(columnIndex);
		vendorInfo.getTable().setSortColumn(
				vendorInfo.getTable().getColumn(columnIndex));
		vendorInfo.getTable().setSortDirection(
				comparator.isAscending() ? SWT.UP : SWT.DOWN);
		vendorInfo.refresh(false);
	}
	
	/**
     * Check if the currently selected plugin has additional information to
     * show.
     * @param bundleInfo 
     * 
     * @return true if the selected plugin has additional info available to
     *         display
     */
    private boolean selectionHasInfo(AboutBundleData bundleInfo) {
        
        URL infoURL = getMoreInfoURL(bundleInfo, false);

        // only report ini problems if the -debug command line argument is used
        if (infoURL == null && WorkbenchPlugin.DEBUG) {
        	WorkbenchPlugin.log("Problem reading plugin info for: " //$NON-NLS-1$
					+ bundleInfo.getName());
		}

        return infoURL != null;
    }

    /** 
     * The More Info button was pressed.  Open a browser showing the license information
     * for the selected bundle or an error dialog if the browser cannot be opened.
     */
    protected void handleMoreInfoPressed() {
        if (vendorInfo == null) {
			return;
		}
        
        if (vendorInfo.getSelection().isEmpty())
        	return;
        
        AboutBundleData bundleInfo = (AboutBundleData) ((IStructuredSelection) vendorInfo
				.getSelection()).getFirstElement();
        
        if (!openBrowser(getMoreInfoURL(bundleInfo, true))) {
			String message = NLS.bind(
					WorkbenchMessages.AboutPluginsDialog_unableToOpenFile,
					PLUGININFO, bundleInfo.getId());
			StatusUtil.handleStatus(
					WorkbenchMessages.AboutPluginsDialog_errorTitle
							+ ": " + message, StatusManager.SHOW, getShell()); //$NON-NLS-1$
		}
    }

    /**
     * Return an url to the plugin's about.html file (what is shown when "More info" is
     * pressed) or null if no such file exists.  The method does nl lookup to allow for
     * i18n.
     * 
     * @param bundleInfo the bundle info
     * @param makeLocal whether to make the about content local
     * @return the url or <code>null</code>
     */
    private URL getMoreInfoURL(AboutBundleData bundleInfo, boolean makeLocal) {
        Bundle bundle = Platform.getBundle(bundleInfo.getId());
        if (bundle == null) {
			return null;
		}

        URL aboutUrl = Platform.find(bundle, baseNLPath.append(PLUGININFO), null);
        if (!makeLocal) {
            return aboutUrl;
        }
		if (aboutUrl != null) {
		    try {
				URL result = Platform.asLocalURL(aboutUrl);
				try {
				    // Make local all content in the "about" directory.
				    // This is needed to handle jar'ed plug-ins.
				    // See Bug 88240 [About] About dialog needs to extract subdirs.
					URL about = new URL(aboutUrl, "about_files"); //$NON-NLS-1$
					if (about != null) {
						Platform.asLocalURL(about);
					}
				} catch (IOException e) {
					// skip the about dir if its not found or there are other problems.
				}
				return result;
		    } catch(IOException e) {
		        // do nothing
		    }
        }
		return null;
    }
}


class TableComparator extends ViewerComparator {

	private int sortColumn = 0;
	private boolean ascending = true;
	
	/* (non-Javadoc)
	 * @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) {
		if (sortColumn == 0 && e1 instanceof AboutBundleData && e2 instanceof AboutBundleData) {
			AboutBundleData d1= (AboutBundleData) e1;
			AboutBundleData d2= (AboutBundleData) e2;
			int diff= getSignedSortValue(d1) - getSignedSortValue(d2);
			return ascending ? diff : -diff;
		}
		if (viewer instanceof TableViewer) {
			TableViewer tableViewer = (TableViewer) viewer;
			IBaseLabelProvider baseLabel = tableViewer.getLabelProvider();
			if (baseLabel instanceof ITableLabelProvider) {
				ITableLabelProvider tableProvider = (ITableLabelProvider) baseLabel;
				String e1p = tableProvider.getColumnText(e1, sortColumn);
				String e2p = tableProvider.getColumnText(e2, sortColumn);
				int result = getComparator().compare(e1p, e2p);
				return ascending ?  result : (-1) * result;
			}
		}
		
		return super.compare(viewer, e1, e2);
	}

	/**
	 * @param data
	 * @return a sort value depending on the signed state
	 */
	private int getSignedSortValue(AboutBundleData data) {
		if (! data.isSignedDetermined()) {
			return 0;
		} else if (data.isSigned()) {
			return 1;
		} else {
			return -1;
		}
	}
	
	/**
	 * @return Returns the sortColumn.
	 */
	public int getSortColumn() {
		return sortColumn;
	}

	/**
	 * @param sortColumn The sortColumn to set.
	 */
	public void setSortColumn(int sortColumn) {
		this.sortColumn = sortColumn;
	}

	/**
	 * @return Returns the ascending.
	 */
	public boolean isAscending() {
		return ascending;
	}

	/**
	 * @param ascending The ascending to set.
	 */
	public void setAscending(boolean ascending) {
		this.ascending = ascending;
	}
}
