// Copyright 2018 Google LLC
//
// 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
//
//     https://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.

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading;
using System.Xml.Linq;
using Google.Android.AppBundle.Editor.Internal.AndroidManifest;
using Google.Android.AppBundle.Editor.Internal.AssetPacks;
using Google.Android.AppBundle.Editor.Internal.Config;
using Google.Android.AppBundle.Editor.Internal.Utils;
using UnityEditor;
using UnityEngine;

#if UNITY_2018_4_OR_NEWER && !NET_LEGACY
using System.Threading.Tasks;
#endif

namespace Google.Android.AppBundle.Editor.Internal.BuildTools
{
    /// <summary>
    /// Helper to build an Android App Bundle file on Unity.
    /// </summary>
    public class AppBundleBuilder : IBuildTool
    {
        public delegate void PostBuildCallback(string finishedAabFilePath);

        /// <summary>
        /// Status for coordinating between the background thread and main thread on async builds.
        /// </summary>
        private enum BuildStatus
        {
            Running,
            Failing,
            Succeeding,
            Halted
        }

        private const string AndroidManifestFileName = "AndroidManifest.xml";
        private const string AssetsDirectoryName = "assets";
        private const string BundleMetadataDirectoryName = "BUNDLE-METADATA";
        private const string ManifestDirectoryName = "manifest";
        private const string RequiredDirectoryName = "assets/bin/Data/Managed";
        private const string ResourceTableFileName = "resources.pb";
        private const float ProgressCreateBaseModule = 0.3f;
        private const float ProgressProcessModules = 0.5f;
        private const float ProgressRunBundletool = 0.7f;
        private const int ProgressBarWaitHandleTimeoutMs = 100;

        // For simplicity we use the same filename when building an APK or an AAB.
        // Note: AAB builds fail if we use a .apk file extension, but APK builds can use any extension.
        private const string AndroidPlayerFileName = "tmp.aab";
        private const string AndroidPlayerFilePrefix = "tmp";

        /// <summary>
        /// The folder where to store the asset pack, inside the "assets" folder of an Android App Bundle module.
        /// This intermediate folder name can be suffixed with a texture compression format targeting (e.g: #tcf_astc),
        /// which will be stripped out by bundletool.
        /// </summary>
        private const string AssetPackFolder = "assetpack";

        private readonly AndroidAssetPackagingTool _androidAssetPackagingTool;
        private readonly AndroidBuilder _androidBuilder;
        private readonly BundletoolHelper _bundletool;
        private readonly JarSigner _jarSigner;
        private readonly string _workingDirectoryPath;
        private readonly ZipUtils _zipUtils;

        private volatile BuildStatus _buildStatus = BuildStatus.Running;
        private volatile string _progressBarMessage = "";
        private volatile float _progressBarProgress;
        private volatile string _buildErrorMessage;
        private volatile string _finishedAabFilePath;
        private volatile bool _canceled;
        private Thread _backgroundThread;
        private EventWaitHandle _progressBarWaitHandle;
        private AndroidSdkVersions _minSdkVersion;
        private string _packageName;
        private int _versionCode;
        private string _versionName;
        private PostBuildCallback _createBundleAsyncOnSuccess = delegate { };
        private IEnumerable<IAssetPackManifestTransformer> _assetPackManifestTransformers;

        /// <summary>
        /// Constructor.
        /// </summary>
        public AppBundleBuilder(
            AndroidAssetPackagingTool androidAssetPackagingTool,
            AndroidBuilder androidBuilder,
            BundletoolHelper bundletool,
            JarSigner jarSigner,
            string workingDirectoryPath,
            ZipUtils zipUtils)
        {
            _androidAssetPackagingTool = androidAssetPackagingTool;
            _androidBuilder = androidBuilder;
            _bundletool = bundletool;
            _jarSigner = jarSigner;
            _workingDirectoryPath = workingDirectoryPath;
            _zipUtils = zipUtils;
        }

        public virtual bool Initialize(BuildToolLogger buildToolLogger)
        {
            // Cache information that is only accessible from the main thread.
            _minSdkVersion = PlayerSettings.Android.minSdkVersion;
            _packageName = PlayerSettings.GetApplicationIdentifier(BuildTargetGroup.Android);
            _versionCode = PlayerSettings.Android.bundleVersionCode;
            _versionName = PlayerSettings.bundleVersion;

            _assetPackManifestTransformers = AssetPackManifestTransformerRegistry.Registry.ConstructInstances();
            var initializedManifestTransformers = true;
            foreach (var transformer in _assetPackManifestTransformers)
            {
                initializedManifestTransformers &= transformer.Initialize(buildToolLogger);
            }

            return CheckUnityVersion(buildToolLogger)
                   && initializedManifestTransformers
                   && _androidAssetPackagingTool.Initialize(buildToolLogger)
                   && _androidBuilder.Initialize(buildToolLogger)
                   && _jarSigner.Initialize(buildToolLogger)
                   && _bundletool.Initialize(buildToolLogger)
                   && _zipUtils.Initialize(buildToolLogger);
        }

