/**
 * 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.
 */
using System;
using System.Collections.Generic;
using Avro.IO;
using System.IO;

namespace Avro.Generic
{
    public delegate T Reader<T>();

    /// <summary>
    /// A general purpose reader of data from avro streams. This can optionally resolve if the reader's and writer's
    /// schemas are different. This class is a wrapper around DefaultReader and offers a little more type safety. The default reader
    /// has the flexibility to return any type of object for each read call because the Read() method is generic. This
    /// class on the other hand can only return a single type because the type is a parameter to the class. Any
    /// user defined extension should, however, be done to DefaultReader. This class is sealed.
    /// </summary>
    /// <typeparam name="T"></typeparam>
    public sealed class GenericReader<T> : DatumReader<T>
    {
        private readonly DefaultReader reader;

        /// <summary>
        /// Constructs a generic reader for the given schemas using the DefaultReader. If the
        /// reader's and writer's schemas are different this class performs the resolution.
        /// </summary>
        /// <param name="writerSchema">The schema used while generating the data</param>
        /// <param name="readerSchema">The schema desired by the reader</param>
        public GenericReader(Schema writerSchema, Schema readerSchema)
            : this(new DefaultReader(writerSchema, readerSchema))
        {
        }

        /// <summary>
        /// Constructs a generic reader by directly using the given DefaultReader
        /// </summary>
        /// <param name="reader">The actual reader to use</param>
        public GenericReader(DefaultReader reader)
        {
            this.reader = reader;
        }

        public Schema WriterSchema { get { return reader.WriterSchema; } }

        public Schema ReaderSchema { get { return reader.ReaderSchema; } }

        public T Read(T reuse, Decoder d)
        {
            return reader.Read(reuse, d);
        }
    }

    /// <summary>
    /// The default implementation for the generic reader. It constructs new .NET objects for avro objects on the
    /// stream and returns the .NET object. Users can directly use this class or, if they want to customize the
    /// object types for differnt Avro schema types, can derive from this class. There are enough hooks in this
    /// class to allow customization.
    /// </summary>
    /// <remarks>
    /// <list type="table">
    /// <listheader><term>Avro Type</term><description>.NET Type</description></listheader>
    /// <item><term>null</term><description>null reference</description></item>
    /// </list>
    /// </remarks>
    public class DefaultReader
    {
        public Schema ReaderSchema { get; private set; }
        public Schema WriterSchema { get; private set; }


        /// <summary>
        /// Constructs the default reader for the given schemas using the DefaultReader. If the
        /// reader's and writer's schemas are different this class performs the resolution.
        /// This default implemenation maps Avro types to .NET types as follows:
        /// </summary>
        /// <param name="writerSchema">The schema used while generating the data</param>
        /// <param name="readerSchema">The schema desired by the reader</param>
        public DefaultReader(Schema writerSchema, Schema readerSchema)
        {
            this.ReaderSchema = readerSchema;
            this.WriterSchema = writerSchema;
        }

        /// <summary>
        /// Reads an object off the stream.
        /// </summary>
        /// <typeparam name="T">The type of object to read. A single schema typically returns an object of a single .NET class.
        /// The only exception is UnionSchema, which can return a object of different types based on the branch selected.
        /// </typeparam>
        /// <param name="reuse">If not null, the implemenation will try to use to return the object</param>
        /// <param name="decoder">The decoder for deserialization</param>
        /// <returns></returns>
        public T Read<T>(T reuse, Decoder decoder)
        {
            if (!ReaderSchema.CanRead(WriterSchema))
                throw new AvroException("Schema mismatch. Reader: " + ReaderSchema + ", writer: " + WriterSchema);

            return (T)Read(reuse, WriterSchema, ReaderSchema, decoder);
        }

