/*
 * Copyright 2013 Google Inc.
 *
 * Licensed 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 com.google.common.jimfs;

import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.jimfs.Util.clear;
import static com.google.common.jimfs.Util.nextPowerOf2;

import com.google.common.annotations.VisibleForTesting;
import com.google.common.primitives.UnsignedBytes;
import com.google.errorprone.annotations.CanIgnoreReturnValue;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.channels.ReadableByteChannel;
import java.nio.channels.WritableByteChannel;
import java.nio.file.attribute.FileTime;
import java.util.Arrays;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

/**
 * A mutable, resizable store for bytes. Bytes are stored in fixed-sized byte arrays (blocks)
 * allocated by a {@link HeapDisk}.
 *
 * @author Colin Decker
 */
final class RegularFile extends File {

  private final ReadWriteLock lock = new ReentrantReadWriteLock();

  private final HeapDisk disk;

  /** Block list for the file. */
  private byte[][] blocks;
  /** Block count for the the file, which also acts as the head of the block list. */
  private int blockCount;

  private long size;

  /** Creates a new regular file with the given ID and using the given disk. */
  public static RegularFile create(int id, FileTime creationTime, HeapDisk disk) {
    return new RegularFile(id, creationTime, disk, new byte[32][], 0, 0);
  }

  RegularFile(
      int id,
      FileTime creationTime,
      HeapDisk disk,
      byte[][] blocks,
      int blockCount,
      long size) {
    super(id, creationTime);
    this.disk = checkNotNull(disk);
    this.blocks = checkNotNull(blocks);
    this.blockCount = blockCount;

    checkArgument(size >= 0);
    this.size = size;
  }

  private int openCount = 0;
  private boolean deleted = false;

  /** Returns the read lock for this file. */
  public Lock readLock() {
    return lock.readLock();
  }

  /** Returns the write lock for this file. */
  public Lock writeLock() {
    return lock.writeLock();
  }

  // lower-level methods dealing with the blocks array

  private void expandIfNecessary(int minBlockCount) {
    if (minBlockCount > blocks.length) {
      this.blocks = Arrays.copyOf(blocks, nextPowerOf2(minBlockCount));
    }
  }

  /** Returns the number of blocks this file contains. */
  int blockCount() {
    return blockCount;
  }

  /** Copies the last {@code count} blocks from this file to the end of the given target file. */
  void copyBlocksTo(RegularFile target, int count) {
    int start = blockCount - count;
    int targetEnd = target.blockCount + count;
    target.expandIfNecessary(targetEnd);

    System.arraycopy(this.blocks, start, target.blocks, target.blockCount, count);
    target.blockCount = targetEnd;
  }

  /** Transfers the last {@code count} blocks from this file to the end of the given target file. */
  void transferBlocksTo(RegularFile target, int count) {
    copyBlocksTo(target, count);
    truncateBlocks(blockCount - count);
  }

  /** Truncates the blocks of this file to the given block count. */
  void truncateBlocks(int count) {
    clear(blocks, count, blockCount - count);
    blockCount = count;
  }

  /** Adds the given block to the end of this file. */
  void addBlock(byte[] block) {
    expandIfNecessary(blockCount + 1);
    blocks[blockCount++] = block;
  }

  /** Gets the block at the given index in this file. */
  @VisibleForTesting
  byte[] getBlock(int index) {
    return blocks[index];
  }

  // end of lower-level methods dealing with the blocks array

  /**
   * Gets the current size of this file in bytes. Does not do locking, so should only be called when
   * holding a lock.
   */
  public long sizeWithoutLocking() {
    return size;
  }

  // need to lock in these methods since they're defined by an interface

  @Override
  public long size() {
    readLock().lock();
    try {
      return size;
    } finally {
      readLock().unlock();
    }
  }

  @Override
  RegularFile copyWithoutContent(int id, FileTime creationTime) {
    byte[][] copyBlocks = new byte[Math.max(blockCount * 2, 32)][];
    return new RegularFile(id, creationTime, disk, copyBlocks, 0, size);
  }

  @Override
  void copyContentTo(File file) throws IOException {
    RegularFile copy = (RegularFile) file;
    disk.allocate(copy, blockCount);

    for (int i = 0; i < blockCount; i++) {
      byte[] block = blocks[i];
      byte[] copyBlock = copy.blocks[i];
      System.arraycopy(block, 0, copyBlock, 0, block.length);
    }
  }

  @Override
  ReadWriteLock contentLock() {
    return lock;
  }

  // opened/closed/delete don't use the read/write lock... they only need to ensure that they are
  // synchronized among themselves

  @Override
  public synchronized void opened() {
    openCount++;
  }

