/*
 * Copyright (C) 2011 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.updater;

import com.android.annotations.NonNull;
import com.android.sdklib.internal.repository.AddonsListFetcher;
import com.android.sdklib.internal.repository.AddonsListFetcher.Site;
import com.android.sdklib.internal.repository.DownloadCache;
import com.android.sdklib.internal.repository.ITask;
import com.android.sdklib.internal.repository.ITaskMonitor;
import com.android.sdklib.internal.repository.NullTaskMonitor;
import com.android.sdklib.internal.repository.archives.Archive;
import com.android.sdklib.internal.repository.packages.Package;
import com.android.sdklib.internal.repository.packages.Package.UpdateInfo;
import com.android.sdklib.internal.repository.sources.SdkAddonSource;
import com.android.sdklib.internal.repository.sources.SdkSource;
import com.android.sdklib.internal.repository.sources.SdkSourceCategory;
import com.android.sdklib.internal.repository.sources.SdkSources;
import com.android.sdklib.internal.repository.sources.SdkSysImgSource;
import com.android.sdklib.repository.SdkAddonsListConstants;
import com.android.sdklib.repository.SdkRepoConstants;

import java.io.File;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * Loads packages fetched from the remote SDK Repository and keeps track
 * of their state compared with the current local SDK installation.
 *
 * @deprecated
 * com.android.sdklib.internal.repository has moved into Studio as
 * com.android.tools.idea.sdk.remote.internal.
 */
@Deprecated
public class PackageLoader {

    /** The update data context. Never null. */
    private final UpdaterData mUpdaterData;

    /**
     * The {@link DownloadCache} override. Can be null, in which case the one from
     * {@link UpdaterData} is used instead.
     * @see #getDownloadCache()
     */
    private final DownloadCache mOverrideCache;

    /**
     * 0 = need to fetch remote addons list once..
     * 1 = fetch succeeded, don't need to do it any more.
     * -1= fetch failed, do it again only if the user requests a refresh
     *     or changes the force-http setting.
     */
    private int mStateFetchRemoteAddonsList;


    /**
     * Interface for the callback called by
     * {@link PackageLoader#loadPackages(boolean, ISourceLoadedCallback)}.
     * <p/>
     * After processing each source, the package loader calls {@link #onUpdateSource}
     * with the list of packages found in that source.
     * By returning true from {@link #onUpdateSource}, the client tells the loader to
     * continue and process the next source. By returning false, it tells to stop loading.
     * <p/>
     * The {@link #onLoadCompleted()} method is guaranteed to be called at the end, no
     * matter how the loader stopped, so that the client can clean up or perform any
     * final action.
     */
    public interface ISourceLoadedCallback {
        /**
         * After processing each source, the package loader calls this method with the
         * list of packages found in that source.
         * By returning true from {@link #onUpdateSource}, the client tells
         * the loader to continue and process the next source.
         * By returning false, it tells to stop loading.
         * <p/>
         * <em>Important</em>: This method is called from a sub-thread, so clients which
         * try to access any UI widgets must wrap their calls into
         * {@code Display.syncExec(Runnable)} or {@code Display.asyncExec(Runnable)}.
         *
         * @param packages All the packages loaded from the source. Never null.
         * @return True if the load operation should continue, false if it should stop.
         */
        boolean onUpdateSource(SdkSource source, Package[] packages);

        /**
         * This method is guaranteed to be called at the end, no matter how the
         * loader stopped, so that the client can clean up or perform any final action.
         */
        void onLoadCompleted();
    }

    /**
     * Interface describing the task of installing a specific package.
     * For details on the operation,
     * see {@link PackageLoader#loadPackagesWithInstallTask(int, IAutoInstallTask)}.
     *
     * @see PackageLoader#loadPackagesWithInstallTask(int, IAutoInstallTask)
     */
    public interface IAutoInstallTask {
        /**
         * Invoked by the loader once a source has been loaded and its package
         * definitions are known. The method should return the {@code packages}
         * array and can modify it if necessary.
         * The loader will call {@link #acceptPackage(Package)} on all the packages returned.
         *
         * @param source The source of the packages. Null for the locally installed packages.
         * @param packages The packages found in the source.
         */
        Package[] filterLoadedSource(SdkSource source, Package[] packages);

