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

}