        public object Read(object reuse, Schema writerSchema, Schema readerSchema, Decoder d)
        {
            if (readerSchema.Tag == Schema.Type.Union && writerSchema.Tag != Schema.Type.Union)
            {
                readerSchema = findBranch(readerSchema as UnionSchema, writerSchema);
            }
            /*
            if (!readerSchema.CanRead(writerSchema))
            {
                throw new AvroException("Schema mismatch. Reader: " + readerSchema + ", writer: " + writerSchema);
            }
            */
            switch (writerSchema.Tag)
            {
                case Schema.Type.Null:
                    return ReadNull(readerSchema, d);
                case Schema.Type.Boolean:
                    return Read<bool>(writerSchema.Tag, readerSchema, d.ReadBoolean);
                case Schema.Type.Int:
                    {
                        int i = Read<int>(writerSchema.Tag, readerSchema, d.ReadInt);
                        switch (readerSchema.Tag)
                        {
                            case Schema.Type.Long:
                                return (long)i;
                            case Schema.Type.Float:
                                return (float)i;
                            case Schema.Type.Double:
                                return (double)i;
                            default:
                                return i;
                        }
                    }
                case Schema.Type.Long:
                    {
                        long l = Read<long>(writerSchema.Tag, readerSchema, d.ReadLong);
                        switch (readerSchema.Tag)
                        {
                            case Schema.Type.Float:
                                return (float)l;
                            case Schema.Type.Double:
                                return (double)l;
                            default:
                                return l;
                        }
                    }
                case Schema.Type.Float:
                    {
                        float f = Read<float>(writerSchema.Tag, readerSchema, d.ReadFloat);
                        switch (readerSchema.Tag)
                        {
                            case Schema.Type.Double:
                                return (double)f;
                            default:
                                return f;
                        }
                    }
                case Schema.Type.Double:
                    return Read<double>(writerSchema.Tag, readerSchema, d.ReadDouble);
                case Schema.Type.String:
                    return Read<string>(writerSchema.Tag, readerSchema, d.ReadString);
                case Schema.Type.Bytes:
                    return Read<byte[]>(writerSchema.Tag, readerSchema, d.ReadBytes);
                case Schema.Type.Error:
                case Schema.Type.Record:
                    return ReadRecord(reuse, (RecordSchema)writerSchema, readerSchema, d);
                case Schema.Type.Enumeration:
                    return ReadEnum(reuse, (EnumSchema)writerSchema, readerSchema, d);
                case Schema.Type.Fixed:
                    return ReadFixed(reuse, (FixedSchema)writerSchema, readerSchema, d);
                case Schema.Type.Array:
                    return ReadArray(reuse, (ArraySchema)writerSchema, readerSchema, d);
                case Schema.Type.Map:
                    return ReadMap(reuse, (MapSchema)writerSchema, readerSchema, d);
                case Schema.Type.Union:
                    return ReadUnion(reuse, (UnionSchema)writerSchema, readerSchema, d);
                default:
                    throw new AvroException("Unknown schema type: " + writerSchema);
            }
        }

        /// <summary>
        /// Deserializes a null from the stream.
        /// </summary>
        /// <param name="readerSchema">Reader's schema, which should be a NullSchema</param>
        /// <param name="d">The decoder for deserialization</param>
        /// <returns></returns>
        protected virtual object ReadNull(Schema readerSchema, Decoder d)
        {
            d.ReadNull();
            return null;
        }

        /// <summary>
        /// A generic function to read primitive types
        /// </summary>
        /// <typeparam name="S">The .NET type to read</typeparam>
        /// <param name="tag">The Avro type tag for the object on the stream</param>
        /// <param name="readerSchema">A schema compatible to the Avro type</param>
        /// <param name="reader">A function that can read the avro type from the stream</param>
        /// <returns>The primitive type just read</returns>
        protected S Read<S>(Schema.Type tag, Schema readerSchema, Reader<S> reader)
        {
            return reader();
        }

