// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements.  See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership.  The ASF licenses this file
// to you 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
//
//   http://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.

namespace Apache.Fory;

public sealed class DynamicAnyObjectSerializer : Serializer<object?>
{
    public override object? DefaultValue => null;

    public override void WriteData(WriteContext context, in object? value, bool hasGenerics)
    {
        if (value is null)
        {
            return;
        }

        DynamicAnyCodec.WriteAnyPayload(value!, context, hasGenerics);
    }

    public override object? ReadData(ReadContext context)
    {
        TypeInfo? typeInfo = context.GetReadTypeInfo(typeof(object));
        if (typeInfo is null)
        {
            throw new InvalidDataException("dynamic Any value requires type info");
        }

        return context.TypeResolver.ReadAnyValue(typeInfo, context);
    }

    public override void Write(WriteContext context, in object? value, RefMode refMode, bool writeTypeInfo, bool hasGenerics)
    {
        if (refMode != RefMode.None)
        {
            if (value is null)
            {
                context.Writer.WriteInt8((sbyte)RefFlag.Null);
                return;
            }

            bool wroteTrackingRefFlag = false;
            if (refMode == RefMode.Tracking && AnyValueIsRefType(value!, context.TypeResolver))
            {
                if (context.RefWriter.TryWriteRef(context.Writer, value!))
                {
                    return;
                }

                wroteTrackingRefFlag = true;
            }

            if (!wroteTrackingRefFlag)
            {
                context.Writer.WriteInt8((sbyte)RefFlag.NotNullValue);
            }
        }

        if (writeTypeInfo)
        {
            DynamicAnyCodec.WriteAnyTypeInfo(value!, context);
        }

        WriteData(context, value, hasGenerics);
    }

    public override object? Read(ReadContext context, RefMode refMode, bool readTypeInfo)
    {
        if (refMode != RefMode.None)
        {
            RefFlag flag = context.RefReader.ReadRefFlag(context.Reader);
            switch (flag)
            {
                case RefFlag.Null:
                    return null;
                case RefFlag.Ref:
                    {
                        uint refId = context.RefReader.ReadRefId(context.Reader);
                        return context.RefReader.GetRefValue(refId);
                    }
                case RefFlag.RefValue:
                    {
                        uint reservedRefId = context.RefReader.ReserveRefId();
                        context.SetReservedRefId(reservedRefId);
                        try
                        {
                            object? value = ReadNonNullDynamicAny(context, readTypeInfo);
                            context.StoreRef(value);
                            return value;
                        }
                        finally
                        {
                            context.ClearReservedRefId();
                        }
                    }
                case RefFlag.NotNullValue:
                    break;
                default:
                    throw new RefException($"invalid ref flag {(sbyte)flag}");
            }
        }

        return ReadNonNullDynamicAny(context, readTypeInfo);
    }

    private static bool AnyValueIsRefType(object value, TypeResolver typeResolver)
    {
        TypeInfo typeInfo = typeResolver.GetTypeInfo(value.GetType());
        return typeInfo.IsRefType;
    }

    private static void ReadAnyTypeInfo(ReadContext context)
    {
        TypeInfo typeInfo = context.TypeResolver.ReadAnyTypeInfo(context);
        context.SetReadTypeInfo(typeof(object), typeInfo);
    }

    private object? ReadNonNullDynamicAny(ReadContext context, bool readTypeInfo)
    {
        context.IncreaseReadDepth();
        bool loadedDynamicTypeInfo = false;
        try
        {
            if (readTypeInfo)
            {
                ReadAnyTypeInfo(context);
                loadedDynamicTypeInfo = true;
            }

            return ReadData(context);
        }
        finally
        {
            if (loadedDynamicTypeInfo)
            {
                context.ClearReadTypeInfo(typeof(object));
            }

            context.DecreaseReadDepth();
        }
    }
}

public static class DynamicAnyCodec
{
    internal static void WriteAnyTypeInfo(object value, WriteContext context)
    {
        if (DynamicContainerCodec.TryGetTypeId(value, out TypeId containerTypeId))
        {
            context.Writer.WriteUInt8((byte)containerTypeId);
            return;
        }

        if (TryWriteKnownTypeInfo(value, context))
        {
            return;
        }

        TypeInfo typeInfo = context.TypeResolver.GetTypeInfo(value.GetType());
        context.TypeResolver.WriteTypeInfo(typeInfo, context);
    }

