/*
 * 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.sdklib.internal.repository.packages;

import com.android.SdkConstants;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.annotations.VisibleForTesting;
import com.android.annotations.VisibleForTesting.Visibility;
import com.android.sdklib.AndroidVersion;
import com.android.sdklib.SdkManager;
import com.android.sdklib.repository.IDescription;
import com.android.sdklib.repository.IListDescription;
import com.android.sdklib.internal.repository.ITaskMonitor;
import com.android.sdklib.internal.repository.archives.Archive;
import com.android.sdklib.internal.repository.sources.SdkAddonSource;
import com.android.sdklib.internal.repository.sources.SdkRepoSource;
import com.android.sdklib.internal.repository.sources.SdkSource;
import com.android.sdklib.io.IFileOp;
import com.android.sdklib.repository.*;
import com.android.sdklib.repository.descriptors.IPkgDesc;

import com.android.sdklib.repository.descriptors.PkgDesc;
import org.apache.commons.compress.archivers.zip.ZipArchiveEntry;
import org.w3c.dom.Node;

import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Map;
import java.util.Properties;

/**
 * A {@link Package} is the base class for "something" that can be downloaded from
 * the SDK repository.
 * <p/>
 * A package has some attributes (revision, description) and a list of archives
 * which represent the downloadable bits.
 * <p/>
 * Packages are contained by a {@link SdkSource} (a download site).
 * <p/>
 * Derived classes must implement the {@link IDescription} methods.
 *
 * @deprecated
 * com.android.sdklib.internal.repository has moved into Studio as
 * com.android.tools.idea.sdk.remote.internal.
 */
@Deprecated
public abstract class Package implements IDescription, IListDescription, Comparable<Package> {

    private final String mObsolete;
    private final License mLicense;
    private final String mListDisplay;
    private final String mDescription;
    private final String mDescUrl;
    @Deprecated
    private final String mReleaseNote;
    @Deprecated
    private final String mReleaseUrl;
    private final Archive[] mArchives;
    private final SdkSource mSource;


    // figure if we'll need to set the unix permissions
    private static final boolean sUsingUnixPerm =
                SdkConstants.CURRENT_PLATFORM == SdkConstants.PLATFORM_DARWIN ||
                SdkConstants.CURRENT_PLATFORM == SdkConstants.PLATFORM_LINUX;

    /**
     * Enum for the result of {@link Package#canBeUpdatedBy(Package)}. This used so that we can
     * differentiate between a package that is totally incompatible, and one that is the same item
     * but just not an update.
     * @see #canBeUpdatedBy(Package)
     */
    public enum UpdateInfo {
        /** Means that the 2 packages are not the same thing */
        INCOMPATIBLE,
        /** Means that the 2 packages are the same thing but one does not upgrade the other.
         *  </p>
         *  TODO: this name is confusing. We need to dig deeper. */
        NOT_UPDATE,
        /** Means that the 2 packages are the same thing, and one is the upgrade of the other */
        UPDATE
    }

    /**
     * Creates a new package from the attributes and elements of the given XML node.
     * This constructor should throw an exception if the package cannot be created.
     *
     * @param source The {@link SdkSource} where this is loaded from.
     * @param packageNode The XML element being parsed.
     * @param nsUri The namespace URI of the originating XML document, to be able to deal with
     *          parameters that vary according to the originating XML schema.
     * @param licenses The licenses loaded from the XML originating document.
     */
    Package(SdkSource source, Node packageNode, String nsUri, Map<String,String> licenses) {
        mSource = source;
        mListDisplay =
                PackageParserUtils.getXmlString(packageNode, SdkRepoConstants.NODE_LIST_DISPLAY);
        mDescription =
            PackageParserUtils.getXmlString(packageNode, SdkRepoConstants.NODE_DESCRIPTION);
        mDescUrl     =
            PackageParserUtils.getXmlString(packageNode, SdkRepoConstants.NODE_DESC_URL);
        mReleaseNote =
            PackageParserUtils.getXmlString(packageNode, SdkRepoConstants.NODE_RELEASE_NOTE);
        mReleaseUrl  =
            PackageParserUtils.getXmlString(packageNode, SdkRepoConstants.NODE_RELEASE_URL);
        mObsolete    =
            PackageParserUtils.getOptionalXmlString(packageNode, SdkRepoConstants.NODE_OBSOLETE);

        mLicense = parseLicense(packageNode, licenses);
        mArchives   = parseArchives(
                PackageParserUtils.findChildElement(packageNode, SdkRepoConstants.NODE_ARCHIVES));
    }