  @Override
  public synchronized void closed() {
    if (--openCount == 0 && deleted) {
      deleteContents();
    }
  }

  /**
   * Marks this file as deleted. If there are no streams or channels open to the file, its contents
   * are deleted if necessary.
   */
  @Override
  public synchronized void deleted() {
    if (links() == 0) {
      deleted = true;
      if (openCount == 0) {
        deleteContents();
      }
    }
  }

  /**
   * Deletes the contents of this file. Called when this file has been deleted and all open streams
   * and channels to it have been closed.
   */
  private void deleteContents() {
    disk.free(this);
    size = 0;
  }

  /**
   * Truncates this file to the given {@code size}. If the given size is less than the current size
   * of this file, the size of the file is reduced to the given size and any bytes beyond that size
   * are lost. If the given size is greater than the current size of the file, this method does
   * nothing. Returns {@code true} if this file was modified by the call (its size changed) and
   * {@code false} otherwise.
   */
  @CanIgnoreReturnValue
  public boolean truncate(long size) {
    if (size >= this.size) {
      return false;
    }

    long lastPosition = size - 1;
    this.size = size;

    int newBlockCount = blockIndex(lastPosition) + 1;
    int blocksToRemove = blockCount - newBlockCount;
    if (blocksToRemove > 0) {
      disk.free(this, blocksToRemove);
    }

    return true;
  }

  /** Prepares for a write of len bytes starting at position pos. */
  private void prepareForWrite(long pos, long len) throws IOException {
    long end = pos + len;

    // allocate any additional blocks needed
    int lastBlockIndex = blockCount - 1;
    int endBlockIndex = blockIndex(end - 1);

    if (endBlockIndex > lastBlockIndex) {
      int additionalBlocksNeeded = endBlockIndex - lastBlockIndex;
      disk.allocate(this, additionalBlocksNeeded);
    }

    // zero bytes between current size and pos
    if (pos > size) {
      long remaining = pos - size;

      int blockIndex = blockIndex(size);
      byte[] block = blocks[blockIndex];
      int off = offsetInBlock(size);

      remaining -= zero(block, off, length(off, remaining));

      while (remaining > 0) {
        block = blocks[++blockIndex];

        remaining -= zero(block, 0, length(remaining));
      }

      size = pos;
    }
  }

  /**
   * Writes the given byte to this file at position {@code pos}. {@code pos} may be greater than the
   * current size of this file, in which case this file is resized and all bytes between the current
   * size and {@code pos} are set to 0. Returns the number of bytes written.
   *
   * @throws IOException if the file needs more blocks but the disk is full
   */
  @CanIgnoreReturnValue
  public int write(long pos, byte b) throws IOException {
    prepareForWrite(pos, 1);

    byte[] block = blocks[blockIndex(pos)];
    int off = offsetInBlock(pos);
    block[off] = b;

    if (pos >= size) {
      size = pos + 1;
    }

    return 1;
  }

  /**
   * Writes {@code len} bytes starting at offset {@code off} in the given byte array to this file
   * starting at position {@code pos}. {@code pos} may be greater than the current size of this
   * file, in which case this file is resized and all bytes between the current size and {@code pos}
   * are set to 0. Returns the number of bytes written.
   *
   * @throws IOException if the file needs more blocks but the disk is full
   */
  @CanIgnoreReturnValue
  public int write(long pos, byte[] b, int off, int len) throws IOException {
    prepareForWrite(pos, len);

    if (len == 0) {
      return 0;
    }

    int remaining = len;

    int blockIndex = blockIndex(pos);
    byte[] block = blocks[blockIndex];
    int offInBlock = offsetInBlock(pos);

    int written = put(block, offInBlock, b, off, length(offInBlock, remaining));
    remaining -= written;
    off += written;

    while (remaining > 0) {
      block = blocks[++blockIndex];

      written = put(block, 0, b, off, length(remaining));
      remaining -= written;
      off += written;
    }

    long endPos = pos + len;
    if (endPos > size) {
      size = endPos;
    }

    return len;
  }

  /**
   * Writes all available bytes from buffer {@code buf} to this file starting at position {@code
   * pos}. {@code pos} may be greater than the current size of this file, in which case this file is
   * resized and all bytes between the current size and {@code pos} are set to 0. Returns the number
   * of bytes written.
   *
   * @throws IOException if the file needs more blocks but the disk is full
   */
  @CanIgnoreReturnValue
  public int write(long pos, ByteBuffer buf) throws IOException {
    int len = buf.remaining();

    prepareForWrite(pos, len);

    if (len == 0) {
      return 0;
    }

    int blockIndex = blockIndex(pos);
    byte[] block = blocks[blockIndex];
    int off = offsetInBlock(pos);

    put(block, off, buf);

    while (buf.hasRemaining()) {
      block = blocks[++blockIndex];

      put(block, 0, buf);
    }

    long endPos = pos + len;
    if (endPos > size) {
      size = endPos;
    }

    return len;
  }

