/* 
 * 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
 *
 *     https://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.
 */

using System;
using System.Collections;
using Avro.IO;
using Avro.Specific;

namespace Avro.Reflect
{
    /// <summary>
    /// Class for writing data from any specific objects
    /// </summary>
    public class ReflectDefaultWriter : SpecificDefaultWriter
    {
        private ClassCache _classCache = new ClassCache();

        /// <summary>
        /// Class cache
        /// </summary>
        public ClassCache ClassCache { get => _classCache; }

        /// <summary>
        /// Constructor
        /// </summary>
        /// <param name="objType"></param>
        /// <param name="schema"></param>
        /// <param name="cache"></param>
        public ReflectDefaultWriter(Type objType, Schema schema, ClassCache cache)
            : base(schema)
        {
            if (cache != null)
            {
                _classCache = cache;
            }

            _classCache.LoadClassCache(objType, schema);
        }

        /// <summary>
        /// Constructor
        /// </summary>
        /// <param name="schema"></param>
        public ReflectDefaultWriter(Schema schema)
            : base(schema)
        {
        }

        /// <summary>
        /// Serialized a record using the given RecordSchema. It uses GetField method
        /// to extract the field value from the given object.
        /// </summary>
        /// <param name="schema">The RecordSchema to use for serialization</param>
        /// <param name="value">The value to be serialized</param>
        /// <param name="encoder">The Encoder for serialization</param>

        protected override void WriteRecord(RecordSchema schema, object value, Encoder encoder)
        {
            foreach (Field field in schema)
            {
                try
                {
                    var v = _classCache.GetClass(schema).GetValue(value, field);

                    Write(field.Schema, v, encoder);
                }
                catch (Exception ex)
                {
                    throw new AvroException(ex.Message + " in field " + field.Name, ex);
                }
            }
        }

        /// <summary>
        /// Validates that the record is a fixed record object and that the schema in the object is the
        /// same as the given writer schema. Writes the given fixed record into the given encoder
        /// </summary>
        /// <param name="schema">writer schema</param>
        /// <param name="value">fixed object to write</param>
        /// <param name="encoder">encoder to write to</param>
        protected override void WriteFixed(FixedSchema schema, object value, Encoder encoder)
        {
            var fixedrec = value as byte[];
            if (fixedrec == null)
            {
                throw new AvroTypeException("Fixed object is not derived from byte[]");
            }

            if (fixedrec.Length != schema.Size)
            {
                throw new AvroTypeException($"Fixed object length is not the same as schema length {schema.Size}");
            }

            encoder.WriteFixed(fixedrec);
        }

        /// <summary>
        /// Serialized an array. The default implementation calls EnsureArrayObject() to ascertain that the
        /// given value is an array. It then calls GetArrayLength() and GetArrayElement()
        /// to access the members of the array and then serialize them.
        /// </summary>
        /// <param name="schema">The ArraySchema for serialization</param>
        /// <param name="value">The value being serialized</param>
        /// <param name="encoder">The encoder for serialization</param>
        protected override void WriteArray(ArraySchema schema, object value, Encoder encoder)
        {
            var arr = value as IEnumerable;
            if (arr == null)
            {
                throw new AvroTypeException("Array does not implement have registered ReflectArray derived type");
            }

            var arrayHelper = _classCache.GetArrayHelper(schema, (IEnumerable)value);
            long l = arrayHelper.Count();
            encoder.WriteArrayStart();
            encoder.SetItemCount(l);
            foreach (var v in arr)
            {
                encoder.StartItem();
                Write(schema.ItemSchema, v, encoder);
            }

            encoder.WriteArrayEnd();
        }

        /// <summary>
        /// Writes the given map into the given encoder.
        /// </summary>
        /// <param name="schema">writer schema</param>
        /// <param name="value">map to write</param>
        /// <param name="encoder">encoder to write to</param>
        protected override void WriteMap(MapSchema schema, object value, Encoder encoder)
        {
            if (value == null)
            {
                throw new AvroTypeException("Map is null - use a union for nullable types");
            }

            base.WriteMap(schema, value, encoder);
        }

        /// <summary>
        /// Determines whether an object matches a schema. In the case of enums and records the code looks up the
        /// Enum and class caches respectively. Used when writing unions.
        /// </summary>
        /// <param name="sc"></param>
        /// <param name="obj"></param>
        /// <returns></returns>
        protected override bool Matches(Schema sc, object obj)
        {
            if (obj == null && sc.Tag != Schema.Type.Null)
            {
                return false;
            }

            switch (sc.Tag)
            {
                case Schema.Type.Null:
                    return obj == null;
                case Schema.Type.Boolean:
                    return obj is bool;
                case Schema.Type.Int:
                    return obj is int;
                case Schema.Type.Long:
                    return obj is long;
                case Schema.Type.Float:
                    return obj is float;
                case Schema.Type.Double:
                    return obj is double;
                case Schema.Type.Bytes:
                    return obj is byte[];
                case Schema.Type.String:
                    return obj is string;
                case Schema.Type.Error:
                case Schema.Type.Record:
                    return _classCache.GetClass(sc as RecordSchema).GetClassType() == obj.GetType();
                case Schema.Type.Enumeration:
                    return EnumCache.GetEnumeration(sc as EnumSchema) == obj.GetType();
                case Schema.Type.Array:
                    return obj is IEnumerable;
                case Schema.Type.Map:
                    return obj is IDictionary;
                case Schema.Type.Union:
                    return false;   // Union directly within another union not allowed!
                case Schema.Type.Fixed:
                    return obj is byte[];
                case Schema.Type.Logical:
                    return ((LogicalSchema)sc).LogicalType.IsInstanceOfLogicalType(obj);
                default:
                    throw new AvroException("Unknown schema type: " + sc.Tag);
            }
        }
    }
}
