// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Text.Json;
using Microsoft.TypeSpec.Generator.Input;
using Microsoft.TypeSpec.Generator.Input.Extensions;
using Microsoft.TypeSpec.Generator.Primitives;
using Microsoft.TypeSpec.Generator.Providers;

namespace Microsoft.TypeSpec.Generator
{
    public class TypeFactory
    {
        private ChangeTrackingListDefinition ChangeTrackingListProvider { get; } = new();

        private ChangeTrackingDictionaryDefinition ChangeTrackingDictionaryProvider { get; } = new();

        private Dictionary<InputModelType, ModelProvider?> InputTypeToModelProvider { get; } = [];

        public IDictionary<CSharpType, TypeProvider?> CSharpTypeMap { get; } = new Dictionary<CSharpType, TypeProvider?>(CSharpType.IgnoreNullableComparer);

        // Maps C# type names to TypeProviders for efficient lookup when resolving types by name
        internal IDictionary<string, TypeProvider> TypeProvidersByName { get; } = new Dictionary<string, TypeProvider>();

        private Dictionary<EnumCacheKey, EnumProvider?> EnumCache { get; } = [];

        private Dictionary<InputType, CSharpType?> TypeCache { get; } = [];
        private Dictionary<InputSerializationOptions, SerializationOptions?> SerializationOptionsCache { get; } = [];

        private Dictionary<InputProperty, PropertyProvider?> PropertyCache { get; } = [];

        private IReadOnlyList<LibraryVisitor> Visitors => CodeModelGenerator.Instance.Visitors;
        private Dictionary<InputType, IReadOnlyList<TypeProvider>> SerializationsCache { get; } = [];

        internal HashSet<string> UnionVariantTypesToKeep { get; } = [];

        protected internal TypeFactory()
        {
        }

        public CSharpType? CreateCSharpType(InputType inputType)
        {
            if (TypeCache.TryGetValue(inputType, out var type))
            {
                return type;
            }

            type = CreateCSharpTypeCore(inputType);
            TypeCache[inputType] = type;
            return type;
        }

        protected internal virtual Type? CreateFrameworkType(string fullyQualifiedTypeName)
        {
            return fullyQualifiedTypeName switch
            {
                // Special case for types that would not be defined in corlib, but should still be considered framework types.
                "System.BinaryData" => typeof(BinaryData),
                "System.Uri" => typeof(Uri),
                "System.Text.Json.JsonElement" => typeof(JsonElement),
                "System.Net.IPAddress" => typeof(IPAddress),
                _ => Type.GetType(fullyQualifiedTypeName)
            };
        }

        protected virtual CSharpType? CreateCSharpTypeCore(InputType inputType)
        {
            // Check if this type has external type information
            if (inputType.External != null)
            {
                return CreateExternalType(inputType.External);
            }

            CSharpType? type;
            switch (inputType)
            {
                case InputLiteralType literalType:
                    var input = CreateCSharpType(literalType.ValueType);
                    type = input != null ? CSharpType.FromLiteral(input, literalType.Value) : null;
                    break;
                case InputEnumTypeValue enumValueType:
                    // for enum value, we redirect to its corresponding enum type as a literal
                    var enumValue = CreateCSharpType(enumValueType.EnumType);
                    type = enumValue != null ? CSharpType.FromLiteral(enumValue, enumValueType.Value) : null;
                    break;
                case InputUnionType unionType:
                    var unionInputs = new List<CSharpType>();
                    foreach (var variant in unionType.VariantTypes)
                    {
                        var unionInput = CreateCSharpType(variant);
                        if (unionInput != null)
                        {
                            unionInputs.Add(unionInput);
                            // we only keep the type if it is not framework type and not literal
                            if (!unionInput.IsFrameworkType && !unionInput.IsLiteral)
                            {
                                UnionVariantTypesToKeep.Add(unionInput.Name);
                            }
                        }
                    }
                    type = CSharpType.FromUnion(unionInputs);
                    break;
                case InputArrayType listType:
                    var arrayInput = CreateCSharpType(listType.ValueType);
                    type = arrayInput != null ? new CSharpType(typeof(IList<>), arrayInput) : null;
                    break;
                case InputDictionaryType dictionaryType:
                    var inputValueType = CreateCSharpType(dictionaryType.ValueType);
                    type = inputValueType != null ? new CSharpType(typeof(IDictionary<,>), typeof(string), inputValueType) : null;
                    break;
                case InputEnumType enumType:
                    type = CreateEnum(enumType)?.Type;
                    break;
                case InputModelType modelType:
                    type = CreateModel(modelType)?.Type;
                    break;
                case InputNullableType nullableType:
                    type = CreateCSharpType(nullableType.Type)?.WithNullable(true);
                    break;
                default:
                    type = CreatePrimitiveCSharpTypeCore(inputType);
                    break;
            }

            return type;
        }