    public static object? CastAnyDynamicValue(object? value, Type targetType)
    {
        if (value is null)
        {
            if (targetType == typeof(object))
            {
                return null;
            }

            if (!targetType.IsValueType || Nullable.GetUnderlyingType(targetType) is not null)
            {
                return null;
            }

            throw new InvalidDataException($"cannot cast null dynamic Any value to non-nullable {targetType}");
        }

        if (targetType.IsInstanceOfType(value))
        {
            return value;
        }

        throw new InvalidDataException($"cannot cast dynamic Any value to {targetType}");
    }

    public static void WriteAny(WriteContext context, object? value, RefMode refMode, bool writeTypeInfo = true, bool hasGenerics = false)
    {
        context.TypeResolver.GetSerializer<object?>().Write(context, value, refMode, writeTypeInfo, hasGenerics);
    }

    public static object? ReadAny(ReadContext context, RefMode refMode, bool readTypeInfo = true)
    {
        return context.TypeResolver.GetSerializer<object?>().Read(context, refMode, readTypeInfo);
    }

    internal static void WriteAnyPayload(object value, WriteContext context, bool hasGenerics)
    {
        if (DynamicContainerCodec.TryWritePayload(value, context, hasGenerics))
        {
            return;
        }

        if (TryWriteKnownPayload(value, context))
        {
            return;
        }

        TypeInfo typeInfo = context.TypeResolver.GetTypeInfo(value.GetType());
        context.TypeResolver.WriteDataObject(typeInfo, context, value, hasGenerics);
    }

    private static bool TryWriteKnownTypeInfo(object value, WriteContext context)
    {
        switch (value)
        {
            case bool:
                context.Writer.WriteUInt8((byte)TypeId.Bool);
                return true;
            case sbyte:
                context.Writer.WriteUInt8((byte)TypeId.Int8);
                return true;
            case short:
                context.Writer.WriteUInt8((byte)TypeId.Int16);
                return true;
            case int:
                context.Writer.WriteUInt8((byte)TypeId.VarInt32);
                return true;
            case long:
                context.Writer.WriteUInt8((byte)TypeId.VarInt64);
                return true;
            case byte:
                context.Writer.WriteUInt8((byte)TypeId.UInt8);
                return true;
            case ushort:
                context.Writer.WriteUInt8((byte)TypeId.UInt16);
                return true;
            case uint:
                context.Writer.WriteUInt8((byte)TypeId.VarUInt32);
                return true;
            case ulong:
                context.Writer.WriteUInt8((byte)TypeId.VarUInt64);
                return true;
            case float:
                context.Writer.WriteUInt8((byte)TypeId.Float32);
                return true;
            case double:
                context.Writer.WriteUInt8((byte)TypeId.Float64);
                return true;
            case string:
                context.Writer.WriteUInt8((byte)TypeId.String);
                return true;
            case byte[]:
                context.Writer.WriteUInt8((byte)TypeId.Binary);
                return true;
            case bool[]:
                context.Writer.WriteUInt8((byte)TypeId.BoolArray);
                return true;
            case sbyte[]:
                context.Writer.WriteUInt8((byte)TypeId.Int8Array);
                return true;
            case short[]:
                context.Writer.WriteUInt8((byte)TypeId.Int16Array);
                return true;
            case int[]:
                context.Writer.WriteUInt8((byte)TypeId.Int32Array);
                return true;
            case long[]:
                context.Writer.WriteUInt8((byte)TypeId.Int64Array);
                return true;
            case ushort[]:
                context.Writer.WriteUInt8((byte)TypeId.UInt16Array);
                return true;
            case uint[]:
                context.Writer.WriteUInt8((byte)TypeId.UInt32Array);
                return true;
            case ulong[]:
                context.Writer.WriteUInt8((byte)TypeId.UInt64Array);
                return true;
            case float[]:
                context.Writer.WriteUInt8((byte)TypeId.Float32Array);
                return true;
            case double[]:
                context.Writer.WriteUInt8((byte)TypeId.Float64Array);
                return true;
            case DateOnly:
                context.Writer.WriteUInt8((byte)TypeId.Date);
                return true;
            case DateTimeOffset:
            case DateTime:
                context.Writer.WriteUInt8((byte)TypeId.Timestamp);
                return true;
            case TimeSpan:
                context.Writer.WriteUInt8((byte)TypeId.Duration);
                return true;
            default:
                return false;
        }
    }