        public string WorkingDirectoryPath
        {
            get { return _workingDirectoryPath; }
        }

        private string AndroidPlayerFilePath
        {
            get { return Path.Combine(_workingDirectoryPath, AndroidPlayerFileName); }
        }

        /// <summary>
        /// Builds an Android Player with the specified options.
        /// Note: the specified <see cref="BuildPlayerOptions.locationPathName"/> field is ignored and the
        /// Android Player is written to a temporary file.
        /// </summary>
        public virtual AndroidBuildResult BuildAndroidPlayer(BuildPlayerOptions buildPlayerOptions)
        {
            var workingDirectory = new DirectoryInfo(_workingDirectoryPath);
            if (workingDirectory.Exists)
            {
                workingDirectory.Delete(true);
            }

            workingDirectory.Create();

            EditorUserBuildSettings.buildAppBundle = true;

            Debug.LogFormat("Building Android Player: {0}", AndroidPlayerFilePath);
            // This Android Player is an intermediate build artifact, so use a temporary path for the output file path.
            var updatedBuildPlayerOptions = new BuildPlayerOptions
            {
                assetBundleManifestPath = buildPlayerOptions.assetBundleManifestPath,
                locationPathName = AndroidPlayerFilePath,
                options = buildPlayerOptions.options,
                scenes = buildPlayerOptions.scenes,
                target = buildPlayerOptions.target,
                targetGroup = buildPlayerOptions.targetGroup
            };

            // Do not use BuildAndSign since this signature won't be used.
            return _androidBuilder.Build(updatedBuildPlayerOptions);
        }

        /// <summary>
        /// Synchronously builds an AAB given the specified options and existing Android Player on disk.
        /// </summary>
        /// <returns>An error message if there was an error, or null if successful.</returns>
        public string CreateBundle(CreateBundleOptions options)
        {
            if (_buildStatus != BuildStatus.Running)
            {
                throw new Exception("Unexpected call to CreateBundle() with status: " + _buildStatus);
            }

            var moduleDirectoryList = new List<DirectoryInfo>();
            var workingDirectory = new DirectoryInfo(_workingDirectoryPath);

            var error = CreateAssetModules(moduleDirectoryList, options.AssetPackConfig, workingDirectory);
            if (error != null)
            {
                return error;
            }

            IList<string> bundleMetadata;
            error = CreateBaseModules(moduleDirectoryList, options, workingDirectory, out bundleMetadata);
            if (error != null)
            {
                return error;
            }

            error = CreateBundle(moduleDirectoryList, options, bundleMetadata);
            if (error != null)
            {
                return error;
            }

            Debug.LogFormat("Finished building app bundle: {0}", options.AabFilePath);
            _finishedAabFilePath = options.AabFilePath;
            _buildStatus = BuildStatus.Succeeding;
            return null;
        }

        /// <summary>
        /// Asynchronously builds an AAB at the specified path.
        /// </summary>
        /// <param name="aabFilePath">The AAB output file path.</param>
        /// <param name="assetPackConfig">Asset packs to include in the AAB.</param>
        /// <param name="onSuccess">
        /// Callback that fires with the final aab file location, when the bundle creation succeeds.
        /// </param>
        public void CreateBundleAsync(CreateBundleOptions options, PostBuildCallback onSuccess)
        {
            // Copy the AssetPackConfig before leaving the main thread in case the original is modified later.
            options.AssetPackConfig = SerializationHelper.DeepCopy(options.AssetPackConfig);
            _createBundleAsyncOnSuccess = onSuccess;
            StartCreateBundleAsync(() =>
            {
                try
                {
                    CreateBundle(options);
                }
                catch (ThreadAbortException ex)
                {
                    if (!_canceled)
                    {
                        // Unexpected ThreadAbortException.
                        DisplayBuildError("Exception", ex.ToString());
                    }
                }
                catch (Exception ex)
                {
                    // Catch and display exceptions since they may otherwise be undetected on a background thread.
                    DisplayBuildError("Exception", ex.ToString());
                }
            });
        }

#if UNITY_2018_4_OR_NEWER && !NET_LEGACY
        /// <summary>
        /// Synchronously builds an Android Player and then produces a final AAB synchronously or asynchronously,
        /// as specified.
        /// </summary>
        /// <param name="androidBuildOptions">Options indicating how to build the AAB, including asset packs.</param>
        /// <returns>An async task that provides an AndroidBuildReport.</returns>
        public async Task<AndroidBuildReport> CreateBundleWithTask(AndroidBuildOptions androidBuildOptions)
        {
            var taskCompletionSource = new TaskCompletionSource<AndroidBuildReport>();
            var androidBuildResult = BuildAndroidPlayer(androidBuildOptions.BuildPlayerOptions);
            if (androidBuildResult.Cancelled)
            {
                taskCompletionSource.SetCanceled();
                return await taskCompletionSource.Task;
            }

            var androidBuildReport = new AndroidBuildReport(androidBuildResult.Report);
            if (androidBuildResult.ErrorMessage != null)
            {
                taskCompletionSource.SetException(
                    new AndroidBuildException(androidBuildResult.ErrorMessage, androidBuildReport));
                return await taskCompletionSource.Task;
            }

            var createBundleOptions = new CreateBundleOptions
            {
                AabFilePath = androidBuildOptions.BuildPlayerOptions.locationPathName,
                AssetPackConfig = androidBuildOptions.AssetPackConfig ?? new AssetPackConfig(),
                CompressionOptions = androidBuildOptions.CompressionOptions
            };
            if (androidBuildOptions.ForceSingleThreadedBuild || Application.isBatchMode)
            {
                CreateBundleInternal(
                    taskCompletionSource,
                    () => CreateBundle(createBundleOptions),
                    androidBuildReport,
                    androidBuildReport);
            }
            else
            {
                // Copy the AssetPackConfig while still on the main thread in case the original is modified later.
                createBundleOptions.AssetPackConfig = SerializationHelper.DeepCopy(createBundleOptions.AssetPackConfig);
                StartCreateBundleAsync(() =>
                {
                    CreateBundleInternal(
                        taskCompletionSource,
                        () => CreateBundle(createBundleOptions),
                        androidBuildReport,
                        androidBuildReport);
                });
            }

            return await taskCompletionSource.Task;
        }