        /// <summary>
        /// Deserializes a record from the stream.
        /// </summary>
        /// <param name="reuse">If not null, a record object that could be reused for returning the result</param>
        /// <param name="writerSchema">The writer's RecordSchema</param>
        /// <param name="readerSchema">The reader's schema, must be RecordSchema too.</param>
        /// <param name="dec">The decoder for deserialization</param>
        /// <returns>The record object just read</returns>
        protected virtual object ReadRecord(object reuse, RecordSchema writerSchema, Schema readerSchema, Decoder dec)
        {
            RecordSchema rs = (RecordSchema)readerSchema;

            object rec = CreateRecord(reuse, rs);
            foreach (Field wf in writerSchema)
            {
                try
                {
                    Field rf;
                    if (rs.TryGetFieldAlias(wf.Name, out rf))
                    {
                        object obj = null;
                        TryGetField(rec, wf.Name, rf.Pos, out obj);
                        AddField(rec, wf.Name, rf.Pos, Read(obj, wf.Schema, rf.Schema, dec));
                    }
                    else
                        Skip(wf.Schema, dec);
                }
                catch (Exception ex)
                {
                    throw new AvroException(ex.Message + " in field " + wf.Name);
                }
            }

            var defaultStream = new MemoryStream();
            var defaultEncoder = new BinaryEncoder(defaultStream);
            var defaultDecoder = new BinaryDecoder(defaultStream);
            foreach (Field rf in rs)
            {
                if (writerSchema.Contains(rf.Name)) continue;
                
                defaultStream.Position = 0; // reset for writing
                Resolver.EncodeDefaultValue(defaultEncoder, rf.Schema, rf.DefaultValue);
                defaultStream.Flush();
                defaultStream.Position = 0; // reset for reading

                object obj = null;
                TryGetField(rec, rf.Name, rf.Pos, out obj);
                AddField(rec, rf.Name, rf.Pos, Read(obj, rf.Schema, rf.Schema, defaultDecoder));
            }

            return rec;
        }

        /// <summary>
        /// Creates a new record object. Derived classes can override this to return an object of their choice.
        /// </summary>
        /// <param name="reuse">If appropriate, will reuse this object instead of constructing a new one</param>
        /// <param name="readerSchema">The schema the reader is using</param>
        /// <returns></returns>
        protected virtual object CreateRecord(object reuse, RecordSchema readerSchema)
        {
            GenericRecord ru = (reuse == null || !(reuse is GenericRecord) || !(reuse as GenericRecord).Schema.Equals(readerSchema)) ?
                new GenericRecord(readerSchema) :
                reuse as GenericRecord;
            return ru;
        }

        /// <summary>
        /// Used by the default implementation of ReadRecord() to get the existing field of a record object. The derived
        /// classes can override this to make their own interpretation of the record object.
        /// </summary>
        /// <param name="record">The record object to be probed into. This is guaranteed to be one that was returned
        /// by a previous call to CreateRecord.</param>
        /// <param name="fieldName">The name of the field to probe.</param>
        /// <param name="value">The value of the field, if found. Null otherwise.</param>
        /// <returns>True if and only if a field with the given name is found.</returns>
        protected virtual bool TryGetField(object record, string fieldName, int fieldPos, out object value)
        {
            return (record as GenericRecord).TryGetValue(fieldName, out value);
        }

        /// <summary>
        /// Used by the default implementation of ReadRecord() to add a field to a record object. The derived
        /// classes can override this to suit their own implementation of the record object.
        /// </summary>
        /// <param name="record">The record object to be probed into. This is guaranteed to be one that was returned
        /// by a previous call to CreateRecord.</param>
        /// <param name="fieldName">The name of the field to probe.</param>
        /// <param name="fieldValue">The value to be added for the field</param>
        protected virtual void AddField(object record, string fieldName, int fieldPos, object fieldValue)
        {
            (record as GenericRecord).Add(fieldName, fieldValue);
        }

        /// <summary>
        /// Deserializes a enum. Uses CreateEnum to construct the new enum object.
        /// </summary>
        /// <param name="reuse">If appropirate, uses this instead of creating a new enum object.</param>
        /// <param name="writerSchema">The schema the writer used while writing the enum</param>
        /// <param name="readerSchema">The schema the reader is using</param>
        /// <param name="d">The decoder for deserialization.</param>
        /// <returns>An enum object.</returns>
        protected virtual object ReadEnum(object reuse, EnumSchema writerSchema, Schema readerSchema, Decoder d)
        {
            EnumSchema es = readerSchema as EnumSchema;
            return CreateEnum(reuse, readerSchema as EnumSchema, writerSchema[d.ReadEnum()]);
        }

