// <copyright file="AndroidSdkManager.cs" company="Google Inc.">
// Copyright (C) 2017 Google Inc. All Rights Reserved.
//
//  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.
// </copyright>

namespace Google.Android.AppBundle.Editor.Internal.PlayServices
{
    using System;
    using System.Diagnostics;
    using System.IO;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text.RegularExpressions;
    using UnityEngine;

    /// <summary>
    /// Subset of Android SDK package metadata required for installation.
    /// </summary>
    public class AndroidSdkPackageNameVersion {
        /// Converts and old "android" package manager package name to a new "sdkmanager" package
        /// name.
        private static Dictionary<string, string> OLD_TO_NEW_PACKAGE_NAME_PREFIX =
            new Dictionary<string, string> { { "extra;", "extras;" } };
        /// List package name components to not convert hyphens / semi-colons.
        /// Some package names contain hyphens and therefore can't simply be converted by
        /// replacing - with ;.  This list of name components is used to preserved when
        /// converting between new and legacy package names.
        /// This list was dervied from:
        /// sdkmanager --verbose --list | \
        ///    grep -vE '^(Info:|  |---)' | \
        ///    grep '-' | \
        ///    tr ';' '\n' | \
        ///    sort | \
        ///    uniq | \
        ///    grep '-' | \
        ///    sed 's/.*/"&",/'
        private static List<Regex> PRESERVED_PACKAGE_NAME_COMPONENTS = new List<Regex> {
            new Regex("\\d+.\\d+.\\d+-[a-zA-Z0-9]+"),
            new Regex("add-ons"),
            new Regex("addon-google_apis-google-\\d+"),
            new Regex("android-\\d+"),
            new Regex("arm64-v8a"),
            new Regex("armeabi-v7a"),
            new Regex("build-tools"),
            new Regex("constraint-layout"),
            new Regex("constraint-layout-solver"),
            new Regex("ndk-bundle"),
            new Regex("platform-tools"),
            new Regex("system-images"),
            new Regex("support-[a-zA-Z0-9]+"),
        };

        /// <summary>
        /// Name of the package.
        /// </summary>
        public string Name { set; get; }

        /// <summary>
        /// Escape components that should not be converted by LegacyName.
        /// </summary>
        /// <returns>Escaped package name.</returns>
        private string EscapeComponents(string packageName) {
            foreach (var componentRegex in PRESERVED_PACKAGE_NAME_COMPONENTS) {
                var match = componentRegex.Match(packageName);
                if (match.Success) {
                    var prefix = packageName.Substring(0, match.Index);
                    var postfix = packageName.Substring(match.Index + match.Length);
                    // Exclaimation marks are guaranteed - at the moment - to not be
                    // part of a package name / path.
                    packageName = prefix + match.Value.Replace("-", "!") + postfix;
                }
            }
            return packageName;
        }

        /// <summary>
        /// Un-escaped components that should not be converted by LegacyName.
        /// </summary>
        /// <returns>Un-escaped package name.</returns>
        private string UnescapeComponents(string packageName) {
            return packageName.Replace("!", "-");
        }

        /// <summary>
        /// Convert to / from a legacy package name.
        /// </summary>
        public string LegacyName {
            set {
                var packageName = UnescapeComponents(EscapeComponents(value).Replace("-", ";"));
                foreach (var kv in OLD_TO_NEW_PACKAGE_NAME_PREFIX) {
                    if (packageName.StartsWith(kv.Key)) {
                        packageName = kv.Value + packageName.Substring(kv.Key.Length);
                        break;
                    }
                }
                Name = packageName;
            }

            get {
                var packageName = Name;
                foreach (var kv in OLD_TO_NEW_PACKAGE_NAME_PREFIX) {
                    if (packageName.StartsWith(kv.Value)) {
                        packageName = kv.Key + packageName.Substring(kv.Value.Length);
                        break;
                    }
                }
                return packageName.Replace(";", "-");
            }
        }

        /// <summary>
        /// Convert to / from a package path to name.
        /// </summary>
        /// Android SDK package names are derived from their path relative to the SDK directory.
        public string Path {
            set {
                Name = value.Replace("\\", "/").Replace("/", ";");
            }
            get {
                return Name.Replace(";", System.IO.Path.PathSeparator.ToString());
            }
        }

        /// <summary>
        /// String representation of the package version.
        /// </summary>
        public string VersionString { set; get; }

        /// <summary>
        /// 64-bit integer representation of the package version.
        /// </summary>
        public long Version { get { return ConvertVersionStringToInteger(VersionString); } }

        /// <summary>
        /// Get a string representation of this object.
        /// </summary>
        public override string ToString() {
            return String.Format("{0} ({1})", Name, VersionString);
        }

        /// <summary>
        /// Hash the name of this package.
        /// </summary>
        public override int GetHashCode() {
            return Name.GetHashCode();
        }

