/**
 *
 * 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.hadoop.hbase.io;

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

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.hadoop.classification.InterfaceAudience;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.FileSystem;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.hbase.HConstants;
import org.apache.hadoop.hbase.KeyValue;
import org.apache.hadoop.hbase.client.Scan;
import org.apache.hadoop.hbase.io.hfile.CacheConfig;
import org.apache.hadoop.hbase.io.hfile.HFileScanner;
import org.apache.hadoop.hbase.regionserver.StoreFile;
import org.apache.hadoop.hbase.util.Bytes;

/**
 * A facade for a {@link org.apache.hadoop.hbase.io.hfile.HFile.Reader} that serves up
 * either the top or bottom half of a HFile where 'bottom' is the first half
 * of the file containing the keys that sort lowest and 'top' is the second half
 * of the file with keys that sort greater than those of the bottom half.
 * The top includes the split files midkey, of the key that follows if it does
 * not exist in the file.
 *
 * <p>This type works in tandem with the {@link Reference} type.  This class
 * is used reading while Reference is used writing.
 *
 * <p>This file is not splitable.  Calls to {@link #midkey()} return null.
 */
@InterfaceAudience.Private
public class HalfStoreFileReader extends StoreFile.Reader {
  final Log LOG = LogFactory.getLog(HalfStoreFileReader.class);
  final boolean top;
  // This is the key we split around.  Its the first possible entry on a row:
  // i.e. empty column and a timestamp of LATEST_TIMESTAMP.
  protected final byte [] splitkey;
  
  private byte[] firstKey = null;
  
  private boolean firstKeySeeked = false;

  /**
   * Creates a half file reader for a normal hfile.
   * @param fs fileystem to read from
   * @param p path to hfile
   * @param cacheConf
   * @param r original reference file (contains top or bottom)
   * @param conf Configuration
   * @throws IOException
   */
  public HalfStoreFileReader(final FileSystem fs, final Path p,
      final CacheConfig cacheConf, final Reference r, final Configuration conf)
      throws IOException {
    super(fs, p, cacheConf, conf);
    // This is not actual midkey for this half-file; its just border
    // around which we split top and bottom.  Have to look in files to find
    // actual last and first keys for bottom and top halves.  Half-files don't
    // have an actual midkey themselves. No midkey is how we indicate file is
    // not splittable.
    this.splitkey = r.getSplitKey();
    // Is it top or bottom half?
    this.top = Reference.isTopFileRegion(r.getFileRegion());
  }

  /**
   * Creates a half file reader for a hfile referred to by an hfilelink.
   * @param fs fileystem to read from
   * @param p path to hfile
   * @param in {@link FSDataInputStreamWrapper}
   * @param size Full size of the hfile file
   * @param cacheConf
   * @param r original reference file (contains top or bottom)
   * @param conf Configuration
   * @throws IOException
   */
  public HalfStoreFileReader(final FileSystem fs, final Path p, final FSDataInputStreamWrapper in,
      long size, final CacheConfig cacheConf,  final Reference r, final Configuration conf)
      throws IOException {
    super(fs, p, in, size, cacheConf, conf);
    // This is not actual midkey for this half-file; its just border
    // around which we split top and bottom.  Have to look in files to find
    // actual last and first keys for bottom and top halves.  Half-files don't
    // have an actual midkey themselves. No midkey is how we indicate file is
    // not splittable.
    this.splitkey = r.getSplitKey();
    // Is it top or bottom half?
    this.top = Reference.isTopFileRegion(r.getFileRegion());
  }

  protected boolean isTop() {
    return this.top;
  }

