package org.basex.io.random;

import java.io.IOException;
import java.io.RandomAccessFile;
import java.util.Arrays;

import org.basex.data.MetaData;
import org.basex.io.IO;
import org.basex.io.in.DataInput;
import org.basex.io.out.DataOutput;
import org.basex.util.Array;
import org.basex.util.BitArray;
import org.basex.util.Util;

/**
 * This class stores the table on disk and reads it block-wise.
 *
 * @author BaseX Team 2005-11, BSD License
 * @author Christian Gruen
 * @author Tim Petrowsky
 */
public final class TableDiskAccess extends TableAccess {
  /** Buffer manager. */
  private final Buffers bm = new Buffers();
  /** File storing all blocks. */
  private final RandomAccessFile file;
  /** Filename prefix. */
  final String pref;

  /** FirstPre values (sorted ascending; length={@link #allBlocks}). */
  private int[] fpres;
  /** Index array storing BlockNumbers (length={@link #allBlocks}). */
  private int[] pages;
  /** Bitmap storing free (=0) and occupied (=1) pages. */
  private BitArray pagemap;

  /** Pre value of the first entry in the current block. */
  private int fpre = -1;
  /** First pre value of the next block. */
  private int npre = -1;

  /** Number of blocks in the data file (including unused). */
  private int allBlocks;
  /** Number of entries in the index (used blocks). */
  private int blocks;
  /** Index of the current block number in the {@link #pages} array. */
  private int index = -1;

  /**
   * Constructor.
   * @param md meta data
   * @param pf file prefix
   * @throws IOException I/O exception
   */
  public TableDiskAccess(final MetaData md, final String pf)
      throws IOException {

    super(md);
    pref = pf;

    // read meta and index data
    final DataInput in = new DataInput(meta.dbfile(pf + 'i'));
    allBlocks  = in.readNum();
    blocks     = in.readNum();
    fpres      = in.readNums();
    pages      = in.readNums();

    final int psize = in.readNum();
    // check if the page map has been stored
    if(psize == 0) {
      // init the map with empty pages
      pagemap = new BitArray(allBlocks);
      for(final int p : pages) pagemap.set(p);
      dirty = true;
    } else {
      pagemap = new BitArray(in.readLongs(psize), allBlocks);
    }
    in.close();

    // initialize data file
    file = new RandomAccessFile(meta.dbfile(pf), "rw");
    readIndex(0);
  }

  @Override
  public synchronized void flush() throws IOException {
    for(final Buffer b : bm.all()) if(b.dirty) writeBlock(b);
    if(!dirty) return;
    final DataOutput out = new DataOutput(meta.dbfile(pref + 'i'));
    out.writeNum(allBlocks);
    out.writeNum(blocks);
    out.writeNums(fpres);
    out.writeNums(pages);
    out.writeLongs(pagemap.toArray());
    out.close();
    dirty = false;
  }

  @Override
  public synchronized void close() throws IOException {
    flush();
    file.close();
  }

  @Override
  public synchronized int read1(final int pre, final int off) {
    final int o = off + cursor(pre);
    final byte[] b = bm.current().data;
    return b[o] & 0xFF;
  }

  @Override
  public synchronized int read2(final int pre, final int off) {
    final int o = off + cursor(pre);
    final byte[] b = bm.current().data;
    return ((b[o] & 0xFF) << 8) + (b[o + 1] & 0xFF);
  }

  @Override
  public synchronized int read4(final int pre, final int off) {
    final int o = off + cursor(pre);
    final byte[] b = bm.current().data;
    return ((b[o] & 0xFF) << 24) + ((b[o + 1] & 0xFF) << 16) +
      ((b[o + 2] & 0xFF) << 8) + (b[o + 3] & 0xFF);
  }