        /// <summary>
        /// Compares two package names.
        /// </summary>
        /// <param name="obj">Object to compare with.</param>
        /// <returns>true if both objects have the same name, false otherwise.</returns>
        public override bool Equals(System.Object obj) {
            var pkg = obj as AndroidSdkPackageNameVersion;
            return pkg != null && pkg.Name == Name;
        }

        /// <summary>
        /// Convert an N component version string into an integer.
        /// </summary>
        /// <param name="versionString">Version string to convert.</param>
        /// <param name="componentMultiplier">Value to multiply each component by.</param>
        /// <returns>An integer representation of the version string.</returns>
        public static long ConvertVersionStringToInteger(string versionString,
                                                         long componentMultiplier = 1000000) {
            if (String.IsNullOrEmpty(versionString)) return 0;
            var components = versionString.Split(new [] { '.' });
            long versionInteger = 0;
            long currentMultiplier = 1;
            Array.Reverse(components);
            foreach (var component in components) {
                long componentInteger = 0;
                try {
                    componentInteger = Convert.ToInt64(component);
                } catch (FormatException) {
                    PlayServicesResolver.Log(
                        String.Format("Unable to convert version string {0} to " +
                                      "integer value", versionString),
                        level: LogLevel.Warning);
                    return 0;
                }
                versionInteger += (componentInteger * currentMultiplier);
                currentMultiplier *= componentMultiplier;
            }
            return versionInteger;
        }

        /// <summary>
        /// Convert a list of package name / versions to a bulleted string list.
        /// </summary>
        /// <param name="packages">List of packages to write to a string.</param>
        /// <returns>Bulleted list of package name / versions.</returns>
        public static string ListToString(
                IEnumerable<AndroidSdkPackageNameVersion> packages) {
            var packageAndVersion = new List<string>();
            foreach (var pkg in packages) {
                packageAndVersion.Add(String.Format(
                    "* {0} {1}", pkg.Name,
                    !String.IsNullOrEmpty(pkg.VersionString) ?
                        String.Format("({0})", pkg.VersionString) : ""));
            }
            return String.Join("\n", packageAndVersion.ToArray());
        }
    }

    /// <summary>
    /// Describes an Android SDK package.
    /// </summary>
    public class AndroidSdkPackage : AndroidSdkPackageNameVersion {

        /// <summary>
        /// Human readable description of the package.
        /// </summary>
        public string Description { set; get; }

        /// <summary>
        /// Whether the package is installed.
        /// </summary>
        public bool Installed { set; get; }

        /// <summary>
        /// Read package metadata from the source.properties file within the specified directory.
        /// </summary>
        /// <param name="sdkDirectory">Android SDK directory to query.</param>
        /// <param name="packageDirectory">Directory containing the package relative to
        /// sdkDirectory.</param>
        public static AndroidSdkPackage ReadFromSourceProperties(string sdkDirectory,
                                                                 string packageDirectory) {
            var propertiesPath = System.IO.Path.Combine(
                sdkDirectory, System.IO.Path.Combine(packageDirectory, "source.properties"));
            string propertiesText = null;
            try {
                propertiesText = File.ReadAllText(propertiesPath);
            } catch (Exception e) {
                PlayServicesResolver.Log(String.Format("Unable to read {0}\n{1}\n",
                                                      propertiesPath, e.ToString()),
                                         level: LogLevel.Verbose);
                return null;
            }
            // Unfortunately the package name is simply based upon the path within the SDK.
            var sdkPackage = new AndroidSdkPackage { Path = packageDirectory };
            const string VERSION_FIELD_NAME = "Pkg.Revision=";
            const string DESCRIPTION_FIELD_NAME = "Pkg.Desc=";
            foreach (var rawLine in CommandLine.SplitLines(propertiesText)) {
                var line = rawLine.Trim();
                // Ignore comments.
                if (line.StartsWith("#")) continue;
                // Parse fields
                if (line.StartsWith(VERSION_FIELD_NAME)) {
                    sdkPackage.VersionString = line.Substring(VERSION_FIELD_NAME.Length);
                } else if (line.StartsWith(DESCRIPTION_FIELD_NAME)) {
                    sdkPackage.Description = line.Substring(DESCRIPTION_FIELD_NAME.Length);
                }
            }
            return sdkPackage;
        }
    }

    /// <summary>
    /// Collection of AndroidSdkPackage instances indexed by package name.
    /// </summary>
    public class AndroidSdkPackageCollection {
        private Dictionary<string, List<AndroidSdkPackage>> packages =
            new Dictionary<string, List<AndroidSdkPackage>>();

        /// <summary>
        /// Get the set of package names in the collection.
        /// </summary>
        public List<string> PackageNames {
            get { return new List<string>(packages.Keys); }
        }

