/*
 * 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.asterix.runtime.evaluators.functions.bitwise;

import java.io.IOException;

import org.apache.asterix.common.exceptions.ErrorCode;
import org.apache.asterix.common.exceptions.WarningUtil;
import org.apache.asterix.formats.nontagged.SerializerDeserializerProvider;
import org.apache.asterix.om.base.AMutableInt64;
import org.apache.asterix.om.exceptions.ExceptionUtil;
import org.apache.asterix.om.types.ATypeTag;
import org.apache.asterix.om.types.BuiltinType;
import org.apache.asterix.om.types.EnumDeserializer;
import org.apache.asterix.om.types.hierachy.ATypeHierarchy;
import org.apache.asterix.runtime.evaluators.common.ListAccessor;
import org.apache.asterix.runtime.evaluators.functions.AbstractScalarEval;
import org.apache.asterix.runtime.evaluators.functions.PointableHelper;
import org.apache.hyracks.algebricks.core.algebra.functions.FunctionIdentifier;
import org.apache.hyracks.algebricks.runtime.base.IEvaluatorContext;
import org.apache.hyracks.algebricks.runtime.base.IScalarEvaluator;
import org.apache.hyracks.algebricks.runtime.base.IScalarEvaluatorFactory;
import org.apache.hyracks.api.dataflow.value.ISerializerDeserializer;
import org.apache.hyracks.api.exceptions.HyracksDataException;
import org.apache.hyracks.api.exceptions.IWarningCollector;
import org.apache.hyracks.api.exceptions.SourceLocation;
import org.apache.hyracks.data.std.api.IPointable;
import org.apache.hyracks.data.std.primitive.VoidPointable;
import org.apache.hyracks.data.std.util.ArrayBackedValueStorage;
import org.apache.hyracks.dataflow.common.data.accessors.IFrameTupleReference;

/**
 * This function receives two arguments, first argument representing the value, and second argument representing
 * the position to apply the operation at. The position can be a single value or an array of values. This evaluator
 * is used by the bitwise CLEAR and SET functions.
 *
 * These functions can be applied only to int64 type numeric values.
 *
 * The function has the following behavior:
 * For the first argument:
 * - If the argument is missing or null, then missing or null is returned, respectively.
 * - If the argument is int64, then int64 is returned.
 * - If the argument is float or double, then int64 or null is returned, depending on the argument value.
 * - If the argument is any other type, then null is returned.
 *
 * For the second argument:
 * - If the argument is missing or null, then missing or null is returned, respectively.
 * - If the argument is int64, then int64 is returned.
 * - If the argument is float or double, then int64 or null is returned, depending on the argument value.
 * - If the argument is an array:
 *      - If the array item type is int64, then int64 or boolean is returned.
 *      - If the array item type is float or double, then int64 or null is returned, depending on the argument.
 *      - If the array item type is any other type, then null is returned.
 * - If the argument is any other type, then null is returned.
 */

abstract class AbstractBitValuePositionEvaluator extends AbstractScalarEval {

    // Result members
    private long longResult;
    private final AMutableInt64 resultMutableInt64 = new AMutableInt64(0);
    private final ArrayBackedValueStorage resultStorage = new ArrayBackedValueStorage();

    // Evaluators and Pointables
    private final IScalarEvaluator valueEvaluator;
    private final IScalarEvaluator positionEvaluator;
    private final IPointable valuePointable = new VoidPointable();
    private final IPointable positionPointable = new VoidPointable();

    // List accessor
    private final ListAccessor listAccessor = new ListAccessor();
    private final IPointable listItemPointable = new VoidPointable();

    // Serializer/Deserializer
    @SuppressWarnings("rawtypes")
    private final ISerializerDeserializer aInt64Serde =
            SerializerDeserializerProvider.INSTANCE.getSerializerDeserializer(BuiltinType.AINT64);

    private final IEvaluatorContext context;
    private static final byte[] secondArgumentExpectedTypes =
            new byte[] { ATypeTag.SERIALIZED_INT64_TYPE_TAG, ATypeTag.SERIALIZED_ORDEREDLIST_TYPE_TAG };

    AbstractBitValuePositionEvaluator(IEvaluatorContext context, IScalarEvaluatorFactory[] argEvaluatorFactories,
            FunctionIdentifier functionIdentifier, SourceLocation sourceLocation) throws HyracksDataException {
        super(sourceLocation, functionIdentifier);
        this.context = context;

        // Evaluators
        valueEvaluator = argEvaluatorFactories[0].createScalarEvaluator(context);
        positionEvaluator = argEvaluatorFactories[1].createScalarEvaluator(context);
    }

    // Abstract methods
    abstract long applyBitwiseOperation(long value, long position);