  @Override
  public synchronized long read5(final int pre, final int off) {
    final int o = off + cursor(pre);
    final byte[] b = bm.current().data;
    return ((long) (b[o] & 0xFF) << 32) + ((long) (b[o + 1] & 0xFF) << 24) +
      ((b[o + 2] & 0xFF) << 16) + ((b[o + 3] & 0xFF) << 8) + (b[o + 4] & 0xFF);
  }

  @Override
  public void write1(final int pre, final int off, final int v) {
    final int o = off + cursor(pre);
    final Buffer bf = bm.current();
    final byte[] b = bf.data;
    b[o] = (byte) v;
    bf.dirty = true;
  }

  @Override
  public void write2(final int pre, final int off, final int v) {
    final int o = off + cursor(pre);
    final Buffer bf = bm.current();
    final byte[] b = bf.data;
    b[o] = (byte) (v >>> 8);
    b[o + 1] = (byte) v;
    bf.dirty = true;
  }

  @Override
  public void write4(final int pre, final int off, final int v) {
    final int o = off + cursor(pre);
    final Buffer bf = bm.current();
    final byte[] b = bf.data;
    b[o]     = (byte) (v >>> 24);
    b[o + 1] = (byte) (v >>> 16);
    b[o + 2] = (byte) (v >>> 8);
    b[o + 3] = (byte) v;
    bf.dirty = true;
  }

  @Override
  public void write5(final int pre, final int off, final long v) {
    final int o = off + cursor(pre);
    final Buffer bf = bm.current();
    final byte[] b = bf.data;
    b[o]     = (byte) (v >>> 32);
    b[o + 1] = (byte) (v >>> 24);
    b[o + 2] = (byte) (v >>> 16);
    b[o + 3] = (byte) (v >>> 8);
    b[o + 4] = (byte) v;
    bf.dirty = true;
  }

  @Override
  protected void copy(final byte[] entries, final int pre, final int last) {
    for(int o = 0, i = pre; i < last; ++i, o += IO.NODESIZE) {
      final int off = cursor(i);
      final Buffer bf = bm.current();
      System.arraycopy(entries, o, bf.data, off, IO.NODESIZE);
      bf.dirty = true;
    }
  }

  @Override
  public void delete(final int pre, final int nr) {
    if(nr == 0) return;
    dirty = true;

    // get first block
    cursor(pre);

    // some useful variables to make code more readable
    int from = pre - fpre;
    final int last = pre + nr;

    // check if all entries are in current block: handle and return
    if(last - 1 < npre) {
      final Buffer bf = bm.current();
      copy(bf.data, from + nr, bf.data, from, npre - last);
      updatePre(nr);

      // if whole block was deleted, remove it from the index
      if(npre == fpre) {
        // mark the block as empty
        pagemap.clear(pages[index]);

        Array.move(fpres, index + 1, -1, blocks - index - 1);
        Array.move(pages, index + 1, -1, blocks - index - 1);

        --blocks;
        readIndex(index);
      }
      return;
    }

    // handle blocks whose entries are to be deleted entirely

    // first count them
    int unused = 0;
    while(npre < last) {
      if(from == 0) {
        ++unused;
        // mark the blocks as empty; range clear cannot be used because the
        // blocks may not be consecutive
        pagemap.clear(pages[index]);
      }
      readIndex(index + 1);
      from = 0;
    }

    // if the last block is empty, clear the corresponding bit
    final Buffer bf = bm.current();
    if(npre == last) {
      pagemap.clear((int) bf.pos);
      ++unused;
      if(index < blocks - 1) readIndex(index + 1);
      else ++index;
    } else {
      // delete entries at beginning of current (last) block
      copy(bf.data, last - fpre, bf.data, 0, npre - last);
    }

    // now remove them from the index
    if(unused > 0) {
      Array.move(fpres, index, -unused, blocks - index);
      Array.move(pages, index, -unused, blocks - index);
      blocks -= unused;
      index -= unused;
    }

    // update index entry for this block
    fpres[index] = pre;
    fpre = pre;
    updatePre(nr);
  }