        /// <summary>
        /// Get the list of package metadata by package name.
        /// </summary>
        /// <returns>List of package metadata.</returns>
        public List<AndroidSdkPackage> this[string packageName] {
            get {
                List<AndroidSdkPackage> packageList = null;
                if (!packages.TryGetValue(packageName, out packageList)) {
                    packageList = new List<AndroidSdkPackage>();
                    packages[packageName] = packageList;
                }
                return packageList;
            }
        }

        /// <summary>
        /// Get the most recent available version of a specified package, prefering installed.
        /// </summary>
        /// <returns>The package if it's available, null otherwise.</returns>
        public AndroidSdkPackage GetMostRecentAvailablePackage(string packageName) {
            var list = this[packageName];
            if (list == null || !list.Any()) {
                return null;
            }
            var maxVersion = list.Max(p => p.Version);
            AndroidSdkPackage mostRecentPackage = null;
            foreach (var package in list) {
                if (package.Version == maxVersion && (mostRecentPackage == null || !mostRecentPackage.Installed)) {
                    mostRecentPackage = package;
                }
            }
            return mostRecentPackage;
        }

        /// <summary>
        /// Get installed package metadata by package name.
        /// </summary>
        /// <returns>The package if it's installed, null otherwise.</returns>
        public AndroidSdkPackage GetInstalledPackage(string packageName) {
            foreach (var sdkPackage in this[packageName]) {
                if (sdkPackage.Installed) return sdkPackage;
            }
            return null;
        }
    }

    /// <summary>
    /// Interface used to interact with Android SDK managers.
    /// </summary>
    public interface IAndroidSdkManager {
        /// <summary>
        /// Use the package manager to retrieve the set of installed and available packages.
        /// </summary>
        /// <param name="complete">Called when the query is complete.</param>
        void QueryPackages(Action<AndroidSdkPackageCollection> complete);

        /// <summary>
        /// Install a set of packages.
        /// </summary>
        /// <param name="packages">Set of packages to install / upgrade.</param>
        /// <param name="complete">Called when installation is complete.</param>
        void InstallPackages(HashSet<AndroidSdkPackageNameVersion> packages,
                             Action<bool> complete);
    }

    // Answers Android SDK manager license questions.
    internal class LicenseResponder : CommandLine.LineReader {
        // String to match in order to respond.
        private string question;
        // Response to provide to the question.
        private string response;

        /// <summary>
        /// Initialize the class to respond "yes" or "no" to license questions.
        /// </summary>
        /// <param name="question">Question to respond to.</param>
        /// <param name="response">Response to provide.</param>
        public LicenseResponder(string question, string response) {
            this.question = question;
            this.response = response;
            LineHandler += CheckAndRespond;
        }

        // Respond license questions with the "response".
        public void CheckAndRespond(Process process, StreamWriter stdin,
                                    CommandLine.StreamData data) {
            if (process.HasExited) return;

            if ((data.data != null && data.text.Contains(question)) ||
                CommandLine.LineReader.Aggregate(GetBufferedData(0)).text.Contains(question)) {
                Flush();
                // Ignore I/O exceptions as this could race with the process exiting.
                try {
                    foreach (byte b in System.Text.Encoding.UTF8.GetBytes(
                                 response + System.Environment.NewLine)) {
                        stdin.BaseStream.WriteByte(b);
                    }
                    stdin.BaseStream.Flush();
                } catch (System.IO.IOException) {
                }
            }
        }
    }

    /// <summary>
    /// Utility methods for implementations of IAndroidSdkManager.
    /// </summary>
    internal class SdkManagerUtil {

        /// <summary>
        /// Message displayed if a package query operation fails.
        /// </summary>
        const string PACKAGES_MISSING =
            "Unable to determine which Android packages are installed.\n{0}";

        /// <summary>
        /// Title of the installation dialog.
        /// </summary>
        private const string DIALOG_TITLE = "Installing Android SDK packages";

        /// <summary>
        /// Use the package manager to retrieve the set of installed and available packages.
        /// </summary>
        /// <param name="toolPath">Tool to run.</param>
        /// <param name="toolArguments">Arguments to pass to the tool.</param>
        /// <param name="complete">Called when the query is complete.</param>
        public static void QueryPackages(string toolPath, string toolArguments,
                                         Action<CommandLine.Result> complete) {
            var window = CommandLineDialog.CreateCommandLineDialog(
                "Querying Android SDK packages");
            PlayServicesResolver.Log(String.Format("Query Android SDK packages\n" +
                                                   "\n" +
                                                   "{0} {1}\n",
                                                   toolPath, toolArguments),
                                     level: LogLevel.Verbose);
            window.summaryText = "Getting Installed Android SDK packages.";
            window.modal = false;
            // Note: not displaying progress bar
            window.autoScrollToBottom = true;
            window.RunAsync(
                toolPath, toolArguments,
                (CommandLine.Result result) => {
                    HandleProcessExited(result, window);
                    complete(result);
                },
                maxProgressLines: 50);
            window.Show();
        }