        /**
         * Called by the install task for every package available (new ones, updates as well
         * as existing ones that don't have a potential update.)
         * The method should return true if this is a package that should be installed.
         * <p/>
         * <em>Important</em>: This method is called from a sub-thread, so clients who try
         * to access any UI widgets must wrap their calls into {@code Display.syncExec(Runnable)}
         * or {@code Display.asyncExec(Runnable)}.
         */
        boolean acceptPackage(Package pkg);

        /**
         * Called when the accepted package has been installed, successfully or not.
         * If an already installed (aka existing) package has been accepted, this will
         * be called with a 'true' success and the actual install paths.
         * <p/>
         * <em>Important</em>: This method is called from a sub-thread, so clients who try
         * to access any UI widgets must wrap their calls into {@code Display.syncExec(Runnable)}
         * or {@code Display.asyncExec(Runnable)}.
         */
        void setResult(boolean success, Map<Package, File> installPaths);

        /**
         * Called when the task is done iterating and completed.
         */
        void taskCompleted();
    }

    /**
     * Creates a new PackageManager associated with the given {@link UpdaterData}
     * and using the {@link UpdaterData}'s default {@link DownloadCache}.
     *
     * @param updaterData The {@link UpdaterData}. Must not be null.
     */
    public PackageLoader(UpdaterData updaterData) {
        mUpdaterData = updaterData;
        mOverrideCache = null;
    }

    /**
     * Creates a new PackageManager associated with the given {@link UpdaterData}
     * but using the specified {@link DownloadCache} instead of the one from
     * {@link UpdaterData}.
     *
     * @param updaterData The {@link UpdaterData}. Must not be null.
     * @param cache The {@link DownloadCache} to use instead of the one from {@link UpdaterData}.
     */
    public PackageLoader(UpdaterData updaterData, DownloadCache cache) {
        mUpdaterData = updaterData;
        mOverrideCache = cache;
    }

    public UpdaterData getUpdaterData() {
        return mUpdaterData;
    }

    /**
     * Runs a runnable on the UI thread.
     * The base implementation just runs the runnable right away.
     *
     * @param r Non-null runnable.
     */
    protected void runOnUiThread(@NonNull Runnable r) {
        r.run();
    }

    /**
     * Loads all packages from the remote repository.
     * This runs in an {@link ITask}. The call is blocking.
     * <p/>
     * The callback is called with each set of {@link PkgItem} found in each source.
     * The caller is responsible to accumulate the packages given to the callback
     * after each source is finished loaded. In return the callback tells the loader
     * whether to continue loading sources.
     * <p/>
     * Normally this method doesn't access the remote source if it's already
     * been loaded in the in-memory source (e.g. don't fetch twice).
     *
     * @param overrideExisting Set this to true  when the caller wants to
     *          check for updates and discard any existing source already
     *          loaded in memory. It should be false for normal use.
     * @param sourceLoadedCallback The callback to invoke for each loaded source.
     */
    public void loadPackages(
            final boolean overrideExisting,
            final ISourceLoadedCallback sourceLoadedCallback) {
        try {
            if (mUpdaterData == null) {
                return;
            }

            mUpdaterData.getTaskFactory().start("Loading Sources", new ITask() {
                @Override
                public void run(ITaskMonitor monitor) {
                    monitor.setProgressMax(10);

                    // get local packages and offer them to the callback
                    Package[] localPkgs =
                        mUpdaterData.getInstalledPackages(monitor.createSubMonitor(1));
                    if (localPkgs == null) {
                        localPkgs = new Package[0];
                    }
                    if (!sourceLoadedCallback.onUpdateSource(null, localPkgs)) {
                        return;
                    }

                    // get remote packages
                    boolean forceHttp =
                        mUpdaterData.getSettingsController().getSettings().getForceHttp();
                    loadRemoteAddonsList(monitor.createSubMonitor(1));

                    SdkSource[] sources = mUpdaterData.getSources().getAllSources();
                    try {
                        if (sources != null && sources.length > 0) {
                            ITaskMonitor subMonitor = monitor.createSubMonitor(8);
                            subMonitor.setProgressMax(sources.length);
                            for (SdkSource source : sources) {
                                Package[] pkgs = source.getPackages();
                                if (pkgs == null || overrideExisting) {
                                    source.load(getDownloadCache(),
                                            subMonitor.createSubMonitor(1),
                                            forceHttp);
                                    pkgs = source.getPackages();
                                }
                                if (pkgs == null) {
                                    continue;
                                }

                                // Notify the callback a new source has finished loading.
                                // If the callback requests so, stop right away.
                                if (!sourceLoadedCallback.onUpdateSource(source, pkgs)) {
                                    return;
                                }
                            }
                        }
                    } catch(Exception e) {
                        monitor.logError("Loading source failed: %1$s", e.toString());
                    } finally {
                        monitor.setDescription("Done loading packages.");
                    }
                }
            });
        } finally {
            sourceLoadedCallback.onLoadCompleted();
        }
    }

