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

import static org.apache.asterix.om.types.EnumDeserializer.ATYPETAGDESERIALIZER;

import java.io.IOException;

import org.apache.asterix.builders.IAsterixListBuilder;
import org.apache.asterix.builders.OrderedListBuilder;
import org.apache.asterix.builders.UnorderedListBuilder;
import org.apache.asterix.formats.nontagged.BinaryComparatorFactoryProvider;
import org.apache.asterix.om.pointables.base.DefaultOpenFieldType;
import org.apache.asterix.om.types.ATypeTag;
import org.apache.asterix.om.types.AbstractCollectionType;
import org.apache.asterix.om.types.BuiltinType;
import org.apache.asterix.om.types.IAType;
import org.apache.asterix.om.types.hierachy.ATypeHierarchy;
import org.apache.asterix.runtime.evaluators.common.ListAccessor;
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.IBinaryComparator;
import org.apache.hyracks.api.exceptions.HyracksDataException;
import org.apache.hyracks.api.exceptions.SourceLocation;
import org.apache.hyracks.data.std.api.AbstractPointable;
import org.apache.hyracks.data.std.api.IPointable;
import org.apache.hyracks.data.std.primitive.TaggedValuePointable;
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;

/**
* <pre>
* array_replace(list, val1, val2, max_num_times?) returns a new list with the occurrences of val1 replaced with
* val2. max_num_times arg is optional. If supplied, it replaces val1 as many as max_num_times. Any negative number for
* max_num_times means "replace all occurrences". val2 can be null meaning you can replace existing items with nulls.
*
* array_replace([2,3,3,3,1], 3, 8, 0) will do nothing and result in [2,3,3,3,1].
*
* It throws an error at compile time if the number of arguments < 3 or > 4
*
* It returns (or throws an error at runtime) in order:
* 1. missing, if any argument is missing.
* 2. null, if:
* - any argument is null (except for val2).
* - input list is not a list.
* - num_times is not numeric or it's a floating-point number with decimals, e.g, 3.2 (3.0 is OK).
* 3. otherwise, a new list.
*
* </pre>
*/

public class ArrayReplaceEvaluator extends AbstractScalarEval {

    private final IAType inputListType;
    private final IAType newValueType;
    private final IScalarEvaluator listEval;
    private final IScalarEvaluator targetValEval;
    private final IScalarEvaluator newValEval;
    private IScalarEvaluator maxEval;
    private final IPointable list;
    private final IPointable tempList;
    private final IPointable target;
    private final IPointable newVal;
    private final IPointable tempVal;
    private TaggedValuePointable maxArg;
    private final AbstractPointable item;
    private final ListAccessor listAccessor;
    private final IBinaryComparator comp;
    private final ArrayBackedValueStorage storage;
    private final CastTypeEvaluator caster;
    private IAsterixListBuilder orderedListBuilder;
    private IAsterixListBuilder unorderedListBuilder;

    ArrayReplaceEvaluator(IScalarEvaluatorFactory[] args, IEvaluatorContext ctx, SourceLocation sourceLocation,
            FunctionIdentifier functionIdentifier, IAType[] argTypes) throws HyracksDataException {
        super(sourceLocation, functionIdentifier);
        storage = new ArrayBackedValueStorage();
        listEval = args[0].createScalarEvaluator(ctx);
        targetValEval = args[1].createScalarEvaluator(ctx);
        newValEval = args[2].createScalarEvaluator(ctx);
        if (args.length == 4) {
            maxEval = args[3].createScalarEvaluator(ctx);
            maxArg = new TaggedValuePointable();
        }
        list = new VoidPointable();
        tempList = new VoidPointable();
        target = new VoidPointable();
        newVal = new VoidPointable();
        tempVal = new VoidPointable();
        item = new VoidPointable();
        listAccessor = new ListAccessor();
        caster = new CastTypeEvaluator();
        orderedListBuilder = null;
        unorderedListBuilder = null;

        inputListType = argTypes[0];
        IAType targetValueType = argTypes[1];
        newValueType = argTypes[2];

        // the input list will be opened, therefore, the type of left (item type) is ANY
        comp = BinaryComparatorFactoryProvider.INSTANCE
                .getBinaryComparatorFactory(BuiltinType.ANY, targetValueType, true).createBinaryComparator();
    }