    /**
     * Manually create a new package with one archive and the given attributes.
     * This is used to create packages from local directories in which case there must be
     * one archive which URL is the actual target location.
     * <p/>
     * Properties from props are used first when possible, e.g. if props is non null.
     * <p/>
     * By design, this creates a package with one and only one archive.
     */
    public Package(
            SdkSource source,
            Properties props,
            int revision,
            String license,
            String description,
            String descUrl,
            String archiveOsPath) {

        if (description == null) {
            description = "";
        }
        if (descUrl == null) {
            descUrl = "";
        }

        license = getProperty(props, PkgProps.PKG_LICENSE, license);
        if (license != null) {
            mLicense = new License(license, getProperty(props, PkgProps.PKG_LICENSE_REF, null));
        }
        else {
            mLicense = null;
        }
        mListDisplay = getProperty(props, PkgProps.PKG_LIST_DISPLAY, "");       //$NON-NLS-1$
        mDescription = getProperty(props, PkgProps.PKG_DESC,         description);
        mDescUrl     = getProperty(props, PkgProps.PKG_DESC_URL,     descUrl);
        mReleaseNote = getProperty(props, PkgProps.PKG_RELEASE_NOTE, "");       //$NON-NLS-1$
        mReleaseUrl  = getProperty(props, PkgProps.PKG_RELEASE_URL,  "");       //$NON-NLS-1$
        mObsolete    = getProperty(props, PkgProps.PKG_OBSOLETE, null);

        // If source is null and we can find a source URL in the properties, generate
        // a dummy source just to store the URL. This allows us to easily remember where
        // a package comes from.
        String srcUrl = getProperty(props, PkgProps.PKG_SOURCE_URL, null);
        if (props != null && source == null && srcUrl != null) {
            // Both Addon and Extra packages can come from an addon source.
            // For Extras, we can tell by looking at the source URL.
            if (this instanceof AddonPackage ||
                    ((this instanceof ExtraPackage) &&
                     srcUrl.endsWith(SdkAddonConstants.URL_DEFAULT_FILENAME))) {
                source = new SdkAddonSource(srcUrl, null /*uiName*/);
            } else {
                source = new SdkRepoSource(srcUrl, null /*uiName*/);
            }
        }
        mSource = source;

        // Note: if archiveOsPath is non-null, this makes a local archive (e.g. a locally
        // installed package.) If it's null, this makes a remote archive.
        mArchives = initializeArchives(props, archiveOsPath);
    }

    /**
     * Returns the {@link IPkgDesc} describing this package's meta data.
     *
     * @return A non-null {@link IPkgDesc}.
     */
    @NonNull
    public abstract IPkgDesc getPkgDesc();

    /**
     * Called by the constructor to get the initial {@link #mArchives} array.
     * <p/>
     * This is invoked by the local-package constructor and allows mock testing
     * classes to override the archives created.
     * This is an <em>implementation</em> details and clients must <em>not</em>
     * rely on this.
     *
     * @return Always return a non-null array. The array may be empty.
     */
    @VisibleForTesting(visibility=Visibility.PRIVATE)
    protected Archive[] initializeArchives(
            Properties props,
            String archiveOsPath) {
        return new Archive[] {
                new Archive(this,
                    props,
                    archiveOsPath) };
    }

    /**
     * Utility method that returns a property from a {@link Properties} object.
     * Returns the default value if props is null or if the property is not defined.
     *
     * @param props The {@link Properties} to search into.
     *   If null, the default value is returned.
     * @param propKey The name of the property. Must not be null.
     * @param defaultValue The default value to return if {@code props} is null or if the
     *   key is not found. Can be null.
     * @return The string value of the given key in the properties, or null if the key
     *   isn't found or if {@code props} is null.
     */
    @Nullable
    static String getProperty(
            @Nullable Properties props,
            @NonNull String propKey,
            @Nullable String defaultValue) {
        return PackageParserUtils.getProperty(props, propKey, defaultValue);
    }

