using System.Collections;
using System.Runtime.CompilerServices;
using static AyCode.Core.Serializers.AcSerializerCommon;

namespace AyCode.Core.Serializers.Binaries;

public static partial class AcBinaryDeserializer
{
    private static readonly BinaryIndexMappingCache IndexMappingCache = new();

    /// <summary>
    /// Helper to get index mapping for cross-type operations.
    /// Uses cached PropertyInfo arrays from TypeMetadataBase.GetSerializableProperties.
    /// </summary>
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    private static int[] GetIndexMapping(Type sourceType, Type destType, PropertyMapperDelegate? customMapper)
        => IndexMappingCache.GetOrBuild(sourceType, destType, customMapper, 
            type => GetTypeMetadata(type).GetSerializableProperties(type, requiresWrite: true));

    #region Cross-Type Deserialization

    /// <summary>
    /// Deserializes binary data from TSource type to TDest type.
    /// Supports cross-type mapping with automatic property name matching or custom PropertyMapper.
    /// Use when binary was serialized as TSource but you want to deserialize into TDest.
    /// </summary>
    /// <typeparam name="TSource">The source type that was serialized</typeparam>
    /// <typeparam name="TDest">The destination type to deserialize into</typeparam>
    /// <param name="data">Binary data to deserialize</param>
    /// <returns>Deserialized instance of TDest</returns>
    public static TDest? Deserialize<TSource, TDest>(byte[] data)
        => Deserialize<TSource, TDest>(data, AcBinarySerializerOptions.Default);

    /// <summary>
    /// Deserializes binary data from TSource type to TDest type with options.
    /// Supports cross-type mapping with automatic property name matching or custom PropertyMapper.
    /// Zero-copy: ArrayBinaryInput references the byte[] directly.
    /// </summary>
    public static TDest? Deserialize<TSource, TDest>(byte[] data, AcBinarySerializerOptions options)
    {
        // Early exit checks
        if (data.Length == 0) return default;
        if (data.Length == 1 && data[0] == BinaryTypeCode.Null) return default;

        var sourceType = typeof(TSource);
        var destType = typeof(TDest);

        // Fast path: same type
        if (DeserializeCrossTypeBase.IsSameType(sourceType, destType))
            return Deserialize<TDest>(data, options);

        // Cross-type path: use index mapping
        var indexMapping = GetIndexMapping(sourceType, destType, options.PropertyMapper);
        var context = DeserializationContextPool<ArrayBinaryInput>.Get(options);
        context.InitInput(new ArrayBinaryInput(data));

        try
        {
            context.ReadHeader();
            var result = ReadValueWithMapping(context, destType, indexMapping, 0);
            return (TDest?)result;
        }
        catch (AcBinaryDeserializationException)
        {
            throw;
        }
        catch (Exception ex)
        {
            throw new AcBinaryDeserializationException(
                $"Failed to deserialize binary data from '{sourceType.Name}' to '{destType.Name}': {ex.Message}",
                context.Position, destType, ex);
        }
        finally
        {
            DeserializationContextPool<ArrayBinaryInput>.Return(context);
        }
    }

    #endregion

    #region Cross-Type Populate

    /// <summary>
    /// Populates existing TDest instance with data serialized as TSource.
    /// Supports cross-type mapping with automatic property name matching or custom PropertyMapper.
    /// </summary>
    /// <typeparam name="TSource">The source type that was serialized</typeparam>
    /// <typeparam name="TDest">The destination type to populate</typeparam>
    /// <param name="data">Binary data to deserialize</param>
    /// <param name="target">Existing instance to populate</param>
    public static void Populate<TSource, TDest>(byte[] data, TDest target)
        where TDest : class
        => Populate<TSource, TDest>(data, target, AcBinarySerializerOptions.Default);

