/*
 * 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.cql3.functions;

import java.math.BigDecimal;
import java.math.BigInteger;
import java.nio.ByteBuffer;
import java.util.List;

import org.apache.commons.lang3.StringUtils;

import org.apache.cassandra.cql3.CQL3Type;
import org.apache.cassandra.cql3.FunctionContext;
import org.apache.cassandra.db.marshal.AbstractType;
import org.apache.cassandra.db.marshal.AsciiType;
import org.apache.cassandra.db.marshal.BooleanType;
import org.apache.cassandra.db.marshal.ByteType;
import org.apache.cassandra.db.marshal.CounterColumnType;
import org.apache.cassandra.db.marshal.DecimalType;
import org.apache.cassandra.db.marshal.DoubleType;
import org.apache.cassandra.db.marshal.FloatType;
import org.apache.cassandra.db.marshal.InetAddressType;
import org.apache.cassandra.db.marshal.Int32Type;
import org.apache.cassandra.db.marshal.IntegerType;
import org.apache.cassandra.db.marshal.LongType;
import org.apache.cassandra.db.marshal.ShortType;
import org.apache.cassandra.db.marshal.SimpleDateType;
import org.apache.cassandra.db.marshal.TimeType;
import org.apache.cassandra.db.marshal.TimeUUIDType;
import org.apache.cassandra.db.marshal.TimestampType;
import org.apache.cassandra.db.marshal.UTF8Type;
import org.apache.cassandra.db.marshal.UUIDType;

import static org.apache.cassandra.cql3.functions.TimeFcts.toDate;
import static org.apache.cassandra.cql3.functions.TimeFcts.toTimestamp;
import static org.apache.cassandra.utils.LocalizeString.toLowerCaseLocalized;

/**
 * Casting functions
 *
 */
public final class CastFcts
{
    private static final String FUNCTION_NAME_PREFIX = "cast_as_";

    // Until 5.0 we have used camel cased names for cast functions. That changed with CASSANDRA-18037, where we decided
    // to adopt snake case for all native function names. However, we should still support the old came case names.
    private static final String LEGACY_FUNCTION_NAME_PREFIX = "castAs";

    public static void addFunctionsTo(NativeFunctions functions)
    {
        @SuppressWarnings("unchecked")
        final AbstractType<? extends Number>[] numericTypes = new AbstractType[] {ByteType.instance,
                                                                                  ShortType.instance,
                                                                                  Int32Type.instance,
                                                                                  LongType.instance,
                                                                                  FloatType.instance,
                                                                                  DoubleType.instance,
                                                                                  DecimalType.instance,
                                                                                  CounterColumnType.instance,
                                                                                  IntegerType.instance};

        for (AbstractType<? extends Number> inputType : numericTypes)
        {
            addFunctionIfNeeded(functions, inputType, ByteType.instance, Number::byteValue);
            addFunctionIfNeeded(functions, inputType, ShortType.instance, Number::shortValue);
            addFunctionIfNeeded(functions, inputType, Int32Type.instance, Number::intValue);
            addFunctionIfNeeded(functions, inputType, LongType.instance, Number::longValue);
            addFunctionIfNeeded(functions, inputType, FloatType.instance, Number::floatValue);
            addFunctionIfNeeded(functions, inputType, DoubleType.instance, Number::doubleValue);
            addFunctionIfNeeded(functions, inputType, DecimalType.instance, getDecimalConversionFunction(inputType));
            addFunctionIfNeeded(functions, inputType, IntegerType.instance, p -> BigInteger.valueOf(p.longValue()));
            functions.add(CastAsTextFunction.create(inputType, AsciiType.instance));
            functions.add(CastAsTextFunction.create(inputType, UTF8Type.instance));
        }

        functions.add(JavaFunctionWrapper.create(AsciiType.instance, UTF8Type.instance, p -> p));

        functions.add(CastAsTextFunction.create(InetAddressType.instance, AsciiType.instance));
        functions.add(CastAsTextFunction.create(InetAddressType.instance, UTF8Type.instance));

        functions.add(CastAsTextFunction.create(BooleanType.instance, AsciiType.instance));
        functions.add(CastAsTextFunction.create(BooleanType.instance, UTF8Type.instance));

        functions.add(CassandraFunctionWrapper.create(TimeUUIDType.instance, SimpleDateType.instance, toDate(TimeUUIDType.instance)));
        functions.add(CassandraFunctionWrapper.create(TimeUUIDType.instance, TimestampType.instance, toTimestamp(TimeUUIDType.instance)));
        functions.add(CastAsTextFunction.create(TimeUUIDType.instance, AsciiType.instance));
        functions.add(CastAsTextFunction.create(TimeUUIDType.instance, UTF8Type.instance));
        functions.add(CassandraFunctionWrapper.create(TimestampType.instance, SimpleDateType.instance, toDate(TimestampType.instance)));
        functions.add(CastAsTextFunction.create(TimestampType.instance, AsciiType.instance));
        functions.add(CastAsTextFunction.create(TimestampType.instance, UTF8Type.instance));
        functions.add(CassandraFunctionWrapper.create(SimpleDateType.instance, TimestampType.instance, toTimestamp(SimpleDateType.instance)));
        functions.add(CastAsTextFunction.create(SimpleDateType.instance, AsciiType.instance));
        functions.add(CastAsTextFunction.create(SimpleDateType.instance, UTF8Type.instance));
        functions.add(CastAsTextFunction.create(TimeType.instance, AsciiType.instance));
        functions.add(CastAsTextFunction.create(TimeType.instance, UTF8Type.instance));

        functions.add(CastAsTextFunction.create(UUIDType.instance, AsciiType.instance));
        functions.add(CastAsTextFunction.create(UUIDType.instance, UTF8Type.instance));
    }