    /**
     * Utility method that returns an integer property from a {@link Properties} object.
     * Returns the default value if props is null or if the property is not defined or
     * cannot be parsed to an integer.
     *
     * @param props The {@link Properties} to search into.
     *   If null, the default value is returned.
     * @param propKey The name of the property. Must not be null.
     * @param defaultValue The default value to return if {@code props} is null or if the
     *   key is not found. Can be null.
     * @return The integer value of the given key in the properties, or the {@code defaultValue}.
     */
    static int getPropertyInt(
            @Nullable Properties props,
            @NonNull String propKey,
            int defaultValue) {
        return PackageParserUtils.getPropertyInt(props, propKey, defaultValue);
    }

    /**
     * Save the properties of the current packages in the given {@link Properties} object.
     * These properties will later be give the constructor that takes a {@link Properties} object.
     */
    public void saveProperties(@NonNull Properties props) {
        if (mLicense != null) {
            String license = mLicense.getLicense();
            if (license != null && !license.isEmpty()) {
                props.setProperty(PkgProps.PKG_LICENSE, license);
            }
            String licenseRef = mLicense.getLicenseRef();
            if (licenseRef != null && !licenseRef.isEmpty()) {
                props.setProperty(PkgProps.PKG_LICENSE_REF, licenseRef);
            }
        }
        if (mListDisplay != null && !mListDisplay.isEmpty()) {
            props.setProperty(PkgProps.PKG_LIST_DISPLAY, mListDisplay);
        }
        if (mDescription != null && !mDescription.isEmpty()) {
            props.setProperty(PkgProps.PKG_DESC, mDescription);
        }
        if (mDescUrl != null && !mDescUrl.isEmpty()) {
            props.setProperty(PkgProps.PKG_DESC_URL, mDescUrl);
        }

        if (mReleaseNote != null && !mReleaseNote.isEmpty()) {
            props.setProperty(PkgProps.PKG_RELEASE_NOTE, mReleaseNote);
        }
        if (mReleaseUrl != null && !mReleaseUrl.isEmpty()) {
            props.setProperty(PkgProps.PKG_RELEASE_URL, mReleaseUrl);
        }
        if (mObsolete != null) {
            props.setProperty(PkgProps.PKG_OBSOLETE, mObsolete);
        }
        if (mSource != null) {
            props.setProperty(PkgProps.PKG_SOURCE_URL,  mSource.getUrl());
        }
    }

    /**
     * Parses the uses-licence node of this package, if any, and returns the license
     * definition if there's one. Returns null if there's no uses-license element or no
     * license of this name defined.
     */
    @Nullable
    private License parseLicense(@NonNull Node packageNode, @NonNull Map<String, String> licenses) {
        Node usesLicense =
            PackageParserUtils.findChildElement(packageNode, SdkRepoConstants.NODE_USES_LICENSE);
        if (usesLicense != null) {
            Node ref = usesLicense.getAttributes().getNamedItem(SdkRepoConstants.ATTR_REF);
            if (ref != null) {
                String licenseRef = ref.getNodeValue();
                return new License(licenses.get(licenseRef), licenseRef);
            }
        }
        return null;
    }

    /**
     * Parses an XML node to process the <archives> element.
     * Always return a non-null array. The array may be empty.
     */
    @NonNull
    private Archive[] parseArchives(@NonNull Node archivesNode) {
        ArrayList<Archive> archives = new ArrayList<Archive>();

        if (archivesNode != null) {
            String nsUri = archivesNode.getNamespaceURI();
            for(Node child = archivesNode.getFirstChild();
                     child != null;
                     child = child.getNextSibling()) {

                if (child.getNodeType() == Node.ELEMENT_NODE &&
                        nsUri.equals(child.getNamespaceURI()) &&
                        SdkRepoConstants.NODE_ARCHIVE.equals(child.getLocalName())) {
                    archives.add(parseArchive(child));
                }
            }
        }

        return archives.toArray(new Archive[archives.size()]);
    }

    /**
     * Parses one <archive> element from an <archives> container.
     */
    @NonNull
    private Archive parseArchive(@NonNull Node archiveNode) {
        Archive a = new Archive(
                    this,
                    PackageParserUtils.parseArchFilter(archiveNode),
                    PackageParserUtils.getXmlString(archiveNode, SdkRepoConstants.NODE_URL),
                    PackageParserUtils.getXmlLong  (archiveNode, SdkRepoConstants.NODE_SIZE, 0),
                    PackageParserUtils.getXmlString(archiveNode, SdkRepoConstants.NODE_CHECKSUM)
                );

        return a;
    }