  @Override
  public void insert(final int pre, final byte[] entries) {
    if(entries.length == 0) return;
    dirty = true;

    // go to the block and find the offset within the block where the new
    // records will be inserted
    final int split = cursor(pre - 1) + IO.NODESIZE;

    // number of records to be inserted
    final int nr = entries.length >>> IO.NODEPOWER;

    // number of bytes occupied by old records in the current block
    final int nold = npre - fpre << IO.NODEPOWER;
    // number of bytes occupied by old records which will be moved at the end
    final int moved = nold - split;

    // special case: all entries fit in the current block
    Buffer bf = bm.current();
    if(nold + entries.length <= IO.BLOCKSIZE) {
      System.arraycopy(bf.data, split, bf.data, split + entries.length, moved);
      System.arraycopy(entries, 0, bf.data, split, entries.length);
      bf.dirty = true;

      // increment first pre-values of blocks after the last modified block
      for(int i = index + 1; i < blocks; ++i) fpres[i] += nr;
      // update cached variables (fpre is not changed)
      npre += nr;
      meta.size += nr;
      return;
    }

    // append old entries at the end of the new entries
    // [DP] the following can be optimized to avoid copying arrays
    final byte[] all = new byte[entries.length + moved];
    System.arraycopy(entries, 0, all, 0, entries.length);
    System.arraycopy(bf.data, split, all, entries.length, moved);

    // fill in the current block with new entries
    // number of bytes which can fit in the first block
    int n = bf.data.length - split;
    if(n > 0) {
      System.arraycopy(all, 0, bf.data, split, n);
      bf.dirty = true;
    }

    int neededBlocks = (all.length - n) / IO.BLOCKSIZE;
    // number of bytes which don't fill one block completely
    final int remain = (all.length - n) % IO.BLOCKSIZE;

    if(remain > 0) {
      // check if the last entries can fit in the block after the current one
      if(index + 1 < blocks) {
        final int o = occSpace(index + 1) << IO.NODEPOWER;
        if(remain <= IO.BLOCKSIZE - o) {
          // copy the last records
          readIndex(index + 1);
          bf = bm.current();
          System.arraycopy(bf.data, 0, bf.data, remain, o);
          System.arraycopy(all, all.length - remain, bf.data, 0, remain);
          bf.dirty = true;
          // reduce the pre value, since it will be later incremented with nr
          fpres[index] -= remain >>> IO.NODEPOWER;
          // go back to the previous block
          readIndex(index - 1);
        } else {
          // there is not enough space in the block - allocate a new one
          ++neededBlocks;
        }
      } else {
        // this is the last block - allocate a new one
        ++neededBlocks;
      }
    }

    // number of new blocks (number of needed block - number of empty blocks)
    final int newBlocks = neededBlocks - (allBlocks - blocks);

    // extend fpres and pages, if new blocks will be allocated
    if(newBlocks > 0) {
      fpres = Arrays.copyOf(fpres, fpres.length + newBlocks);
      pages = Arrays.copyOf(pages, pages.length + newBlocks);
    }

    // make place for the blocks where the new entries will be written
    Array.move(fpres, index + 1, neededBlocks, blocks - index - 1);
    Array.move(pages, index + 1, neededBlocks, blocks - index - 1);

    // write the all remaining entries
    while(neededBlocks-- > 0) {
      freeBlock();
      n += write(all, n);
      fpres[index] = fpres[index - 1] + IO.ENTRIES;
      pages[index] = (int) bm.current().pos;
    }

    // increment first pre-values of blocks after the last modified block
    for(int i = index + 1; i < blocks; ++i) fpres[i] += nr;

    meta.size += nr;

    // update cached variables
    fpre = fpres[index];
    npre = index + 1 < blocks && fpres[index + 1] < meta.size ?
        fpres[index + 1] : meta.size;
  }

  // PRIVATE METHODS ==========================================================

