// Copyright 2016-2019, Pulumi Corporation

using System;
using System.Collections.Immutable;
using System.Diagnostics.CodeAnalysis;
using Google.Protobuf.Collections;
using Google.Protobuf.WellKnownTypes;

namespace Pulumi.Serialization
{
    internal static class Deserializer
    {
        private static OutputData<T> DeserializeCore<T>(Value value, Func<Value, OutputData<T>> func)
        {
            var (innerVal, isSecret) = UnwrapSecret(value);
            value = innerVal;

            if (value.KindCase == Value.KindOneofCase.StringValue &&
                value.StringValue == Constants.UnknownValue)
            {
                // always deserialize unknown as the null value.
                return new OutputData<T>(ImmutableHashSet<Resource>.Empty, default!, isKnown: false, isSecret);
            }

            if (TryDeserializeAssetOrArchive(value, out var assetOrArchive))
            {
                return new OutputData<T>(ImmutableHashSet<Resource>.Empty, (T)(object)assetOrArchive, isKnown: true, isSecret);
            }

            var innerData = func(value);
            return OutputData.Create(innerData.Resources, innerData.Value, innerData.IsKnown, isSecret || innerData.IsSecret);
        }

        private static OutputData<T> DeserializeOneOf<T>(Value value, Value.KindOneofCase kind, Func<Value, OutputData<T>> func)
            => DeserializeCore(value, v =>
                v.KindCase == kind ? func(v) : throw new InvalidOperationException($"Trying to deserialize {v.KindCase} as a {kind}"));

        private static OutputData<T> DeserializePrimitive<T>(Value value, Value.KindOneofCase kind, Func<Value, T> func)
            => DeserializeOneOf(value, kind, v => OutputData.Create(
                ImmutableHashSet<Resource>.Empty, func(v), isKnown: true, isSecret: false));

        private static OutputData<bool> DeserializeBoolean(Value value)
            => DeserializePrimitive(value, Value.KindOneofCase.BoolValue, v => v.BoolValue);

        private static OutputData<string> DeserializerString(Value value)
            => DeserializePrimitive(value, Value.KindOneofCase.StringValue, v => v.StringValue);

        private static OutputData<double> DeserializerDouble(Value value)
            => DeserializePrimitive(value, Value.KindOneofCase.NumberValue, v => v.NumberValue);

        private static OutputData<ImmutableArray<object?>> DeserializeList(Value value)
            => DeserializeOneOf(value, Value.KindOneofCase.ListValue,
                v =>
                {
                    var resources = ImmutableHashSet.CreateBuilder<Resource>();
                    var result = ImmutableArray.CreateBuilder<object?>();
                    var isKnown = true;
                    var isSecret = false;

                    foreach (var element in v.ListValue.Values)
                    {
                        var elementData = Deserialize(element);
                        (isKnown, isSecret) = OutputData.Combine(elementData, isKnown, isSecret);
                        resources.UnionWith(elementData.Resources);
                        result.Add(elementData.Value);
                    }

                    return OutputData.Create(resources.ToImmutable(), result.ToImmutable(), isKnown, isSecret);
                });

        private static OutputData<ImmutableDictionary<string, object?>> DeserializeStruct(Value value)
            => DeserializeOneOf(value, Value.KindOneofCase.StructValue,
                v =>
                {
                    var resources = ImmutableHashSet.CreateBuilder<Resource>();
                    var result = ImmutableDictionary.CreateBuilder<string, object?>();
                    var isKnown = true;
                    var isSecret = false;

                    foreach (var (key, element) in v.StructValue.Fields)
                    {
                        // Unilaterally skip properties considered internal by the Pulumi engine.
                        // These don't actually contribute to the exposed shape of the object, do
                        // not need to be passed back to the engine, and often will not match the
                        // expected type we are deserializing into.
                        if (key.StartsWith("__", StringComparison.Ordinal))
                            continue;

                        var elementData = Deserialize(element);
                        (isKnown, isSecret) = OutputData.Combine(elementData, isKnown, isSecret);
                        result.Add(key, elementData.Value);
                        resources.UnionWith(elementData.Resources);
                    }

                    return OutputData.Create(resources.ToImmutable(), result.ToImmutable(), isKnown, isSecret);
                });

