/*
 * 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 java.math.BigDecimal;
import java.sql.Timestamp;
import java.sql.Types;
import java.text.Format;
import org.apache.hadoop.hbase.io.ImmutableBytesWritable;
import org.apache.hadoop.hbase.util.Bytes;
import org.apache.phoenix.query.QueryConstants;
import org.apache.phoenix.schema.SortOrder;
import org.apache.phoenix.schema.TypeMismatchException;
import org.apache.phoenix.util.ByteUtil;
import org.apache.phoenix.util.DateUtil;
import org.apache.phoenix.util.NumberUtil;

import org.apache.phoenix.thirdparty.com.google.common.base.Preconditions;

public class PDecimal extends PRealNumber<BigDecimal> {

  public static final PDecimal INSTANCE = new PDecimal();

  private static final BigDecimal MIN_DOUBLE_AS_BIG_DECIMAL = BigDecimal.valueOf(-Double.MAX_VALUE);
  private static final BigDecimal MAX_DOUBLE_AS_BIG_DECIMAL = BigDecimal.valueOf(Double.MAX_VALUE);
  private static final BigDecimal MIN_FLOAT_AS_BIG_DECIMAL = BigDecimal.valueOf(-Float.MAX_VALUE);
  private static final BigDecimal MAX_FLOAT_AS_BIG_DECIMAL = BigDecimal.valueOf(Float.MAX_VALUE);

  private PDecimal() {
    super("DECIMAL", Types.DECIMAL, BigDecimal.class, null, 8);
  }

  @Override
  public byte[] toBytes(Object object) {
    if (object == null) {
      return ByteUtil.EMPTY_BYTE_ARRAY;
    }
    BigDecimal v = (BigDecimal) object;
    v = NumberUtil.normalize(v);
    int len = getLength(v);
    byte[] result = new byte[Math.min(len, MAX_BIG_DECIMAL_BYTES)];
    PDataType.toBytes(v, result, 0, len);
    return result;
  }

  @Override
  public int toBytes(Object object, byte[] bytes, int offset) {
    if (object == null) {
      return 0;
    }
    BigDecimal v = (BigDecimal) object;
    v = NumberUtil.normalize(v);
    int len = getLength(v);
    return PDataType.toBytes(v, bytes, offset, len);
  }

  private int getLength(BigDecimal v) {
    int signum = v.signum();
    if (signum == 0) { // Special case for zero
      return 1;
    }
    /*
     * Size of DECIMAL includes: 1) one byte for exponent 2) one byte for terminal byte if negative
     * 3) one byte for every two digits with the following caveats: a) add one to round up in the
     * case when there is an odd number of digits b) add one in the case that the scale is odd to
     * account for 10x of lowest significant digit (basically done to increase the range of
     * exponents that can be represented)
     */
    return (signum < 0 ? 2 : 1) + (v.precision() + 1 + (v.scale() % 2 == 0 ? 0 : 1)) / 2;
  }

  @Override
  public int estimateByteSize(Object o) {
    if (o == null) {
      return 1;
    }
    BigDecimal v = (BigDecimal) o;
    // TODO: should we strip zeros and round here too?
    return Math.min(getLength(v), MAX_BIG_DECIMAL_BYTES);
  }

  @Override
  public Integer getMaxLength(Object o) {
    if (o == null) {
      return MAX_PRECISION;
    }
    BigDecimal v = (BigDecimal) o;
    return v.precision();
  }

  @Override
  public Integer getScale(Object o) {
    return null;
  }

  @Override
  public Object toObject(byte[] b, int o, int l, PDataType actualType, SortOrder sortOrder,
    Integer maxLength, Integer scale) {
    Preconditions.checkNotNull(sortOrder);
    if (l == 0) {
      return null;
    }
    if (actualType == PDecimal.INSTANCE) {
      if (sortOrder == SortOrder.DESC) {
        b = SortOrder.invert(b, o, new byte[l], 0, l);
        o = 0;
      }
      return toBigDecimal(b, o, l);
    } else if (
      equalsAny(actualType, PDate.INSTANCE, PTime.INSTANCE, PUnsignedDate.INSTANCE,
        PUnsignedTime.INSTANCE, PLong.INSTANCE, PUnsignedLong.INSTANCE, PInteger.INSTANCE,
        PUnsignedInt.INSTANCE, PSmallint.INSTANCE, PUnsignedSmallint.INSTANCE, PTinyint.INSTANCE,
        PUnsignedTinyint.INSTANCE)
    ) {
      return BigDecimal.valueOf(actualType.getCodec().decodeLong(b, o, sortOrder));
    } else if (equalsAny(actualType, PFloat.INSTANCE, PUnsignedFloat.INSTANCE)) {
      return BigDecimal.valueOf(actualType.getCodec().decodeFloat(b, o, sortOrder));
    } else if (equalsAny(actualType, PDouble.INSTANCE, PUnsignedDouble.INSTANCE)) {
      return BigDecimal.valueOf(actualType.getCodec().decodeDouble(b, o, sortOrder));
    } else if (equalsAny(actualType, PTimestamp.INSTANCE, PUnsignedTimestamp.INSTANCE)) {
      long millisPart = DateUtil.getCodecFor(actualType).decodeLong(b, o, sortOrder);
      int nanoPart =
        PUnsignedInt.INSTANCE.getCodec().decodeInt(b, o + Bytes.SIZEOF_LONG, sortOrder);
      BigDecimal nanosPart =
        BigDecimal.valueOf((nanoPart % QueryConstants.MILLIS_TO_NANOS_CONVERTOR)
          / QueryConstants.MILLIS_TO_NANOS_CONVERTOR);
      return BigDecimal.valueOf(millisPart).add(nanosPart);
    } else if (actualType == PBoolean.INSTANCE) {
      return (Boolean) PBoolean.INSTANCE.toObject(b, o, l, actualType, sortOrder)
        ? BigDecimal.ONE
        : BigDecimal.ZERO;
    }
    return throwConstraintViolationException(actualType, this);
  }

  @Override
  public Object toObject(Object object, PDataType actualType) {
    if (object == null) {
      return null;
    }
    if (equalsAny(actualType, PInteger.INSTANCE, PUnsignedInt.INSTANCE)) {
      return BigDecimal.valueOf((Integer) object);
    } else if (equalsAny(actualType, PLong.INSTANCE, PUnsignedLong.INSTANCE)) {
      return BigDecimal.valueOf((Long) object);
    } else if (equalsAny(actualType, PSmallint.INSTANCE, PUnsignedSmallint.INSTANCE)) {
      return BigDecimal.valueOf((Short) object);
    } else if (equalsAny(actualType, PTinyint.INSTANCE, PUnsignedTinyint.INSTANCE)) {
      return BigDecimal.valueOf((Byte) object);
    } else if (equalsAny(actualType, PFloat.INSTANCE, PUnsignedFloat.INSTANCE)) {
      return BigDecimal.valueOf((Float) object);
    } else if (equalsAny(actualType, PDouble.INSTANCE, PUnsignedDouble.INSTANCE)) {
      return BigDecimal.valueOf((Double) object);
    } else if (actualType == PDecimal.INSTANCE) {
      return object;
    } else if (
      equalsAny(actualType, PDate.INSTANCE, PUnsignedDate.INSTANCE, PTime.INSTANCE,
        PUnsignedTime.INSTANCE)
    ) {
      java.util.Date d = (java.util.Date) object;
      return BigDecimal.valueOf(d.getTime());
    } else if (equalsAny(actualType, PTimestamp.INSTANCE, PUnsignedTimestamp.INSTANCE)) {
      Timestamp ts = (Timestamp) object;
      long millisPart = ts.getTime();
      BigDecimal nanosPart =
        BigDecimal.valueOf((ts.getNanos() % QueryConstants.MILLIS_TO_NANOS_CONVERTOR)
          / QueryConstants.MILLIS_TO_NANOS_CONVERTOR);
      BigDecimal value = BigDecimal.valueOf(millisPart).add(nanosPart);
      return value;
    } else if (actualType == PBoolean.INSTANCE) {
      return ((Boolean) object) ? BigDecimal.ONE : BigDecimal.ZERO;
    }
    return throwConstraintViolationException(actualType, this);
  }

  @Override
  public boolean isFixedWidth() {
    return false;
  }

  @Override
  public Integer getByteSize() {
    return MAX_BIG_DECIMAL_BYTES;
  }

  @Override
  public int compareTo(Object lhs, Object rhs, PDataType rhsType) {
    if (lhs == rhs) {
      return 0;
    }
    if (lhs == null) {
      return -1;
    }
    if (rhs == null) {
      return 1;
    }
    if (rhsType == PDecimal.INSTANCE) {
      return ((BigDecimal) lhs).compareTo((BigDecimal) rhs);
    }
    return -rhsType.compareTo(rhs, lhs, this);
  }

  @Override
  public boolean isCastableTo(PDataType targetType) {
    return super.isCastableTo(targetType) || targetType.isCoercibleTo(PTimestamp.INSTANCE)
      || targetType.equals(PBoolean.INSTANCE);
  }

  @Override
  public boolean isCoercibleTo(PDataType targetType, Object value) {
    if (value != null) {
      BigDecimal bd;
      if (
        equalsAny(targetType, PUnsignedLong.INSTANCE, PUnsignedInt.INSTANCE,
          PUnsignedSmallint.INSTANCE, PUnsignedTinyint.INSTANCE)
      ) {
        bd = (BigDecimal) value;
        if (bd.signum() == -1) {
          return false;
        }
      } else if (targetType.equals(PLong.INSTANCE)) {
        bd = (BigDecimal) value;
        try {
          bd.longValueExact();
          return true;
        } catch (ArithmeticException e) {
          return false;
        }
      } else if (targetType.equals(PInteger.INSTANCE)) {
        bd = (BigDecimal) value;
        try {
          bd.intValueExact();
          return true;
        } catch (ArithmeticException e) {
          return false;
        }
      } else if (targetType.equals(PSmallint.INSTANCE)) {
        bd = (BigDecimal) value;
        try {
          bd.shortValueExact();
          return true;
        } catch (ArithmeticException e) {
          return false;
        }
      } else if (targetType.equals(PTinyint.INSTANCE)) {
        bd = (BigDecimal) value;
        try {
          bd.byteValueExact();
          return true;
        } catch (ArithmeticException e) {
          return false;
        }
      } else if (targetType.equals(PUnsignedFloat.INSTANCE)) {
        bd = (BigDecimal) value;
        try {
          BigDecimal maxFloat = MAX_FLOAT_AS_BIG_DECIMAL;
          boolean isNegtive = (bd.signum() == -1);
          return bd.compareTo(maxFloat) <= 0 && !isNegtive;
        } catch (Exception e) {
          return false;
        }
      } else if (targetType.equals(PFloat.INSTANCE)) {
        bd = (BigDecimal) value;
        try {
          BigDecimal maxFloat = MAX_FLOAT_AS_BIG_DECIMAL;
          // Float.MIN_VALUE should not be used here, as this is the
          // smallest in terms of closest to zero.
          BigDecimal minFloat = MIN_FLOAT_AS_BIG_DECIMAL;
          return bd.compareTo(maxFloat) <= 0 && bd.compareTo(minFloat) >= 0;
        } catch (Exception e) {
          return false;
        }
      } else if (targetType.equals(PUnsignedDouble.INSTANCE)) {
        bd = (BigDecimal) value;
        try {
          BigDecimal maxDouble = MAX_DOUBLE_AS_BIG_DECIMAL;
          boolean isNegtive = (bd.signum() == -1);
          return bd.compareTo(maxDouble) <= 0 && !isNegtive;
        } catch (Exception e) {
          return false;
        }
      } else if (targetType.equals(PDouble.INSTANCE)) {
        bd = (BigDecimal) value;
        try {
          BigDecimal maxDouble = MAX_DOUBLE_AS_BIG_DECIMAL;
          BigDecimal minDouble = MIN_DOUBLE_AS_BIG_DECIMAL;
          return bd.compareTo(maxDouble) <= 0 && bd.compareTo(minDouble) >= 0;
        } catch (Exception e) {
          return false;
        }
      }
    }
    return super.isCoercibleTo(targetType, value);
  }

  @Override
  public boolean isSizeCompatible(ImmutableBytesWritable ptr, Object value, PDataType srcType,
    SortOrder sortOrder, Integer maxLength, Integer scale, Integer desiredMaxLength,
    Integer desiredScale) {
    if (ptr.getLength() == 0) {
      return true;
    }
    // Any numeric type fits into a DECIMAL
    if (srcType != PDecimal.INSTANCE) {
      if (!srcType.isCoercibleTo(this)) {
        throw new IllegalArgumentException(TypeMismatchException.newException(srcType, this));
      }
      return true;
    }
    // Use the scale from the value if provided, as it prevents a deserialization.
    // The maxLength and scale for the underlying expression are ignored, because they
    // are not relevant in this case: for example a DECIMAL(10,2) may be assigned to a
    // DECIMAL(5,0) as long as the value fits.
    if (value != null) {
      BigDecimal v = (BigDecimal) value;
      maxLength = v.precision();
      scale = v.scale();
    } else {
      this.coerceBytes(ptr, value, srcType, maxLength, scale, sortOrder, desiredMaxLength,
        desiredScale, sortOrder, true);
      int[] v = getDecimalPrecisionAndScale(ptr.get(), ptr.getOffset(), ptr.getLength(), sortOrder);
      maxLength = v[0];
      scale = v[1];
    }
    if (
      desiredMaxLength != null && desiredScale != null && maxLength != null && scale != null
        && ((desiredMaxLength - desiredScale) < (maxLength - scale))
    ) {
      return false;
    }
    return true;
  }

  @Override
  public void coerceBytes(ImmutableBytesWritable ptr, Object object, PDataType actualType,
    Integer maxLength, Integer scale, SortOrder actualModifier, Integer desiredMaxLength,
    Integer desiredScale, SortOrder expectedModifier) {
    if (desiredScale == null) {
      // deiredScale not available, or we do not have scale requirement, delegate to parents.
      super.coerceBytes(ptr, object, actualType, maxLength, scale, actualModifier, desiredMaxLength,
        desiredScale, expectedModifier);
      return;
    }
    if (ptr.getLength() == 0) {
      return;
    }
    if (scale == null) {
      if (object != null) {
        BigDecimal v = (BigDecimal) object;
        scale = v.scale();
      } else {
        int[] v =
          getDecimalPrecisionAndScale(ptr.get(), ptr.getOffset(), ptr.getLength(), actualModifier);
        scale = v[1];
      }
    }
    if (this == actualType && scale <= desiredScale) {
      // No coerce and rescale necessary
      return;
    } else {
      BigDecimal decimal;
      // Rescale is necessary.
      if (object != null) { // value object is passed in.
        decimal = (BigDecimal) toObject(object, actualType);
      } else { // only value bytes is passed in, need to convert to object first.
        decimal = (BigDecimal) toObject(ptr);
      }
      decimal = decimal.setScale(desiredScale, BigDecimal.ROUND_DOWN);
      ptr.set(toBytes(decimal));
    }
  }

  @Override
  public Object toObject(String value) {
    if (value == null || value.length() == 0) {
      return null;
    }
    try {
      return new BigDecimal(value);
    } catch (NumberFormatException e) {
      throw newIllegalDataException(e);
    }
  }

  @Override
  public Integer estimateByteSizeFromLength(Integer length) {
    // No association of runtime byte size from decimal precision.
    return null;
  }

  @Override
  public String toStringLiteral(byte[] b, int offset, int length, Format formatter) {
    if (formatter == null) {
      BigDecimal o = (BigDecimal) toObject(b, offset, length);
      return o.toPlainString();
    }
    return super.toStringLiteral(b, offset, length, formatter);
  }

  @Override
  public String toStringLiteral(Object o, Format formatter) {
    if (formatter == null) {
      if (o == null) {
        return String.valueOf(o);
      }
      return ((BigDecimal) o).toPlainString();
    }
    return super.toStringLiteral(o, formatter);
  }

  @Override
  public Object getSampleValue(Integer maxLength, Integer arrayLength) {
    return new BigDecimal((Long) PLong.INSTANCE.getSampleValue(maxLength, arrayLength));
  }

  // take details from org.apache.phoenix.schema.types.PDataType#toBigDecimal(byte[], int, int)
  @Override
  public int signum(byte[] bytes, int offset, int length, SortOrder sortOrder, Integer maxLength,
    Integer scale) {
    byte signByte;
    if (sortOrder == SortOrder.DESC) {
      signByte = SortOrder.invert(bytes[offset]);
    } else {
      signByte = bytes[offset];
    }
    if (length == 1 && signByte == ZERO_BYTE) {
      return 0;
    }
    return ((signByte & 0x80) == 0) ? -1 : 1;
  }

  @Override
  public void abs(byte[] bytes, int offset, int length, SortOrder sortOrder,
    ImmutableBytesWritable outPtr) {
    if (sortOrder == SortOrder.DESC) {
      bytes = SortOrder.invert(bytes, offset, new byte[length], 0, length);
      offset = 0;
    }
    BigDecimal bigDecimal = toBigDecimal(bytes, offset, length);
    outPtr.set(toBytes(bigDecimal.abs()));
  }
}