        /// <summary>
        /// Factory method for creating a <see cref="CSharpType"/> based on an input type <paramref name="inputType"/>.
        /// </summary>
        /// <param name="inputType">The <see cref="InputType"/> to convert.</param>
        /// <returns>An instance of <see cref="CSharpType"/>.</returns>
        internal static Type CreatePrimitiveCSharpTypeCore(InputType inputType) => inputType switch
        {
            InputPrimitiveType primitiveType => primitiveType.Kind switch
            {
                InputPrimitiveTypeKind.Boolean => typeof(bool),
                InputPrimitiveTypeKind.Bytes => typeof(BinaryData),
                InputPrimitiveTypeKind.PlainDate => typeof(DateTimeOffset),
                InputPrimitiveTypeKind.Decimal => typeof(decimal),
                InputPrimitiveTypeKind.Decimal128 => typeof(decimal),
                InputPrimitiveTypeKind.PlainTime => typeof(TimeSpan),
                InputPrimitiveTypeKind.Float32 => typeof(float),
                InputPrimitiveTypeKind.Float64 => typeof(double),
                InputPrimitiveTypeKind.Int8 => typeof(sbyte),
                InputPrimitiveTypeKind.UInt8 => typeof(byte),
                InputPrimitiveTypeKind.Int32 => typeof(int),
                InputPrimitiveTypeKind.Int64 => typeof(long),
                InputPrimitiveTypeKind.SafeInt => typeof(long),
                InputPrimitiveTypeKind.Integer => typeof(long), // in typespec, integer is the base type of int related types, see type relation: https://typespec.io/docs/language-basics/type-relations
                InputPrimitiveTypeKind.Float => typeof(double), // in typespec, float is the base type of float32 and float64, see type relation: https://typespec.io/docs/language-basics/type-relations
                InputPrimitiveTypeKind.Numeric => typeof(double), // in typespec, numeric is the base type of number types, see type relation: https://typespec.io/docs/language-basics/type-relations
                InputPrimitiveTypeKind.Stream => typeof(Stream),
                InputPrimitiveTypeKind.String => typeof(string),
                InputPrimitiveTypeKind.Url => typeof(Uri),
                InputPrimitiveTypeKind.Unknown => typeof(BinaryData),
                _ => typeof(object),
            },
            InputDateTimeType dateTimeType => typeof(DateTimeOffset),
            InputDurationType durationType => typeof(TimeSpan),
            _ => throw new InvalidOperationException($"Unknown type: {inputType}")
        };

        /// <summary>
        /// Factory method for creating a <see cref="TypeProvider"/> based on an <see cref="InputModelType"> <paramref name="model"/>.
        /// </summary>
        /// <param name="model">The <see cref="InputModelType"/> to convert.</param>
        /// <returns>An instance of <see cref="TypeProvider"/>.</returns>
        public ModelProvider? CreateModel(InputModelType model)
        {
            if (InputTypeToModelProvider.TryGetValue(model, out var modelProvider))
                return modelProvider;

            // Add sentinel before construction to prevent re-entrant creation of the same model
            // (e.g., when BuildBaseModelProvider triggers CreateModel for all input models).
            InputTypeToModelProvider[model] = null;

            modelProvider = CreateModelCore(model);

            foreach (var visitor in Visitors)
            {
                modelProvider = visitor.PreVisitModel(model, modelProvider);
            }

            InputTypeToModelProvider[model] = modelProvider;

            if (modelProvider != null)
            {
                CSharpTypeMap[modelProvider.Type] = modelProvider;
                TypeProvidersByName[modelProvider.Type.Name] = modelProvider;
            }
            return modelProvider;
        }