    @SuppressWarnings("unchecked")
    @Override
    public void evaluate(IFrameTupleReference tuple, IPointable result) throws HyracksDataException {
        valueEvaluator.evaluate(tuple, valuePointable);
        positionEvaluator.evaluate(tuple, positionPointable);

        if (PointableHelper.checkAndSetMissingOrNull(result, valuePointable, positionPointable)) {
            return;
        }

        // Value argument
        byte[] valueBytes = valuePointable.getByteArray();
        int valueStartOffset = valuePointable.getStartOffset();

        // Type and value validity check
        if (!PointableHelper.isValidLongValue(valueBytes, valueStartOffset, true)) {
            ExceptionUtil.warnTypeMismatch(context, srcLoc, funID, valueBytes[valueStartOffset], 0, ATypeTag.BIGINT);
            PointableHelper.setNull(result);
            return;
        }

        // Position argument
        byte[] positionBytes = positionPointable.getByteArray();
        int positionStartOffset = positionPointable.getStartOffset();
        ATypeTag positionTypeTag =
                EnumDeserializer.ATYPETAGDESERIALIZER.deserialize(positionBytes[positionStartOffset]);

        // Type validity check (for position argument, array is a valid type as well)
        if (!ATypeHierarchy.canPromote(positionTypeTag, ATypeTag.DOUBLE) && positionTypeTag != ATypeTag.ARRAY) {
            ExceptionUtil.warnTypeMismatch(context, srcLoc, funID, positionBytes[positionStartOffset], 1,
                    secondArgumentExpectedTypes);
            PointableHelper.setNull(result);
            return;
        }

        // Result long value
        longResult = ATypeHierarchy.getLongValue(funID.getName(), 0, valueBytes, valueStartOffset);

        // If any operation returns false, the result should be null
        boolean isSuccessfulOperation;

        // From here onward, we got valid arguments, so let's handle numeric or array position argument
        if (positionTypeTag != ATypeTag.ARRAY) {
            isSuccessfulOperation = applyBitWiseOperationWithNumericAsPosition(positionBytes, positionStartOffset);
        } else {
            isSuccessfulOperation = applyBitWiseOperationWithArrayAsPosition(positionBytes, positionStartOffset);
        }

        if (!isSuccessfulOperation) {
            PointableHelper.setNull(result);
            return;
        }

        // Write the result
        resultStorage.reset();
        resultMutableInt64.setValue(longResult);
        aInt64Serde.serialize(resultMutableInt64, resultStorage.getDataOutput());
        result.set(resultStorage);
    }

    /**
     * This method applies the bitwise operation on the value at the specified positions in the array.
     *
     * @param bytes bytes of the position
     * @param startOffset start offset of the position
     * @return returns {@code true} if all operations are successful, {@code false} otherwise.
     *
     * @throws HyracksDataException HyracksDataException
     */
    private boolean applyBitWiseOperationWithArrayAsPosition(byte[] bytes, int startOffset)
            throws HyracksDataException {

        listAccessor.reset(bytes, startOffset);

        // In case of an empty array, we return the value as is without change
        if (listAccessor.size() == 0) {
            return true;
        }

        try {
            // For each item in the array, apply the bitwise operation at that position
            for (int i = 0; i < listAccessor.size(); i++) {
                resultStorage.reset();
                listAccessor.getOrWriteItem(i, listItemPointable, resultStorage);

                byte[] itemBytes = listItemPointable.getByteArray();
                int itemStartOffset = listItemPointable.getStartOffset();

                if (!applyBitWiseOperationWithNumericAsPosition(itemBytes, itemStartOffset)) {
                    return false;
                }
            }

            return true;
        } catch (IOException ex) {
            throw HyracksDataException.create(ex);
        }
    }

    /**
     * This method applies the bitwise operation on the value at the specified position.
     *
     * @param bytes data bytes
     * @param startOffset start offset
     * @return returns {@code true} if all operations are successful, {@code false} otherwise.
     */
    private boolean applyBitWiseOperationWithNumericAsPosition(byte[] bytes, int startOffset)
            throws HyracksDataException {

        // Value validity check
        if (!PointableHelper.isValidLongValue(bytes, startOffset, true)) {
            ExceptionUtil.warnTypeMismatch(context, srcLoc, funID, bytes[startOffset], 1, ATypeTag.BIGINT);
            return false;
        }

        long position = ATypeHierarchy.getLongValue(funID.getName(), 1, bytes, startOffset);

        // Ensure the position is between 1 and 64 (int64 has 64 bits)
        if (position < 1 || position > 64) {
            handleOutOfRangeInput(1, 1, 64, position);
            return false;
        }

        longResult = applyBitwiseOperation(longResult, position);

        return true;
    }

    private void handleOutOfRangeInput(int inputPosition, int startLimit, int endLimit, long actual) {
        IWarningCollector warningCollector = context.getWarningCollector();
        if (warningCollector.shouldWarn()) {
            warningCollector.warn(WarningUtil.forAsterix(srcLoc, ErrorCode.VALUE_OUT_OF_RANGE, funID,
                    ExceptionUtil.indexToPosition(inputPosition), startLimit, endLimit, actual));
        }
    }
}