    /// <summary>
    /// Populates existing TDest instance with data serialized as TSource with options.
    /// Supports cross-type mapping with automatic property name matching or custom PropertyMapper.
    /// Zero-copy: ArrayBinaryInput references the byte[] directly.
    /// </summary>
    public static void Populate<TSource, TDest>(byte[] data, TDest target, AcBinarySerializerOptions options)
        where TDest : class
    {
        // Validation
        DeserializeCrossTypeBase.ValidatePopulateTarget(target);

        // Early exit checks
        if (data.Length == 0) return;
        if (data.Length == 1 && data[0] == BinaryTypeCode.Null) return;

        var sourceType = typeof(TSource);
        var destType = typeof(TDest);

        // Fast path: same type
        if (DeserializeCrossTypeBase.IsSameType(sourceType, destType))
        {
            Populate(data, target, options);
            return;
        }

        // Cross-type path: use index mapping
        var indexMapping = GetIndexMapping(sourceType, destType, options.PropertyMapper);
        var context = DeserializationContextPool<ArrayBinaryInput>.Get(options);
        context.InitInput(new ArrayBinaryInput(data));

        try
        {
            context.ReadHeader();
            var typeCode = context.PeekByte();

            if (typeCode == BinaryTypeCode.Null)
            {
                context.ReadByte(); // consume null
                return;
            }

            if (typeCode == BinaryTypeCode.Object)
            {
                context.ReadByte(); // consume Object marker
                PopulateObjectWithMapping(context, target, destType, indexMapping, 0);
            }
            else
            {
                throw new AcBinaryDeserializationException(
                    $"Expected Object type code but got {typeCode}",
                    context.Position, destType);
            }
        }
        catch (AcBinaryDeserializationException)
        {
            throw;
        }
        catch (Exception ex)
        {
            throw new AcBinaryDeserializationException(
                $"Failed to populate target of type '{destType.Name}' with data from '{sourceType.Name}': {ex.Message}",
                context.Position, destType, ex);
        }
        finally
        {
            DeserializationContextPool<ArrayBinaryInput>.Return(context);
        }
    }

    #endregion

    #region Helper Methods

    /// <summary>
    /// Reads a value with index mapping applied.
    /// Maps source PropertyIndex to destination PropertyIndex using the provided mapping.
    /// </summary>
    private static object? ReadValueWithMapping<TInput>(BinaryDeserializationContext<TInput> context, Type destType, int[] indexMapping, int depth)
    where TInput : struct, IBinaryInputBase
    {
        var typeCode = context.ReadByte();

        return typeCode switch
        {
            BinaryTypeCode.Null => null,
            BinaryTypeCode.Object => ReadObjectWithMapping(context, destType, indexMapping, depth, registerInCache: false),
            BinaryTypeCode.ObjectRefFirst => ReadObjectWithMapping(context, destType, indexMapping, depth, registerInCache: true),
            _ => ReadValue(context, destType) // Primitives, arrays, etc. use normal path
        };
    }

    /// <summary>
    /// Reads an object using index mapping for property resolution.
    /// Note: Object marker already consumed by caller.
    /// </summary>
    private static object? ReadObjectWithMapping<TInput>(BinaryDeserializationContext<TInput> context, Type destType, int[] indexMapping, int depth, bool registerInCache)
    where TInput : struct, IBinaryInputBase
    {
        var wrapper = context.GetWrapper(destType);
        var metadata = wrapper.Metadata;

        var obj = CreateInstance(destType, metadata);
        if (obj != null)
        {
            if (registerInCache)
            {
                context.RegisterNextInternedValue(obj);
            }

            PopulateObjectWithMapping(context, obj, destType, indexMapping, depth);
        }
        return obj;
    }

    /// <summary>
    /// Populates an object using index mapping.
    /// Source property indices are remapped to destination indices.
    /// </summary>
    private static void PopulateObjectWithMapping<TInput>(
        BinaryDeserializationContext<TInput> context,
        object target,
        Type destType,
        int[] indexMapping,
        int depth)
    where TInput : struct, IBinaryInputBase
    {
        var wrapper = context.GetWrapper(destType);
        var metadata = wrapper.Metadata;
        var propertyCount = (int)context.ReadVarUInt();
        var nextDepth = depth + 1;

        for (int i = 0; i < propertyCount; i++)
        {
            var sourcePropIndex = (int)context.ReadVarUInt();

            // Map source index to destination index
            var destPropIndex = MapPropertyIndex(sourcePropIndex, indexMapping);

            if (destPropIndex == -1)
            {
                // No mapping - skip this property
                SkipValue(context, metadata);
                continue;
            }

            // Get destination property by mapped index
            var propInfo = metadata.GetPropertyByIndex(destPropIndex);
            if (propInfo == null)
            {
                // Destination property not found - skip
                SkipValue(context, metadata);
                continue;
            }

            // Reuse common populate logic
            PopulatePropertyValue(context, target, propInfo, wrapper, nextDepth, sourcePropIndex, destPropIndex, i, propertyCount, depth);
        }
    }