        /// <summary>
        /// Builds an Android App Bundle containing only asset packs.
        /// </summary>
        public async Task CreateAssetOnlyBundle(AssetOnlyBuildOptions assetOnlyBuildOptions)
        {
            var assetOnlyOptions = new AssetOnlyOptions
            {
                AppVersions = new List<long>(assetOnlyBuildOptions.AppVersions),
                AssetVersionTag = assetOnlyBuildOptions.AssetVersionTag
            };

            var createBundleOptions = new CreateBundleOptions
            {
                AabFilePath = assetOnlyBuildOptions.LocationPathName,
                AssetPackConfig = SerializationHelper.DeepCopy(assetOnlyBuildOptions.AssetPackConfig),
                AssetOnlyOptions = assetOnlyOptions
            };

            var completionSource = new TaskCompletionSource<bool>();
            if (assetOnlyBuildOptions.ForceSingleThreadedBuild || Application.isBatchMode)
            {
                CreateBundleInternal(
                    completionSource,
                    () => CreateAssetOnlyBundle(createBundleOptions),
                    true);
            }
            else
            {
                StartCreateBundleAsync(() =>
                {
                    CreateBundleInternal(
                        completionSource,
                        () => CreateAssetOnlyBundle(createBundleOptions),
                        true);
                });
            }

            await completionSource.Task;
        }

        private string CreateAssetOnlyBundle(CreateBundleOptions options)
        {
            if (_buildStatus != BuildStatus.Running)
            {
                throw new Exception("Unexpected call to CreateAssetOnlyBundle() with status: " + _buildStatus);
            }

            var moduleDirectoryList = new List<DirectoryInfo>();
            var workingDirectory = new DirectoryInfo(_workingDirectoryPath);

            var error = CreateAssetModules(moduleDirectoryList, options.AssetPackConfig, workingDirectory);
            if (error != null)
            {
                return error;
            }

            error = CreateBundle(moduleDirectoryList, options);
            if (error != null)
            {
                return error;
            }

            Debug.LogFormat("Finished building asset-only app bundle: {0}", options.AabFilePath);
            _finishedAabFilePath = options.AabFilePath;
            _buildStatus = BuildStatus.Succeeding;
            return null;
        }

        private void CreateBundleInternal<T>(
            TaskCompletionSource<T> taskCompletionSource,
            Func<string> createBundleFunc,
            T successResult,
            AndroidBuildReport androidBuildReport = null)
        {
            try
            {
                var errorMessage = createBundleFunc.Invoke();
                if (errorMessage == null)
                {
                    taskCompletionSource.SetResult(successResult);
                }
                else
                {
                    // Already logged.
                    taskCompletionSource.SetException(new AndroidBuildException(errorMessage, androidBuildReport));
                }
            }
            catch (ThreadAbortException ex)
            {
                if (_canceled)
                {
                    taskCompletionSource.SetCanceled();
                }
                else
                {
                    // Unexpected ThreadAbortException.
                    taskCompletionSource.SetException(new AndroidBuildException(ex, androidBuildReport));
                    DisplayBuildError("Exception", ex.ToString());
                }
            }
            catch (Exception ex)
            {
                taskCompletionSource.SetException(new AndroidBuildException(ex, androidBuildReport));
                DisplayBuildError("Exception", ex.ToString());
            }
        }
#endif

