// 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;

/// <summary>
/// Base class for custom serializers.
/// </summary>
/// <typeparam name="T">Runtime value type handled by this serializer.</typeparam>
public abstract class Serializer<T>
{
    /// <summary>
    /// Gets the default value returned when a null marker is read for this serializer.
    /// </summary>
    public virtual T DefaultValue => default!;

    internal object? DefaultObject => DefaultValue;

    /// <summary>
    /// Writes the serializer-specific payload body.
    /// </summary>
    /// <param name="context">Write context.</param>
    /// <param name="value">Value to encode.</param>
    /// <param name="hasGenerics">Whether generic type metadata is present for the current field path.</param>
    public abstract void WriteData(WriteContext context, in T value, bool hasGenerics);

    /// <summary>
    /// Reads the serializer-specific payload body.
    /// </summary>
    /// <param name="context">Read context.</param>
    /// <returns>Decoded value.</returns>
    public abstract T ReadData(ReadContext context);

    /// <summary>
    /// Writes ref metadata and optional type metadata, then delegates to <see cref="WriteData"/>.
    /// </summary>
    /// <param name="context">Write context.</param>
    /// <param name="value">Value to write.</param>
    /// <param name="refMode">Ref handling mode.</param>
    /// <param name="writeTypeInfo">Whether type metadata should be written.</param>
    /// <param name="hasGenerics">Whether generic type metadata is present for the current field path.</param>
    public virtual void Write(WriteContext context, in T value, RefMode refMode, bool writeTypeInfo, bool hasGenerics)
    {
        if (refMode != RefMode.None)
        {
            bool wroteTrackingRefFlag = false;
            if (refMode == RefMode.Tracking &&
                value is object obj)
            {
                if (context.RefWriter.TryWriteRef(context.Writer, obj))
                {
                    return;
                }

                wroteTrackingRefFlag = true;
            }

            if (!wroteTrackingRefFlag)
            {
                if (value is null)
                {
                    context.Writer.WriteInt8((sbyte)RefFlag.Null);
                    return;
                }

                context.Writer.WriteInt8((sbyte)RefFlag.NotNullValue);
            }
        }

        if (writeTypeInfo)
        {
            context.TypeResolver.WriteTypeInfo(this, context);
        }

        WriteData(context, value, hasGenerics);
    }

    /// <summary>
    /// Reads ref metadata and optional type metadata, then delegates to <see cref="ReadData"/>.
    /// </summary>
    /// <param name="context">Read context.</param>
    /// <param name="refMode">Ref handling mode.</param>
    /// <param name="readTypeInfo">Whether type metadata should be read.</param>
    /// <returns>Decoded value.</returns>
    public virtual T Read(ReadContext context, RefMode refMode, bool readTypeInfo)
    {
        if (refMode != RefMode.None)
        {
            RefFlag flag = context.RefReader.ReadRefFlag(context.Reader);
            switch (flag)
            {
                case RefFlag.Null:
                    return DefaultValue;
                case RefFlag.Ref:
                    {
                        uint refId = context.RefReader.ReadRefId(context.Reader);
                        return context.RefReader.GetRef<T>(refId);
                    }
                case RefFlag.RefValue:
                    {
                        uint reservedRefId = context.RefReader.ReserveRefId();
                        context.SetReservedRefId(reservedRefId);
                        try
                        {
                            if (readTypeInfo)
                            {
                                context.TypeResolver.ReadTypeInfo(this, context);
                            }

                            T value = ReadData(context);
                            context.StoreRef(value);
                            return value;
                        }
                        finally
                        {
                            context.ClearReservedRefId();
                        }
                    }
                case RefFlag.NotNullValue:
                    break;
                default:
                    throw new RefException($"invalid ref flag {(sbyte)flag}");
            }
        }

        if (readTypeInfo)
        {
            context.TypeResolver.ReadTypeInfo(this, context);
        }

        return ReadData(context);
    }
}