        protected virtual ModelProvider? CreateModelCore(InputModelType model) => new ModelProvider(model);

        /// <summary>
        /// Factory method for creating a <see cref="TypeProvider"/> based on an <see cref="InputEnumType"> <paramref name="enumType"/>.
        /// </summary>
        /// <param name="enumType">The <see cref="InputEnumType"/> to convert.</param>
        /// <param name="declaringType"/> The declaring <see cref="TypeProvider".</param>
        /// <returns>An instance of <see cref="EnumProvider"/>.</returns>
        public EnumProvider? CreateEnum(InputEnumType enumType, TypeProvider? declaringType = null)
        {
            var enumCacheKey = new EnumCacheKey(enumType, declaringType);
            if (EnumCache.TryGetValue(enumCacheKey, out var enumProvider))
                return enumProvider;

            enumProvider = CreateEnumCore(enumType, declaringType);

            foreach (var visitor in Visitors)
            {
                enumProvider = visitor.PreVisitEnum(enumType, enumProvider);
                // visit the linked enum variants
                if (enumProvider is FixedEnumProvider)
                {
                    enumProvider.ExtensibleEnumView = visitor.PreVisitEnum(enumType, enumProvider.ExtensibleEnumView);
                }
                else if (enumProvider is ExtensibleEnumProvider)
                {
                    enumProvider.FixedEnumView = visitor.PreVisitEnum(enumType, enumProvider.FixedEnumView);
                }
            }

            if (enumProvider == null)
            {
                EnumCache.TryAdd(enumCacheKey, null);
                return null;
            }

            // Check to see if there is custom code that customizes the enum
            enumProvider = enumProvider.CustomCodeView switch
            {
                { Type: { IsValueType: true, IsStruct: true } } => enumProvider.ExtensibleEnumView ?? enumProvider,
                { Type: { IsValueType: true, IsStruct: false } } => enumProvider.FixedEnumView ?? enumProvider,
                _ => enumProvider,
            };

            if (enumType.Access == "public")
            {
                CodeModelGenerator.Instance.AddTypeToKeep(enumProvider);
            }

            EnumCache.Add(enumCacheKey, enumProvider);

            if (enumProvider != null)
            {
                CSharpTypeMap[enumProvider.Type] = enumProvider;
                TypeProvidersByName[enumProvider.Type.Name] = enumProvider;
            }

            return enumProvider;
        }

        protected virtual EnumProvider? CreateEnumCore(InputEnumType enumType, TypeProvider? declaringType)
            => EnumProvider.Create(enumType, declaringType);

        /// <summary>
        /// Factory method for creating a <see cref="CSharpType"/> based on external type properties.
        /// </summary>
        /// <param name="externalProperties">The <see cref="InputExternalTypeMetadata"/> to convert.</param>
        /// <returns>A <see cref="CSharpType"/> representing the external type, or null if the type cannot be resolved.</returns>
        private CSharpType? CreateExternalType(InputExternalTypeMetadata externalProperties)
        {
            // Try to create a framework type from the fully qualified name
            var frameworkType = CreateFrameworkType(externalProperties.Identity);
            if (frameworkType != null)
            {
                return new CSharpType(frameworkType);
            }

            // External types that cannot be resolved as framework types are not supported
            // Report a diagnostic to inform the user
            CodeModelGenerator.Instance.Emitter.ReportDiagnostic(
                "unsupported-external-type",
                $"External type '{externalProperties.Identity}' is not currently supported.");

            return null;
        }

        /// <summary>
        /// Factory method for creating a <see cref="ParameterProvider"/> based on an input parameter <paramref name="parameter"/>.
        /// </summary>
        /// <param name="parameter">The <see cref="InputParameter"/> to convert.</param>
        /// <returns>An instance of <see cref="ParameterProvider"/>.</returns>
        public ParameterProvider? CreateParameter(InputParameter parameter)
            => CreateParameterCore(parameter);