        private string CreateBaseModules(IList<DirectoryInfo> moduleDirectoryList, CreateBundleOptions options,
            DirectoryInfo workingDirectory, out IList<string> bundleMetadata)
        {
            // Create base module directory.
            bundleMetadata = new List<string>();
            var baseDirectory = workingDirectory.CreateSubdirectory(AndroidAppBundle.BaseModuleName);
            var baseErrorMessage = CreateBaseModule(baseDirectory, out bundleMetadata);
            if (baseErrorMessage != null)
            {
                // Already displayed the error.
                return baseErrorMessage;
            }

            moduleDirectoryList.Add(baseDirectory);

            if (options.AssetPackConfig.SplitBaseModuleAssets)
            {
                // Move assets from base module directory to the separate module's directory.
                var splitBaseDirectory = workingDirectory.CreateSubdirectory(AndroidAppBundle.BaseAssetsModuleName);
                var splitBaseErrorMessage = CreateSplitBaseModule(baseDirectory, splitBaseDirectory);
                if (splitBaseErrorMessage != null)
                {
                    // Already displayed the error.
                    return splitBaseErrorMessage;
                }

                moduleDirectoryList.Add(splitBaseDirectory);
            }

            return null;
        }

        private string CreateAssetModules(IList<DirectoryInfo> moduleDirectoryList, AssetPackConfig assetPackConfig,
            DirectoryInfo workingDirectory)
        {
            // Create asset pack module directories.
            var index = 0;
            var assetPacks = assetPackConfig.DeliveredAssetPacks;
            foreach (var entry in assetPacks)
            {
                DisplayProgress(
                    string.Format("Processing asset pack {0} of {1}", index + 1, assetPacks.Count),
                    Mathf.Lerp(0.1f, ProgressCreateBaseModule, (float)index / assetPacks.Count));
                index++;

                var assetPackName = entry.Key;
                var assetPack = entry.Value;
                var assetPackDirectoryInfo = workingDirectory.CreateSubdirectory(assetPackName);
                var assetPackErrorMessage = CreateAssetPackModule(assetPackName, assetPack, assetPackDirectoryInfo);
                if (assetPackErrorMessage != null)
                {
                    // Already displayed the error.
                    return assetPackErrorMessage;
                }

                moduleDirectoryList.Add(assetPackDirectoryInfo);
            }

            return null;
        }

        private string CreateBundle(List<DirectoryInfo> moduleDirectoryList, CreateBundleOptions options,
            IList<string> bundleMetadata = null)
        {
            // Create a ZIP file for each module directory.
            var moduleFiles = new List<string>();
            var numModules = moduleDirectoryList.Count;
            for (var i = 0; i < numModules; i++)
            {
                if (numModules == 1)
                {
                    DisplayProgress("Processing base module", ProgressProcessModules);
                }
                else
                {
                    DisplayProgress(
                        string.Format("Processing module {0} of {1}", i + 1, numModules),
                        Mathf.Lerp(ProgressProcessModules, ProgressRunBundletool, (float)i / numModules));
                }

                var moduleDirectoryInfo = moduleDirectoryList[i];
                var destinationDirectoryInfo = GetDestinationSubdirectory(moduleDirectoryInfo);

                // Create ZIP file path, for example /path/to/files/base becomes /path/to/files/base/base.zip
                var zipFilePath = Path.Combine(moduleDirectoryInfo.FullName, moduleDirectoryInfo.Name + ".zip");
                var zipErrorMessage = _zipUtils.CreateZipFile(zipFilePath, destinationDirectoryInfo.FullName, ".");
                if (zipErrorMessage != null)
                {
                    return DisplayBuildError("Zip creation", zipErrorMessage);
                }

                moduleFiles.Add(zipFilePath);
            }

            if (bundleMetadata == null)
            {
                bundleMetadata = new List<string>();
            }

            DisplayProgress("Running bundletool", ProgressRunBundletool);
            var configParams = CreateBuildBundleConfigParams(options);
            var buildBundleErrorMessage =
                _bundletool.BuildBundle(options.AabFilePath, moduleFiles, bundleMetadata, configParams);
            if (buildBundleErrorMessage != null)
            {
                return DisplayBuildError("Bundletool", buildBundleErrorMessage);
            }

            // Only sign the .aab if a custom keystore is configured.
            if (_jarSigner.UseCustomKeystore)
            {
                DisplayProgress("Signing bundle", 0.9f);
                var signingErrorMessage = _jarSigner.Sign(options.AabFilePath);
                if (signingErrorMessage != null)
                {
                    Debug.LogError("Failed to sign");
                    return DisplayBuildError("Signing", signingErrorMessage);
                }
            }
            else
            {
                Debug.LogFormat("Skipped signing since a Custom Keystore isn't configured in Android Player Settings");
            }

            MoveSymbolsZipFile(options.AabFilePath);

            return null;
        }

