/*
 * 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.dataflow.data.nontagged.serde.ABooleanSerializerDeserializer;
import org.apache.asterix.formats.nontagged.SerializerDeserializerProvider;
import org.apache.asterix.om.base.ABoolean;
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.api.exceptions.Warning;
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 or three arguments, first argument representing the value, second argument representing
 * the position to apply the operation at, and third argument (optional) represents a boolean flag. The position can
 * be a single value or an array of values. This evaluator is used by the bitwise BITTEST and ISBITSET
 * 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 boolean is returned.
 * - If the argument is float or double, then boolean 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 boolean is returned.
 * - If the argument is float or double, then boolean 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 boolean 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.
 *
 * For the third argument:
 * - If the argument is missing or null, then missing or null is returned, respectively.
 * - If the argument is boolean, then boolean is returned.
 * - If the argument is any other type, then null is returned.
 */

class BitValuePositionFlagEvaluator extends AbstractScalarEval {

    // Result members
    private ABoolean resultBoolean;
    private final ArrayBackedValueStorage resultStorage = new ArrayBackedValueStorage();

    // This flag has a special purpose. In some cases, when an array of positions is passed, checking
    // some arguments might be enough to return the final result, but, instead of stopping, we need to
    // continue looping because if any value in the positions array is invalid, we should return a null instead of
    // the boolean result, this flag will keep the loop going just for checking the values, while the
    // final result is already set previously.
    private boolean isStopUpdatingResultBoolean = false;

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

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

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

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

    BitValuePositionFlagEvaluator(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);

        if (argEvaluatorFactories.length > 2) {
            flagEvaluator = argEvaluatorFactories[2].createScalarEvaluator(context);
            flagPointable = new VoidPointable();
        }
    }

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

        if (flagEvaluator != null) {
            flagEvaluator.evaluate(tuple, flagPointable);
        }

        if (PointableHelper.checkAndSetMissingOrNull(result, valuePointable, positionPointable, flagPointable)) {
            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;
        }

        // Third argument
        boolean isAllSet = false;
        isStopUpdatingResultBoolean = false; // Reset the flag to false for each new tuple
        if (flagEvaluator != null) {
            byte[] flagBytes = flagPointable.getByteArray();
            int flagStartOffset = flagPointable.getStartOffset();
            ATypeTag flagTypeTag = EnumDeserializer.ATYPETAGDESERIALIZER.deserialize(flagBytes[flagStartOffset]);

            if (flagTypeTag != ATypeTag.BOOLEAN) {
                ExceptionUtil.warnTypeMismatch(context, srcLoc, funID, flagBytes[flagStartOffset], 2, ATypeTag.BOOLEAN);
                PointableHelper.setNull(result);
                return;
            }

            isAllSet = ABooleanSerializerDeserializer.getBoolean(flagBytes, flagStartOffset + 1);
        }

        // Result long value
        long 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(longResult, positionBytes, positionStartOffset);
        } else {
            isSuccessfulOperation =
                    applyBitWiseOperationWithArrayAsPosition(longResult, positionBytes, positionStartOffset, isAllSet);
        }

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

        // Write the result
        resultStorage.reset();
        aBooleanSerde.serialize(resultBoolean, resultStorage.getDataOutput());
        result.set(resultStorage);
    }

    /**
     * This method applies the bitwise operation on the int64 value at the specified positions in
     * the array.
     *
     * @param value The long value
     * @param bytes bytes of the position
     * @param startOffset start offset of the position
     * @param isAllSet is AllSet check flag
     * @return returns {@code true} if all operations are successful, {@code false} otherwise.
     *
     * @throws HyracksDataException HyracksDataException
     */
    private boolean applyBitWiseOperationWithArrayAsPosition(long value, byte[] bytes, int startOffset,
            boolean isAllSet) 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++) {
                listAccessor.getOrWriteItem(i, listItemPointable, resultStorage);

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

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

                // Based on the isAllSet flag, we might want to stop re-calculating the result, this
                // isStopUpdatingResultBoolean keeps an eye on that.
                isStopUpdatingResultBoolean =
                        !isAllSet ? resultBoolean == ABoolean.TRUE : resultBoolean == ABoolean.FALSE;
            }

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

    /**
     * This method applies the bitwise operation on the int64 value at the specified position.
     *
     * @param value the long value
     * @param bytes data bytes
     * @param startOffset start offset
     * @return returns {@code true} if all operations are successful, {@code false} otherwise.
     */
    private boolean applyBitWiseOperationWithNumericAsPosition(long value, 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;
        }

        // Checks if a certain position is bit 1
        if (!isStopUpdatingResultBoolean) {
            resultBoolean = (value & (1L << (position - 1))) != 0 ? ABoolean.TRUE : ABoolean.FALSE;
        }

        return true;
    }

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