  /**
   * Writes all available bytes from each buffer in {@code bufs}, in order, to this file starting at
   * position {@code pos}. {@code pos} may be greater than the current size of this file, in which
   * case this file is resized and all bytes between the current size and {@code pos} are set to 0.
   * Returns the number of bytes written.
   *
   * @throws IOException if the file needs more blocks but the disk is full
   */
  @CanIgnoreReturnValue
  public long write(long pos, Iterable<ByteBuffer> bufs) throws IOException {
    long start = pos;
    for (ByteBuffer buf : bufs) {
      pos += write(pos, buf);
    }
    return pos - start;
  }

  /**
   * Transfers up to {@code count} bytes from the given channel to this file starting at position
   * {@code pos}. Returns the number of bytes transferred. If {@code pos} is greater than the
   * current size of this file, then no bytes are transferred.
   *
   * @throws IOException if the file needs more blocks but the disk is full or if reading from src
   *     throws an exception
   */
  public long transferFrom(ReadableByteChannel src, long startPos, long count) throws IOException {
    if (count == 0
        // Unlike the write() methods, attempting to transfer to a position that is greater than the
        // current file size simply does nothing.
        || startPos > size) {
      return 0;
    }

    long remaining = count;
    long currentPos = startPos;

    int blockIndex = blockIndex(startPos);
    int off = offsetInBlock(startPos);

    outer:
    while (remaining > 0) {
      byte[] block = blockForWrite(blockIndex);

      ByteBuffer buf = ByteBuffer.wrap(block, off, length(off, remaining));
      while (buf.hasRemaining()) {
        int read = src.read(buf);
        // Note: we stop if we read 0 bytes from the src; even though the src is not at EOF, the
        // spec of transferFrom is to stop immediately when reading from a non-blocking channel that
        // has no bytes available rather than continuing until it reaches EOF. This makes sense
        // because we'd otherwise just spin attempting to read bytes from the src repeatedly.
        if (read < 1) {
          if (currentPos >= size && buf.position() == 0) {
            // The current position is at or beyond the end of file (prior to transfer start) and
            // the current buffer position is 0. This means that a new block must have just been
            // allocated to hold any potential transferred bytes, but no bytes were transferred to
            // it because the src had no remaining bytes. So we need to de-allocate that block.
            // It's possible that it would be preferable to always transfer to a temporary block
            // first and then copy that block to a newly allocated block when it's full or src
            // doesn't have any further bytes. Then if we hadn't read anything into the temporary
            // block, we could simply discard it. But I think this scenario is likely rare enough
            // that it's fine to temporarily allocate a block that _might_ not get used.
            disk.free(this, 1);
          }
          break outer;
        }

        currentPos += read;
        remaining -= read;
      }

      // Current write block is full
      blockIndex++;
      off = 0;
    }

    if (currentPos > size) {
      size = currentPos;
    }

    return currentPos - startPos;
  }

  /**
   * Reads the byte at position {@code pos} in this file as an unsigned integer in the range 0-255.
   * If {@code pos} is greater than or equal to the size of this file, returns -1 instead.
   */
  public int read(long pos) {
    if (pos >= size) {
      return -1;
    }

    byte[] block = blocks[blockIndex(pos)];
    int off = offsetInBlock(pos);
    return UnsignedBytes.toInt(block[off]);
  }

  /**
   * Reads up to {@code len} bytes starting at position {@code pos} in this file to the given byte
   * array starting at offset {@code off}. Returns the number of bytes actually read or -1 if {@code
   * pos} is greater than or equal to the size of this file.
   */
  public int read(long pos, byte[] b, int off, int len) {
    // since max is len (an int), result is guaranteed to be an int
    int bytesToRead = (int) bytesToRead(pos, len);

    if (bytesToRead > 0) {
      int remaining = bytesToRead;

      int blockIndex = blockIndex(pos);
      byte[] block = blocks[blockIndex];
      int offsetInBlock = offsetInBlock(pos);

      int read = get(block, offsetInBlock, b, off, length(offsetInBlock, remaining));
      remaining -= read;
      off += read;

      while (remaining > 0) {
        int index = ++blockIndex;
        block = blocks[index];

        read = get(block, 0, b, off, length(remaining));
        remaining -= read;
        off += read;
      }
    }

    return bytesToRead;
  }

