/*
 * 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.Ignite.Internal.Table.Serialization
{
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using Common;
    using Ignite.Table;
    using Proto.BinaryTuple;
    using Proto.MsgPack;

    /// <summary>
    /// Serializer handler for <see cref="IIgniteTuple"/>.
    /// </summary>
    internal sealed class TupleSerializerHandler : IRecordSerializerHandler<IIgniteTuple>
    {
        /// <summary>
        /// Singleton instance.
        /// </summary>
        public static readonly IRecordSerializerHandler<IIgniteTuple> Instance = new TupleSerializerHandler();

        /// <summary>
        /// Initializes a new instance of the <see cref="TupleSerializerHandler"/> class.
        /// </summary>
        private TupleSerializerHandler()
        {
            // No-op.
        }

        /// <summary>
        /// Reads tuple from the buffer.
        /// </summary>
        /// <param name="buf">Buffer.</param>
        /// <param name="schema">Schema.</param>
        /// <param name="keyOnly">Whether to read only the key columns.</param>
        /// <returns>Tuple.</returns>
        public static IgniteTuple ReadTuple(ReadOnlySpan<byte> buf, Schema schema, bool keyOnly)
        {
            var columns = schema.GetColumnsFor(keyOnly);
            var tuple = new IgniteTuple(columns.Length);
            var tupleReader = new BinaryTupleReader(buf, columns.Length);

            foreach (var column in columns)
            {
                tuple[column.Name] = tupleReader.GetObject(column.GetBinaryTupleIndex(keyOnly), column.Type, column.Scale);
            }

            return tuple;
        }

        /// <summary>
        /// Reads single column from the binary tuple.
        /// </summary>
        /// <param name="buf">Binary tuple buffer.</param>
        /// <param name="schema">Schema.</param>
        /// <param name="keyOnly">Whether <paramref name="buf"/> is a key-only binary tuple.</param>
        /// <param name="index">Column index.</param>
        /// <returns>Column value.</returns>
        public static object? ReadObject(ReadOnlySpan<byte> buf, Schema schema, bool keyOnly, int index)
        {
            var columns = schema.GetColumnsFor(keyOnly);
            var tupleReader = new BinaryTupleReader(buf, columns.Length);
            var column = columns[index];

            return tupleReader.GetObject(index, column.Type, column.Scale);
        }

        /// <inheritdoc/>
        public IIgniteTuple Read(ref MsgPackReader reader, Schema schema, bool keyOnly = false) =>
            new BinaryTupleIgniteTupleAdapter(
                data: reader.ReadBinary().ToArray(),
                schema: schema,
                keyOnly);

        /// <inheritdoc/>
        public void Write(ref BinaryTupleBuilder tupleBuilder, IIgniteTuple record, Schema schema, bool keyOnly, Span<byte> noValueSet)
        {
            int written = 0;
            var columns = keyOnly ? schema.KeyColumns : schema.Columns;

            foreach (var col in columns)
            {
                var colIdx = record.GetOrdinal(col.Name);

                if (colIdx >= 0)
                {
                    tupleBuilder.AppendObject(record[colIdx], col.Type, col.Scale, col.Precision);
                    written++;
                }
                else
                {
                    tupleBuilder.AppendNoValue(noValueSet);
                }
            }

            ValidateMappedCount(record, schema, columns.Length, written);
        }

        private static void ValidateMappedCount(IIgniteTuple record, Schema schema, int columnCount, int written)
        {
            if (written == 0)
            {
                var columnStr = schema.Columns.Select(x => x.Type + " " + x.Name).StringJoin();
                throw new ArgumentException($"Can't map '{record}' to columns '{columnStr}'. Matching fields not found.");
            }

            if (record.FieldCount > written)
            {
                var extraColumns = new HashSet<string>(record.FieldCount, StringComparer.OrdinalIgnoreCase);
                for (int i = 0; i < record.FieldCount; i++)
                {
                    var name = record.GetName(i);

                    if (!extraColumns.Add(name))
                    {
                        throw new ArgumentException("Duplicate column in Tuple: " + name, nameof(record));
                    }
                }

                for (var i = 0; i < columnCount; i++)
                {
                    extraColumns.Remove(schema.Columns[i].Name);
                }

                throw SerializerExceptionExtensions.GetUnmappedColumnsException("Tuple", schema, extraColumns);
            }
        }
    }
}
