/* write a chunk data to the region file at specified sector number */
 private void write(int sectorNumber, byte[] data, int length) throws IOException {
   debugln(" " + sectorNumber);
   file.seek(sectorNumber * SECTOR_BYTES);
   file.writeInt(length + 1); // chunk length
   file.writeByte(VERSION_DEFLATE); // chunk version number
   file.write(data, 0, length); // chunk data
 }
  /*
   * gets an (uncompressed) stream representing the chunk data returns null if
   * the chunk is not found or an error occurs
   */
  public synchronized DataInputStream getChunkDataInputStream(int x, int z) {
    if (outOfBounds(x, z)) {
      debugln("READ", x, z, "out of bounds");
      return null;
    }

    try {
      int offset = getOffset(x, z);
      if (offset == 0) {
        // debugln("READ", x, z, "miss");
        return null;
      }

      int sectorNumber = offset >> 8;
      int numSectors = offset & 0xFF;

      if (sectorNumber + numSectors > sectorFree.size()) {
        debugln("READ", x, z, "invalid sector");
        return null;
      }

      file.seek(sectorNumber * SECTOR_BYTES);
      int length = file.readInt();

      if (length > SECTOR_BYTES * numSectors) {
        debugln("READ", x, z, "invalid length: " + length + " > 4096 * " + numSectors);
        return null;
      }

      byte version = file.readByte();
      if (version == VERSION_GZIP) {
        byte[] data = new byte[length - 1];
        file.read(data);
        DataInputStream ret =
            new DataInputStream(
                new BufferedInputStream(new GZIPInputStream(new ByteArrayInputStream(data))));
        // debug("READ", x, z, " = found");
        return ret;
      } else if (version == VERSION_DEFLATE) {
        byte[] data = new byte[length - 1];
        file.read(data);
        DataInputStream ret =
            new DataInputStream(
                new BufferedInputStream(new InflaterInputStream(new ByteArrayInputStream(data))));
        // debug("READ", x, z, " = found");
        return ret;
      }

      debugln("READ", x, z, "unknown version " + version);
      return null;
    } catch (IOException e) {
      debugln("READ", x, z, "exception");
      return null;
    }
  }
  public static long checksumRandomAccessFile(Path filename) throws IOException {
    try (RandomAccessFile file = new RandomAccessFile(filename.toFile(), "r")) {
      long length = file.length();
      CRC32 crc = new CRC32();

      for (long p = 0; p < length; p++) {
        file.seek(p);
        int c = file.readByte();
        crc.update(c);
      }
      return crc.getValue();
    }
  }
  /**
   * Read block from file.
   *
   * @param file - File to read.
   * @param off - Marker position in file to start read from if {@code -1} read last blockSz bytes.
   * @param blockSz - Maximum number of chars to read.
   * @param lastModified - File last modification time.
   * @return Read file block.
   * @throws IOException In case of error.
   */
  public static VisorFileBlock readBlock(File file, long off, int blockSz, long lastModified)
      throws IOException {
    RandomAccessFile raf = null;

    try {
      long fSz = file.length();
      long fLastModified = file.lastModified();

      long pos = off >= 0 ? off : Math.max(fSz - blockSz, 0);

      // Try read more that file length.
      if (fLastModified == lastModified && fSz != 0 && pos >= fSz)
        throw new IOException(
            "Trying to read file block with wrong offset: " + pos + " while file size: " + fSz);

      if (fSz == 0)
        return new VisorFileBlock(file.getPath(), pos, fLastModified, 0, false, EMPTY_FILE_BUF);
      else {
        int toRead = Math.min(blockSz, (int) (fSz - pos));

        byte[] buf = new byte[toRead];

        raf = new RandomAccessFile(file, "r");

        raf.seek(pos);

        int cntRead = raf.read(buf, 0, toRead);

        if (cntRead != toRead)
          throw new IOException(
              "Count of requested and actually read bytes does not match [cntRead="
                  + cntRead
                  + ", toRead="
                  + toRead
                  + ']');

        boolean zipped = buf.length > 512;

        return new VisorFileBlock(
            file.getPath(), pos, fSz, fLastModified, zipped, zipped ? zipBytes(buf) : buf);
      }
    } finally {
      U.close(raf, null);
    }
  }
  public RegionFile(File path) {
    offsets = new int[SECTOR_INTS];
    chunkTimestamps = new int[SECTOR_INTS];

    fileName = path;
    debugln("REGION LOAD " + fileName);

    sizeDelta = 0;

    try {
      if (path.exists()) {
        lastModified = path.lastModified();
      }

      file = new RandomAccessFile(path, "rw");

      if (file.length() < SECTOR_BYTES) {
        /* we need to write the chunk offset table */
        for (int i = 0; i < SECTOR_INTS; ++i) {
          file.writeInt(0);
        }
        // write another sector for the timestamp info
        for (int i = 0; i < SECTOR_INTS; ++i) {
          file.writeInt(0);
        }

        sizeDelta += SECTOR_BYTES * 2;
      }

      if ((file.length() & 0xfff) != 0) {
        /* the file size is not a multiple of 4KB, grow it */
        for (int i = 0; i < (file.length() & 0xfff); ++i) {
          file.write((byte) 0);
        }
      }

      /* set up the available sector map */
      int nSectors = (int) file.length() / SECTOR_BYTES;
      sectorFree = new ArrayList<Boolean>(nSectors);

      for (int i = 0; i < nSectors; ++i) {
        sectorFree.add(true);
      }

      sectorFree.set(0, false); // chunk offset table
      sectorFree.set(1, false); // for the last modified info

      file.seek(0);
      for (int i = 0; i < SECTOR_INTS; ++i) {
        int offset = file.readInt();
        offsets[i] = offset;
        if (offset != 0 && (offset >> 8) + (offset & 0xFF) <= sectorFree.size()) {
          for (int sectorNum = 0; sectorNum < (offset & 0xFF); ++sectorNum) {
            sectorFree.set((offset >> 8) + sectorNum, false);
          }
        }
      }
      for (int i = 0; i < SECTOR_INTS; ++i) {
        int lastModValue = file.readInt();
        chunkTimestamps[i] = lastModValue;
      }
    } catch (IOException e) {
      e.printStackTrace();
    }
  }
 private void setTimestamp(int x, int z, int value) throws IOException {
   chunkTimestamps[x + z * 32] = value;
   file.seek(SECTOR_BYTES + (x + z * 32) * 4);
   file.writeInt(value);
 }
 private void setOffset(int x, int z, int offset) throws IOException {
   offsets[x + z * 32] = offset;
   file.seek((x + z * 32) * 4);
   file.writeInt(offset);
 }
  /* write a chunk at (x,z) with length bytes of data to disk */
  protected synchronized void write(int x, int z, byte[] data, int length) {
    try {
      int offset = getOffset(x, z);
      int sectorNumber = offset >> 8;
      int sectorsAllocated = offset & 0xFF;
      int sectorsNeeded = (length + CHUNK_HEADER_SIZE) / SECTOR_BYTES + 1;

      // maximum chunk size is 1MB
      if (sectorsNeeded >= 256) {
        return;
      }

      if (sectorNumber != 0 && sectorsAllocated == sectorsNeeded) {
        /* we can simply overwrite the old sectors */
        debug("SAVE", x, z, length, "rewrite");
        write(sectorNumber, data, length);
      } else {
        /* we need to allocate new sectors */

        /* mark the sectors previously used for this chunk as free */
        for (int i = 0; i < sectorsAllocated; ++i) {
          sectorFree.set(sectorNumber + i, true);
        }

        /* scan for a free space large enough to store this chunk */
        int runStart = sectorFree.indexOf(true);
        int runLength = 0;
        if (runStart != -1) {
          for (int i = runStart; i < sectorFree.size(); ++i) {
            if (runLength != 0) {
              if (sectorFree.get(i)) runLength++;
              else runLength = 0;
            } else if (sectorFree.get(i)) {
              runStart = i;
              runLength = 1;
            }
            if (runLength >= sectorsNeeded) {
              break;
            }
          }
        }

        if (runLength >= sectorsNeeded) {
          /* we found a free space large enough */
          debug("SAVE", x, z, length, "reuse");
          sectorNumber = runStart;
          setOffset(x, z, (sectorNumber << 8) | sectorsNeeded);
          for (int i = 0; i < sectorsNeeded; ++i) {
            sectorFree.set(sectorNumber + i, false);
          }
          write(sectorNumber, data, length);
        } else {
          /*
           * no free space large enough found -- we need to grow the
           * file
           */
          debug("SAVE", x, z, length, "grow");
          file.seek(file.length());
          sectorNumber = sectorFree.size();
          for (int i = 0; i < sectorsNeeded; ++i) {
            file.write(emptySector);
            sectorFree.add(false);
          }
          sizeDelta += SECTOR_BYTES * sectorsNeeded;

          write(sectorNumber, data, length);
          setOffset(x, z, (sectorNumber << 8) | sectorsNeeded);
        }
      }
      setTimestamp(x, z, (int) (System.currentTimeMillis() / 1000L));
    } catch (IOException e) {
      e.printStackTrace();
    }
  }