        /// <summary>
        /// Used by the default implementation of ReadEnum to construct a new enum object.
        /// </summary>
        /// <param name="reuse">If appropriate, use this enum object instead of a new one.</param>
        /// <param name="es">The enum schema used by the reader.</param>
        /// <param name="symbol">The symbol that needs to be used.</param>
        /// <returns>The default implemenation returns a GenericEnum.</returns>
        protected virtual object CreateEnum(object reuse, EnumSchema es, string symbol)
        {
            if (reuse is GenericEnum)
            {
                GenericEnum ge = reuse as GenericEnum;
                if (ge.Schema.Equals(es))
                {
                    ge.Value = symbol;
                    return ge;
                }
            }
            return new GenericEnum(es, symbol);
        }

        /// <summary>
        /// Deserializes an array and returns an array object. It uses CreateArray() and works on it before returning it.
        /// It also uses GetArraySize(), ResizeArray(), SetArrayElement() and GetArrayElement() methods. Derived classes can
        /// override these methods to customize their behavior.
        /// </summary>
        /// <param name="reuse">If appropriate, uses this instead of creating a new array object.</param>
        /// <param name="writerSchema">The schema used by the writer.</param>
        /// <param name="readerSchema">The schema that the reader uses.</param>
        /// <param name="d">The decoder for deserialization.</param>
        /// <returns>The deserialized array object.</returns>
        protected virtual object ReadArray(object reuse, ArraySchema writerSchema, Schema readerSchema, Decoder d)
        {

            ArraySchema rs = (ArraySchema)readerSchema;
            object result = CreateArray(reuse, rs);
            int i = 0;
            for (int n = (int)d.ReadArrayStart(); n != 0; n = (int)d.ReadArrayNext())
            {
                if (GetArraySize(result) < (i + n)) ResizeArray(ref result, i + n);
                for (int j = 0; j < n; j++, i++)
                {
                    SetArrayElement(result, i, Read(GetArrayElement(result, i), writerSchema.ItemSchema, rs.ItemSchema, d));
                }
            }
            if (GetArraySize(result) != i) ResizeArray(ref result, i);
            return result;
        }

        /// <summary>
        /// Creates a new array object. The initial size of the object could be anything. The users
        /// should use GetArraySize() to determine the size. The default implementation creates an <c>object[]</c>.
        /// </summary>
        /// <param name="reuse">If appropriate use this instead of creating a new one.</param>
        /// <returns>An object suitable to deserialize an avro array</returns>
        protected virtual object CreateArray(object reuse, ArraySchema rs)
        {
            return (reuse != null && reuse is object[]) ? (object[])reuse : new object[0];
        }

        /// <summary>
        /// Returns the size of the given array object.
        /// </summary>
        /// <param name="array">Array object whose size is required. This is guaranteed to be somthing returned by
        /// a previous call to CreateArray().</param>
        /// <returns>The size of the array</returns>
        protected virtual int GetArraySize(object array)
        {
            return (array as object[]).Length;
        }

        /// <summary>
        /// Resizes the array to the new value.
        /// </summary>
        /// <param name="array">Array object whose size is required. This is guaranteed to be somthing returned by
        /// a previous call to CreateArray().</param>
        /// <param name="n">The new size.</param>
        protected virtual void ResizeArray(ref object array, int n)
        {
            object[] o = array as object[];
            Array.Resize(ref o, n);
            array = o;
        }

        /// <summary>
        /// Assigns a new value to the object at the given index
        /// </summary>
        /// <param name="array">Array object whose size is required. This is guaranteed to be somthing returned by
        /// a previous call to CreateArray().</param>
        /// <param name="index">The index to reassign to.</param>
        /// <param name="value">The value to assign.</param>
        protected virtual void SetArrayElement(object array, int index, object value)
        {
            object[] a = array as object[];
            a[index] = value;
        }