        /// <summary>
        /// Retrieve package licenses, display license dialog and then install packages.
        /// </summary>
        /// <param name="toolPath">Tool to run.</param>
        /// <param name="toolArguments">Arguments to pass to the tool.</param>
        /// <param name="packages">List of package versions to install / upgrade.</param>
        /// <param name="licenseQuestion">License question to respond to.</param>
        /// <param name="licenseAgree">String used to agree to a license.</param>
        /// <param name="licenseDecline">String used to decline a license.</param>
        /// <param name="licenseTextHeader">Regex which matches the line which is the start of a
        /// license agreement.</param>
        /// <param name="complete">Called when installation is complete.</param>
        public static void InstallPackages(
                string toolPath, string toolArguments,
                HashSet<AndroidSdkPackageNameVersion> packages,
                string licenseQuestion, string licenseAgree, string licenseDecline,
                Regex licenseTextHeader, Action<bool> complete) {
            PlayServicesResolver.Log(String.Format("Install Android SDK packages\n" +
                                                   "\n" +
                                                   "{0} {1}\n",
                                                   toolPath, toolArguments),
                                     level: LogLevel.Verbose);
            // Display the license retrieval dialog.
            DisplayInstallLicenseDialog(
                toolPath, toolArguments, true,
                new LicenseResponder(licenseQuestion, licenseDecline), packages,
                (CommandLine.Result licensesResult) => {
                    if (licensesResult.exitCode != 0) {
                        complete(false);
                        return;
                    }
                    // Get the license text.
                    var licensesLines = new List<string>();
                    bool foundLicenses = false;
                    foreach (var line in CommandLine.SplitLines(licensesResult.stdout)) {
                        foundLicenses = foundLicenses || licenseTextHeader.Match(line).Success;
                        if (foundLicenses) licensesLines.Add(line);
                    }
                    if (licensesLines.Count == 0) {
                        LogInstallLicenseResult(toolPath, toolArguments, false, packages,
                                                licensesResult);
                        complete(true);
                        return;
                    }
                    // Display the license agreement dialog.
                    DisplayLicensesDialog(
                        String.Join("\n", licensesLines.ToArray()),
                        (bool agreed) => {
                            if (!agreed) {
                                complete(false);
                                return;
                            }
                            // Finally install the packages agreeing to the license questions.
                            DisplayInstallLicenseDialog(
                                toolPath, toolArguments, false,
                                new LicenseResponder(licenseQuestion, licenseAgree), packages,
                                (CommandLine.Result installResult) => {
                                    complete(installResult.exitCode == 0);
                                });
                        });
                });
        }

        /// <summary>
        /// Log a message that describes the installation / license fetching operation.
        /// </summary>
        /// <param name="toolPath">Tool that was executed.</param>
        /// <param name="toolArguments">Arguments to passed to the tool.</param>
        /// <param name="retrievingLicenses">Whether the command is retrieving licenses.</param>
        /// <param name="packages">List of package versions to install / upgrade..</param>
        /// <param name="toolResult">Result of the tool's execution.</param>
        private static void LogInstallLicenseResult(
                string toolPath, string toolArguments, bool retrievingLicenses,
                IEnumerable<AndroidSdkPackageNameVersion> packages,
                CommandLine.Result toolResult) {
            bool succeeded = toolResult.exitCode == 0;
            if (!retrievingLicenses || !succeeded) {
                var failedMessage = retrievingLicenses ?
                    "Failed to retrieve Android SDK package licenses.\n\n" +
                    "Aborted installation of the following packages:\n" :
                    "Android package installation failed.\n\n" +
                    "Failed when installing the following packages:\n";
                PlayServicesResolver.Log(
                    String.Format(
                        "{0}\n" +
                        "{1}\n\n" +
                        "{2}\n",
                        succeeded ? "Successfully installed Android packages.\n\n" : failedMessage,
                        AndroidSdkPackageNameVersion.ListToString(packages),
                        toolResult.message),
                    level: succeeded ? LogLevel.Info : LogLevel.Warning);
            }
        }

        /// <summary>
        /// Open a install / license window and execute a command.
        /// </summary>
        /// <param name="toolPath">Tool to run.</param>
        /// <param name="toolArguments">Arguments to pass to the tool.</param>
        /// <param name="retrievingLicenses">Whether the command is retrieving licenses.</param>
        /// <param name="licenseResponder">Responds to license questions.</param>
        /// <param name="packages">List of package versions to install / upgrade.</param>
        /// <param name="complete">Called when installation is complete.</param>
        private static void DisplayInstallLicenseDialog(
                string toolPath, string toolArguments, bool retrievingLicenses,
                LicenseResponder licenseResponder,
                IEnumerable<AndroidSdkPackageNameVersion> packages,
                Action<CommandLine.Result> complete) {
            var summary = retrievingLicenses ?
                "Attempting Android SDK package installation..." : DIALOG_TITLE + "...";
            var window = CommandLineDialog.CreateCommandLineDialog(DIALOG_TITLE);
            window.summaryText = summary;
            window.modal = false;
            window.bodyText = String.Format("{0} {1}\n\n", toolPath, toolArguments);
            // Note: not displaying progress bar
            window.autoScrollToBottom = true;
            CommandLine.IOHandler ioHandler = null;
            if (licenseResponder != null) ioHandler = licenseResponder.AggregateLine;
            PlayServicesResolver.Log(String.Format("{0} {1}", toolPath, toolArguments),
                                     level: LogLevel.Verbose);
            window.RunAsync(
                toolPath, toolArguments,
                (CommandLine.Result result) => {
                    HandleProcessExited(result, window);
                    LogInstallLicenseResult(toolPath, toolArguments, retrievingLicenses, packages,
                                            result);
                    complete(result);
                },
                ioHandler: ioHandler,
                maxProgressLines: retrievingLicenses ? 250 : 500);
            window.Show();
        }