  @Override
  public HFileScanner getScanner(final boolean cacheBlocks,
      final boolean pread, final boolean isCompaction) {
    final HFileScanner s = super.getScanner(cacheBlocks, pread, isCompaction);
    return new HFileScanner() {
      final HFileScanner delegate = s;
      public boolean atEnd = false;

      public ByteBuffer getKey() {
        if (atEnd) return null;
        return delegate.getKey();
      }

      public String getKeyString() {
        if (atEnd) return null;

        return delegate.getKeyString();
      }

      public ByteBuffer getValue() {
        if (atEnd) return null;

        return delegate.getValue();
      }

      public String getValueString() {
        if (atEnd) return null;

        return delegate.getValueString();
      }

      public KeyValue getKeyValue() {
        if (atEnd) return null;

        return delegate.getKeyValue();
      }

      public boolean next() throws IOException {
        if (atEnd) return false;

        boolean b = delegate.next();
        if (!b) {
          return b;
        }
        // constrain the bottom.
        if (!top) {
          ByteBuffer bb = getKey();
          if (getComparator().compareFlatKey(bb.array(), bb.arrayOffset(), bb.limit(),
              splitkey, 0, splitkey.length) >= 0) {
            atEnd = true;
            return false;
          }
        }
        return true;
      }

      public boolean seekBefore(byte[] key) throws IOException {
        return seekBefore(key, 0, key.length);
      }

      public boolean seekBefore(byte [] key, int offset, int length)
      throws IOException {
        if (top) {
          byte[] fk = getFirstKey();
          // This will be null when the file is empty in which we can not seekBefore to any key
          if (fk == null) return false;
          if (getComparator().compareFlatKey(key, offset, length, fk, 0,
              fk.length) <= 0) {
            return false;
          }
        } else {
          // The equals sign isn't strictly necessary just here to be consistent with seekTo
          if (getComparator().compareFlatKey(key, offset, length, splitkey, 0,
              splitkey.length) >= 0) {
            return this.delegate.seekBefore(splitkey, 0, splitkey.length);
          }
        }
        return this.delegate.seekBefore(key, offset, length);
      }

      public boolean seekTo() throws IOException {
        if (top) {
          int r = this.delegate.seekTo(splitkey);
          if (r == HConstants.INDEX_KEY_MAGIC) {
            return true;
          }
          if (r < 0) {
            // midkey is < first key in file
            return this.delegate.seekTo();
          }
          if (r > 0) {
            return this.delegate.next();
          }
          return true;
        }

        boolean b = delegate.seekTo();
        if (!b) {
          return b;
        }
        // Check key.
        ByteBuffer k = this.delegate.getKey();
        return this.delegate.getReader().getComparator().
          compareFlatKey(k.array(), k.arrayOffset(), k.limit(),
            splitkey, 0, splitkey.length) < 0;
      }

      public int seekTo(byte[] key) throws IOException {
        return seekTo(key, 0, key.length);
      }

      public int seekTo(byte[] key, int offset, int length) throws IOException {
        if (top) {
          if (getComparator().compareFlatKey(key, offset, length, splitkey, 0,
              splitkey.length) < 0) {
            return -1;
          }
        } else {
          if (getComparator().compareFlatKey(key, offset, length, splitkey, 0,
              splitkey.length) >= 0) {
            // we would place the scanner in the second half.
            // it might be an error to return false here ever...
            boolean res = delegate.seekBefore(splitkey, 0, splitkey.length);
            if (!res) {
              throw new IOException("Seeking for a key in bottom of file, but key exists in top of file, failed on seekBefore(midkey)");
            }
            return 1;
          }
        }
        return delegate.seekTo(key, offset, length);
      }

      @Override
      public int reseekTo(byte[] key) throws IOException {
        return reseekTo(key, 0, key.length);
      }

      @Override
      public int reseekTo(byte[] key, int offset, int length)
      throws IOException {
        //This function is identical to the corresponding seekTo function except
        //that we call reseekTo (and not seekTo) on the delegate.
        if (top) {
          if (getComparator().compareFlatKey(key, offset, length, splitkey, 0,
              splitkey.length) < 0) {
            return -1;
          }
        } else {
          if (getComparator().compareFlatKey(key, offset, length, splitkey, 0,
              splitkey.length) >= 0) {
            // we would place the scanner in the second half.
            // it might be an error to return false here ever...
            boolean res = delegate.seekBefore(splitkey, 0, splitkey.length);
            if (!res) {
              throw new IOException("Seeking for a key in bottom of file, but" +
                  " key exists in top of file, failed on seekBefore(midkey)");
            }
            return 1;
          }
        }
        if (atEnd) {
          // skip the 'reseek' and just return 1.
          return 1;
        }
        return delegate.reseekTo(key, offset, length);
      }

      public org.apache.hadoop.hbase.io.hfile.HFile.Reader getReader() {
        return this.delegate.getReader();
      }

      public boolean isSeeked() {
        return this.delegate.isSeeked();
      }
    };
  }
  
  @Override
  public boolean passesKeyRangeFilter(Scan scan) {
    return true;
  }
  
  @Override
  public byte[] getLastKey() {
    if (top) {
      return super.getLastKey();
    }
    // Get a scanner that caches the block and that uses pread.
    HFileScanner scanner = getScanner(true, true);
    try {
      if (scanner.seekBefore(this.splitkey)) {
        return Bytes.toBytes(scanner.getKey());
      }
    } catch (IOException e) {
      LOG.warn("Failed seekBefore " + Bytes.toStringBinary(this.splitkey), e);
    }
    return null;
  }

  @Override
  public byte[] midkey() throws IOException {
    // Returns null to indicate file is not splitable.
    return null;
  }
  
  @Override
  public byte[] getFirstKey() {
    if (!firstKeySeeked) {
      HFileScanner scanner = getScanner(true, true, false);
      try {
        if (scanner.seekTo()) {
          this.firstKey = Bytes.toBytes(scanner.getKey());
        }
        firstKeySeeked = true;
      } catch (IOException e) {
        LOG.warn("Failed seekTo first KV in the file", e);
      }
    }
    return this.firstKey;
  }

  @Override
  public long getEntries() {
    // Estimate the number of entries as half the original file; this may be wildly inaccurate.
    return super.getEntries() / 2;
  }

  @Override
  public long getFilterEntries() {
    // Estimate the number of entries as half the original file; this may be wildly inaccurate.
    return super.getFilterEntries() / 2;
  }
}