        /// <summary>
        /// Returns the element at the given index.
        /// </summary>
        /// <param name="array">Array object whose size is required. This is guaranteed to be somthing returned by
        /// a previous call to CreateArray().</param>
        /// <param name="index">The index to look into.</param>
        /// <returns>The object the given index. Null if no object has been assigned to that index.</returns>
        protected virtual object GetArrayElement(object array, int index)
        {
            return (array as object[])[index];
        }

        /// <summary>
        /// Deserialized an avro map. The default implemenation creats a new map using CreateMap() and then
        /// adds elements to the map using AddMapEntry().
        /// </summary>
        /// <param name="reuse">If appropriate, use this instead of creating a new map object.</param>
        /// <param name="writerSchema">The schema the writer used to write the map.</param>
        /// <param name="readerSchema">The schema the reader is using.</param>
        /// <param name="d">The decoder for serialization.</param>
        /// <returns>The deserialized map object.</returns>
        protected virtual object ReadMap(object reuse, MapSchema writerSchema, Schema readerSchema, Decoder d)
        {
            MapSchema rs = (MapSchema)readerSchema;
            object result = CreateMap(reuse, rs);
            for (int n = (int)d.ReadMapStart(); n != 0; n = (int)d.ReadMapNext())
            {
                for (int j = 0; j < n; j++)
                {
                    string k = d.ReadString();
                    AddMapEntry(result, k, Read(null, writerSchema.ValueSchema, rs.ValueSchema, d));
                }
            }
            return result;
        }

        /// <summary>
        /// Used by the default implementation of ReadMap() to create a fresh map object. The default
        /// implementaion of this method returns a IDictionary<string, map>.
        /// </summary>
        /// <param name="reuse">If appropriate, use this map object instead of creating a new one.</param>
        /// <returns>An empty map object.</returns>
        protected virtual object CreateMap(object reuse, MapSchema ms)
        {
            if (reuse != null && reuse is IDictionary<string, object>)
            {
                IDictionary<string, object> result = reuse as IDictionary<string, object>;
                result.Clear();
                return result;
            }
            return new Dictionary<string, object>();
        }

        /// <summary>
        /// Adds an entry to the map.
        /// </summary>
        /// <param name="map">A map object, which is guaranteed to be one returned by a previous call to CreateMap().</param>
        /// <param name="key">The key to add.</param>
        /// <param name="value">The value to add.</param>
        protected virtual void AddMapEntry(object map, string key, object value)
        {
            (map as IDictionary<string, object>).Add(key, value);
        }

        /// <summary>
        /// Deserialized an object based on the writer's uninon schema.
        /// </summary>
        /// <param name="reuse">If appropriate, uses this object instead of creating a new one.</param>
        /// <param name="writerSchema">The UnionSchema that the writer used.</param>
        /// <param name="readerSchema">The schema the reader uses.</param>
        /// <param name="d">The decoder for serialization.</param>
        /// <returns>The deserialized object.</returns>
        protected virtual object ReadUnion(object reuse, UnionSchema writerSchema, Schema readerSchema, Decoder d)
        {
            int index = d.ReadUnionIndex();
            Schema ws = writerSchema[index];

            if (readerSchema is UnionSchema)
                readerSchema = findBranch(readerSchema as UnionSchema, ws);
            else
                if (!readerSchema.CanRead(ws))
                    throw new AvroException("Schema mismatch. Reader: " + ReaderSchema + ", writer: " + WriterSchema);

            return Read(reuse, ws, readerSchema, d);
        }

