/*
 * Copyright (C) 2009 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.sdkuilib.internal.repository;

import com.android.SdkConstants;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.sdklib.AndroidVersion;
import com.android.sdklib.internal.repository.archives.Archive;
import com.android.sdklib.internal.repository.packages.IAndroidVersionProvider;
import com.android.sdklib.internal.repository.packages.Package;
import com.android.sdklib.internal.repository.packages.Package.License;
import com.android.sdklib.internal.repository.sources.SdkSource;
import com.android.sdklib.internal.repository.updater.ArchiveInfo;
import com.android.sdklib.internal.repository.updater.SdkUpdaterLogic;
import com.android.sdklib.repository.FullRevision;
import com.android.sdkuilib.internal.repository.icons.ImageFactory;
import com.android.sdkuilib.ui.GridDialog;

import org.eclipse.jface.dialogs.IDialogConstants;
import org.eclipse.jface.viewers.ISelection;
import org.eclipse.jface.viewers.IStructuredSelection;
import org.eclipse.jface.viewers.ITreeContentProvider;
import org.eclipse.jface.viewers.LabelProvider;
import org.eclipse.jface.viewers.TreeViewer;
import org.eclipse.jface.viewers.Viewer;
import org.eclipse.jface.window.Window;
import org.eclipse.swt.SWT;
import org.eclipse.swt.custom.SashForm;
import org.eclipse.swt.custom.StyleRange;
import org.eclipse.swt.custom.StyledText;
import org.eclipse.swt.events.ControlAdapter;
import org.eclipse.swt.events.ControlEvent;
import org.eclipse.swt.events.SelectionAdapter;
import org.eclipse.swt.events.SelectionEvent;
import org.eclipse.swt.graphics.Image;
import org.eclipse.swt.graphics.Point;
import org.eclipse.swt.graphics.Rectangle;
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.Link;
import org.eclipse.swt.widgets.Shell;
import org.eclipse.swt.widgets.Tree;
import org.eclipse.swt.widgets.TreeColumn;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Comparator;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.TreeMap;


/**
 * Implements an {@link SdkUpdaterChooserDialog}.
 */
final class SdkUpdaterChooserDialog extends GridDialog {

    /** Last dialog size for this session. */
    private static Point sLastSize;
    /** Precomputed flag indicating whether the "accept license" radio is checked. */
    private boolean mAcceptSameAllLicense;
    private boolean mInternalLicenseRadioUpdate;

    // UI fields
    private SashForm mSashForm;
    private Composite mPackageRootComposite;
    private TreeViewer mTreeViewPackage;
    private Tree mTreePackage;
    private TreeColumn mTreeColum;
    private StyledText mPackageText;
    private Button mLicenseRadioAccept;
    private Button mLicenseRadioReject;
    private Button mLicenseRadioAcceptLicense;
    private Group mPackageTextGroup;
    private final SwtUpdaterData mSwtUpdaterData;
    private Group mTableGroup;
    private Label mErrorLabel;

    /**
     * List of all archives to be installed with dependency information.
     * <p/>
     * Note: in a lot of cases, we need to find the archive info for a given archive. This
     * is currently done using a simple linear search, which is fine since we only have a very
     * limited number of archives to deal with (e.g. < 10 now). We might want to revisit
     * this later if it becomes an issue. Right now just do the simple thing.
     * <p/>
     * Typically we could add a map Archive=>ArchiveInfo later.
     */
    private final Collection<ArchiveInfo> mArchives;



    /**
     * Create the dialog.
     *
     * @param parentShell The shell to use, typically updaterData.getWindowShell()
     * @param swtUpdaterData The updater data
     * @param archives The archives to be installed
     */
    public SdkUpdaterChooserDialog(Shell parentShell,
            SwtUpdaterData swtUpdaterData,
            Collection<ArchiveInfo> archives) {
        super(parentShell, 3, false/*makeColumnsEqual*/);
        mSwtUpdaterData = swtUpdaterData;
        mArchives = archives;
    }

    @Override
    protected boolean isResizable() {
        return true;
    }

    /**
     * Returns the results, i.e. the list of selected new archives to install.
     * This is similar to the {@link ArchiveInfo} list instance given to the constructor
     * except only accepted archives are present.
     * <p/>
     * An empty list is returned if cancel was chosen.
     */
    public ArrayList<ArchiveInfo> getResult() {
        ArrayList<ArchiveInfo> ais = new ArrayList<ArchiveInfo>();

        if (getReturnCode() == Window.OK) {
            for (ArchiveInfo ai : mArchives) {
                if (ai.isAccepted()) {
                    ais.add(ai);
                }
            }
        }

        return ais;
    }