        protected virtual ParameterProvider? CreateParameterCore(InputParameter parameter)
            => new ParameterProvider(parameter);

        /// <summary>
        /// Creates a <see cref="PropertyProvider"/> based on an input property <paramref name="property"/>.
        /// </summary>
        /// <param name="property">The input property.</param>
        /// <returns>The property provider.</returns>
        public PropertyProvider? CreateProperty(InputProperty property, TypeProvider enclosingType)
        {
            if (PropertyCache.TryGetValue(property, out var propertyProvider))
                return propertyProvider;

            propertyProvider = CreatePropertyCore(property, enclosingType);
            PropertyCache.Add(property, propertyProvider);
            return propertyProvider;
        }

        /// <summary>
        /// Factory method for creating a <see cref="PropertyProvider"/> based on an input property <paramref name="property"/>.
        /// </summary>
        /// <param name="property">The input model property.</param>
        /// <param name="enclosingType">The enclosing type.</param>
        /// <returns>An instance of <see cref="PropertyProvider"/>.</returns>
        protected virtual PropertyProvider? CreatePropertyCore(InputProperty property, TypeProvider enclosingType)
        {
            PropertyProvider.TryCreate(property, enclosingType, out var propertyProvider);
            if (Visitors.Count == 0)
            {
                return propertyProvider;
            }
            foreach (var visitor in Visitors)
            {
                propertyProvider = visitor.PreVisitProperty(property, propertyProvider);
            }
            return propertyProvider;
        }

        /// <summary>
        /// Factory method for retrieving the serialization format for a given input type.
        /// </summary>
        /// <param name="input">The <see cref="InputType"/> to retrieve the serialization format for.</param>
        /// <returns>The <see cref="SerializationFormat"/> for the input type.</returns>
        public SerializationFormat GetSerializationFormat(InputType input) => input switch
        {
            InputLiteralType literalType => GetSerializationFormat(literalType.ValueType),
            InputArrayType listType => GetSerializationFormat(listType.ValueType),
            InputDictionaryType dictionaryType => GetSerializationFormat(dictionaryType.ValueType),
            InputNullableType nullableType => GetSerializationFormat(nullableType.Type),
            InputDateTimeType dateTimeType => dateTimeType.Encode switch
            {
                var e when e == DateTimeKnownEncoding.Rfc3339 => SerializationFormat.DateTime_RFC3339,
                var e when e == DateTimeKnownEncoding.Rfc7231 => SerializationFormat.DateTime_RFC7231,
                var e when e == DateTimeKnownEncoding.UnixTimestamp => SerializationFormat.DateTime_Unix,
                _ => SerializationFormat.Default, // Custom encoding formats use default serialization
            },
            InputDurationType durationType => durationType.Encode switch
            {
                // there is no such thing as `DurationConstant`
                var e when e == DurationKnownEncoding.Iso8601 => SerializationFormat.Duration_ISO8601,
                var e when e == DurationKnownEncoding.Seconds => durationType.WireType.Kind switch
                {
                    InputPrimitiveTypeKind.Int32 => SerializationFormat.Duration_Seconds,
                    InputPrimitiveTypeKind.Float or InputPrimitiveTypeKind.Float32 => SerializationFormat.Duration_Seconds_Float,
                    _ => SerializationFormat.Duration_Seconds_Double
                },
                var e when e == DurationKnownEncoding.Milliseconds => durationType.WireType.Kind switch
                {
                    InputPrimitiveTypeKind.Int32 => SerializationFormat.Duration_Milliseconds,
                    InputPrimitiveTypeKind.Float or InputPrimitiveTypeKind.Float32 => SerializationFormat.Duration_Milliseconds_Float,
                    _ => SerializationFormat.Duration_Milliseconds_Double
                },
                var e when e == DurationKnownEncoding.Constant => SerializationFormat.Duration_Constant,
                _ => SerializationFormat.Default // Custom encoding formats use default serialization
            },
            InputPrimitiveType primitiveType => primitiveType.Kind switch
            {
                InputPrimitiveTypeKind.PlainDate => SerializationFormat.Date_ISO8601,
                InputPrimitiveTypeKind.PlainTime => SerializationFormat.Time_ISO8601,
                InputPrimitiveTypeKind.Bytes => primitiveType.Encode switch
                {
                    BytesKnownEncoding.Base64 => SerializationFormat.Bytes_Base64,
                    BytesKnownEncoding.Base64Url => SerializationFormat.Bytes_Base64Url,
                    null => SerializationFormat.Default,
                    _ => throw new IndexOutOfRangeException($"unknown encode {primitiveType.Encode}")
                },
                InputPrimitiveTypeKind.Integer or InputPrimitiveTypeKind.Int8 or InputPrimitiveTypeKind.Int16 or InputPrimitiveTypeKind.Int32
                    or InputPrimitiveTypeKind.Int64 or InputPrimitiveTypeKind.UInt8 or InputPrimitiveTypeKind.UInt16 or InputPrimitiveTypeKind.UInt32
                    or InputPrimitiveTypeKind.UInt64 or InputPrimitiveTypeKind.SafeInt when primitiveType.Encode is "string" => SerializationFormat.Int_String,
                _ => SerializationFormat.Default
            },
            _ => SerializationFormat.Default
        };