        private BundletoolHelper.BuildBundleConfigParams CreateBuildBundleConfigParams(CreateBundleOptions options)
        {
            var configParams = new BundletoolHelper.BuildBundleConfigParams
            {
                defaultTcfSuffix = TextureTargetingTools.GetBundleToolTextureCompressionFormatName(
                    options.AssetPackConfig.DefaultTextureCompressionFormat),
                defaultDeviceTier =
                    DeviceTierTargetingTools.GetBundleToolDeviceTierFormatName(
                        options.AssetPackConfig.DefaultDeviceTier),
                minSdkVersion = _minSdkVersion,
                compressionOptions = options.CompressionOptions ?? new CompressionOptions(),
                containsInstallTimeAssetPack = options.AssetPackConfig.SplitBaseModuleAssets
            };

            var assetPacks = options.AssetPackConfig.DeliveredAssetPacks;
            foreach (var entry in assetPacks)
            {
                var assetPackName = entry.Key;
                var assetPack = entry.Value;

                configParams.enableTcfTargeting |= assetPack.CompressionFormatToAssetBundleFilePath != null;
                configParams.enableTcfTargeting |= assetPack.CompressionFormatToAssetPackDirectoryPath != null;
                configParams.enableDeviceTierTargeting |= assetPack.DeviceTierToAssetBundleFilePath != null;
                configParams.enableDeviceTierTargeting |= assetPack.DeviceTierToAssetPackDirectoryPath != null;
                configParams.containsInstallTimeAssetPack |=
                    assetPack.DeliveryMode == AssetPackDeliveryMode.InstallTime;
            }

            configParams.containsInstallTimeAssetPack |= options.AssetPackConfig.SplitBaseModuleAssets;
            configParams.assetOnlyOptions = options.AssetOnlyOptions;
            return configParams;
        }

        private void StartCreateBundleAsync(ThreadStart threadStart)
        {
            _progressBarWaitHandle = new EventWaitHandle(false, EventResetMode.AutoReset);
            EditorApplication.update += HandleUpdate;
            _backgroundThread = new Thread(threadStart);
            _backgroundThread.Name = "AppBundle";
            _backgroundThread.Start();
        }

        // Handler for EditorApplication.update callbacks on the main thread.
        private void HandleUpdate()
        {
            switch (_buildStatus)
            {
                case BuildStatus.Running:
                    // Checking for task cancellation during EditorApplication.update is much more responsive
                    // than only calling DisplayCancelableProgressBar() when the message changes.
                    if (EditorUtility.DisplayCancelableProgressBar(
                            "Building App Bundle", _progressBarMessage, _progressBarProgress))
                    {
                        Debug.Log("Cancelling app bundle build...");
                        EditorUtility.ClearProgressBar();
                        _canceled = true;
                        _backgroundThread.Abort();
                        _buildStatus = BuildStatus.Halted;
                    }

                    // Signal the background thread that we've handled an update since DisplayProgress() was called.
                    _progressBarWaitHandle.Set();
                    break;
                case BuildStatus.Failing:
                    EditorUtility.ClearProgressBar();
                    EditorUtility.DisplayDialog(
                        BuildToolLogger.BuildErrorTitle, _buildErrorMessage, WindowUtils.OkButtonText);
                    _buildStatus = BuildStatus.Halted;
                    break;
                case BuildStatus.Succeeding:
                    EditorUtility.ClearProgressBar();
                    _buildStatus = BuildStatus.Halted;
                    _createBundleAsyncOnSuccess(_finishedAabFilePath);
                    break;
                case BuildStatus.Halted:
                    _progressBarWaitHandle.Close();
                    EditorApplication.update -= HandleUpdate;
                    break;
                default:
                    throw new Exception("Unexpected BuildStatus: " + _buildStatus);
            }
        }