    /**
     * Returns the source that created (and owns) this package. Can be null.
     */
    @Nullable
    public SdkSource getParentSource() {
        return mSource;
    }

    /**
     * Returns true if the package is deemed obsolete, that is it contains an
     * actual <code>&lt;obsolete&gt;</code> element.
     */
    public boolean isObsolete() {
        return mObsolete != null;
    }

    /**
     * Returns the revision, an int > 0, for all packages (platform, add-on, tool, doc).
     * Can be 0 if this is a local package of unknown revision.
     */
    @NonNull
    public abstract FullRevision getRevision();

    /**
     * Returns the optional description for all packages (platform, add-on, tool, doc) or
     * for a lib. It is null if the element has not been specified in the repository XML.
     */
    @Nullable
    public License getLicense() {
        return mLicense;
    }

    /**
     * Returns the optional description for all packages (platform, add-on, tool, doc) or
     * for a lib. This is the raw description available from the XML meta data and is typically
     * only used internally.
     * <p/>
     * For actual display in the UI, use the methods from {@link IDescription} instead.
     * <p/>
     * Can be empty but not null.
     */
    @NonNull
    public String getDescription() {
        return mDescription;
    }

    /**
     * Returns the optional list-display for all packages as defined in the XML meta data
     * and is typically only used internally.
     * <p/>
     * For actual display in the UI, use {@link IListDescription} instead.
     * <p/>
     * Can be empty but not null.
     */
    @NonNull
    public String getListDisplay() {
        return mListDisplay;
    }

    /**
     * Returns the optional description URL for all packages (platform, add-on, tool, doc).
     * Can be empty but not null.
     */
    @NonNull
    public String getDescUrl() {
        return mDescUrl;
    }

    /**
     * Returns the optional release note for all packages (platform, add-on, tool, doc) or
     * for a lib. Can be empty but not null.
     */
    @NonNull
    public String getReleaseNote() {
        return mReleaseNote;
    }

    /**
     * Returns the optional release note URL for all packages (platform, add-on, tool, doc).
     * Can be empty but not null.
     */
    @NonNull
    public String getReleaseNoteUrl() {
        return mReleaseUrl;
    }

    /**
     * Returns the archives defined in this package.
     * Can be an empty array but not null.
     */
    @NonNull
    public Archive[] getArchives() {
        return mArchives;
    }

    /**
     * Returns true if this package contains the exact given archive.
     * Important: This compares object references, not object equality.
     */
    public boolean hasArchive(Archive archive) {
        for (Archive a : mArchives) {
            if (a == archive) {
                return true;
            }
        }
        return false;
    }

    /**
     * Returns whether the {@link Package} has at least one {@link Archive} compatible with
     * the host platform.
     */
    public boolean hasCompatibleArchive() {
        for (Archive archive : mArchives) {
            if (archive.isCompatible()) {
                return true;
            }
        }

        return false;
    }

    /**
     * Returns a short, reasonably unique string identifier that can be used
     * to identify this package when installing from the command-line interface.
     * {@code 'android list sdk'} will show these IDs and then in turn they can
     * be provided to {@code 'android update sdk --no-ui --filter'} to select
     * some specific packages.
     * <p/>
     * The identifiers must have the following properties: <br/>
     * - They must contain only simple alphanumeric characters. <br/>
     * - Commas, whitespace and any special character that could be obviously problematic
     *   to a shell interface should be avoided (so dash/underscore are OK, but things
     *   like colon, pipe or dollar should be avoided.) <br/>
     * - The name must be consistent across calls and reasonably unique for the package
     *   type. Collisions can occur but should be rare. <br/>
     * - Different package types should have a clearly different name pattern. <br/>
     * - The revision number should not be included, as this would prevent updates
     *   from being automated (which is the whole point.) <br/>
     * - It must remain reasonably human readable. <br/>
     * - If no such id can exist (for example for a local package that cannot be installed)
     *   then an empty string should be returned. Don't return null.
     * <p/>
     * Important: This is <em>not</em> a strong unique identifier for the package.
     * If you need a strong unique identifier, you should use {@link #comparisonKey()}
     * and the {@link Comparable} interface.
     */
    @NonNull
    public abstract String installId();

    /**
     * Returns the short description of the source, if not null.
     * Otherwise returns the default Object toString result.
     * <p/>
     * This is mostly helpful for debugging.
     * For UI display, use the {@link IDescription} interface.
     */
    @NonNull
    @Override
    public String toString() {
        String s = getShortDescription();
        if (s != null) {
            return s;
        }
        return super.toString();
    }

