// Copyright 2016-2021, Pulumi Corporation

using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Threading.Tasks;
using Google.Protobuf.WellKnownTypes;

namespace Pulumi
{
    public partial class Deployment
    {
        private async Task<PrepareResult> PrepareResourceAsync(
            string label, Resource res, bool custom, bool remote,
            ResourceArgs args, ResourceOptions options)
        {
            /* IMPORTANT!  We should never await prior to this line, otherwise the Resource will be partly uninitialized. */

            // Before we can proceed, all our dependencies must be finished.
            var type = res.GetResourceType();
            var name = res.GetResourceName();

            LogExcessive($"Gathering explicit dependencies: t={type}, name={name}, custom={custom}, remote={remote}");
            var explicitDirectDependencies = new HashSet<Resource>(
                await GatherExplicitDependenciesAsync(options.DependsOn).ConfigureAwait(false));
            LogExcessive($"Gathered explicit dependencies: t={type}, name={name}, custom={custom}, remote={remote}");

            // Serialize out all our props to their final values.  In doing so, we'll also collect all
            // the Resources pointed to by any Dependency objects we encounter, adding them to 'propertyDependencies'.
            LogExcessive($"Serializing properties: t={type}, name={name}, custom={custom}, remote={remote}");
            var dictionary = await args.ToDictionaryAsync().ConfigureAwait(false);
            var (serializedProps, propertyToDirectDependencies) =
                await SerializeResourcePropertiesAsync(
                        label,
                        dictionary,
                        await this.MonitorSupportsResourceReferences().ConfigureAwait(false)).ConfigureAwait(false);
            LogExcessive($"Serialized properties: t={type}, name={name}, custom={custom}, remote={remote}");

            // Wait for the parent to complete.
            // If no parent was provided, parent to the root resource.
            LogExcessive($"Getting parent urn: t={type}, name={name}, custom={custom}, remote={remote}");
            var parentURN = options.Parent != null
                ? await options.Parent.Urn.GetValueAsync().ConfigureAwait(false)
                : await GetRootResourceAsync(type).ConfigureAwait(false);
            LogExcessive($"Got parent urn: t={type}, name={name}, custom={custom}, remote={remote}");

            string? providerRef = null;
            if (custom)
            {
                var customOpts = options as CustomResourceOptions;
                providerRef = await ProviderResource.RegisterAsync(customOpts?.Provider).ConfigureAwait(false);
            }

            var providerRefs = new Dictionary<string, string>();
            if (remote)
            {
                var componentOpts = options as ComponentResourceOptions;
                if (componentOpts != null)
                {
                    // If only the Provider opt is set, move it to the Providers list for further processing.
                    if (componentOpts.Provider != null && componentOpts.Providers.Count == 0)
                    {
                        componentOpts.Providers.Add(componentOpts.Provider);
                        componentOpts.Provider = null;
                    }

                    foreach (var provider in componentOpts.Providers)
                    {
                        var pref = await ProviderResource.RegisterAsync(provider).ConfigureAwait(false);
                        if (pref != null)
                        {
                            providerRefs.Add(provider.Package, pref);
                        }
                    }
                }
            }

            // Collect the URNs for explicit/implicit dependencies for the engine so that it can understand
            // the dependency graph and optimize operations accordingly.

            // The list of all dependencies (implicit or explicit).
            var allDirectDependencies = new HashSet<Resource>(explicitDirectDependencies);

            var allDirectDependencyURNs = await GetAllTransitivelyReferencedCustomResourceURNsAsync(explicitDirectDependencies).ConfigureAwait(false);
            var propertyToDirectDependencyURNs = new Dictionary<string, HashSet<string>>();

            foreach (var (propertyName, directDependencies) in propertyToDirectDependencies)
            {
                allDirectDependencies.AddRange(directDependencies);

                var urns = await GetAllTransitivelyReferencedCustomResourceURNsAsync(directDependencies).ConfigureAwait(false);
                allDirectDependencyURNs.AddRange(urns);
                propertyToDirectDependencyURNs[propertyName] = urns;
            }

            // Wait for all aliases. Note that we use 'res._aliases' instead of 'options.aliases' as
            // the former has been processed in the Resource constructor prior to calling
            // 'registerResource' - both adding new inherited aliases and simplifying aliases down
            // to URNs.
            var aliases = new List<string>();
            var uniqueAliases = new HashSet<string>();
            foreach (var alias in res._aliases)
            {
                var aliasVal = await alias.ToOutput().GetValueAsync().ConfigureAwait(false);
                if (uniqueAliases.Add(aliasVal))
                {
                    aliases.Add(aliasVal);
                }
            }

            return new PrepareResult(
                serializedProps,
                parentURN ?? "",
                providerRef ?? "",
                providerRefs,
                allDirectDependencyURNs,
                propertyToDirectDependencyURNs,
                aliases);

            void LogExcessive(string message)
            {
                if (_excessiveDebugOutput)
                    Log.Debug(message);
            }
        }