        private string CreateAssetPackModule(
            string assetPackName, AssetPack assetPack, DirectoryInfo assetPackDirectoryInfo)
        {
            var aaptErrorMessage =
                CreateAssetPackWithManifest(assetPackDirectoryInfo, assetPackName, assetPack.DeliveryMode);
            if (aaptErrorMessage != null)
            {
                // Already displayed the error.
                return aaptErrorMessage;
            }

            // Copy all assets to an "assets" subdirectory in the destination folder.
            var destinationAssetsDirectory =
                GetDestinationSubdirectory(assetPackDirectoryInfo).CreateSubdirectory(AssetsDirectoryName);
            if (assetPack.AssetBundleFilePath != null)
            {
                // Copy AssetBundle files into the module's "assets" folder, inside an "assetpack" folder.
                var outputDirectory = destinationAssetsDirectory.CreateSubdirectory(AssetPackFolder).FullName;
                File.Copy(assetPack.AssetBundleFilePath, Path.Combine(outputDirectory, assetPackName));
            }
            else if (assetPack.CompressionFormatToAssetBundleFilePath != null)
            {
                // Copy the AssetBundle files into the module's "assets" folder, inside an "assetpack#tcf_xxx" folder.
                foreach (var compressionAndFilePath in assetPack.CompressionFormatToAssetBundleFilePath)
                {
                    var targetedAssetsFolderName =
                        AssetPackFolder + TextureTargetingTools.GetTargetingSuffix(compressionAndFilePath.Key);
                    var outputDirectory = destinationAssetsDirectory.CreateSubdirectory(targetedAssetsFolderName);
                    File.Copy(compressionAndFilePath.Value, Path.Combine(outputDirectory.FullName, assetPackName));
                }
            }
            else if (assetPack.DeviceTierToAssetBundleFilePath != null)
            {
                // Copy the AssetBundle files into the module's "assets" folder, inside an "assetpack#tier_xxx" folder.
                foreach (var deviceTierAndFilePath in assetPack.DeviceTierToAssetBundleFilePath)
                {
                    var targetedAssetsFolderName =
                        AssetPackFolder + DeviceTierTargetingTools.GetTargetingSuffix(deviceTierAndFilePath.Key);
                    var outputDirectory = destinationAssetsDirectory.CreateSubdirectory(targetedAssetsFolderName);
                    File.Copy(deviceTierAndFilePath.Value, Path.Combine(outputDirectory.FullName, assetPackName));
                }
            }
            else if (assetPack.AssetPackDirectoryPath != null)
            {
                var sourceAssetsDirectory = new DirectoryInfo(assetPack.AssetPackDirectoryPath);
                if (!sourceAssetsDirectory.Exists)
                {
                    // TODO: check this earlier.
                    return DisplayBuildError("Missing directory for " + assetPackName, sourceAssetsDirectory.FullName);
                }

                // Copy asset pack files into the module's "assets" folder, inside an "assetpack" folder.
                var outputDirectory = destinationAssetsDirectory.CreateSubdirectory(AssetPackFolder);
                CopyFilesRecursively(sourceAssetsDirectory, outputDirectory);
            }
            else if (assetPack.CompressionFormatToAssetPackDirectoryPath != null)
            {
                // Copy asset pack files into the module's "assets" folder, inside an "assetpack#tcf_xxx" folder.
                foreach (var compressionAndDirectoryPath in assetPack.CompressionFormatToAssetPackDirectoryPath)
                {
                    var sourceAssetsDirectory = new DirectoryInfo(compressionAndDirectoryPath.Value);
                    if (!sourceAssetsDirectory.Exists)
                    {
                        // TODO: check this earlier.
                        return DisplayBuildError(
                            "Missing directory for " + assetPackName, sourceAssetsDirectory.FullName);
                    }

                    var targetedAssetsFolderName =
                        AssetPackFolder + TextureTargetingTools.GetTargetingSuffix(compressionAndDirectoryPath.Key);
                    var outputDirectory = destinationAssetsDirectory.CreateSubdirectory(targetedAssetsFolderName);
                    CopyFilesRecursively(sourceAssetsDirectory, outputDirectory);
                }
            }
            else if (assetPack.DeviceTierToAssetPackDirectoryPath != null)
            {
                // Copy asset pack files into the module's "assets" folder, inside an "assetpack#tier_xxx" folder.
                foreach (var deviceTierAndDirectoryPath in assetPack.DeviceTierToAssetPackDirectoryPath)
                {
                    var sourceAssetsDirectory = new DirectoryInfo(deviceTierAndDirectoryPath.Value);
                    if (!sourceAssetsDirectory.Exists)
                    {
                        // TODO: check this earlier.
                        return DisplayBuildError("Missing directory for " + assetPackName,
                            sourceAssetsDirectory.FullName);
                    }

                    var targetedAssetsFolderName =
                        AssetPackFolder + DeviceTierTargetingTools.GetTargetingSuffix(deviceTierAndDirectoryPath.Key);
                    var outputDirectory = destinationAssetsDirectory.CreateSubdirectory(targetedAssetsFolderName);
                    CopyFilesRecursively(sourceAssetsDirectory, outputDirectory);
                }
            }
            else
            {
                throw new InvalidOperationException("Missing asset pack files: " + assetPackName);
            }

            return null;
        }

        private void MoveSymbolsZipFile(string aabFilePath)
        {
            var outputDirectoryPath = Path.GetDirectoryName(aabFilePath);
            if (_workingDirectoryPath == outputDirectoryPath)
            {
                // If the temporary player file and final output file are in the same directory, don't move the symbols.
                // (This is likely a Build & Run.)
                return;
            }

            var symbolsFilePath = FindSymbolsFile(_workingDirectoryPath);
            if (symbolsFilePath == null)
            {
                return;
            }

            var outputSymbolsFileName = GetSymbolsFileName(Path.GetFileNameWithoutExtension(aabFilePath));
            var outputSymbolsFilePath = Path.Combine(outputDirectoryPath, outputSymbolsFileName);
            if (File.Exists(outputSymbolsFilePath))
            {
                // If the symbols file already exists, we need to delete it first.
                File.Delete(outputSymbolsFilePath);
            }
            File.Move(symbolsFilePath, outputSymbolsFilePath);
        }

        private string FindSymbolsFile(string path)
        {
            var pattern = string.Format("{0}-{1}-v{2}*.symbols.zip", AndroidPlayerFilePrefix, _versionName, _versionCode);
            var matchingFiles = Directory.EnumerateFiles(path, pattern).ToList();
            if (matchingFiles.Count == 0)
            {
                // The file won't exist for Mono builds or if EditorUserBuildSettings.androidCreateSymbolsZip is false.
                return null;
            }
            if (matchingFiles.Count > 1) {
                throw new Exception("Expected to find only one file matching " + pattern + ", but found " + matchingFiles.Count);
            }
            return matchingFiles[0];
        }