    /**
     * Returns the conversion function to convert the specified type into a Decimal type
     *
     * @param inputType the input type
     * @return the conversion function to convert the specified type into a Decimal type
     */
    private static <I extends Number> java.util.function.Function<I, BigDecimal> getDecimalConversionFunction(AbstractType<? extends Number> inputType)
    {
        if (inputType == FloatType.instance)
            return p -> new BigDecimal(Float.toString(p.floatValue()));

        if (inputType == DoubleType.instance)
            return p -> BigDecimal.valueOf(p.doubleValue());

        if (inputType == IntegerType.instance)
            return p -> new BigDecimal((BigInteger) p);

        return p -> BigDecimal.valueOf(p.longValue());
    }

    /**
     * Creates the snake-cased name of the cast function used to cast to the specified type.
     *
     * @param outputType the output type
     * @return the name of the cast function used to cast to the specified type
     */
    public static String getFunctionName(CQL3Type outputType)
    {
        return FUNCTION_NAME_PREFIX + toLowerCaseString(outputType);
    }

    /**
     * Creates the legacy camel-cased name of the cast function used to cast to the specified type.
     *
     * @param outputType the output type
     * @return the legacy camel-cased name of the cast function used to cast to the specified type
     */
    private static String getLegacyFunctionName(CQL3Type outputType)
    {
        return LEGACY_FUNCTION_NAME_PREFIX + StringUtils.capitalize(toLowerCaseString(outputType));
    }

    /**
     * Creates the name of the cast function used to cast to the specified type.
     *
     * @param outputType the output type
     * @param legacy whether to use the old cameCase names, instead of the new snake_case names
     * @return the name of the cast function used to cast to the specified type
     */
    private static String getFunctionName(AbstractType<?> outputType, boolean legacy)
    {
        CQL3Type type = outputType.asCQL3Type();
        return legacy ? getLegacyFunctionName(type) : getFunctionName(type);
    }

    /**
     * Adds to the list a function converting the input type in to the output type if they are not the same.
     *
     * @param functions the list to add to
     * @param inputType the input type
     * @param outputType the output type
     * @param converter the function use to convert the input type into the output type
     */
    private static <I, O> void addFunctionIfNeeded(NativeFunctions functions,
                                                   AbstractType<I> inputType,
                                                   AbstractType<O> outputType,
                                                   java.util.function.Function<I, O> converter)
    {
        if (!inputType.equals(outputType))
            functions.add(wrapJavaFunction(inputType, outputType, converter));
    }

    @SuppressWarnings("unchecked")
    private static <O, I> CastFunction<?, O> wrapJavaFunction(AbstractType<I> inputType,
                                                              AbstractType<O> outputType,
                                                              java.util.function.Function<I, O> converter)
    {
        return inputType.equals(CounterColumnType.instance)
                ? JavaCounterFunctionWrapper.create(outputType, (java.util.function.Function<Long, O>) converter)
                : JavaFunctionWrapper.create(inputType, outputType, converter);
    }

    private static String toLowerCaseString(CQL3Type type)
    {
        return toLowerCaseLocalized(type.toString());
    }

    /**
     * Base class for the CAST functions.
     *
     * @param <I> the input type
     * @param <O> the output type
     */
    private static abstract class CastFunction<I, O> extends NativeScalarFunction
    {
        public CastFunction(AbstractType<I> inputType, AbstractType<O> outputType, boolean useLegacyName)
        {
            super(getFunctionName(outputType, useLegacyName), outputType, inputType);
        }

        @Override
        public String columnName(List<String> columnNames)
        {
            return String.format("cast(%s as %s)", columnNames.get(0), toLowerCaseString(outputType().asCQL3Type()));
        }

        @SuppressWarnings("unchecked")
        protected AbstractType<O> outputType()
        {
            return (AbstractType<O>) returnType;
        }

        @SuppressWarnings("unchecked")
        protected AbstractType<I> inputType()
        {
            return (AbstractType<I>) argTypes.get(0);
        }
    }

    /**
     * <code>CastFunction</code> that implements casting by wrapping a java <code>Function</code>.
     *
     * @param <I> the input parameter
     * @param <O> the output parameter
     */
    private static class JavaFunctionWrapper<I, O> extends CastFunction<I, O>
    {
        /**
         * The java function used to convert the input type into the output one.
         */
        protected final java.util.function.Function<I, O> converter;