    @Override
    public void evaluate(IFrameTupleReference tuple, IPointable result) throws HyracksDataException {
        storage.reset();
        listEval.evaluate(tuple, tempList);
        targetValEval.evaluate(tuple, target);
        newValEval.evaluate(tuple, tempVal);
        ATypeTag listType = ATYPETAGDESERIALIZER.deserialize(tempList.getByteArray()[tempList.getStartOffset()]);
        ATypeTag targetTag = ATYPETAGDESERIALIZER.deserialize(target.getByteArray()[target.getStartOffset()]);
        ATypeTag newValTag = ATYPETAGDESERIALIZER.deserialize(tempVal.getByteArray()[tempVal.getStartOffset()]);
        if (listType == ATypeTag.MISSING || targetTag == ATypeTag.MISSING || newValTag == ATypeTag.MISSING) {
            PointableHelper.setMissing(result);
            return;
        }

        double maxDouble = -1;
        String name = funID.getName();
        if (maxEval != null) {
            maxEval.evaluate(tuple, maxArg);
            ATypeTag maxTag = ATYPETAGDESERIALIZER.deserialize(maxArg.getTag());
            if (maxTag == ATypeTag.MISSING) {
                PointableHelper.setMissing(result);
                return;
            } else if (!ATypeHierarchy.isCompatible(maxTag, ATypeTag.DOUBLE)) {
                PointableHelper.setNull(result);
                return;
            }
            maxDouble = ATypeHierarchy.getDoubleValue(name, 3, maxArg.getByteArray(), maxArg.getStartOffset());
        }

        if (!listType.isListType() || Math.floor(maxDouble) < maxDouble || targetTag == ATypeTag.NULL
                || Double.isInfinite(maxDouble) || Double.isNaN(maxDouble)) {
            PointableHelper.setNull(result);
            return;
        }
        try {
            IAType defaultOpenType = DefaultOpenFieldType.getDefaultOpenFieldType(listType);
            caster.resetAndAllocate(defaultOpenType, inputListType, listEval);
            caster.cast(tempList, list);
            defaultOpenType = DefaultOpenFieldType.getDefaultOpenFieldType(newValTag);
            if (defaultOpenType != null) {
                caster.resetAndAllocate(defaultOpenType, newValueType, newValEval);
                caster.cast(tempVal, newVal);
            } else {
                newVal.set(tempVal);
            }
            int max = (int) maxDouble;
            // create list
            IAsterixListBuilder listBuilder;
            if (listType == ATypeTag.ARRAY) {
                if (orderedListBuilder == null) {
                    orderedListBuilder = new OrderedListBuilder();
                }
                listBuilder = orderedListBuilder;
            } else {
                if (unorderedListBuilder == null) {
                    unorderedListBuilder = new UnorderedListBuilder();
                }
                listBuilder = unorderedListBuilder;
            }
            listBuilder.reset((AbstractCollectionType) DefaultOpenFieldType.getDefaultOpenFieldType(listType));
            listAccessor.reset(list.getByteArray(), list.getStartOffset());
            int counter = 0;
            byte[] targetBytes = target.getByteArray();
            int offset = target.getStartOffset();
            int length = target.getLength();
            for (int i = 0; i < listAccessor.size(); i++) {
                listAccessor.getOrWriteItem(i, item, storage);
                if (counter != max && comp.compare(item.getByteArray(), item.getStartOffset(), item.getLength(),
                        targetBytes, offset, length) == 0) {
                    listBuilder.addItem(newVal);
                    counter++;
                } else {
                    listBuilder.addItem(item);
                }
            }
            storage.reset();
            listBuilder.write(storage.getDataOutput(), true);
            result.set(storage);
        } catch (IOException e) {
            throw HyracksDataException.create(e);
        } finally {
            caster.deallocatePointables();
        }
    }
}