        /// <summary>
        /// Handles window behavior when the CommandLineDialog finishes, closing the window automatically if
        /// execution succeeded and leaving it open with a "Close" button if it failed.
        /// </summary>
        /// <param name="result">The result of the process execution.</param>
        /// <param name="window">The window that should be adjusted based on the process execution result.</param>
        private static void HandleProcessExited(CommandLine.Result result, CommandLineDialog window) {
            if (result.exitCode == 0) {
                window.Close();
            } else {
                PlayServicesResolver.Log(string.Format(PACKAGES_MISSING, result.message));
                window.noText = "Close";
                // After adding the button we need to scroll down a little more.
                window.scrollPosition.y = Mathf.Infinity;
                window.Repaint();
            }
        }

        /// <summary>
        /// Display license dialog.
        /// </summary>
        /// <param name="licenses">String containing the licenses to display.</param>
        /// <param name="complete">Called when the user agrees / disagrees to the licenses.</param>
        private static void DisplayLicensesDialog(string licenses, Action<bool> complete) {
            var window = CommandLineDialog.CreateCommandLineDialog(DIALOG_TITLE);
            window.summaryText = "License agreement(s) required to install Android SDK packages";
            window.modal = false;
            window.bodyText = licenses;
            window.yesText = "agree";
            window.noText = "decline";
            window.result = false;
            window.Repaint();
            window.buttonClicked = (TextAreaDialog dialog) => {
                window.Close();
                if (!dialog.result) {
                    complete(false);
                    return;
                }
                complete(true);
            };
            window.Show();
        }
    }

    /// <summary>
    /// Interacts with the legacy Android SDK manager "android".
    /// </summary>
    internal class AndroidToolSdkManager : IAndroidSdkManager {
        /// Name of the SDK manager command line tool.
        public const string TOOL_NAME = "android";

        /// <summary>
        /// Extracts the package identifer from the SDK list output.
        /// </summary>
        private static Regex PACKAGE_ID_REGEX = new Regex(
            "^id:\\W+\\d+\\W+or\\W+\"([^\"]+)\"");

        /// <summary>
        /// Extracts the package description from the SDK list output.
        /// </summary>
        private static Regex PACKAGE_DESCRIPTION_REGEX = new Regex(
            "^\\WDesc:\\W+(.+)");

        /// <summary>
        /// Extracts the install location from the SDK list output.
        /// </summary>
        private static Regex PACKAGE_INSTALL_LOCATION_REGEX = new Regex(
            "^\\W+Install[^:]+:\\W+([^ ]+)");

        // Path to the SDK manager tool.
        private string toolPath;
        // Path to the Android SDK.
        private string sdkPath;

        /// <summary>
        /// Initialize this instance.
        /// </summary>
        /// <param name="toolPath">Path of the android tool.</param>
        /// <param name="sdkPath">Required to validate that a package is really installed.</param>
        public AndroidToolSdkManager(string toolPath, string sdkPath) {
            this.toolPath = toolPath;
            this.sdkPath = sdkPath;
        }

        /// <summary>
        /// Determines whether this is the legacy tool or the sdkmanager wrapper.
        /// </summary>
        public bool IsWrapper {
            get {
                // It's only possible to differentiate between the "android" package manager or
                // sdkmanager wrapper by searching the output string for "deprecated" which is
                // present in the wrapper.
                var result = CommandLine.Run(
                    toolPath, "list sdk",
                    envVars: new Dictionary<string, string> { { "USE_SDK_WRAPPER", "1" } });
                if (result.stdout.IndexOf("deprecated") >= 0) {
                    return true;
                }
                return false;
            }
        }