        /// <summary>
        /// Deserializes a fixed object and returns the object. The default implementation uses CreateFixed()
        /// and GetFixedBuffer() and returns what CreateFixed() returned.
        /// </summary>
        /// <param name="reuse">If appropriate, uses this object instead of creating a new one.</param>
        /// <param name="writerSchema">The FixedSchema the writer used during serialization.</param>
        /// <param name="readerSchema">The schema that the readr uses. Must be a FixedSchema with the same
        /// size as the writerSchema.</param>
        /// <param name="d">The decoder for deserialization.</param>
        /// <returns>The deserilized object.</returns>
        protected virtual object ReadFixed(object reuse, FixedSchema writerSchema, Schema readerSchema, Decoder d)
        {
            FixedSchema rs = (FixedSchema)readerSchema;
            if (rs.Size != writerSchema.Size)
            {
                throw new AvroException("Size mismatch between reader and writer fixed schemas. Writer: " + writerSchema +
                    ", reader: " + readerSchema);
            }

            object ru = CreateFixed(reuse, rs);
            byte[] bb = GetFixedBuffer(ru);
            d.ReadFixed(bb);
            return ru;
        }

        /// <summary>
        /// Returns a fixed object.
        /// </summary>
        /// <param name="reuse">If appropriate, uses this object instead of creating a new one.</param>
        /// <param name="rs">The reader's FixedSchema.</param>
        /// <returns>A fixed object with an appropriate buffer.</returns>
        protected virtual object CreateFixed(object reuse, FixedSchema rs)
        {
            return (reuse != null && reuse is GenericFixed && (reuse as GenericFixed).Schema.Equals(rs)) ?
                (GenericFixed)reuse : new GenericFixed(rs);
        }

        /// <summary>
        /// Returns a buffer of appropriate size to read data into.
        /// </summary>
        /// <param name="f">The fixed object. It is guaranteed that this is something that has been previously
        /// returned by CreateFixed</param>
        /// <returns>A byte buffer of fixed's size.</returns>
        protected virtual byte[] GetFixedBuffer(object f)
        {
            return (f as GenericFixed).Value;
        }

        protected virtual void Skip(Schema writerSchema, Decoder d)
        {
            switch (writerSchema.Tag)
            {
                case Schema.Type.Null:
                    d.SkipNull();
                    break;
                case Schema.Type.Boolean:
                    d.SkipBoolean();
                    break;
                case Schema.Type.Int:
                    d.SkipInt();
                    break;
                case Schema.Type.Long:
                    d.SkipLong();
                    break;
                case Schema.Type.Float:
                    d.SkipFloat();
                    break;
                case Schema.Type.Double:
                    d.SkipDouble();
                    break;
                case Schema.Type.String:
                    d.SkipString();
                    break;
                case Schema.Type.Bytes:
                    d.SkipBytes();
                    break;
                case Schema.Type.Record:
                    foreach (Field f in writerSchema as RecordSchema) Skip(f.Schema, d);
                    break;
                case Schema.Type.Enumeration:
                    d.SkipEnum();
                    break;
                case Schema.Type.Fixed:
                    d.SkipFixed((writerSchema as FixedSchema).Size);
                    break;
                case Schema.Type.Array:
                    {
                        Schema s = (writerSchema as ArraySchema).ItemSchema;
                        for (long n = d.ReadArrayStart(); n != 0; n = d.ReadArrayNext())
                        {
                            for (long i = 0; i < n; i++) Skip(s, d);
                        }
                    }
                    break;
                case Schema.Type.Map:
                    {
                        Schema s = (writerSchema as MapSchema).ValueSchema;
                        for (long n = d.ReadMapStart(); n != 0; n = d.ReadMapNext())
                        {
                            for (long i = 0; i < n; i++) { d.SkipString(); Skip(s, d); }
                        }
                    }
                    break;
                case Schema.Type.Union:
                    Skip((writerSchema as UnionSchema)[d.ReadUnionIndex()], d);
                    break;
                default:
                    throw new AvroException("Unknown schema type: " + writerSchema);
            }
        }

        protected static Schema findBranch(UnionSchema us, Schema s)
        {
            int index = us.MatchingBranch(s);
            if (index >= 0) return us[index];
            throw new AvroException("No matching schema for " + s + " in " + us);
        }

    }
}