    /**
     * Load packages, source by source using
     * {@link #loadPackages(boolean, ISourceLoadedCallback)},
     * and executes the given {@link IAutoInstallTask} on the current package list.
     * That is for each package known, the install task is queried to find if
     * the package is the one to be installed or updated.
     * <p/>
     * - If an already installed package is accepted by the task, it is returned. <br/>
     * - If a new package (remotely available but not installed locally) is accepted,
     * the user will be <em>prompted</em> for permission to install it. <br/>
     * - If an existing package has updates, the install task will be accept if it
     * accepts one of the updating packages, and if yes the the user will be
     * <em>prompted</em> for permission to install it. <br/>
     * <p/>
     * Only one package can be accepted, after which the task is completed.
     * There is no direct return value, {@link IAutoInstallTask#setResult} is called on the
     * result of the accepted package.
     * When the task is completed, {@link IAutoInstallTask#taskCompleted()} is called.
     * <p/>
     * The call is blocking. Although the name says "Task", this is not an {@link ITask}
     * running in its own thread but merely a synchronous call.
     *
     * @param installFlags Flags for installation such as
     *  {@link UpdaterData#TOOLS_MSG_UPDATED_FROM_ADT}.
     * @param installTask The task to perform.
     */
    public void loadPackagesWithInstallTask(
            final int installFlags,
            final IAutoInstallTask installTask) {

        loadPackages(false /*overrideExisting*/, new ISourceLoadedCallback() {
            List<Archive> mArchivesToInstall = new ArrayList<Archive>();
            Map<Package, File> mInstallPaths = new HashMap<Package, File>();

            @Override
            public boolean onUpdateSource(SdkSource source, Package[] packages) {
                packages = installTask.filterLoadedSource(source, packages);
                if (packages == null || packages.length == 0) {
                    // Tell loadPackages() to process the next source.
                    return true;
                }

                for (Package pkg : packages) {
                    if (pkg.isLocal()) {
                        // This is a local (aka installed) package
                        if (installTask.acceptPackage(pkg)) {
                            // If the caller is accepting an installed package,
                            // return a success and give the package's install path
                            Archive[] a = pkg.getArchives();
                            // an installed package should have one local compatible archive
                            if (a.length == 1 && a[0].isCompatible()) {
                                mInstallPaths.put(pkg, new File(a[0].getLocalOsPath()));
                            }
                        }

                    } else {
                        // This is a remote package
                        if (installTask.acceptPackage(pkg)) {
                            // The caller is accepting this remote package. We'll install it.
                            for (Archive archive : pkg.getArchives()) {
                                if (archive.isCompatible()) {
                                    mArchivesToInstall.add(archive);
                                    break;
                                }
                            }
                        }
                    }
                }

                // Tell loadPackages() to process the next source.
                return true;
            }

            @Override
            public void onLoadCompleted() {
                if (!mArchivesToInstall.isEmpty()) {
                    installArchives(mArchivesToInstall);
                }
                if (mInstallPaths == null) {
                    installTask.setResult(false, null);
                } else {
                    installTask.setResult(true, mInstallPaths);
                }

                installTask.taskCompleted();
            }

            /**
             * Shows the UI of the install selector.
             * If the package is then actually installed, refresh the local list and
             * notify the install task of the installation path.
             *
             * @param archivesToInstall The archives to install.
             */
            private void installArchives(final List<Archive> archivesToInstall) {
                // Actually install the new archives that we just found.
                // This will display some UI so we need a shell's sync exec.

                final List<Archive> installedArchives = new ArrayList<Archive>();

                runOnUiThread(new Runnable() {
                    @Override
                    public void run() {
                        List<Archive> archives =
                            mUpdaterData.updateOrInstallAll_WithGUI(
                                archivesToInstall,
                                true /* includeObsoletes */,
                                installFlags);

                        if (archives != null) {
                            installedArchives.addAll(archives);
                        }
                    }
                });

                if (installedArchives.isEmpty()) {
                    // We failed to install anything.
                    mInstallPaths = null;
                    return;
                }

                // The local package list has changed, make sure to refresh it
                mUpdaterData.getSdkManager().reloadSdk(mUpdaterData.getSdkLog());
                mUpdaterData.getLocalSdkParser().clearPackages();
                final Package[] localPkgs = mUpdaterData.getInstalledPackages(
                        new NullTaskMonitor(mUpdaterData.getSdkLog()));

                // Scan the installed package list to find the install paths.
                for (Archive installedArchive : installedArchives) {
                    Package pkg = installedArchive.getParentPackage();

                    for (Package localPkg : localPkgs) {
                        if (localPkg.canBeUpdatedBy(pkg) == UpdateInfo.NOT_UPDATE) {
                            Archive[] localArchive = localPkg.getArchives();
                            if (localArchive.length == 1 && localArchive[0].isCompatible()) {
                                mInstallPaths.put(
                                    localPkg,
                                    new File(localArchive[0].getLocalOsPath()));
                            }
                        }
                    }
                }
            }
        });
    }