        private static Task<ImmutableArray<Resource>> GatherExplicitDependenciesAsync(InputList<Resource> resources)
            => resources.ToOutput().GetValueAsync();

        private static async Task<HashSet<string>> GetAllTransitivelyReferencedCustomResourceURNsAsync(
            HashSet<Resource> resources)
        {
            // Go through 'resources', but transitively walk through **Component** resources,
            // collecting any of their child resources.  This way, a Component acts as an
            // aggregation really of all the reachable custom resources it parents.  This walking
            // will transitively walk through other child ComponentResources, but will stop when it
            // hits custom resources.  in other words, if we had:
            //
            //              Comp1
            //              /   \
            //          Cust1   Comp2
            //                  /   \
            //              Cust2   Cust3
            //              /
            //          Cust4
            //
            // Then the transitively reachable custom resources of Comp1 will be [Cust1, Cust2,
            // Cust3]. It will *not* include 'Cust4'.

            // To do this, first we just get the transitively reachable set of resources (not diving
            // into custom resources).  In the above picture, if we start with 'Comp1', this will be
            // [Comp1, Cust1, Comp2, Cust2, Cust3]
            var transitivelyReachableResources = GetTransitivelyReferencedChildResourcesOfComponentResources(resources);

            var transitivelyReachableCustomResources = transitivelyReachableResources.OfType<CustomResource>();
            var tasks = transitivelyReachableCustomResources.Select(r => r.Urn.GetValueAsync());
            var urns = await Task.WhenAll(tasks).ConfigureAwait(false);
            return new HashSet<string>(urns);
        }

        /// <summary>
        /// Recursively walk the resources passed in, returning them and all resources reachable from
        /// <see cref="Resource.ChildResources"/> through any **Component** resources we encounter.
        /// </summary>
        private static HashSet<Resource> GetTransitivelyReferencedChildResourcesOfComponentResources(HashSet<Resource> resources)
        {
            // Recursively walk the dependent resources through their children, adding them to the result set.
            var result = new HashSet<Resource>();
            AddTransitivelyReferencedChildResourcesOfComponentResources(resources, result);
            return result;
        }

        private static void AddTransitivelyReferencedChildResourcesOfComponentResources(HashSet<Resource> resources, HashSet<Resource> result)
        {
            foreach (var resource in resources)
            {
                if (result.Add(resource))
                {
                    if (resource is ComponentResource)
                    {
                        HashSet<Resource> childResources;
                        lock (resource.ChildResources)
                        {
                            childResources = new HashSet<Resource>(resource.ChildResources);
                        }
                        AddTransitivelyReferencedChildResourcesOfComponentResources(childResources, result);
                    }
                }
            }
        }

        private struct PrepareResult
        {
            public readonly Struct SerializedProps;
            public readonly string ParentUrn;
            public readonly string ProviderRef;
            public readonly Dictionary<string, string> ProviderRefs;
            public readonly HashSet<string> AllDirectDependencyURNs;
            public readonly Dictionary<string, HashSet<string>> PropertyToDirectDependencyURNs;
            public readonly List<string> Aliases;

            public PrepareResult(Struct serializedProps, string parentUrn, string providerRef, Dictionary<string, string> providerRefs, HashSet<string> allDirectDependencyURNs, Dictionary<string, HashSet<string>> propertyToDirectDependencyURNs, List<string> aliases)
            {
                SerializedProps = serializedProps;
                ParentUrn = parentUrn;
                ProviderRef = providerRef;
                ProviderRefs = providerRefs;
                AllDirectDependencyURNs = allDirectDependencyURNs;
                PropertyToDirectDependencyURNs = propertyToDirectDependencyURNs;
                Aliases = aliases;
            }
        }
    }
}