    /**
     * Returns a description of this package that is suitable for a list display.
     * Should not be empty. Can never be null.
     * <p/>
     * Derived classes should use {@link #getListDisplay()} if it's not empty.
     * <p/>
     * When it is empty, the default behavior is to recompute a string that depends
     * on the package type.
     * <p/>
     * In both cases, the string should indicate whether the package is marked as obsolete.
     * <p/>
     * Note that this is the "base" name for the package with no specific revision nor API
     * mentioned as this is likely used in a table that will already provide these details.
     * In contrast, {@link #getShortDescription()} should be used if you want more details
     * such as the package revision number or the API, if applicable, all in the same string.
     */
    @NonNull
    @Override
    public abstract String getListDescription();

    /**
     * Returns a short description for an {@link IDescription}.
     * Can be empty but not null.
     */
    @NonNull
    @Override
    public abstract String getShortDescription();

    /**
     * Returns a long description for an {@link IDescription}.
     * Can be empty but not null.
     */
    @NonNull
    @Override
    public String getLongDescription() {
        StringBuilder sb = new StringBuilder();

        String s = getDescription();
        if (s != null) {
            sb.append(s);
        }
        if (sb.length() > 0) {
            sb.append("\n");
        }

        sb.append(String.format("Revision %1$s%2$s",
                getRevision().toShortString(),
                isObsolete() ? " (Obsolete)" : ""));

        s = getDescUrl();
        if (s != null && !s.isEmpty()) {
            sb.append(String.format("\n\nMore information at %1$s", s));
        }

        s = getReleaseNote();
        if (s != null && !s.isEmpty()) {
            sb.append("\n\nRelease note:\n").append(s);
        }

        s = getReleaseNoteUrl();
        if (s != null && !s.isEmpty()) {
            sb.append("\nRelease note URL: ").append(s);
        }

        return sb.toString();
    }

    /**
     * A package is local (that is 'installed locally') if it contains a single
     * archive that is local. If not local, it's a remote package, only available
     * on a remote source for download and installation.
     */
    public boolean isLocal() {
        return mArchives.length == 1 && mArchives[0].isLocal();
    }

    /**
     * Computes a potential installation folder if an archive of this package were
     * to be installed right away in the given SDK root.
     * <p/>
     * Some types of packages install in a fix location, for example docs and tools.
     * In this case the returned folder may already exist with a different archive installed
     * at the desired location. <br/>
     * For other packages types, such as add-on or platform, the folder name is only partially
     * relevant to determine the content and thus a real check will be done to provide an
     * existing or new folder depending on the current content of the SDK.
     * <p/>
     * Note that the installer *will* create all directories returned here just before
     * installation so this method must not attempt to create them.
     *
     * @param osSdkRoot The OS path of the SDK root folder.
     * @param sdkManager An existing SDK manager to list current platforms and addons.
     * @return A new {@link File} corresponding to the directory to use to install this package.
     */
    @NonNull
    public abstract File getInstallFolder(String osSdkRoot, SdkManager sdkManager);

    /**
     * Hook called right before an archive is installed. The archive has already
     * been downloaded successfully and will be installed in the directory specified by
     * <var>installFolder</var> when this call returns.
     * <p/>
     * The hook lets the package decide if installation of this specific archive should
     * be continue. The installer will still install the remaining packages if possible.
     * <p/>
     * The base implementation always return true.
     * <p/>
     * Note that the installer *will* create all directories specified by
     * {@link #getInstallFolder} just before installation, so they must not be
     * created here. This is also called before the previous install dir is removed
     * so the previous content is still there during upgrade.
     *
     * @param archive The archive that will be installed
     * @param monitor The {@link ITaskMonitor} to display errors.
     * @param osSdkRoot The OS path of the SDK root folder.
     * @param installFolder The folder where the archive will be installed. Note that this
     *                      is <em>not</em> the folder where the archive was temporary
     *                      unzipped. The installFolder, if it exists, contains the old
     *                      archive that will soon be replaced by the new one.
     * @return True if installing this archive shall continue, false if it should be skipped.
     */
    public boolean preInstallHook(Archive archive, ITaskMonitor monitor,
            String osSdkRoot, File installFolder) {
        // Nothing to do in base class.
        return true;
    }