        /// <summary>
        /// Parse "android list sdk -u -e -a" output.
        /// </summary>
        private AndroidSdkPackageCollection ParseAndroidListSdkOutput(
                string androidListSdkOutput) {
            var packages = new AndroidSdkPackageCollection();
            AndroidSdkPackage currentPackage = null;
            foreach (string line in CommandLine.SplitLines(androidListSdkOutput)) {
                // Check for the start of a new package entry.
                if (line.StartsWith("---")) {
                    currentPackage = null;
                    continue;
                }
                Match match;
                // If this is the start of a package description, add a package.
                match = PACKAGE_ID_REGEX.Match(line);
                if (match.Success) {
                    // TODO(smiles): Convert the legacy package name to a new package name.
                    currentPackage = new AndroidSdkPackage { LegacyName = match.Groups[1].Value };
                    packages[currentPackage.Name].Add(currentPackage);
                    continue;
                }
                if (currentPackage == null) continue;

                // Add a package description.
                match = PACKAGE_DESCRIPTION_REGEX.Match(line);
                if (match.Success) {
                    currentPackage.Description = match.Groups[1].Value;
                    continue;
                }
                // Parse the install path and record whether the package is installed.
                match = PACKAGE_INSTALL_LOCATION_REGEX.Match(line);
                if (match.Success) {
                    currentPackage.Installed = File.Exists(
                        Path.Combine(Path.Combine(sdkPath, match.Groups[1].Value),
                                     "source.properties"));
                }
            }
            return packages;
        }

        /// <summary>
        /// Use the package manager to retrieve the set of installed and available packages.
        /// </summary>
        /// <param name="complete">Called when the query is complete.</param>
        public void QueryPackages(Action<AndroidSdkPackageCollection> complete) {
            SdkManagerUtil.QueryPackages(
                toolPath, "list sdk -u -e -a",
                (CommandLine.Result result) => {
                    complete(result.exitCode == 0 ?
                             ParseAndroidListSdkOutput(result.stdout) : null);
                });
        }

        /// <summary>
        /// Install a set of packages.
        /// </summary>
        /// <param name="packages">List of package versions to install / upgrade.</param>
        /// <param name="complete">Called when installation is complete.</param>
        public void InstallPackages(HashSet<AndroidSdkPackageNameVersion> packages,
                                    Action<bool> complete) {
            var packageNames = new List<string>();
            foreach (var pkg in packages) packageNames.Add(pkg.LegacyName);
            SdkManagerUtil.InstallPackages(
                toolPath, String.Format(
                    "update sdk -a -u -t {0}", String.Join(",", packageNames.ToArray())),
                packages, "Do you accept the license", "yes", "no",
                new Regex("^--------"), complete);
        }
    }

    /// <summary>
    /// Interacts with the Android SDK manager "sdkmanager".
    /// </summary>
    internal class SdkManager : IAndroidSdkManager {
        /// Name of the SDK manager command line tool.
        public const string TOOL_NAME = "sdkmanager";

        // Marker followed by the list of installed packages.
        private const string INSTALLED_PACKAGES_HEADER = "installed packages:";
        // Marker followed by the list of available packages.
        private const string AVAILABLE_PACKAGES_HEADER = "available packages:";
        // Marker followed by the list of available package updates.
        private const string AVAILABLE_UPDATES_HEADER = "available updates:";

        /// Minimum version of the package that supports the --verbose flag.
        public static long MINIMUM_VERSION_FOR_VERBOSE_OUTPUT =
            AndroidSdkPackageNameVersion.ConvertVersionStringToInteger("26.0.2");

        // Path to the SDK manager tool.
        private string toolPath;
        // Metadata for this package.
        private AndroidSdkPackage toolsPackage;

        /// <summary>
        /// Initialize this instance.
        /// </summary>
        /// <param name="toolPath">Path of the android tool.</param>
        public SdkManager(string toolPath) {
            this.toolPath = toolPath;
            var toolsDir = Path.GetDirectoryName(Path.GetDirectoryName(toolPath));
            var sdkDir = Path.GetDirectoryName(toolsDir);
            toolsPackage = AndroidSdkPackage.ReadFromSourceProperties(
                sdkDir, toolsDir.Substring((sdkDir + Path.PathSeparator).Length));
        }

        /// <summary>
        /// Read the metadata for the package that contains the package manager.
        /// </summary>
        public AndroidSdkPackage Package { get { return toolsPackage; } }