        public static <I, O> JavaFunctionWrapper<I, O> create(AbstractType<I> inputType,
                                                              AbstractType<O> outputType,
                                                              java.util.function.Function<I, O> converter)
        {
            return new JavaFunctionWrapper<>(inputType, outputType, converter, false);
        }

        protected JavaFunctionWrapper(AbstractType<I> inputType,
                                      AbstractType<O> outputType,
                                      java.util.function.Function<I, O> converter,
                                      boolean useLegacyName)
        {
            super(inputType, outputType, useLegacyName);
            this.converter = converter;
        }

        @Override
        public JavaFunctionWrapper<I, O> withLegacyName()
        {
            return new JavaFunctionWrapper<>(inputType(), outputType(), converter, true);
        }

        @Override
        public final ByteBuffer execute(Arguments arguments)
        {
            if (arguments.containsNulls())
                return null;

            return outputType().decompose(converter.apply(arguments.get(0)));
        }

        protected I compose(ByteBuffer bb)
        {
            return inputType().compose(bb);
        }
    }

    /**
     * <code>JavaFunctionWrapper</code> for counter columns.
     *
     * <p>Counter columns need to be handled in a special way because their binary representation is converted into
     * the one of a BIGINT before functions are applied.</p>
     *
     * @param <O> the output parameter
     */
    private static class JavaCounterFunctionWrapper<O> extends JavaFunctionWrapper<Long, O>
    {
        public static <O> JavaFunctionWrapper<Long, O> create(AbstractType<O> outputType,
                                                              java.util.function.Function<Long, O> converter)
        {
            return new JavaCounterFunctionWrapper<>(outputType, converter, false);
        }

        protected JavaCounterFunctionWrapper(AbstractType<O> outputType,
                                             java.util.function.Function<Long, O> converter,
                                             boolean useLegacyName)
        {
            super(CounterColumnType.instance, outputType, converter, useLegacyName);
        }

        @Override
        public JavaCounterFunctionWrapper<O> withLegacyName()
        {
            return new JavaCounterFunctionWrapper<>(outputType(), converter, true);
        }

        protected Long compose(ByteBuffer bb)
        {
            return LongType.instance.compose(bb);
        }
    }

    /**
     * <code>CastFunction</code> that implements casting by wrapping an existing <code>NativeScalarFunction</code>.
     *
     * @param <I> the input parameter
     * @param <O> the output parameter
     */
    private static final class CassandraFunctionWrapper<I, O> extends CastFunction<I, O>
    {
        /**
         * The native scalar function used to perform the conversion.
         */
        private final NativeScalarFunction delegate;

        public static <I, O> CassandraFunctionWrapper<I, O> create(AbstractType<I> inputType,
                                                                   AbstractType<O> outputType,
                                                                   NativeScalarFunction delegate)
        {
            return new CassandraFunctionWrapper<>(inputType, outputType, delegate, false);
        }

        private CassandraFunctionWrapper(AbstractType<I> inputType,
                                         AbstractType<O> outputType,
                                         NativeScalarFunction delegate,
                                         boolean useLegacyName)
        {
            super(inputType, outputType, useLegacyName);
            assert delegate.argTypes().size() == 1 && inputType.equals(delegate.argTypes().get(0));
            assert outputType.equals(delegate.returnType());
            this.delegate = delegate;
        }

        @Override
        public CassandraFunctionWrapper<I, O> withLegacyName()
        {
            return new CassandraFunctionWrapper<>(inputType(), outputType(), delegate, true);
        }

        @Override
        public ByteBuffer execute(Arguments arguments)
        {
            return delegate.execute(arguments);
        }
    }

    /**
     * <code>CastFunction</code> that can be used to cast a type into ascii or text types.
     *
     * @param <I> the input parameter
     */
    private static final class CastAsTextFunction<I> extends CastFunction<I, String>
    {

        public static <I> CastAsTextFunction<I> create(AbstractType<I> inputType,
                                                       AbstractType<String> outputType)
        {
            return new CastAsTextFunction<>(inputType, outputType, false);
        }

        private CastAsTextFunction(AbstractType<I> inputType, AbstractType<String> outputType, boolean useLegacyName)
        {
            super(inputType, outputType, useLegacyName);
        }

        @Override
        public CastAsTextFunction<I> withLegacyName()
        {
            return new CastAsTextFunction<>(inputType(), outputType(), true);
        }

        @Override
        public Arguments newArguments(FunctionContext context)
        {
            return new FunctionArguments(context, (protocolVersion, buffer) -> {
                AbstractType<?> argType = argTypes.get(0);
                if (buffer == null || (!buffer.hasRemaining() && argType.isEmptyValueMeaningless()))
                    return null;

                return argType.getSerializer().toCQLLiteralNoQuote(buffer);
            });
        }

        @Override
        public ByteBuffer execute(Arguments arguments)
        {
            if (arguments.containsNulls())
                return null;

            return outputType().decompose(arguments.get(0));
        }
    }

    /**
     * The class must not be instantiated as it contains only static variables.
     */
    private CastFcts()
    {
    }
}
