/*
 * 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.phoenix.schema.types;

import org.apache.hadoop.hbase.io.ImmutableBytesWritable;
import org.apache.hadoop.hbase.util.Bytes;
import org.apache.phoenix.expression.Expression;
import org.apache.phoenix.query.QueryConstants;
import org.apache.phoenix.schema.ColumnValueDecoder;
import org.apache.phoenix.schema.SortOrder;
import org.apache.phoenix.schema.tuple.Tuple;
import org.apache.phoenix.util.ByteUtil;


public class PArrayDataTypeDecoder implements ColumnValueDecoder {
    
    @Override
    public boolean decode(ImmutableBytesWritable ptr, int index) {
        return PArrayDataTypeDecoder.positionAtArrayElement(ptr, index, PVarbinary.INSTANCE, null);
    }

    public static boolean positionAtArrayElement(Tuple tuple, ImmutableBytesWritable ptr, int index,
            Expression arrayExpr, PDataType pDataType, Integer maxLen) {
        if (!arrayExpr.evaluate(tuple, ptr)) {
            return false;
        } else if (ptr.getLength() == 0) { return true; }
    
        // Given a ptr to the entire array, set ptr to point to a particular element within that array
        // given the type of an array element (see comments in PDataTypeForArray)
        return positionAtArrayElement(ptr, index - 1, pDataType, maxLen);
    }

    public static boolean positionAtArrayElement(ImmutableBytesWritable ptr, int arrayIndex, PDataType baseDataType,
            Integer byteSize) {
        byte[] bytes = ptr.get();
        int initPos = ptr.getOffset();
        if (!baseDataType.isFixedWidth()) {
        	byte serializationVersion = bytes[ptr.getOffset() + ptr.getLength() - Bytes.SIZEOF_BYTE];
            int noOfElements = Bytes.toInt(bytes,
                    (ptr.getOffset() + ptr.getLength() - (Bytes.SIZEOF_BYTE + Bytes.SIZEOF_INT)), Bytes.SIZEOF_INT);
            boolean useShort = true;
            if (noOfElements < 0) {
                noOfElements = -noOfElements;
                useShort = false;
            }
            if (arrayIndex >= noOfElements) {
                ptr.set(ByteUtil.EMPTY_BYTE_ARRAY);
                return false;
            }
    
            int indexOffset = Bytes.toInt(bytes,
                    (ptr.getOffset() + ptr.getLength() - (Bytes.SIZEOF_BYTE + 2 * Bytes.SIZEOF_INT))) + ptr.getOffset();
            // Skip those many offsets as given in the arrayIndex
            // If suppose there are 5 elements in the array and the arrayIndex = 3
            // This means we need to read the 4th element of the array
            // So inorder to know the length of the 4th element we will read the offset of 4th element and the
            // offset of 5th element.
            // Subtracting the offset of 5th element and 4th element will give the length of 4th element
            // So we could just skip reading the other elements.
            int currOffset = PArrayDataType.getSerializedOffset(bytes, arrayIndex, useShort, indexOffset, serializationVersion);
            if (currOffset<0) {
            	ptr.set(ByteUtil.EMPTY_BYTE_ARRAY);
                return false;
            }
            int elementLength = 0;
            if (arrayIndex == (noOfElements - 1)) {
                // in the original IMMUTABLE_SERIALIZATION_VERSION (v1), for nulls we store
                // (separatorByte, #_of_nulls) in the data. Because of the separatorByte, we can't
                // distinguish between nulls and actual data values that start with the separator
                // byte. We do a hack here to limit the damage by checking offsets - if the prior
                // offset had a length of 0, then we know we're storing 2 or more nulls. However, we
                // still can't fix the case distinguishing a single null from a short value. There
                // are two kinds of separatorByte, so the results will be potentially incorrect for
                // 2 short values that correspond to (separatorByte, 1)
                if (serializationVersion == PArrayDataType.IMMUTABLE_SERIALIZATION_VERSION) {
                    elementLength = indexOffset - (currOffset + initPos);
                    if (isNullValue(arrayIndex, bytes, initPos, serializationVersion, useShort, indexOffset, currOffset, elementLength)) {
                        elementLength = 0;
                    }
                } else {
                    int separatorBytes =  serializationVersion == PArrayDataType.SORTABLE_SERIALIZATION_VERSION ? 3 : 0;
                    elementLength = isSeparatorByte(bytes, initPos, currOffset) && serializationVersion == PArrayDataType.SORTABLE_SERIALIZATION_VERSION ? 0 : indexOffset
                            - (currOffset + initPos) - separatorBytes;
                }
            } else {
                if (serializationVersion == PArrayDataType.IMMUTABLE_SERIALIZATION_VERSION) {
                    elementLength = PArrayDataType.getOffset(bytes, arrayIndex + 1,
                        useShort, indexOffset, serializationVersion)
                            - currOffset;
                    if (isNullValue(arrayIndex, bytes, initPos, serializationVersion, useShort, indexOffset, currOffset, elementLength)) {
                        elementLength = 0;
                    }
                } else {
                    int separatorByte =
                            serializationVersion == PArrayDataType.SORTABLE_SERIALIZATION_VERSION
                                    ? 1
                                    : 0;
                    elementLength =
                            isSeparatorByte(bytes, initPos, currOffset)
                                    && serializationVersion == PArrayDataType.SORTABLE_SERIALIZATION_VERSION
                                            ? 0
                                            : PArrayDataType.getOffset(bytes, arrayIndex + 1,
                                                useShort, indexOffset, serializationVersion)
                                                    - currOffset - separatorByte;
                }
            }
            ptr.set(bytes, currOffset + initPos, elementLength);
        } else {
            int elemByteSize = (byteSize == null ? baseDataType.getByteSize() : byteSize);
            int offset = arrayIndex * elemByteSize;
            if (offset >= ptr.getLength()) {
                ptr.set(ByteUtil.EMPTY_BYTE_ARRAY);
            } else {
                ptr.set(bytes, ptr.getOffset() + offset, elemByteSize);
            }
        }
        return true;
    }

    // returns true if the prior element in the array is a null
    private static boolean isNullValue(int arrayIndex, byte[] bytes, int initPos,
            byte serializationVersion, boolean useShort, int indexOffset, int currOffset,
            int elementLength) {
        if (isSeparatorByte(bytes, initPos, currOffset)) {
            if (isPriorValueZeroLength(arrayIndex, bytes,
                serializationVersion, useShort, indexOffset, currOffset)) {
                return true;
            } else {
                // if there's no prior null, there can be at most 1 null
                if (elementLength == 2) {
                    // nullByte calculation comes from the encoding of one null
                    // see PArrayDataType#serializeNulls
                    byte nullByte = SortOrder.invert((byte)(0));
                    if (bytes[initPos+currOffset+1] == nullByte) {
                        return true;
                    }
                }
            }
        }
        return false;
    }

    // checks prior value length by subtracting offset of the previous item from the current offset
    private static boolean isPriorValueZeroLength(int arrayIndex, byte[] bytes, byte serializationVersion,
            boolean useShort, int indexOffset, int currOffset) {
        return arrayIndex > 0 && currOffset - PArrayDataType.getOffset(bytes, arrayIndex - 1,
            useShort, indexOffset, serializationVersion) == 0;
    }

    private static boolean isSeparatorByte(byte[] bytes, int initPos, int currOffset) {
        return bytes[currOffset + initPos] == QueryConstants.SEPARATOR_BYTE || bytes[currOffset + initPos] == QueryConstants.DESC_SEPARATOR_BYTE;
    }

}