    /**
     * Loads the remote add-ons list.
     */
    public void loadRemoteAddonsList(ITaskMonitor monitor) {

        if (mStateFetchRemoteAddonsList != 0) {
            return;
        }

        mUpdaterData.getTaskFactory().start("Load Add-ons List", monitor, new ITask() {
            @Override
            public void run(ITaskMonitor subMonitor) {
                loadRemoteAddonsListInTask(subMonitor);
            }
        });
    }

    private void loadRemoteAddonsListInTask(ITaskMonitor monitor) {
        mStateFetchRemoteAddonsList = -1;

        String url = SdkAddonsListConstants.URL_ADDON_LIST;

        // We override SdkRepoConstants.URL_GOOGLE_SDK_SITE if this is defined
        String baseUrl = System.getenv("SDK_TEST_BASE_URL");            //$NON-NLS-1$
        if (baseUrl != null) {
            if (!baseUrl.isEmpty() && baseUrl.endsWith("/")) {        //$NON-NLS-1$
                if (url.startsWith(SdkRepoConstants.URL_GOOGLE_SDK_SITE)) {
                    url = baseUrl + url.substring(SdkRepoConstants.URL_GOOGLE_SDK_SITE.length());
                }
            } else {
                monitor.logError("Ignoring invalid SDK_TEST_BASE_URL: %1$s", baseUrl);  //$NON-NLS-1$
            }
        }

        if (mUpdaterData.getSettingsController().getSettings().getForceHttp()) {
            url = url.replaceAll("https://", "http://");    //$NON-NLS-1$ //$NON-NLS-2$
        }

        // Hook to bypass loading 3rd party addons lists.
        boolean fetch3rdParties = System.getenv("SDK_SKIP_3RD_PARTIES") == null;

        AddonsListFetcher fetcher = new AddonsListFetcher();
        Site[] sites = fetcher.fetch(url, getDownloadCache(), monitor);
        if (sites != null) {
            SdkSources sources = mUpdaterData.getSources();
            sources.removeAll(SdkSourceCategory.ADDONS_3RD_PARTY);

            if (fetch3rdParties) {
                for (Site s : sites) {
                    switch (s.getType()) {
                    case ADDON_SITE:
                        sources.add(SdkSourceCategory.ADDONS_3RD_PARTY,
                                new SdkAddonSource(s.getUrl(), s.getUiName()));
                        break;
                    case SYS_IMG_SITE:
                        sources.add(SdkSourceCategory.ADDONS_3RD_PARTY,
                                new SdkSysImgSource(s.getUrl(), s.getUiName()));
                        break;
                    }
                }
            }

            sources.notifyChangeListeners();

            mStateFetchRemoteAddonsList = 1;
        }

        monitor.setDescription("Fetched Add-ons List successfully");
    }

    /**
     * Returns the {@link DownloadCache} to use.
     *
     * @return Returns {@link #mOverrideCache} if not null; otherwise returns the
     *  one from {@link UpdaterData} is used instead.
     */
    private DownloadCache getDownloadCache() {
        return mOverrideCache != null ? mOverrideCache : mUpdaterData.getDownloadCache();
    }
}
