/*
 * 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.iotdb.tsfile.encoding.encoder;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import org.apache.iotdb.tsfile.encoding.common.EndianType;
import org.apache.iotdb.tsfile.exception.encoding.TsFileEncodingException;
import org.apache.iotdb.tsfile.file.metadata.enums.TSDataType;
import org.apache.iotdb.tsfile.file.metadata.enums.TSEncoding;
import org.apache.iotdb.tsfile.utils.ReadWriteForEncodingUtils;

/**
 * Encoder for float or double value using rle or two-diff according to following grammar.
 *
 * <pre>
 * {@code
 * float encoder: <maxPointvalue> <encoded-data>
 * maxPointvalue := number for accuracy of decimal places, store as unsigned var int
 * encoded-data := same as encoder's pattern
 * }
 * </pre>
 */
public class FloatEncoder extends Encoder {

  private Encoder encoder;

  /**
   * number for accuracy of decimal places.
   */
  private int maxPointNumber;

  /**
   * maxPointValue = 10^(maxPointNumber).
   */
  private double maxPointValue;

  /**
   * flag to check whether maxPointNumber is saved in the stream.
   */
  private boolean isMaxPointNumberSaved;

  public FloatEncoder(TSEncoding encodingType, TSDataType dataType, int maxPointNumber) {
    super(encodingType);
    this.maxPointNumber = maxPointNumber;
    calculateMaxPonitNum();
    isMaxPointNumberSaved = false;
    if (encodingType == TSEncoding.RLE) {
      if (dataType == TSDataType.FLOAT) {
        encoder = new IntRleEncoder(EndianType.BIG_ENDIAN);
      } else if (dataType == TSDataType.DOUBLE) {
        encoder = new LongRleEncoder(EndianType.BIG_ENDIAN);
      } else {
        throw new TsFileEncodingException(
            String.format("data type %s is not supported by FloatEncoder", dataType));
      }
    } else if (encodingType == TSEncoding.TS_2DIFF) {
      if (dataType == TSDataType.FLOAT) {
        encoder = new DeltaBinaryEncoder.IntDeltaEncoder();
      } else if (dataType == TSDataType.DOUBLE) {
        encoder = new DeltaBinaryEncoder.LongDeltaEncoder();
      } else {
        throw new TsFileEncodingException(
            String.format("data type %s is not supported by FloatEncoder", dataType));
      }
    } else {
      throw new TsFileEncodingException(
          String.format("%s encoding is not supported by FloatEncoder", encodingType));
    }
  }

  @Override
  public void encode(float value, ByteArrayOutputStream out) {
    saveMaxPointNumber(out);
    int valueInt = convertFloatToInt(value);
    encoder.encode(valueInt, out);
  }

  @Override
  public void encode(double value, ByteArrayOutputStream out) {
    saveMaxPointNumber(out);
    long valueLong = convertDoubleToLong(value);
    encoder.encode(valueLong, out);
  }

  private void calculateMaxPonitNum() {
    if (maxPointNumber <= 0) {
      maxPointNumber = 0;
      maxPointValue = 1;
    } else {
      maxPointValue = Math.pow(10, maxPointNumber);
    }
  }

  private int convertFloatToInt(float value) {
    return (int) Math.round(value * maxPointValue);
  }

  private long convertDoubleToLong(double value) {
    return Math.round(value * maxPointValue);
  }

  @Override
  public void flush(ByteArrayOutputStream out) throws IOException {
    encoder.flush(out);
    reset();
  }

  private void reset() {
    isMaxPointNumberSaved = false;
  }

  private void saveMaxPointNumber(ByteArrayOutputStream out) {
    if (!isMaxPointNumberSaved) {
      ReadWriteForEncodingUtils.writeUnsignedVarInt(maxPointNumber, out);
      isMaxPointNumberSaved = true;
    }
  }

  @Override
  public int getOneItemMaxSize() {
    return encoder.getOneItemMaxSize();
  }

  @Override
  public long getMaxByteSize() {
    return encoder.getMaxByteSize();
  }

}
