/*
 * 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.carbondata.core.indexstore;

import java.io.ByteArrayInputStream;
import java.io.DataInput;
import java.io.DataInputStream;
import java.io.DataOutput;
import java.io.IOException;
import java.io.Serializable;
import java.util.List;

import org.apache.carbondata.common.logging.LogServiceFactory;
import org.apache.carbondata.core.metadata.blocklet.BlockletInfo;
import org.apache.carbondata.core.metadata.schema.table.column.ColumnSchema;
import org.apache.carbondata.core.util.BlockletIndexUtil;

import org.apache.hadoop.io.Writable;
import org.apache.log4j.Logger;

/**
 * Blocklet detail information to be sent to each executor
 */
public class BlockletDetailInfo implements Serializable, Writable {

  /**
   * LOGGER
   */
  private static final Logger LOGGER =
      LogServiceFactory.getLogService(BlockletDetailInfo.class.getName());

  private static final long serialVersionUID = 7957493757421513808L;

  private int rowCount;

  private short pagesCount;

  private short versionNumber;

  // default blockletId should be -1,which means consider all the blocklets in block
  private short blockletId = -1;

  private long schemaUpdatedTimeStamp;

  private BlockletInfo blockletInfo;

  private byte[] blockletInfoBinary;

  private long blockFooterOffset;

  private List<ColumnSchema> columnSchemas;

  private byte[] columnSchemaBinary;

  private long blockSize;

  /**
   * flag to check whether to serialize min max values. The flag will be set to true in case
   * 1. When CACHE_LEVEL = BLOCKLET and filter column min/max in not cached in the driver using the
   * property COLUMN_META_CACHE
   * 2. for CACHE_LEVEL = BLOCK, it will always be true which is also the default value
   */
  private boolean useMinMaxForPruning = true;

  public int getRowCount() {
    return rowCount;
  }

  public void setRowCount(int rowCount) {
    this.rowCount = rowCount;
  }

  public int getPagesCount() {
    return pagesCount;
  }

  public void setPagesCount(short pagesCount) {
    this.pagesCount = pagesCount;
  }

  public short getVersionNumber() {
    return versionNumber;
  }

  public void setVersionNumber(short versionNumber) {
    this.versionNumber = versionNumber;
  }

  public BlockletInfo getBlockletInfo() {
    if (null == blockletInfo) {
      try {
        synchronized (this) {
          if (null == blockletInfo) {
            setBlockletInfoFromBinary();
          }
        }
      } catch (IOException e) {
        throw new RuntimeException(e);
      }
    }
    return blockletInfo;
  }

  public void setBlockletInfo(BlockletInfo blockletInfo) {
    this.blockletInfo = blockletInfo;
  }

  private void setBlockletInfoFromBinary() throws IOException {
    if (null == this.blockletInfo && null != blockletInfoBinary && blockletInfoBinary.length > 0) {
      blockletInfo = new BlockletInfo();
      ByteArrayInputStream stream = new ByteArrayInputStream(blockletInfoBinary);
      DataInputStream inputStream = new DataInputStream(stream);
      try {
        blockletInfo.readFields(inputStream);
      } catch (IOException e) {
        LOGGER.error("Problem in reading blocklet info", e);
        throw new IOException("Problem in reading blocklet info." + e.getMessage(), e);
      } finally {
        try {
          inputStream.close();
        } catch (IOException e) {
          LOGGER.error("Problem in closing input stream of reading blocklet info.", e);
        }
      }
    }
  }

  public long getSchemaUpdatedTimeStamp() {
    return schemaUpdatedTimeStamp;
  }

  public void setSchemaUpdatedTimeStamp(long schemaUpdatedTimeStamp) {
    this.schemaUpdatedTimeStamp = schemaUpdatedTimeStamp;
  }

  public long getBlockSize() {
    return blockSize;
  }

  public void setBlockSize(long blockSize) {
    this.blockSize = blockSize;
  }

