/**
 * 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.trevni;

import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.Iterator;

/** An iterator over column values. */
public class ColumnValues<T extends Comparable>
  implements Iterator<T>, Iterable<T> {

  private final ColumnDescriptor column;
  private final ValueType type;
  private final Codec codec;
  private final Checksum checksum;
  private final InputBuffer in;

  private InputBuffer values;
  private int block = -1;
  private long row = 0;
  private T previous;

  private int arrayLength;

  ColumnValues(ColumnDescriptor column) throws IOException {
    this.column = column;
    this.type = column.metaData.getType();
    this.codec = Codec.get(column.metaData);
    this.checksum = Checksum.get(column.metaData);
    this.in = new InputBuffer(column.file);

    column.ensureBlocksRead();
  }

  /** Return the current row number within this file. */
  public long getRow() { return row; }

  /** Seek to the named row. */
  public void seek(long r) throws IOException {
    if (r < row || r >= column.lastRow(block))    // not in current block
      startBlock(column.findBlock(r));            // seek to block start
    while (r > row && hasNext()) {                // skip within block
      values.skipValue(type);
      row++;
    }
    previous = null;
  }

  /** Seek to the named value. */
  public void seek(T v) throws IOException {
    if (!column.metaData.hasIndexValues())
      throw new TrevniRuntimeException
        ("Column does not have value index: " +column.metaData.getName());

    if (previous == null                          // not in current block?
        || previous.compareTo(v) > 0
        || (block != column.blockCount()-1
            && column.firstValues[block+1].compareTo(v) <= 0))
      startBlock(column.findBlock(v));            // seek to block start

    while (hasNext()) {                           // scan block
      long savedPosition = values.tell();
      T savedPrevious = previous;
      if (next().compareTo(v) >= 0) {
        values.seek(savedPosition);
        previous = savedPrevious;
        row--;
        return;
      }
    }
  }

  private void startBlock(int block) throws IOException {
    this.block = block;
    this.row = column.firstRows[block];

    in.seek(column.blockStarts[block]);
    int end = column.blocks[block].compressedSize;
    byte[] raw = new byte[end+checksum.size()];
    in.readFully(raw);
    ByteBuffer data = codec.decompress(ByteBuffer.wrap(raw, 0, end));
    if (!checksum.compute(data).equals
        (ByteBuffer.wrap(raw, end, checksum.size())))
      throw new IOException("Checksums mismatch.");
    values = new InputBuffer(new InputBytes(data));
  }

  @Override public Iterator iterator() { return this; }

  @Override public boolean hasNext() {
    return block < column.blockCount()-1 || row < column.lastRow(block);
  }

  @Override public T next() {
    if (column.metaData.isArray() || column.metaData.getParent() != null)
      throw new TrevniRuntimeException
        ("Column is array: " +column.metaData.getName());
    try {
      startRow();
      return nextValue();
    } catch (IOException e) {
      throw new TrevniRuntimeException(e);
    }
  }

  /** Expert: Must be called before any calls to {@link #nextLength()} or
   * {@link #nextValue()}. */
  public void startRow() throws IOException {
    if (row >= column.lastRow(block)) {
      if (block >= column.blockCount())
        throw new TrevniRuntimeException("Read past end of column.");
      startBlock(block+1);
    }
    row++;
  }

  /** Expert: Returns the next length in an array column. */
  public int nextLength() throws IOException {
    if (!column.metaData.isArray())
      throw new TrevniRuntimeException
        ("Column is not array: " +column.metaData.getName());
    assert arrayLength == 0;
    return arrayLength = values.readLength();
  }

  /** Expert: Returns the next value in a column. */
  public T nextValue() throws IOException {
    arrayLength--;
    return previous = values.<T>readValue(type);
  }

  @Override public void remove() { throw new UnsupportedOperationException(); }

}
