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

package org.apache.cassandra.db.marshal;

import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Random;

import org.junit.AfterClass;
import org.junit.Assert;
import org.junit.BeforeClass;
import org.junit.Test;

import org.apache.cassandra.cql3.FieldIdentifier;
import org.apache.cassandra.utils.ByteBufferUtil;

import static org.apache.cassandra.db.marshal.ValueAccessors.ACCESSORS;

public class CompositeAndTupleTypesTest
{

    private static final TestNativeDataAllocator allocator = new TestNativeDataAllocator();
    @BeforeClass
    public static void setSetMemoryAllocator()
    {
        NativeAccessor.setNativeMemoryAllocator(allocator);
    }

    @AfterClass
    public static void releaseMemory()
    {
        allocator.close();
    }

    interface TypeFactory<T extends AbstractType<?>> { T createType(List<AbstractType<?>> types); }
    interface ValueCombiner<V> { V combine(AbstractType<?> type, ValueAccessor<V> accessor, V[] values); }

    public static Object[] createValues(ValueGenerator[] generators, Random random)
    {
        Object[] values = new Object[generators.length];
        for (int i=0; i<generators.length; i++)
            values[i] = generators[i].nextValue(random);
        return values;
    }

    public static ByteBuffer[] decompose(ValueGenerator[] generators, Object[] values)
    {
        assert generators.length == values.length;
        ByteBuffer[] decomposed = new ByteBuffer[generators.length];
        for (int i=0; i<generators.length; i++)
        {
            decomposed[i] = ((AbstractType<Object>) generators[i].getType()).decompose(values[i]);
        }
        return decomposed;
    }

    public static <S, D> D[] convert(S[] src, ValueAccessor<S> srcAccessor, ValueAccessor<D> dstAccessor)
    {
        D[] dst = dstAccessor.createArray(src.length);
        for (int i=0; i<src.length; i++)
            dst[i] = dstAccessor.convert(src[i], srcAccessor);
        return dst;
    }

    public static <V> Object[] compose(ValueGenerator[] generators, V[] values, ValueAccessor<V> accessor)
    {
        assert generators.length == values.length;

        Object[] composed = new Object[generators.length];
        for (int i=0; i<generators.length; i++)
        {
            composed[i] = ((AbstractType<Object>) generators[i].getType()).compose(values[i], accessor);
        }
        return composed;
    }

    public <AT extends AbstractType<?>> void testSerializationDeserialization(TypeFactory<AT> typeFactory, ValueCombiner combiner)
    {
        for (int i=0; i<100; i++)
        {
            Random random = new Random(i);
            ValueGenerator[] generators = ValueGenerator.randomGenerators(random, 100);
            AT type = typeFactory.createType(ValueGenerator.toTypes(generators));

            for (int j=0; j<100; j++)
            {
                Object[] expected = createValues(generators, random);
                for (ValueAccessor<Object> srcAccessor : ACCESSORS)
                {
                    ByteBuffer[] srcBuffers = decompose(generators, expected);
                    Object[] srcValues = convert(srcBuffers, ByteBufferAccessor.instance, srcAccessor);
                    Object srcJoined = combiner.combine(type, srcAccessor, srcValues);
                    String srcString  = type.getString(srcJoined, srcAccessor);

                    for (ValueAccessor<Object> dstAccessor : ACCESSORS)
                    {
                        // convert data types and deserialize with
                        Object[] dstValues = convert(srcValues, srcAccessor, dstAccessor);
                        Object dstJoined = combiner.combine(type, dstAccessor, dstValues);
                        String dstString = type.getString(dstJoined, dstAccessor);

                        Object[] composed = compose(generators, dstValues, dstAccessor);
                        Assert.assertArrayEquals(expected, composed);
                        ValueAccessors.assertDataEquals(srcJoined, srcAccessor, dstJoined, dstAccessor);
                        Assert.assertEquals(srcString, dstString);
                    }
                }
                allocator.releaseMemory();
            }
        }
    }

    @Test
    public void tuple()
    {
        testSerializationDeserialization(TupleType::new, getTupleValueCombiner());
    }

    private static <V> ValueCombiner<V> getTupleValueCombiner()
    {
        return (type, accessor, values) -> TupleType.pack(accessor, Arrays.asList(values));
    }

    @Test
    public void userType()
    {
        TypeFactory<UserType> factory = types -> {
            List<FieldIdentifier> names = new ArrayList<>(types.size());
            for (int i=0; i<types.size(); i++)
            {
                names.add(FieldIdentifier.forUnquoted("t" + i));
            }
            return new UserType("ks", ByteBufferUtil.bytes("user_type"), names, types, false);
        };
        testSerializationDeserialization(factory, getTupleValueCombiner());
    }

    @Test
    public void composite()
    {
        testSerializationDeserialization(CompositeType::new, (type, accessor, values) -> CompositeType.build(accessor, values));
    }
}