    private static bool TryWriteKnownPayload(object value, WriteContext context)
    {
        switch (value)
        {
            case bool v:
                context.Writer.WriteUInt8(v ? (byte)1 : (byte)0);
                return true;
            case sbyte v:
                context.Writer.WriteInt8(v);
                return true;
            case short v:
                context.Writer.WriteInt16(v);
                return true;
            case int v:
                context.Writer.WriteVarInt32(v);
                return true;
            case long v:
                context.Writer.WriteVarInt64(v);
                return true;
            case byte v:
                context.Writer.WriteUInt8(v);
                return true;
            case ushort v:
                context.Writer.WriteUInt16(v);
                return true;
            case uint v:
                context.Writer.WriteVarUInt32(v);
                return true;
            case ulong v:
                context.Writer.WriteVarUInt64(v);
                return true;
            case float v:
                context.Writer.WriteFloat32(v);
                return true;
            case double v:
                context.Writer.WriteFloat64(v);
                return true;
            case string v:
                StringSerializer.WriteString(context, v);
                return true;
            case DateOnly v:
                TimeCodec.WriteDate(context, v);
                return true;
            case DateTimeOffset v:
                TimeCodec.WriteTimestamp(context, v);
                return true;
            case DateTime v:
                TimeCodec.WriteTimestamp(context, TimeCodec.ToDateTimeOffset(v));
                return true;
            case TimeSpan v:
                TimeCodec.WriteDuration(context, v);
                return true;
            case byte[] v:
                context.Writer.WriteVarUInt32((uint)v.Length);
                context.Writer.WriteBytes(v);
                return true;
            case bool[] v:
                context.Writer.WriteVarUInt32((uint)v.Length);
                for (int i = 0; i < v.Length; i++)
                {
                    context.Writer.WriteUInt8(v[i] ? (byte)1 : (byte)0);
                }
                return true;
            case sbyte[] v:
                context.Writer.WriteVarUInt32((uint)v.Length);
                for (int i = 0; i < v.Length; i++)
                {
                    context.Writer.WriteInt8(v[i]);
                }
                return true;
            case short[] v:
                context.Writer.WriteVarUInt32((uint)(v.Length * 2));
                for (int i = 0; i < v.Length; i++)
                {
                    context.Writer.WriteInt16(v[i]);
                }
                return true;
            case int[] v:
                context.Writer.WriteVarUInt32((uint)(v.Length * 4));
                for (int i = 0; i < v.Length; i++)
                {
                    context.Writer.WriteInt32(v[i]);
                }
                return true;
            case long[] v:
                context.Writer.WriteVarUInt32((uint)(v.Length * 8));
                for (int i = 0; i < v.Length; i++)
                {
                    context.Writer.WriteInt64(v[i]);
                }
                return true;
            case ushort[] v:
                context.Writer.WriteVarUInt32((uint)(v.Length * 2));
                for (int i = 0; i < v.Length; i++)
                {
                    context.Writer.WriteUInt16(v[i]);
                }
                return true;
            case uint[] v:
                context.Writer.WriteVarUInt32((uint)(v.Length * 4));
                for (int i = 0; i < v.Length; i++)
                {
                    context.Writer.WriteUInt32(v[i]);
                }
                return true;
            case ulong[] v:
                context.Writer.WriteVarUInt32((uint)(v.Length * 8));
                for (int i = 0; i < v.Length; i++)
                {
                    context.Writer.WriteUInt64(v[i]);
                }
                return true;
            case float[] v:
                context.Writer.WriteVarUInt32((uint)(v.Length * 4));
                for (int i = 0; i < v.Length; i++)
                {
                    context.Writer.WriteFloat32(v[i]);
                }
                return true;
            case double[] v:
                context.Writer.WriteVarUInt32((uint)(v.Length * 8));
                for (int i = 0; i < v.Length; i++)
                {
                    context.Writer.WriteFloat64(v[i]);
                }
                return true;
            default:
                return false;
        }
    }
}