  @Override
  public void write(DataOutput out) throws IOException {
    out.writeInt(rowCount);
    out.writeShort(pagesCount);
    out.writeShort(versionNumber);
    out.writeShort(blockletId);
    out.writeLong(schemaUpdatedTimeStamp);
    out.writeBoolean(blockletInfo != null);
    if (blockletInfo != null) {
      blockletInfo.write(out);
    }
    out.writeLong(blockFooterOffset);
    // convert column schema list to binary format for serializing
    convertColumnSchemaToBinary();
    if (null != columnSchemaBinary) {
      out.writeInt(columnSchemaBinary.length);
      out.write(columnSchemaBinary);
    } else {
      // write -1 if columnSchemaBinary is null so that at the time of reading it can distinguish
      // whether schema is written or not
      out.writeInt(-1);
    }
    out.writeInt(blockletInfoBinary.length);
    out.write(blockletInfoBinary);
    out.writeLong(blockSize);
    out.writeBoolean(useMinMaxForPruning);
  }

  @Override
  public void readFields(DataInput in) throws IOException {
    rowCount = in.readInt();
    pagesCount = in.readShort();
    versionNumber = in.readShort();
    blockletId = in.readShort();
    schemaUpdatedTimeStamp = in.readLong();
    if (in.readBoolean()) {
      blockletInfo = new BlockletInfo();
      blockletInfo.readFields(in);
    }
    blockFooterOffset = in.readLong();
    int bytesSize = in.readInt();
    // if byteSize is -1 that means schema binary is not written
    if (bytesSize != -1) {
      byte[] schemaArray = new byte[bytesSize];
      in.readFully(schemaArray);
      readColumnSchema(schemaArray);
    }
    int byteSize = in.readInt();
    blockletInfoBinary = new byte[byteSize];
    in.readFully(blockletInfoBinary);
    setBlockletInfoFromBinary();
    blockSize = in.readLong();
    useMinMaxForPruning = in.readBoolean();
  }

  /**
   * Read column schema from binary
   * @param schemaArray
   * @throws IOException
   */
  public void readColumnSchema(byte[] schemaArray) throws IOException {
    if (null != schemaArray) {
      columnSchemas = BlockletIndexUtil.readColumnSchema(schemaArray);
    }
  }

  private void convertColumnSchemaToBinary() throws IOException {
    if (null != columnSchemas) {
      columnSchemaBinary = BlockletIndexUtil.convertSchemaToBinary(columnSchemas);
    }
  }

  /**
   * Create copy of BlockletDetailInfo
   */
  public BlockletDetailInfo copy() {
    BlockletDetailInfo detailInfo = new BlockletDetailInfo();
    detailInfo.rowCount = rowCount;
    detailInfo.pagesCount = pagesCount;
    detailInfo.versionNumber = versionNumber;
    detailInfo.blockletId = blockletId;
    detailInfo.schemaUpdatedTimeStamp = schemaUpdatedTimeStamp;
    detailInfo.blockletInfo = blockletInfo;
    detailInfo.blockletInfoBinary = blockletInfoBinary;
    detailInfo.blockFooterOffset = blockFooterOffset;
    detailInfo.columnSchemas = columnSchemas;
    detailInfo.columnSchemaBinary = columnSchemaBinary;
    detailInfo.blockSize = blockSize;
    detailInfo.useMinMaxForPruning = useMinMaxForPruning;
    return detailInfo;
  }

  public Short getBlockletId() {
    return blockletId;
  }

  public void setBlockletId(Short blockletId) {
    this.blockletId = blockletId;
  }

  public long getBlockFooterOffset() {
    return blockFooterOffset;
  }

  public void setBlockFooterOffset(long blockFooterOffset) {
    this.blockFooterOffset = blockFooterOffset;
  }

  public List<ColumnSchema> getColumnSchemas() throws IOException {
    if (columnSchemas == null && columnSchemaBinary != null) {
      readColumnSchema(columnSchemaBinary);
    }
    return columnSchemas;
  }

  public byte[] getColumnSchemaBinary() {
    return columnSchemaBinary;
  }

  public void setBlockletInfoBinary(byte[] blockletInfoBinary) {
    this.blockletInfoBinary = blockletInfoBinary;
  }

  public void setColumnSchemas(List<ColumnSchema> columnSchemas) {
    this.columnSchemas = columnSchemas;
  }

  public boolean isUseMinMaxForPruning() {
    return useMinMaxForPruning;
  }

  public void setUseMinMaxForPruning(boolean useMinMaxForPruning) {
    this.useMinMaxForPruning = useMinMaxForPruning;
  }
}