        /// <summary>
        /// Retrieves the serialization format for a given input property. For array-typed properties
        /// this checks the property-level <see cref="InputModelProperty.Encode"/> before falling
        /// back to <see cref="GetSerializationFormat(InputType)"/>.
        /// </summary>
        /// <param name="inputProperty">The <see cref="InputProperty"/> to retrieve the serialization format for.</param>
        /// <returns>The <see cref="SerializationFormat"/> for the input property.</returns>
        internal SerializationFormat GetSerializationFormat(InputProperty inputProperty)
        {
            if (inputProperty is InputModelProperty modelProperty &&
                inputProperty.Type is InputArrayType &&
                modelProperty.Encode.HasValue)
            {
                return modelProperty.Encode.Value.ToSerializationFormat();
            }

            return GetSerializationFormat(inputProperty.Type);
        }

        /// <summary>
        /// The initialization type of list properties. This type should implement both <see cref="IList{T}"/> and <see cref="IReadOnlyList{T}"/>.
        /// </summary>
        public virtual CSharpType ListInitializationType => ChangeTrackingListProvider.Type;

        /// <summary>
        /// The initialization type of dictionary properties. This type should implement both <see cref="IDictionary{TKey, TValue}"/> and <see cref="IReadOnlyDictionary{TKey, TValue}"/>.
        /// </summary>
        public virtual CSharpType DictionaryInitializationType => ChangeTrackingDictionaryProvider.Type;

        /// <summary>
        /// Returns the serialization type providers for the given model type provider.
        /// </summary>
        /// <param name="inputType">The input model.</param>
        /// <param name="typeProvider">The type provider.</param>
        public IReadOnlyList<TypeProvider> CreateSerializations(InputType inputType, TypeProvider typeProvider)
        {
            if (SerializationsCache.TryGetValue(inputType, out var serializations))
                return serializations;

            serializations = CreateSerializationsCore(inputType, typeProvider);
            SerializationsCache.Add(inputType, serializations);
            return serializations;
        }

        protected virtual IReadOnlyList<TypeProvider> CreateSerializationsCore(InputType inputType, TypeProvider typeProvider)
        {
            return [];
        }

        public virtual NewProjectScaffolding CreateNewProjectScaffolding()
        {
            return new NewProjectScaffolding();
        }

        /// <summary>
        /// Creates serialization options for the given input serialization options.
        /// </summary>
        /// <param name="inputSerializationOptions">The input serialization options.</param>
        /// <returns>The serialization options, or <c>null</c> if not applicable.</returns>
        public SerializationOptions? CreateSerializationOptions(InputSerializationOptions inputSerializationOptions)
        {
            if (SerializationOptionsCache.TryGetValue(inputSerializationOptions, out var options))
            {
                return options;
            }

            options = CreateSerializationOptionsCore(inputSerializationOptions);
            SerializationOptionsCache.Add(inputSerializationOptions, options);

            return options;
        }