        /// <summary>
        /// Parse "sdkmanager --list --verbose" output.
        /// NOTE: The --verbose output format is only reported by sdkmanager 26.0.2 and above.
        /// </summary>
        private AndroidSdkPackageCollection ParseListVerboseOutput(
                string sdkManagerListVerboseOutput) {
            var packages = new AndroidSdkPackageCollection();
            // Whether we're parsing a set of packages.
            bool parsingPackages = false;
            // Whether we're parsing within the set of installed packages vs. available packages.
            bool parsingInstalledPackages = false;
            // Fields of the package being parsed.
            AndroidSdkPackage currentPackage = null;
            foreach (var rawLine in CommandLine.SplitLines(sdkManagerListVerboseOutput)) {
                var line = rawLine.Trim();
                var lowerCaseLine = line.ToLower();
                if (lowerCaseLine == AVAILABLE_UPDATES_HEADER) {
                    parsingPackages = false;
                    continue;
                }
                bool installedPackagesLine = lowerCaseLine == INSTALLED_PACKAGES_HEADER;
                bool availablePackagesLine = lowerCaseLine == AVAILABLE_PACKAGES_HEADER;
                if (installedPackagesLine || availablePackagesLine) {
                    parsingPackages = true;
                    parsingInstalledPackages = installedPackagesLine;
                    continue;
                } else if (line.StartsWith("---")) {
                    // Ignore section separators.
                    continue;
                } else if (String.IsNullOrEmpty(line)) {
                    if (currentPackage != null &&
                        !(String.IsNullOrEmpty(currentPackage.Name) ||
                          String.IsNullOrEmpty(currentPackage.VersionString))) {
                        packages[currentPackage.Name].Add(currentPackage);
                    }
                    currentPackage = null;
                    continue;
                } else if (!parsingPackages) {
                    continue;
                }
                // Fields of the package are indented.
                bool indentedLine = rawLine.StartsWith("    ");
                if (!indentedLine) {
                    // If this isn't an indented line it should be a package name.
                    if (currentPackage == null) {
                        currentPackage = new AndroidSdkPackage {
                            Name = line,
                            Installed = parsingInstalledPackages
                        };
                    }
                } else if (currentPackage != null) {
                    // Parse the package field.
                    var fieldSeparatorIndex = line.IndexOf(":");
                    if (fieldSeparatorIndex >= 0) {
                        var fieldName = line.Substring(0, fieldSeparatorIndex).Trim().ToLower();
                        var fieldValue = line.Substring(fieldSeparatorIndex + 1).Trim();
                        if (fieldName == "description") {
                            currentPackage.Description = fieldValue;
                        } else if (fieldName == "version") {
                            currentPackage.VersionString = fieldValue;
                        }
                    }
                }
            }
            return packages;
        }

        /// <summary>
        /// Parse "sdkmanager --list" output.
        /// </summary>
        /// <returns>Dictionary of packages bucketed by package name</returns>
        private AndroidSdkPackageCollection ParseListOutput(
                string sdkManagerListOutput) {
            var packages = new AndroidSdkPackageCollection();
            // Whether we're parsing a set of packages.
            bool parsingPackages = false;
            // Whether we're parsing within the set of installed packages vs. available packages.
            bool parsingInstalledPackages = false;
            // Whether we're parsing the contents of the package table vs. the header.
            bool inPackageTable = false;
            foreach (var rawLine in CommandLine.SplitLines(sdkManagerListOutput)) {
                var line = rawLine.Trim();
                var lowerCaseLine = line.ToLower();
                if (lowerCaseLine == AVAILABLE_UPDATES_HEADER) {
                    parsingPackages = false;
                    continue;
                }
                bool installedPackagesLine = lowerCaseLine == INSTALLED_PACKAGES_HEADER;
                bool availablePackagesLine = lowerCaseLine == AVAILABLE_PACKAGES_HEADER;
                if (installedPackagesLine || availablePackagesLine) {
                    parsingPackages = true;
                    parsingInstalledPackages = installedPackagesLine;
                    inPackageTable = false;
                    continue;
                }
                if (!parsingPackages) continue;
                if (!inPackageTable) {
                    // If we've reached end of the table header, start parsing the set of packages.
                    if (line.StartsWith("----")) {
                        inPackageTable = true;
                    }
                    continue;
                }
                // Split into the fields package_name|version|description|location.
                // Where "location" is an optional field that contains the install path.
                var rawTokens = line.Split(new [] { '|' });
                if (rawTokens.Length < 3 || String.IsNullOrEmpty(line)) {
                    parsingPackages = false;
                    continue;
                }
                // Each field is surrounded by whitespace so trim the fields.
                string[] tokens = new string[rawTokens.Length];
                for (int i = 0; i < rawTokens.Length; ++i) {
                    tokens[i] = rawTokens[i].Trim();
                }
                var packageName = tokens[0];
                packages[packageName].Add(new AndroidSdkPackage {
                        Name = packageName,
                        Description = tokens[2],
                        VersionString = tokens[1],
                        Installed = parsingInstalledPackages
                    });
            }
            return packages;
        }

        /// <summary>
        /// Use the package manager to retrieve the set of installed and available packages.
        /// </summary>
        /// <param name="complete">Called when the query is complete.</param>
        public void QueryPackages(Action<AndroidSdkPackageCollection> complete) {
            bool useVerbose = Package != null &&
                Package.Version >= MINIMUM_VERSION_FOR_VERBOSE_OUTPUT;
            SdkManagerUtil.QueryPackages(
                toolPath, "--list" + (useVerbose ? " --verbose" : ""),
                (CommandLine.Result result) => {
                    complete(result.exitCode == 0 ?
                                useVerbose ? ParseListVerboseOutput(result.stdout) :
                                   ParseListOutput(result.stdout) :
                             null);
                });
        }