    /**
     * Hook called right after a file has been unzipped (during an install).
     * <p/>
     * The base class implementation makes sure to properly adjust set executable
     * permission on Linux and MacOS system if the zip entry was marked as +x.
     *
     * @param archive The archive that is being installed.
     * @param monitor The {@link ITaskMonitor} to display errors.
     * @param fileOp The {@link IFileOp} used by the archive installer.
     * @param unzippedFile The file that has just been unzipped in the install temp directory.
     * @param zipEntry The {@link ZipArchiveEntry} that has just been unzipped.
     */
    public void postUnzipFileHook(
            Archive archive,
            ITaskMonitor monitor,
            IFileOp fileOp,
            File unzippedFile,
            ZipArchiveEntry zipEntry) {

        // if needed set the permissions.
        if (sUsingUnixPerm && fileOp.isFile(unzippedFile)) {
            // get the mode and test if it contains the executable bit
            int mode = zipEntry.getUnixMode();
            if ((mode & 0111) != 0) {
                try {
                    fileOp.setExecutablePermission(unzippedFile);
                } catch (IOException ignore) {}
            }
        }

    }

    /**
     * Hook called right after an archive has been installed.
     *
     * @param archive The archive that has been installed.
     * @param monitor The {@link ITaskMonitor} to display errors.
     * @param installFolder The folder where the archive was successfully installed.
     *                      Null if the installation failed, in case the archive needs to
     *                      do some cleanup after <code>preInstallHook</code>.
     */
    public void postInstallHook(Archive archive, ITaskMonitor monitor, File installFolder) {
        // Nothing to do in base class.
    }

    /**
     * Returns whether the give package represents the same item as the current package.
     * <p/>
     * Two packages are considered the same if they represent the same thing, except for the
     * revision number.
     * @param pkg the package to compare.
     * @return true if the item as equivalent.
     */
    public abstract boolean sameItemAs(Package pkg);

    /**
     * Computes whether the given package is a suitable update for the current package.
     * <p/>
     * An update is just that: a new package that supersedes the current one. If the new
     * package does not represent the same item or if it has the same or lower revision as the
     * current one, it's not an update.
     *
     * @param replacementPackage The potential replacement package.
     * @return One of the {@link UpdateInfo} values.
     *
     * @see #sameItemAs(Package)
     */
    @NonNull
    public abstract UpdateInfo canBeUpdatedBy(Package replacementPackage);

    /**
     * Returns an ordering <b>suitable for display</b> like this: <br/>
     * - Tools <br/>
     * - Platform-Tools <br/>
     * - Built-Tools <br/>
     * - Docs. <br/>
     * - Platform n preview <br/>
     * - Platform n <br/>
     * - Platform n-1 <br/>
     * - Samples packages <br/>
     * - Add-on based on n preview <br/>
     * - Add-on based on n <br/>
     * - Add-on based on n-1 <br/>
     * - Extra packages <br/>
     * <p/>
     * Important: this must NOT be used to compare if two packages are the same thing.
     * This is achieved by {@link #sameItemAs(Package)} or {@link #canBeUpdatedBy(Package)}.
     * <p/>
     * The order done here is suitable for display, and this may not be the appropriate
     * order when comparing whether packages are equal or of greater revision -- if you need
     * to compare revisions, then use {@link #getRevision()}{@code .compareTo(rev)} directly.
     * <p/>
     * This {@link #compareTo(Package)} method is purely an implementation detail to
     * perform the right ordering of the packages in the list of available or installed packages.
     * <p/>
     * <em>Important</em>: Derived classes should consider overriding {@link #comparisonKey()}
     * instead of this method.
     */
    @Override
    public int compareTo(Package other) {
        String s1 = this.comparisonKey();
        String s2 = other.comparisonKey();

        int r = s1.compareTo(s2);
        return r;
    }