  /**
   * Searches for the block containing the entry for that pre. then it
   * reads the block and returns it's offset inside the block.
   * @param pre pre of the entry to search for
   * @return offset of the entry in currentBlock
   */
  private int cursor(final int pre) {
    int fp = fpre;
    int np = npre;

    if(pre < fp || pre >= np) {
      final int last = blocks - 1;
      int l = 0;
      int h = last;
      int m = index;
      while(l <= h) {
        if(pre < fp) h = m - 1;
        else if(pre >= np) l = m + 1;
        else break;
        m = h + l >>> 1;
        fp = fpres[m];
        np = m == last ? meta.size : fpres[m + 1];
      }
      if(l > h) Util.notexpected("Data Access out of bounds [pre:" + pre +
          ", indexSize:" + blocks + ", access:" + l + " > " + h + "]");

      readIndex(m);
    }
    return pre - fpre << IO.NODEPOWER;
  }

  /**
   * Updates the index pointers and fetches the requested block.
   * @param i index of the block to fetch
   */
  private void readIndex(final int i) {
    index = i;
    fpre = fpres[i];
    npre = i + 1 >= blocks ? meta.size : fpres[i + 1];
    readBlock(pages[i]);
  }

  /**
   * Sets the disk cursor.
   * @param b block to fetch
   */
  private void readBlock(final int b) {
    if(!bm.cursor(b)) return;

    final Buffer bf = bm.current();
    try {
      if(bf.dirty) writeBlock(bf);
      bf.pos = b;
      if(b >= allBlocks) {
        allBlocks = b + 1;
      } else {
        file.seek(bf.pos * IO.BLOCKSIZE);
        file.readFully(bf.data);
      }
    } catch(final IOException ex) {
      Util.stack(ex);
    }
  }

  /**
   * Move the cursor to a free block (either new or existing empty one).
   */
  private void freeBlock() {
    final int b = pagemap.nextClearBit(0);
    pagemap.set(b);
    readBlock(b);
    ++blocks;
    ++index;
  }

  /**
   * Writes the specified block to disk and resets the dirty flag.
   * @param bf buffer to write
   * @throws IOException I/O exception
   */
  private void writeBlock(final Buffer bf) throws IOException {
    file.seek(bf.pos * IO.BLOCKSIZE);
    file.write(bf.data);
    bf.dirty = false;
  }

  /**
   * Updates the firstPre index entries.
   * @param nr number of entries to move
   */
  private void updatePre(final int nr) {
    // update index entries for all following blocks and reduce counter
    for(int i = index + 1; i < blocks; ++i) fpres[i] -= nr;
    meta.size -= nr;
    npre = index + 1 < blocks && fpres[index + 1] < meta.size ? fpres[index + 1]
        : meta.size;
  }

  /**
   * Convenience method for copying blocks.
   * @param s source array
   * @param sp source position
   * @param d destination array
   * @param dp destination position
   * @param l source length
   */
  private void copy(final byte[] s, final int sp, final byte[] d,
      final int dp, final int l) {
    System.arraycopy(s, sp << IO.NODEPOWER, d, dp << IO.NODEPOWER,
        l << IO.NODEPOWER);
    bm.current().dirty = true;
  }

  /**
   * Fill the current buffer with bytes from the specified array from the
   * specified offset.
   * @param s source array
   * @param o offset from the beginning of the array
   * @return number of written bytes
   */
  private int write(final byte[] s, final int o) {
    final Buffer bf = bm.current();
    final int len = Math.min(bf.data.length, s.length - o);
    System.arraycopy(s, o, bf.data, 0, len);
    bf.dirty = true;
    return len;
  }

  /**
   * Calculate the occupied space in a block.
   * @param i index of the block
   * @return occupied space in number of records
   */
  private int occSpace(final int i) {
    return (i + 1 < blocks ? fpres[i + 1] : meta.size) - fpres[i];
  }
}