        public static OutputData<object?> Deserialize(Value value)
            => DeserializeCore(value, 
                v => v.KindCase switch
                {
                    Value.KindOneofCase.NumberValue => DeserializerDouble(v),
                    Value.KindOneofCase.StringValue => DeserializerString(v),
                    Value.KindOneofCase.BoolValue => DeserializeBoolean(v),
                    Value.KindOneofCase.StructValue => DeserializeStruct(v),
                    Value.KindOneofCase.ListValue => DeserializeList(v),
                    Value.KindOneofCase.NullValue => new OutputData<object?>(ImmutableHashSet<Resource>.Empty, null, isKnown: true, isSecret: false),
                    Value.KindOneofCase.None => throw new InvalidOperationException("Should never get 'None' type when deserializing protobuf"),
                    _ => throw new InvalidOperationException("Unknown type when deserializing protobuf: " + v.KindCase),
                });

        private static (Value unwrapped, bool isSecret) UnwrapSecret(Value value)
        {
            var isSecret = false;

            while (IsSpecialStruct(value, out var sig) &&
                   sig == Constants.SpecialSecretSig)
            {
                if (!value.StructValue.Fields.TryGetValue(Constants.SecretValueName, out var secretValue))
                    throw new InvalidOperationException("Secrets must have a field called 'value'");

                isSecret = true;
                value = secretValue;
            }

            return (value, isSecret);
        }

        private static bool IsSpecialStruct(
            Value value, [NotNullWhen(true)] out string? sig)
        {
            if (value.KindCase == Value.KindOneofCase.StructValue &&
                value.StructValue.Fields.TryGetValue(Constants.SpecialSigKey, out var sigVal) &&
                sigVal.KindCase == Value.KindOneofCase.StringValue)
            {
                sig = sigVal.StringValue;
                return true;
            }

            sig = null;
            return false;
        }

        private static bool TryDeserializeAssetOrArchive(
            Value value, [NotNullWhen(true)] out AssetOrArchive? assetOrArchive)
        {
            if (IsSpecialStruct(value, out var sig))
            {
                if (sig == Constants.SpecialAssetSig)
                {
                    assetOrArchive = DeserializeAsset(value);
                    return true;
                }
                else if (sig == Constants.SpecialArchiveSig)
                {
                    assetOrArchive = DeserializeArchive(value);
                    return true;
                }
            }

            assetOrArchive = null;
            return false;
        }

        private static Archive DeserializeArchive(Value value)
        {
            if (TryGetStringValue(value.StructValue.Fields, Constants.AssetOrArchivePathName, out var path))
                return new FileArchive(path);

            if (TryGetStringValue(value.StructValue.Fields, Constants.AssetOrArchiveUriName, out var uri))
                return new RemoteArchive(uri);

            if (value.StructValue.Fields.TryGetValue(Constants.ArchiveAssetsName, out var assetsValue))
            {
                if (assetsValue.KindCase == Value.KindOneofCase.StructValue)
                {
                    var assets = ImmutableDictionary.CreateBuilder<string, AssetOrArchive>();
                    foreach (var (name, val) in assetsValue.StructValue.Fields)
                    {
                        if (!TryDeserializeAssetOrArchive(val, out var innerAssetOrArchive))
                            throw new InvalidOperationException("AssetArchive contained an element that wasn't itself an Asset or Archive.");

                        assets[name] = innerAssetOrArchive;
                    }

                    return new AssetArchive(assets.ToImmutable());
                }
            }

            throw new InvalidOperationException("Value was marked as Archive, but did not conform to required shape.");
        }

        private static Asset DeserializeAsset(Value value)
        {
            if (TryGetStringValue(value.StructValue.Fields, Constants.AssetOrArchivePathName, out var path))
                return new FileAsset(path);

            if (TryGetStringValue(value.StructValue.Fields, Constants.AssetOrArchiveUriName, out var uri))
                return new RemoteAsset(uri);

            if (TryGetStringValue(value.StructValue.Fields, Constants.AssetTextName, out var text))
                return new StringAsset(text);

            throw new InvalidOperationException("Value was marked as Asset, but did not conform to required shape.");
        }

        private static bool TryGetStringValue(
            MapField<string, Value> fields, string keyName, [NotNullWhen(true)] out string? result)
        {
            if (fields.TryGetValue(keyName, out var value) &&
                value.KindCase == Value.KindOneofCase.StringValue)
            {
                result = value.StringValue;
                return true;
            }

            result = null;
            return false;
        }
    }
}