  /**
   * Reads up to {@code buf.remaining()} bytes starting at position {@code pos} in this file to the
   * given buffer. Returns the number of bytes read or -1 if {@code pos} is greater than or equal to
   * the size of this file.
   */
  public int read(long pos, ByteBuffer buf) {
    // since max is buf.remaining() (an int), result is guaranteed to be an int
    int bytesToRead = (int) bytesToRead(pos, buf.remaining());

    if (bytesToRead > 0) {
      int remaining = bytesToRead;

      int blockIndex = blockIndex(pos);
      byte[] block = blocks[blockIndex];
      int off = offsetInBlock(pos);

      remaining -= get(block, off, buf, length(off, remaining));

      while (remaining > 0) {
        int index = ++blockIndex;
        block = blocks[index];
        remaining -= get(block, 0, buf, length(remaining));
      }
    }

    return bytesToRead;
  }

  /**
   * Reads up to the total {@code remaining()} number of bytes in each of {@code bufs} starting at
   * position {@code pos} in this file to the given buffers, in order. Returns the number of bytes
   * read or -1 if {@code pos} is greater than or equal to the size of this file.
   */
  public long read(long pos, Iterable<ByteBuffer> bufs) {
    if (pos >= size()) {
      return -1;
    }

    long start = pos;
    for (ByteBuffer buf : bufs) {
      int read = read(pos, buf);
      if (read == -1) {
        break;
      } else {
        pos += read;
      }
    }

    return pos - start;
  }

  /**
   * Transfers up to {@code count} bytes to the given channel starting at position {@code pos} in
   * this file. Returns the number of bytes transferred, possibly 0. Note that unlike all other read
   * methods in this class, this method does not return -1 if {@code pos} is greater than or equal
   * to the current size. This for consistency with {@link FileChannel#transferTo}, which this
   * method is primarily intended as an implementation of.
   */
  public long transferTo(long pos, long count, WritableByteChannel dest) throws IOException {
    long bytesToRead = bytesToRead(pos, count);

    if (bytesToRead > 0) {
      long remaining = bytesToRead;

      int blockIndex = blockIndex(pos);
      byte[] block = blocks[blockIndex];
      int off = offsetInBlock(pos);

      ByteBuffer buf = ByteBuffer.wrap(block, off, length(off, remaining));
      while (buf.hasRemaining()) {
        remaining -= dest.write(buf);
      }
      Java8Compatibility.clear(buf);

      while (remaining > 0) {
        int index = ++blockIndex;
        block = blocks[index];

        buf = ByteBuffer.wrap(block, 0, length(remaining));
        while (buf.hasRemaining()) {
          remaining -= dest.write(buf);
        }
        Java8Compatibility.clear(buf);
      }
    }

    return Math.max(bytesToRead, 0); // don't return -1 for this method
  }

  /** Gets the block at the given index, expanding to create the block if necessary. */
  private byte[] blockForWrite(int index) throws IOException {
    if (index >= blockCount) {
      int additionalBlocksNeeded = index - blockCount + 1;
      disk.allocate(this, additionalBlocksNeeded);
    }

    return blocks[index];
  }

  private int blockIndex(long position) {
    return (int) (position / disk.blockSize());
  }

  private int offsetInBlock(long position) {
    return (int) (position % disk.blockSize());
  }

  private int length(long max) {
    return (int) Math.min(disk.blockSize(), max);
  }

  private int length(int off, long max) {
    return (int) Math.min(disk.blockSize() - off, max);
  }

  /**
   * Returns the number of bytes that can be read starting at position {@code pos} (up to a maximum
   * of {@code max}) or -1 if {@code pos} is greater than or equal to the current size.
   */
  private long bytesToRead(long pos, long max) {
    long available = size - pos;
    if (available <= 0) {
      return -1;
    }
    return Math.min(available, max);
  }

  /** Zeroes len bytes in the given block starting at the given offset. Returns len. */
  private static int zero(byte[] block, int offset, int len) {
    Util.zero(block, offset, len);
    return len;
  }

  /** Puts the given slice of the given array at the given offset in the given block. */
  private static int put(byte[] block, int offset, byte[] b, int off, int len) {
    System.arraycopy(b, off, block, offset, len);
    return len;
  }

  /** Puts the contents of the given byte buffer at the given offset in the given block. */
  private static void put(byte[] block, int offset, ByteBuffer buf) {
    int len = Math.min(block.length - offset, buf.remaining());
    buf.get(block, offset, len);
  }

  /**
   * Reads len bytes starting at the given offset in the given block into the given slice of the
   * given byte array.
   */
  private static int get(byte[] block, int offset, byte[] b, int off, int len) {
    System.arraycopy(block, offset, b, off, len);
    return len;
  }

  /** Reads len bytes starting at the given offset in the given block into the given byte buffer. */
  private static int get(byte[] block, int offset, ByteBuffer buf, int len) {
    buf.put(block, offset, len);
    return len;
  }
}