        private string GetSymbolsFileName(string prefix)
        {
            return string.Format("{0}-{1}-v{2}.symbols.zip", prefix, _versionName, _versionCode);
        }

        private static void CopyFilesRecursively(DirectoryInfo sourceDirectory, DirectoryInfo destinationDirectory)
        {
            foreach (var sourceSubdirectory in sourceDirectory.GetDirectories())
            {
                var destinationSubdirectory = destinationDirectory.CreateSubdirectory(sourceSubdirectory.Name);
                CopyFilesRecursively(sourceSubdirectory, destinationSubdirectory);
            }

            foreach (var file in sourceDirectory.GetFiles())
            {
                file.CopyTo(Path.Combine(destinationDirectory.FullName, file.Name), false);
            }
        }

        private XDocument CreateAssetPackManifestXDocument(string packName, AssetPackDeliveryMode deliveryMode)
        {
            if (_assetPackManifestTransformers == null)
            {
                throw new BuildToolNotInitializedException(this);
            }

            var doc = AssetPackManifestHelper.CreateAssetPackManifestXDocument(
                _packageName,
                packName,
                deliveryMode);
            foreach (var transformer in _assetPackManifestTransformers)
            {
                var error = transformer.Transform(doc);
                if (!string.IsNullOrEmpty(error))
                {
                    DisplayBuildError("AndroidManifest configuration", error);
                    break;
                }
            }

            return doc;
        }

        private string CreateBaseModule(DirectoryInfo baseWorkingDirectory, out IList<string> bundleMetadata)
        {
            DisplayProgress("Creating base module", ProgressCreateBaseModule);

            var sourceDirectoryInfo = baseWorkingDirectory.CreateSubdirectory("source");
            var unzipErrorMessage = _zipUtils.UnzipFile(AndroidPlayerFilePath, sourceDirectoryInfo.FullName);
            if (unzipErrorMessage != null)
            {
                bundleMetadata = null;
                return DisplayBuildError("Unzip", unzipErrorMessage);
            }

            bundleMetadata = GetExistingBundleMetadata(sourceDirectoryInfo);

            var destinationDirectoryInfo = GetDestinationSubdirectory(baseWorkingDirectory);
            var baseModuleDirectories = sourceDirectoryInfo.GetDirectories(AndroidAppBundle.BaseModuleName);
            if (baseModuleDirectories.Length != 1)
            {
                return DisplayBuildError("Find base directory", sourceDirectoryInfo.FullName);
            }

            ArrangeFilesForExistingModule(baseModuleDirectories[0], destinationDirectoryInfo);
            return null;
        }

        private string CreateSplitBaseModule(DirectoryInfo baseDirectory, DirectoryInfo splitBaseDirectory)
        {
            var aaptErrorMessage =
                CreateAssetPackWithManifest(
                    splitBaseDirectory, AndroidAppBundle.BaseAssetsModuleName, AssetPackDeliveryMode.InstallTime);
            if (aaptErrorMessage != null)
            {
                // Already displayed the error.
                return aaptErrorMessage;
            }

            var baseDestination = GetDestinationSubdirectory(baseDirectory);
            var baseAssetsDirectories = baseDestination.GetDirectories(AssetsDirectoryName);
            if (baseAssetsDirectories.Length != 1)
            {
                return DisplayBuildError("Find base assets directory",
                    string.Format("Expected 1 directory but found {0}", baseAssetsDirectories.Length));
            }

            var splitBaseDestination = GetDestinationSubdirectory(splitBaseDirectory);
            var splitBaseAssetsPath = Path.Combine(splitBaseDestination.FullName, AssetsDirectoryName);
            baseAssetsDirectories[0].MoveTo(splitBaseAssetsPath);

            // IL2CPP build crash unless the assets/bin/Data/Managed folder exists in the base apk.
            // Create an empty file in that folder to ensure that folder is present.
            var requiredDirPath = Path.Combine(baseDestination.FullName, RequiredDirectoryName);
            var requiredDirInfo = Directory.CreateDirectory(requiredDirPath);
            File.Create(Path.Combine(requiredDirInfo.FullName, ".keep_folder")).Close();

            return null;
        }

        // Don't support certain versions of Unity due to the "Failed to load 'libmain.so'" crash.
        // See https://github.com/google/play-unity-plugins/issues/80 and
        // https://issuetracker.unity3d.com/issues/android-app-installed-using-apk-from-app-bundle-option-in-android-studio-fails-to-run
        private static bool CheckUnityVersion(BuildToolLogger buildToolLogger)
        {
#if UNITY_2020_2 || UNITY_2020_3_0 || UNITY_2020_3_1 || UNITY_2020_3_2 || UNITY_2021_1_0 || UNITY_2021_1_1
            buildToolLogger.DisplayErrorDialog(
                "Apps built as AABs with this version of Unity may crash at runtime. Upgrade to 2020.3.3f1, 2021.1.2f1, or later to avoid this issue.");
            return false;
#else
            return true;
#endif
        }