        /// <summary>
        /// Install a set of packages.
        /// </summary>
        /// <param name="packages">List of package versions to install / upgrade.</param>
        /// <param name="complete">Called when installation is complete.</param>
        public void InstallPackages(HashSet<AndroidSdkPackageNameVersion> packages,
                                    Action<bool> complete)
        {
            var packageNames = new List<string>();
            foreach (var pkg in packages) packageNames.Add(pkg.Name);
            SdkManagerUtil.InstallPackages(toolPath, String.Join(" ", packageNames.ToArray()),
                                           packages, "Accept? (y/N):", "y", "N",
                                           new Regex("^License\\W+[^ ]+:"), complete);
        }
    }

    /// <summary>
    /// Interacts with the available Android SDK package manager.
    /// </summary>
    public class AndroidSdkManager {
        /// <summary>
        /// Find a tool in the Android SDK.
        /// </summary>
        /// <param name="toolName">Name of the tool to search for.</param>
        /// <param name="sdkPath">SDK path to search for the tool.  If this is null or empty, the
        // system path is searched instead.</param>
        /// <returns>String with the path to the tool if found, null otherwise.</returns>
        private static string FindAndroidSdkTool(string toolName, string sdkPath = null) {
            if (String.IsNullOrEmpty(sdkPath)) {
                PlayServicesResolver.Log(String.Format(
                    "Falling back to searching for the Android SDK tool {0} in the system path.",
                    toolName));
            } else {
                var extensions = new List<string> { CommandLine.GetExecutableExtension() };
                if (UnityEngine.RuntimePlatform.WindowsEditor ==
                    UnityEngine.Application.platform) {
                    extensions.AddRange(new [] { ".bat", ".cmd" });
                }
                foreach (var dir in new [] { "tools", Path.Combine("tools", "bin") }) {
                    foreach (var extension in extensions) {
                        var currentPath = Path.Combine(sdkPath,
                                                       Path.Combine(dir, toolName + extension));
                        if (File.Exists(currentPath)) {
                            return currentPath;
                        }
                    }
                }
            }
            var toolPath = CommandLine.FindExecutable(toolName);
            return toolPath != null && File.Exists(toolPath) ? toolPath : null;
        }

        /// <summary>
        /// Log an error and complete a Create() operation.
        /// </summary>
        /// <param name="complete">Action called with null.</param>
        private static void CreateFailed(Action<IAndroidSdkManager> complete) {
            PlayServicesResolver.Log(String.Format(
                "Unable to find either the {0} or {1} command line tool.\n\n" +
                "It is not possible to query or install Android SDK packages.\n" +
                "To resolve this issue, open the Android Package Manager" +
                "and install the latest tools package.",
                SdkManager.TOOL_NAME, AndroidToolSdkManager.TOOL_NAME));
            complete(null);
        }

        /// <summary>
        /// Create an instance of this class.
        ///
        /// If the package manager is out of date, the user is prompted to update it.
        /// </summary>
        /// <param name="androidSdkRoot">The path to the Android SDK root folder.</param>
        /// <param name="complete">Used to report a AndroidSdkManager instance if a SDK manager is
        /// available, returns null otherwise.</param>
        public static void Create(string androidSdkRoot, Action<IAndroidSdkManager> complete) {
            // Search for the new package manager
            var sdkManagerTool = FindAndroidSdkTool(SdkManager.TOOL_NAME, androidSdkRoot);
            if (sdkManagerTool != null) {
                var sdkManager = new SdkManager(sdkManagerTool);
                var sdkManagerPackage = sdkManager.Package;
                if (sdkManagerPackage != null) {
                    // If the package manager is out of date, try updating it.
                    if (sdkManagerPackage.Version < SdkManager.MINIMUM_VERSION_FOR_VERBOSE_OUTPUT) {
                        sdkManager.QueryPackages(
                            (AndroidSdkPackageCollection packages) => {
                                sdkManagerPackage = packages.GetMostRecentAvailablePackage(
                                    sdkManagerPackage.Name);
                                if (sdkManagerPackage != null) {
                                    sdkManager.InstallPackages(
                                        new HashSet<AndroidSdkPackageNameVersion>(
                                            new [] { sdkManagerPackage }),
                                        (bool success) => {
                                            complete(success ? sdkManager : null);
                                        });
                                } else {
                                    CreateFailed(complete);
                                }
                            });
                    } else {
                        complete(sdkManager);
                    }
                    return;
                }
            }

            // Search for the legacy package manager.
            var androidTool = FindAndroidSdkTool("android", androidSdkRoot);
            if (androidTool != null) {
                var sdkManager = new AndroidToolSdkManager(androidTool, androidSdkRoot);
                if (!sdkManager.IsWrapper) {
                    complete(sdkManager);
                    return;
                }
            }
            CreateFailed(complete);
        }
    }
}