    /**
     * Create the main content of the dialog.
     * See also {@link #createButtonBar(Composite)} below.
     */
    @Override
    public void createDialogContent(Composite parent) {
        // Sash form
        mSashForm = new SashForm(parent, SWT.NONE);
        mSashForm.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true, 3, 1));


        // Left part of Sash Form

        mTableGroup = new Group(mSashForm, SWT.NONE);
        mTableGroup.setText("Packages");
        mTableGroup.setLayout(new GridLayout(1, false/*makeColumnsEqual*/));

        mTreeViewPackage = new TreeViewer(mTableGroup, SWT.BORDER | SWT.V_SCROLL | SWT.SINGLE);
        mTreePackage = mTreeViewPackage.getTree();
        mTreePackage.setHeaderVisible(false);
        mTreePackage.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true, 1, 1));

        mTreePackage.addSelectionListener(new SelectionAdapter() {
            @Override
            public void widgetSelected(SelectionEvent e) {
                onPackageSelected();  //$hide$
            }
            @Override
            public void widgetDefaultSelected(SelectionEvent e) {
                onPackageDoubleClick();
            }
        });

        mTreeColum = new TreeColumn(mTreePackage, SWT.NONE);
        mTreeColum.setWidth(100);
        mTreeColum.setText("Packages");

        // Right part of Sash form

        mPackageRootComposite = new Composite(mSashForm, SWT.NONE);
        mPackageRootComposite.setLayout(new GridLayout(4, false/*makeColumnsEqual*/));
        mPackageRootComposite.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true));

        mPackageTextGroup = new Group(mPackageRootComposite, SWT.NONE);
        mPackageTextGroup.setText("Package Description && License");
        mPackageTextGroup.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true, 4, 1));
        mPackageTextGroup.setLayout(new GridLayout(1, false/*makeColumnsEqual*/));

        mPackageText = new StyledText(mPackageTextGroup,
                        SWT.MULTI | SWT.READ_ONLY | SWT.WRAP | SWT.V_SCROLL);
        mPackageText.setBackground(
                getParentShell().getDisplay().getSystemColor(SWT.COLOR_WIDGET_BACKGROUND));
        mPackageText.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true, 1, 1));

        mLicenseRadioAccept = new Button(mPackageRootComposite, SWT.RADIO);
        mLicenseRadioAccept.setText("Accept");
        mLicenseRadioAccept.setToolTipText("Accept this package.");
        mLicenseRadioAccept.addSelectionListener(new SelectionAdapter() {
            @Override
            public void widgetSelected(SelectionEvent e) {
                onLicenseRadioSelected();
            }
        });

        mLicenseRadioReject = new Button(mPackageRootComposite, SWT.RADIO);
        mLicenseRadioReject.setText("Reject");
        mLicenseRadioReject.setToolTipText("Reject this package.");
        mLicenseRadioReject.addSelectionListener(new SelectionAdapter() {
            @Override
            public void widgetSelected(SelectionEvent e) {
                onLicenseRadioSelected();
            }
        });

        Link link = new Link(mPackageRootComposite, SWT.NONE);
        link.setLayoutData(new GridData(SWT.CENTER, SWT.CENTER, true, false, 1, 1));
        final String printAction = "Print"; // extracted for NLS, to compare with below.
        link.setText(String.format("<a>Copy to clipboard</a> | <a>%1$s</a>", printAction));
        link.setToolTipText("Copies all text and license to clipboard | Print using system defaults.");
        link.addSelectionListener(new SelectionAdapter() {
            @Override
            public void widgetSelected(SelectionEvent e) {
                super.widgetSelected(e);
                if (printAction.equals(e.text)) {
                    mPackageText.print();
                } else {
                    Point p = mPackageText.getSelection();
                    mPackageText.selectAll();
                    mPackageText.copy();
                    mPackageText.setSelection(p);
                }
            }
        });


        mLicenseRadioAcceptLicense = new Button(mPackageRootComposite, SWT.RADIO);
        mLicenseRadioAcceptLicense.setText("Accept License");
        mLicenseRadioAcceptLicense.setToolTipText("Accept all packages that use the same license.");
        mLicenseRadioAcceptLicense.addSelectionListener(new SelectionAdapter() {
            @Override
            public void widgetSelected(SelectionEvent e) {
                onLicenseRadioSelected();
            }
        });

        mSashForm.setWeights(new int[] {200, 300});
    }

    /**
     * Creates and returns the contents of this dialog's button bar.
     * <p/>
     * This reimplements most of the code from the base class with a few exceptions:
     * <ul>
     * <li>Enforces 3 columns.
     * <li>Inserts a full-width error label.
     * <li>Inserts a help label on the left of the first button.
     * <li>Renames the OK button into "Install"
     * </ul>
     */
    @Override
    protected Control createButtonBar(Composite parent) {
        Composite composite = new Composite(parent, SWT.NONE);
        GridLayout layout = new GridLayout();
        layout.numColumns = 0; // this is incremented by createButton
        layout.makeColumnsEqualWidth = false;
        layout.marginWidth = convertHorizontalDLUsToPixels(IDialogConstants.HORIZONTAL_MARGIN);
        layout.marginHeight = convertVerticalDLUsToPixels(IDialogConstants.VERTICAL_MARGIN);
        layout.horizontalSpacing = convertHorizontalDLUsToPixels(IDialogConstants.HORIZONTAL_SPACING);
        layout.verticalSpacing = convertVerticalDLUsToPixels(IDialogConstants.VERTICAL_SPACING);
        composite.setLayout(layout);
        GridData data = new GridData(SWT.FILL, SWT.CENTER, true, false, 3, 1);
        composite.setLayoutData(data);
        composite.setFont(parent.getFont());

        // Error message area
        mErrorLabel = new Label(composite, SWT.NONE);
        mErrorLabel.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false, 3, 1));

        // Label at the left of the install/cancel buttons
        Label label = new Label(composite, SWT.NONE);
        label.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false, 1, 1));
        label.setText("[*] Something depends on this package");
        label.setEnabled(false);
        layout.numColumns++;

        // Add the ok/cancel to the button bar.
        createButtonsForButtonBar(composite);

        // the ok button should be an "install" button
        Button button = getButton(IDialogConstants.OK_ID);
        button.setText("Install");

        return composite;
    }

    // -- End of UI, Start of internal logic ----------
    // Hide everything down-below from SWT designer
    //$hide>>$

    @Override
    public void create() {
        super.create();

        // set window title
        getShell().setText("Choose Packages to Install");

        setWindowImage();

        // Automatically accept those with an empty license or no license
        for (ArchiveInfo ai : mArchives) {
            Archive a = ai.getNewArchive();
            if (a != null) {
                License license = a.getParentPackage().getLicense();
                boolean hasLicense = license != null &&
                                     license.getLicense() != null &&
                                     license.getLicense().length() > 0;
                ai.setAccepted(!hasLicense);
            }
        }

        // Fill the list with the replacement packages
        mTreeViewPackage.setLabelProvider(new NewArchivesLabelProvider());
        mTreeViewPackage.setContentProvider(new NewArchivesContentProvider());
        mTreeViewPackage.setInput(createTreeInput(mArchives));
        mTreeViewPackage.expandAll();

        adjustColumnsWidth();

        // select first item
        onPackageSelected();
    }

    /**
     * Creates the icon of the window shell.
     */
    private void setWindowImage() {
        String imageName = "android_icon_16.png"; //$NON-NLS-1$
        if (SdkConstants.currentPlatform() == SdkConstants.PLATFORM_DARWIN) {
            imageName = "android_icon_128.png";   //$NON-NLS-1$
        }

        if (mSwtUpdaterData != null) {
            ImageFactory imgFactory = mSwtUpdaterData.getImageFactory();
            if (imgFactory != null) {
                getShell().setImage(imgFactory.getImageByName(imageName));
            }
        }
    }

    /**
     * Adds a listener to adjust the columns width when the parent is resized.
     * <p/>
     * If we need something more fancy, we might want to use this:
     * http://dev.eclipse.org/viewcvs/index.cgi/org.eclipse.swt.snippets/src/org/eclipse/swt/snippets/Snippet77.java?view=co
     */
    private void adjustColumnsWidth() {
        // Add a listener to resize the column to the full width of the table
        ControlAdapter resizer = new ControlAdapter() {
            @Override
            public void controlResized(ControlEvent e) {
                Rectangle r = mTreePackage.getClientArea();
                mTreeColum.setWidth(r.width);
            }
        };
        mTreePackage.addControlListener(resizer);
        resizer.controlResized(null);
    }

    /**
     * Captures the window size before closing this.
     * @see #getInitialSize()
     */
    @Override
    public boolean close() {
        sLastSize = getShell().getSize();
        return super.close();
    }

    /**
     * Tries to reuse the last window size during this session.
     * <p/>
     * Note: the alternative would be to implement {@link #getDialogBoundsSettings()}
     * since the default {@link #getDialogBoundsStrategy()} is to persist both location
     * and size.
     */
    @Override
    protected Point getInitialSize() {
        if (sLastSize != null) {
            return sLastSize;
        } else {
            // Arbitrary values that look good on my screen and fit on 800x600
            return new Point(740, 470);
        }
    }

    /**
     * Callback invoked when a package item is selected in the list.
     */
    private void onPackageSelected() {
        Object item = getSelectedItem();

        // Update mAcceptSameAllLicense : true if all items under the same license are accepted.
        ArchiveInfo ai = null;
        List<ArchiveInfo> list = null;
        if (item instanceof ArchiveInfo) {
            ai = (ArchiveInfo) item;

            Object p =
                ((NewArchivesContentProvider) mTreeViewPackage.getContentProvider()).getParent(ai);
            if (p instanceof LicenseEntry) {
                list = ((LicenseEntry) p).getArchives();
            }
            displayPackageInformation(ai);

        } else if (item instanceof LicenseEntry) {
            LicenseEntry entry = (LicenseEntry) item;
            list = entry.getArchives();
            displayLicenseInformation(entry);

        } else {
            // Fallback, should not happen.
            displayEmptyInformation();
        }

        // the "Accept License" radio is selected if there's a license with >= 0 items
        // and they are all in "accepted" state.
        mAcceptSameAllLicense = list != null && list.size() > 0;
        if (mAcceptSameAllLicense) {
            assert list != null;
            License lic0 = getLicense(list.get(0));
            for (ArchiveInfo ai2 : list) {
                License lic2 = getLicense(ai2);
                if (ai2.isAccepted() && (lic0 == lic2 || lic0.equals(lic2))) {
                    continue;
                } else {
                    mAcceptSameAllLicense = false;
                    break;
                }
            }
        }

        displayMissingDependency(ai);
        updateLicenceRadios(ai);
    }

    /** Returns the currently selected tree item.
     * @return Either {@link ArchiveInfo} or {@link LicenseEntry} or null. */
    private Object getSelectedItem() {
        ISelection sel = mTreeViewPackage.getSelection();
        if (sel instanceof IStructuredSelection) {
            Object elem = ((IStructuredSelection) sel).getFirstElement();
            if (elem instanceof ArchiveInfo || elem instanceof LicenseEntry) {
                return elem;
            }
        }
        return null;
    }

    /**
     * Information displayed when nothing valid is selected.
     */
    private void displayEmptyInformation() {
        mPackageText.setText("Please select a package or a license.");
    }

    /**
     * Updates the package description and license text depending on the selected package.
     * <p/>
     * Note that right now there is no logic to support more than one level of dependencies
     * (e.g. A <- B <- C and A is disabled so C should be disabled; currently C's state depends
     * solely on B's state). We currently don't need this. It would be straightforward to add
     * if we had a need for it, though. This would require changes to {@link ArchiveInfo} and
     * {@link SdkUpdaterLogic}.
     */
    private void displayPackageInformation(ArchiveInfo ai) {
        Archive aNew = ai   == null ? null : ai.getNewArchive();
        Package pNew = aNew == null ? null : aNew.getParentPackage();

        if (pNew == null) {
            displayEmptyInformation();
            return;
        }
        assert ai   != null;                        // make Eclipse null detector happy
        assert aNew != null;

        mPackageText.setText("");                   //$NON-NLS-1$

        addSectionTitle("Package Description\n");
        addText(pNew.getLongDescription(), "\n\n"); //$NON-NLS-1$

        Archive aOld = ai.getReplaced();
        if (aOld != null) {
            Package pOld = aOld.getParentPackage();

            FullRevision rOld = pOld.getRevision();
            FullRevision rNew = pNew.getRevision();

            boolean showRev = true;

            if (pNew instanceof IAndroidVersionProvider &&
                    pOld instanceof IAndroidVersionProvider) {
                AndroidVersion vOld = ((IAndroidVersionProvider) pOld).getAndroidVersion();
                AndroidVersion vNew = ((IAndroidVersionProvider) pNew).getAndroidVersion();

                if (!vOld.equals(vNew)) {
                    // Versions are different, so indicate more than just the revision.
                    addText(String.format("This update will replace API %1$s revision %2$s with API %3$s revision %4$s.\n\n",
                            vOld.getApiString(), rOld.toShortString(),
                            vNew.getApiString(), rNew.toShortString()));
                    showRev = false;
                }
            }

            if (showRev) {
                addText(String.format("This update will replace revision %1$s with revision %2$s.\n\n",
                        rOld.toShortString(),
                        rNew.toShortString()));
            }
        }

        ArchiveInfo[] aDeps = ai.getDependsOn();
        if ((aDeps != null && aDeps.length > 0) || ai.isDependencyFor()) {
            addSectionTitle("Dependencies\n");

            if (aDeps != null && aDeps.length > 0) {
                addText("Installing this package also requires installing:");
                for (ArchiveInfo aDep : aDeps) {
                    addText(String.format("\n- %1$s",
                            aDep.getShortDescription()));
                }
                addText("\n\n");
            }

            if (ai.isDependencyFor()) {
                addText("This package is a dependency for:");
                for (ArchiveInfo ai2 : ai.getDependenciesFor()) {
                    addText(String.format("\n- %1$s",
                            ai2.getShortDescription()));
                }
                addText("\n\n");
            }
        }

        addSectionTitle("Archive Description\n");
        addText(aNew.getLongDescription(), "\n\n");                             //$NON-NLS-1$

        License license = pNew.getLicense();
        if (license != null) {
            String text = license.getLicense();
            if (text != null) {
                addSectionTitle("License\n");
                addText(text.trim(), "\n\n");                                   //$NON-NLS-1$
            }
        }

        addSectionTitle("Site\n");
        SdkSource source = pNew.getParentSource();
        if (source != null) {
            addText(source.getShortDescription());
        }
    }

    /**
     * Updates the description for a license entry.
     */
    private void displayLicenseInformation(LicenseEntry entry) {
        List<ArchiveInfo> archives = entry == null ? null : entry.getArchives();
        if (archives == null) {
            // There should not be a license entry without any package in it.
            displayEmptyInformation();
            return;
        }
        assert entry != null;

        mPackageText.setText("");                   //$NON-NLS-1$

        License license = null;
        addSectionTitle("Packages\n");
        for (ArchiveInfo ai : entry.getArchives()) {
            Archive aNew = ai.getNewArchive();
            if (aNew != null) {
                Package pNew = aNew.getParentPackage();
                if (pNew != null) {
                    if (license == null) {
                        license = pNew.getLicense();
                    } else {
                        assert license.equals(pNew.getLicense()); // all items have the same license
                    }
                    addText("- ", pNew.getShortDescription(), "\n"); //$NON-NLS-1$ //$NON-NLS-2$
                }
            }
        }

        if (license != null) {
            String text = license.getLicense();
            if (text != null) {
                addSectionTitle("\nLicense\n");
                addText(text.trim(), "\n\n");                                   //$NON-NLS-1$
            }
        }
    }

    /**
     * Computes and displays missing dependencies.
     *
     * If there's a selected package, check the dependency for that one.
     * Otherwise display the first missing dependency of any other package.
     */
    private void displayMissingDependency(ArchiveInfo ai) {
        String error = null;

        try {
            if (ai != null) {
                if (ai.isAccepted()) {
                    // Case where this package is accepted but blocked by another non-accepted one
                    ArchiveInfo[] adeps = ai.getDependsOn();
                    if (adeps != null) {
                        for (ArchiveInfo adep : adeps) {
                            if (!adep.isAccepted()) {
                                error = String.format("This package depends on '%1$s'.",
                                        adep.getShortDescription());
                                return;
                            }
                        }
                    }
                } else {
                    // Case where this package blocks another one when not accepted
                    for (ArchiveInfo adep : ai.getDependenciesFor()) {
                        // It only matters if the blocked one is accepted
                        if (adep.isAccepted()) {
                            error = String.format("Package '%1$s' depends on this one.",
                                    adep.getShortDescription());
                            return;
                        }
                    }
                }
            }

            // If there is no missing dependency on the current selection,
            // just find the first missing dependency of any other package.
            for (ArchiveInfo ai2 : mArchives) {
                if (ai2 == ai) {
                    // We already processed that one above.
                    continue;
                }
                if (ai2.isAccepted()) {
                    // The user requested to install this package.
                    // Check if all its dependencies are met.
                    ArchiveInfo[] adeps = ai2.getDependsOn();
                    if (adeps != null) {
                        for (ArchiveInfo adep : adeps) {
                            if (!adep.isAccepted()) {
                                error = String.format("Package '%1$s' depends on '%2$s'",
                                        ai2.getShortDescription(),
                                        adep.getShortDescription());
                                return;
                            }
                        }
                    }
                } else {
                    // The user did not request to install this package.
                    // Check whether this package blocks another one when not accepted.
                    for (ArchiveInfo adep : ai2.getDependenciesFor()) {
                        // It only matters if the blocked one is accepted
                        // or if it's a local archive that is already installed (these
                        // are marked as implicitly accepted, so it's the same test.)
                        if (adep.isAccepted()) {
                            error = String.format("Package '%1$s' depends on '%2$s'",
                                    adep.getShortDescription(),
                                    ai2.getShortDescription());
                            return;
                        }
                    }
                }
            }
        } finally {
            mErrorLabel.setText(error == null ? "" : error);        //$NON-NLS-1$
        }
    }

    private void addText(String...string) {
        for (String s : string) {
            mPackageText.append(s);
        }
    }

    private void addSectionTitle(String string) {
        String s = mPackageText.getText();
        int start = (s == null ? 0 : s.length());
        mPackageText.append(string);

        StyleRange sr = new StyleRange();
        sr.start = start;
        sr.length = string.length();
        sr.fontStyle = SWT.BOLD;
        sr.underline = true;
        mPackageText.setStyleRange(sr);
    }

    private void updateLicenceRadios(ArchiveInfo ai) {
        if (mInternalLicenseRadioUpdate) {
            return;
        }
        mInternalLicenseRadioUpdate = true;

        boolean oneAccepted = false;

        mLicenseRadioAcceptLicense.setSelection(mAcceptSameAllLicense);
        oneAccepted = ai != null && ai.isAccepted();
        mLicenseRadioAccept.setEnabled(ai != null);
        mLicenseRadioReject.setEnabled(ai != null);
        mLicenseRadioAccept.setSelection(oneAccepted);
        mLicenseRadioReject.setSelection(ai != null && ai.isRejected());

        // The install button is enabled if there's at least one package accepted.
        // If the current one isn't, look for another one.
        boolean missing = mErrorLabel.getText() != null && mErrorLabel.getText().length() > 0;
        if (!missing && !oneAccepted) {
            for(ArchiveInfo ai2 : mArchives) {
                if (ai2.isAccepted()) {
                    oneAccepted = true;
                    break;
                }
            }
        }

        getButton(IDialogConstants.OK_ID).setEnabled(!missing && oneAccepted);

        mInternalLicenseRadioUpdate = false;
    }

    /**
     * Callback invoked when one of the radio license buttons is selected.
     *
     * - accept/refuse: toggle, update item checkbox
     * - accept all: set accept-all, check all items with the *same* license
     */
    private void onLicenseRadioSelected() {
        if (mInternalLicenseRadioUpdate) {
            return;
        }
        mInternalLicenseRadioUpdate = true;

        Object item = getSelectedItem();
        ArchiveInfo ai = (item instanceof ArchiveInfo) ? (ArchiveInfo) item : null;
        boolean needUpdate = true;

        if (!mAcceptSameAllLicense && mLicenseRadioAcceptLicense.getSelection()) {
            // Accept all has been switched on. Mark all packages as accepted

            List<ArchiveInfo> list = null;
            if (item instanceof LicenseEntry) {
                list = ((LicenseEntry) item).getArchives();
            } else if (ai != null) {
                Object p = ((NewArchivesContentProvider) mTreeViewPackage.getContentProvider())
                                                                         .getParent(ai);
                if (p instanceof LicenseEntry) {
                    list = ((LicenseEntry) p).getArchives();
                }
            }

            if (list != null && list.size() > 0) {
                mAcceptSameAllLicense = true;
                for(ArchiveInfo ai2 : list) {
                    ai2.setAccepted(true);
                    ai2.setRejected(false);
                }
            }

        } else if (ai != null && mLicenseRadioAccept.getSelection()) {
            // Accept only this one
            mAcceptSameAllLicense = false;
            ai.setAccepted(true);
            ai.setRejected(false);

        } else if (ai != null && mLicenseRadioReject.getSelection()) {
            // Reject only this one
            mAcceptSameAllLicense = false;
            ai.setAccepted(false);
            ai.setRejected(true);

        } else {
            needUpdate = false;
        }

        mInternalLicenseRadioUpdate = false;

        if (needUpdate) {
            if (mAcceptSameAllLicense) {
                mTreeViewPackage.refresh();
            } else {
               mTreeViewPackage.refresh(ai);
               mTreeViewPackage.refresh(
                       ((NewArchivesContentProvider) mTreeViewPackage.getContentProvider()).
                       getParent(ai));
            }
            displayMissingDependency(ai);
            updateLicenceRadios(ai);
        }
    }

    /**
     * Callback invoked when a package item is double-clicked in the list.
     */
    private void onPackageDoubleClick() {
        Object item = getSelectedItem();

        if (item instanceof ArchiveInfo) {
            ArchiveInfo ai = (ArchiveInfo) item;
            boolean wasAccepted = ai.isAccepted();
            ai.setAccepted(!wasAccepted);
            ai.setRejected(wasAccepted);

            // update state
            mAcceptSameAllLicense = false;
            mTreeViewPackage.refresh(ai);
            // refresh parent since its icon might have changed.
            mTreeViewPackage.refresh(
                    ((NewArchivesContentProvider) mTreeViewPackage.getContentProvider()).
                    getParent(ai));

            displayMissingDependency(ai);
            updateLicenceRadios(ai);

        } else if (item instanceof LicenseEntry) {
            mTreeViewPackage.setExpandedState(item, !mTreeViewPackage.getExpandedState(item));
        }
    }

    /**
     * Provides the labels for the tree view.
     * Root branches are {@link LicenseEntry} elements.
     * Leave nodes are {@link ArchiveInfo} which all have the same license.
     */
    private class NewArchivesLabelProvider extends LabelProvider {
        @Override
        public Image getImage(Object element) {
            if (element instanceof ArchiveInfo) {
                // Archive icon: accepted (green), rejected (red), not set yet (question mark)
                ArchiveInfo ai = (ArchiveInfo) element;

                ImageFactory imgFactory = mSwtUpdaterData.getImageFactory();
                if (imgFactory != null) {
                    if (ai.isAccepted()) {
                        return imgFactory.getImageByName("accept_icon16.png");
                    } else if (ai.isRejected()) {
                        return imgFactory.getImageByName("reject_icon16.png");
                    }
                    return imgFactory.getImageByName("unknown_icon16.png");
                }
                return super.getImage(element);

            } else if (element instanceof LicenseEntry) {
                // License icon: green if all below are accepted, red if all rejected, otherwise
                // no icon.
                ImageFactory imgFactory = mSwtUpdaterData.getImageFactory();
                if (imgFactory != null) {
                    boolean allAccepted = true;
                    boolean allRejected = true;
                    for (ArchiveInfo ai : ((LicenseEntry) element).getArchives()) {
                        allAccepted = allAccepted && ai.isAccepted();
                        allRejected = allRejected && ai.isRejected();
                    }
                    if (allAccepted && !allRejected) {
                        return imgFactory.getImageByName("accept_icon16.png");
                    } else if (!allAccepted && allRejected) {
                        return imgFactory.getImageByName("reject_icon16.png");
                    }
                }
            }

            return null;
        }

        @Override
        public String getText(Object element) {
            if (element instanceof LicenseEntry) {
                return ((LicenseEntry) element).getLicenseRef();

            } else if (element instanceof ArchiveInfo) {
                ArchiveInfo ai = (ArchiveInfo) element;

                String desc = ai.getShortDescription();

                if (ai.isDependencyFor()) {
                    desc += " [*]";
                }

                return desc;

            }

            assert element instanceof String || element instanceof ArchiveInfo;
            return null;
        }
    }

    /**
     * Provides the content for the tree view.
     * Root branches are {@link LicenseEntry} elements.
     * Leave nodes are {@link ArchiveInfo} which all have the same license.
     */
    private class NewArchivesContentProvider implements ITreeContentProvider {
        private List<LicenseEntry> mInput;

        @Override
        public void dispose() {
            // pass
        }

        @SuppressWarnings("unchecked")
        @Override
        public void inputChanged(Viewer viewer, Object oldInput, Object newInput) {
            // Input should be the result from createTreeInput.
            if (newInput instanceof List<?> &&
                    ((List<?>) newInput).size() > 0 &&
                    ((List<?>) newInput).get(0) instanceof LicenseEntry) {
                mInput = (List<LicenseEntry>) newInput;
            } else {
                mInput = null;
            }
        }

        @Override
        public boolean hasChildren(Object parent) {
            if (parent instanceof List<?>) {
                // This is the root of the tree.
                return true;

            } else if (parent instanceof LicenseEntry) {
                return ((LicenseEntry) parent).getArchives().size() > 0;
            }

            return false;
        }

        @Override
        public Object[] getElements(Object parent) {
            return getChildren(parent);
        }

        @Override
        public Object[] getChildren(Object parent) {
            if (parent instanceof List<?>) {
                return ((List<?>) parent).toArray();

            } else if (parent instanceof LicenseEntry) {
                return ((LicenseEntry) parent).getArchives().toArray();
            }

            return new Object[0];
        }

        @Override
        public Object getParent(Object child) {
            if (child instanceof LicenseEntry) {
                return ((LicenseEntry) child).getRoot();

            } else if (child instanceof ArchiveInfo && mInput != null) {
                for (LicenseEntry entry : mInput) {
                    if (entry.getArchives().contains(child)) {
                        return entry;
                    }
                }
            }

            return null;
        }
    }

    /**
     * Represents a branch in the view tree: an entry where all the sub-archive info
     * share the same license. Contains a link to the share root list for convenience.
     */
    private static class LicenseEntry {
        private final List<LicenseEntry> mRoot;
        private final String mLicenseRef;
        private final List<ArchiveInfo> mArchives;

        public LicenseEntry(
                @NonNull List<LicenseEntry> root,
                @NonNull String licenseRef,
                @NonNull List<ArchiveInfo> archives) {
            mRoot = root;
            mLicenseRef = licenseRef;
            mArchives = archives;
        }

        @NonNull
        public List<LicenseEntry> getRoot() {
            return mRoot;
        }

        @NonNull
        public String getLicenseRef() {
            return mLicenseRef;
        }

        @NonNull
        public List<ArchiveInfo> getArchives() {
            return mArchives;
        }
    }

    /**
     * Creates the tree structure based on the given archives.
     * The current structure is to have a branch per license type,
     * with all the archives sharing the same license under it.
     * Elements with no license are left at the root.
     *
     * @param archives The non-null collection of archive info to display. Ideally non-empty.
     * @return A list of {@link LicenseEntry}, each containing a list of {@link ArchiveInfo}.
     */
    @NonNull
    private List<LicenseEntry> createTreeInput(@NonNull Collection<ArchiveInfo> archives) {
        // Build an ordered map with all the licenses, ordered by license ref name.
        final String noLicense = "No license";      //NLS

        Comparator<String> comp = new Comparator<String>() {
            @Override
            public int compare(String s1, String s2) {
                boolean first1 = noLicense.equals(s1);
                boolean first2 = noLicense.equals(s2);
                if (first1 && first2) {
                    return 0;
                } else if (first1) {
                    return -1;
                } else if (first2) {
                    return 1;
                }
                return s1.compareTo(s2);
            }
        };

        Map<String, List<ArchiveInfo>> map = new TreeMap<String, List<ArchiveInfo>>(comp);

        for (ArchiveInfo info : archives) {
            String ref = noLicense;
            License license = getLicense(info);
            if (license != null && license.getLicenseRef() != null) {
                ref = prettyLicenseRef(license.getLicenseRef());
            }

            List<ArchiveInfo> list = map.get(ref);
            if (list == null) {
                map.put(ref, list = new ArrayList<ArchiveInfo>());
            }
            list.add(info);
        }

        // Transform result into a list
        List<LicenseEntry> licensesList = new ArrayList<LicenseEntry>();
        for (Map.Entry<String, List<ArchiveInfo>> entry : map.entrySet()) {
            licensesList.add(new LicenseEntry(licensesList, entry.getKey(), entry.getValue()));
        }

        return licensesList;
    }

    /**
     * Helper method to retrieve the {@link License} for a given {@link ArchiveInfo}.
     *
     * @param ai The archive info. Can be null.
     * @return The license for the package owning the archive. Can be null.
     */
    @Nullable
    private License getLicense(@Nullable ArchiveInfo ai) {
        if (ai != null) {
            Archive aNew = ai.getNewArchive();
            if (aNew != null) {
                Package pNew = aNew.getParentPackage();
                if (pNew != null) {
                    return pNew.getLicense();
                }
            }
        }
        return null;
    }

    /**
     * Reformats the licenseRef to be more human-readable.
     * It's an XML ref and in practice it looks like [oem-]android-[type]-license.
     * If it's not a format we can deal with, leave it alone.
     */
    private String prettyLicenseRef(String ref) {
        // capitalize every word
        StringBuilder sb = new StringBuilder();
        boolean capitalize = true;
        for (char c : ref.toCharArray()) {
            if (c >= 'a' && c <= 'z') {
                if (capitalize) {
                    c = (char) (c + 'A' - 'a');
                    capitalize = false;
                }
            } else {
                if (c == '-') {
                    c = ' ';
                }
                capitalize = true;
            }
            sb.append(c);
        }

        ref = sb.toString();

        // A few acronyms should stay upper-case
        for (String w : new String[] { "Sdk", "Mips", "Arm" }) {
            ref = ref.replaceAll(w, w.toUpperCase(Locale.US));
        }

        return ref;
    }

    // End of hiding from SWT Designer
    //$hide<<$
}