        private string CreateAssetPackWithManifest(
            DirectoryInfo rootDirectory, string assetPackName, AssetPackDeliveryMode deliveryMode)
        {
            var androidManifestFilePath = Path.Combine(rootDirectory.FullName, AndroidManifestFileName);
            var assetPackManifestXDocument = CreateAssetPackManifestXDocument(assetPackName, deliveryMode);
            assetPackManifestXDocument.Save(androidManifestFilePath);

            var source = rootDirectory.CreateSubdirectory("manifest");
            var aaptErrorMessage = _androidAssetPackagingTool.Link(androidManifestFilePath, source.FullName);
            if (aaptErrorMessage != null)
            {
                return DisplayBuildError("AAPT2 link " + assetPackName, aaptErrorMessage);
            }

            // aapt2 link creates an empty resource table even though asset packs have no resources.
            // Bundletool fails if the asset pack has a resources.pb. Only retain the AndroidManifest.xml file.
            var destination = GetDestinationSubdirectory(rootDirectory);
            foreach (var sourceFileInfo in source.GetFiles())
            {
                if (sourceFileInfo.Name == AndroidManifestFileName)
                {
                    var destinationSubdirectory = destination.CreateSubdirectory(ManifestDirectoryName);
                    sourceFileInfo.MoveTo(Path.Combine(destinationSubdirectory.FullName, sourceFileInfo.Name));
                }
            }

            return null;
        }

        private static void ArrangeFilesForExistingModule(DirectoryInfo source, DirectoryInfo destination)
        {
            // Only include the resources.pb file located directly in the source directory.
            foreach (var sourceFileInfo in source.GetFiles())
            {
                if (sourceFileInfo.Name == ResourceTableFileName)
                {
                    sourceFileInfo.MoveTo(Path.Combine(destination.FullName, ResourceTableFileName));
                }
            }

            // Include all directories located in the source directory.
            foreach (var sourceDirectoryInfo in source.GetDirectories())
            {
                sourceDirectoryInfo.MoveTo(Path.Combine(destination.FullName, sourceDirectoryInfo.Name));
            }
        }

        /// <summary>
        /// Given the root directory of an existing AAB, return a list of all bundle metadata files in the specific
        /// format expected by bundletool.
        /// </summary>
        private static IList<string> GetExistingBundleMetadata(DirectoryInfo rootDirectoryInfo)
        {
            var bundleMetadata = new List<string>();
            var bundleMetadataDirectories = rootDirectoryInfo.GetDirectories(BundleMetadataDirectoryName);
            if (bundleMetadataDirectories.Length != 1)
            {
                return bundleMetadata;
            }

            // The metadata files are usually one directory deep, but use Directory.GetFiles() since this isn't a given.
            var bundleMetadataPath = bundleMetadataDirectories[0].FullName;
            var metadataFiles = Directory.GetFiles(bundleMetadataPath, "*", SearchOption.AllDirectories);
            foreach (var physicalFile in metadataFiles)
            {
                if (physicalFile.Substring(0, bundleMetadataPath.Length) != bundleMetadataPath)
                {
                    throw new InvalidOperationException(
                        "A bundle metadata file doesn't match the expected parent path: " + physicalFile);
                }

                var startIndex = bundleMetadataPath.Length + 1;
                var relativePath = physicalFile.Substring(startIndex, physicalFile.Length - startIndex);
                // Expected format is "<bundle-path>:<physical-file>". "<bundle-path>" uses "/" directory separators.
                var bundlePath = relativePath.Replace("\\", "/");
                bundleMetadata.Add(string.Format("{0}:{1}", bundlePath, physicalFile));
            }

            return bundleMetadata;
        }

        private static DirectoryInfo GetDestinationSubdirectory(DirectoryInfo directoryInfo)
        {
            return directoryInfo.CreateSubdirectory("destination");
        }

        private void DisplayProgress(string info, float progress)
        {
            Debug.LogFormat("{0}...", info);
            if (_backgroundThread == null)
            {
                // Running synchronously in batch mode.
                return;
            }

            _progressBarMessage = info;
            _progressBarProgress = progress;

            // Wait for the main thread to display the message in at least one callback of the update handler.
            // Give up after a short timeout since this is best effort, and we don't want to block indefinitely.
            _progressBarWaitHandle.WaitOne(ProgressBarWaitHandleTimeoutMs);
        }

        private string DisplayBuildError(string errorType, string errorMessage)
        {
            if (_buildStatus == BuildStatus.Halted)
            {
                // Ignore any errors after we've halted, e.g. if the thread abort causes an exception to be thrown.
                return null;
            }

            _buildStatus = BuildStatus.Failing;
            _buildErrorMessage = string.Format("{0} failed: {1}", errorType, errorMessage);
            Debug.LogError(_buildErrorMessage);
            return _buildErrorMessage;
        }
    }
}