        /// <summary>
        /// Factory method for creating <see cref="SerializationOptions"/> for the given input serialization options.
        /// </summary>
        /// <param name="inputSerializationOptions">The input serialization options.</param>
        /// <returns>The serialization options, or <c>null</c> if not applicable.</returns>
        protected virtual SerializationOptions? CreateSerializationOptionsCore(InputSerializationOptions inputSerializationOptions)
        {
            return null;
        }

        private readonly struct EnumCacheKey
        {
            public InputEnumType EnumType { get; }
            public TypeProvider? DeclaringType { get; }
            public EnumCacheKey(InputEnumType enumType, TypeProvider? declaringType)
            {
                EnumType = enumType;
                DeclaringType = declaringType;
            }
        }

        private string? _primaryNamespace;
        public string PrimaryNamespace => _primaryNamespace ??= GetCleanNameSpace(CodeModelGenerator.Instance.InputLibrary.InputNamespace.Name);

        public string ServiceName => _serviceName ??= BuildServiceName();
        private string? _serviceName;

        /// <summary>
        /// Builds the service name which is used as the base name for various types.
        /// </summary>
        protected virtual string BuildServiceName()
        {
            var span = CodeModelGenerator.Instance.InputLibrary.InputNamespace.Name;
            if (span.IndexOf('.') == -1)
            {
                return CodeModelGenerator.Instance.InputLibrary.InputNamespace.Name;
            }

            Span<char> dest = stackalloc char[span.Length];
            int j = 0;

            for (int i = 0; i < span.Length; i++)
            {
                if (span[i] != '.')
                {
                    dest[j] = span[i];
                    j++;
                }
            }

            return dest[..j].ToString();
        }

        public string GetCleanNameSpace(string clientNamespace)
        {
            Span<char> dest = stackalloc char[clientNamespace.Length + GetSegmentCount(clientNamespace)];
            var source = clientNamespace.AsSpan();
            var destIndex = 0;
            var nextDot = source.IndexOf('.');
            while (nextDot != -1)
            {
                var segment = source.Slice(0, nextDot);
                var segmentStr = segment.ToString();
                var cleanedSegment = segmentStr.ToIdentifierName();
                if (IsSpecialSegment(cleanedSegment))
                {
                    cleanedSegment = "_" + cleanedSegment;
                }
                cleanedSegment.AsSpan().CopyTo(dest.Slice(destIndex));
                destIndex += cleanedSegment.Length;
                dest[destIndex] = '.';
                destIndex++;
                source = source.Slice(nextDot + 1);
                nextDot = source.IndexOf('.');
            }
            var lastSegmentStr = source.ToString();
            var cleanedLastSegment = lastSegmentStr.ToIdentifierName();
            if (IsSpecialSegment(cleanedLastSegment))
            {
                cleanedLastSegment = "_" + cleanedLastSegment;
            }
            cleanedLastSegment.AsSpan().CopyTo(dest.Slice(destIndex));
            destIndex += cleanedLastSegment.Length;
            return dest.Slice(0, destIndex).ToString();
        }

        private bool IsSpecialSegment(ReadOnlySpan<char> readOnlySpan)
        {
            var badNamespaceSegments = CodeModelGenerator.Instance.InputLibrary.InputNamespace.InvalidNamespaceSegments;
            for (int i = 0; i < badNamespaceSegments.Count; i++)
            {
                if (readOnlySpan.Equals(badNamespaceSegments[i], StringComparison.Ordinal))
                {
                    return true;
                }
            }
            return false;
        }

        private bool IsSpecialSegment(string segment) => IsSpecialSegment(segment.AsSpan());

        private static int GetSegmentCount(string clientNamespace)
        {
            int count = 0;
            for (int i = 0; i < clientNamespace.Length; i++)
            {
                if (clientNamespace[i] == '.')
                {
                    count++;
                }
            }
            return ++count;
        }
    }
}