    /// <summary>
    /// Maps source property index to destination index using mapping array.
    /// Returns -1 if no mapping exists.
    /// </summary>
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    private static int MapPropertyIndex(int sourcePropIndex, int[] indexMapping)
    {
        if ((uint)sourcePropIndex < (uint)indexMapping.Length)
            return indexMapping[sourcePropIndex];

        return -1; // Out of range - skip
    }

    /// <summary>
    /// Common logic for populating a single property value.
    /// Shared between normal populate and cross-type populate.
    /// </summary>
    private static void PopulatePropertyValue<TInput>(
        BinaryDeserializationContext<TInput> context,
        object target,
        BinaryPropertySetterInfo propInfo,
        TypeMetadataWrapper<BinaryDeserializeTypeMetadata> wrapper,
        int nextDepth,
        int sourcePropIndex,
        int destPropIndex,
        int currentIndex,
        int totalCount,
        int depth)
    where TInput : struct, IBinaryInputBase
    {
        var peekCode = context.PeekByte();

        // Optimization: Skip null values early
        if (peekCode == BinaryTypeCode.Null)
        {
            context.ReadByte(); // consume Null marker
            propInfo.SetValue(target, null);
            return;
        }

        // Handle nested complex objects - reuse existing if available
        if ((peekCode == BinaryTypeCode.Object || peekCode == BinaryTypeCode.ObjectRefFirst) && propInfo.IsComplexType)
        {
            var existingObj = propInfo.GetValue(target);
            if (existingObj != null)
            {
                var registerInCache = peekCode == BinaryTypeCode.ObjectRefFirst;
                context.ReadByte(); // consume Object/ObjectRefFirst marker

                if (registerInCache)
                {
                    context.RegisterNextInternedValue(existingObj);
                }

                PopulateObjectCore(context, existingObj, wrapper, nextDepth, skipDefaultWrite: false);
                return;
            }
        }

        // Handle collections - reuse existing collection and populate items
        if (peekCode == BinaryTypeCode.Array && propInfo.IsCollection)
        {
            var existingCollection = propInfo.GetValue(target);
            if (existingCollection is IList existingList)
            {
                context.ReadByte(); // consume Array marker
                PopulateListOptimized(context, existingList, propInfo, nextDepth);
                return;
            }
        }

        // Default: read value and set
        var positionBeforeRead = context.Position;
        try
        {
            if (TryReadAndSetTypedValue(context, target, propInfo, peekCode))
                return;

            var value = ReadValue(context, propInfo.PropertyType);
            propInfo.SetValue(target, value);
        }
        catch (InvalidCastException ex)
        {
            var targetTypeName = target.GetType().Name;
            var indexInfo = sourcePropIndex == destPropIndex
                ? $"propIndex={sourcePropIndex}"
                : $"sourcePropIndex={sourcePropIndex}, destPropIndex={destPropIndex}";

            throw new AcBinaryDeserializationException(
                $"Type mismatch for property '{propInfo.Name}' (index {currentIndex}/{totalCount}, {indexInfo}) on '{targetTypeName}'. " +
                $"Expected type: '{propInfo.PropertyType.FullName}'. " +
                $"PeekCode before read: {peekCode} (0x{peekCode:X2}). " +
                $"Position before read: {positionBeforeRead}, current: {context.Position}. " +
                $"Depth: {depth}. " +
                $"Error: {ex.Message}",
                positionBeforeRead,
                propInfo.PropertyType,
                ex);
        }
    }

    #endregion
}