    /**
     * Computes a comparison key for each package used by {@link #compareTo(Package)}.
     * The key is a string.
     * The base package class return a string that encodes the package type,
     * the revision number and the platform version, if applicable, in the form:
     * <pre>
     *      t:N|v:NNNN.P|r:NNNN|
     * </pre>
     * All fields must start by a "letter colon" prefix and end with a vertical pipe (|, ASCII 124).
     * <p/>
     * The string format <em>may</em> change between releases and clients should not
     * store them outside of the session or expect them to be consistent between
     * different releases. They are purely an internal implementation details of the
     * {@link #compareTo(Package)} method.
     * <p/>
     * Derived classes should get the string from the super class and then append
     * or <em>insert</em> their own |-separated content.
     * For example an extra vendor name & path can be inserted before the revision
     * number, since it has more sorting weight.
     */
    @NonNull
    protected String comparisonKey() {

        StringBuilder sb = new StringBuilder();

        sb.append("t:");                                                        //$NON-NLS-1$
        if (this instanceof ToolPackage) {
            sb.append(0);
        } else if (this instanceof PlatformToolPackage) {
            sb.append(1);
        } else if (this instanceof BuildToolPackage) {
            sb.append(2);
        } else if (this instanceof DocPackage) {
            sb.append(3);
        } else if (this instanceof PlatformPackage) {
            sb.append(4);
        } else if (this instanceof SamplePackage) {
            sb.append(5);
        } else if ((this instanceof SystemImagePackage) && ((SystemImagePackage) this).isPlatform()) {
            sb.append(6);
        } else if (this instanceof AddonPackage) {
            sb.append(7);
        } else if (this instanceof SystemImagePackage) {
            sb.append(8);
        } else {
            // extras and everything else
            sb.append(9);
        }


        // We insert the package version here because it is more important
        // than the revision number.
        // In the list display, we want package version to be sorted
        // top-down, so we'll use 10k-api as the sorting key. The day we
        // reach 10k APIs, we'll need to revisit this.
        sb.append("|v:");                                                       //$NON-NLS-1$
        if (this instanceof IAndroidVersionProvider) {
            AndroidVersion v = ((IAndroidVersionProvider) this).getAndroidVersion();

            sb.append(String.format("%1$04d.%2$d",                              //$NON-NLS-1$
                    10000 - v.getApiLevel(),
                    v.isPreview() ? 1 : 0
                    ));
        }

        // Append revision number
        // Note: pad revision numbers to 4 digits (e.g. make sure 12>3 by comparing 0012 and 0003)
        sb.append("|r:");                                                       //$NON-NLS-1$
        FullRevision rev = getRevision();
        sb.append(String.format("%1$04d.%2$04d.%3$04d.",                        //$NON-NLS-1$
                                rev.getMajor(),
                                rev.getMinor(),
                                rev.getMicro()));
        // Hack: When comparing packages for installation purposes, we want to treat
        // "final releases" packages as more important than rc/preview packages.
        // However like for the API level above, when sorting for list display purposes
        // we want the final release package listed before its rc/preview packages.
        if (rev.isPreview()) {
            sb.append(rev.getPreview());
        } else {
            sb.append('0'); // 0=Final (!preview), to make "18.0" < "18.1" (18 Final < 18 RC1)
        }

        sb.append('|');
        return sb.toString();
    }

    @Override
    public int hashCode() {
        final int prime = 31;
        int result = 1;
        result = prime * result + Arrays.hashCode(mArchives);
        result = prime * result + ((mObsolete == null) ? 0 : mObsolete.hashCode());
        result = prime * result + getRevision().hashCode();
        result = prime * result + ((mSource == null) ? 0 : mSource.hashCode());
        return result;
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj) {
            return true;
        }
        if (obj == null) {
            return false;
        }
        if (!(obj instanceof Package)) {
            return false;
        }
        Package other = (Package) obj;
        if (!Arrays.equals(mArchives, other.mArchives)) {
            return false;
        }
        if (mObsolete == null) {
            if (other.mObsolete != null) {
                return false;
            }
        } else if (!mObsolete.equals(other.mObsolete)) {
            return false;
        }
        if (!getRevision().equals(other.getRevision())) {
            return false;
        }
        if (mSource == null) {
            if (other.mSource != null) {
                return false;
            }
        } else if (!mSource.equals(other.mSource)) {
            return false;
        }
        return true;
    }

    // TODO(jbakermalone): This is moved here from the more logical location in PkgDesc.Builder since Package will soon be forked into
    //                     studio and this version deprecated, whereas PkgDesc will not.
    protected PkgDesc.Builder setDescriptions(PkgDesc.Builder builder) {
        builder.setDescriptionShort(getShortDescription());
        builder.setDescriptionUrl(getDescUrl());
        builder.setListDisplay(getListDisplay());
        builder.setIsObsolete(isObsolete());
        builder.setLicense(getLicense());
        return builder;
    }
}
