class ImagePoolImpl implements ImagePool {

  @VisibleForTesting
  @NotNull
  ImageImpl create(final int w, final int h, final int type, @Nullable Consumer<BufferedImage> freedCallback) {
    assert !isDisposed : "ImagePool already disposed";

    // To avoid creating a large number of EvictingQueues, we distribute the images in buckets and use that
    Bucket bucket = getTypeBucket(w, h, type);
    BucketStatsImpl bucketStats = myBucketStats.get(bucket);
    if (LOG.isDebugEnabled()) {
      LOG.debug(String.format("create(%dx%d-%d) in bucket (%dx%d) hasStats=%b\n", w, h, type, bucket.myMinWidth, bucket.myMinHeight,
                              bucketStats != null));
    }

    BufferedImage image;
    SoftReference<BufferedImage> imageRef;
    try {
      imageRef = bucket.remove();
      while ((image = imageRef.get()) == null) {
        imageRef = bucket.remove();
      }

      long totalSize = image.getWidth() * image.getHeight();
      if (bucketStats != null) {
        bucketStats.bucketHit();
      }
      if (LOG.isDebugEnabled()) {
        double wasted = (totalSize - w * h);
        LOG.debug(String.format("  Re-used image %dx%d - %d\n  pool buffer %dx%d\n  wasted %d%%\n",
                                w, h, type,
                                image.getWidth(), image.getHeight(),
                                (int)((wasted / totalSize) * 100)));
      }
      myTotalInUseBytes.add(totalSize * 4);
      // Clear the image
      if (image.getRaster().getDataBuffer().getDataType() == java.awt.image.DataBuffer.TYPE_INT) {
        Arrays.fill(((DataBufferInt)image.getRaster().getDataBuffer()).getData(), 0);
      }
      else {
        Graphics2D g = image.createGraphics();
        g.setComposite(AlphaComposite.Clear);
        g.fillRect(0, 0, w, h);
        g.dispose();
      }
    }
    catch (NoSuchElementException e) {
      if (LOG.isDebugEnabled()) {
        LOG.debug(String.format("  New image %dx%d - %d\n", w, h, type));
      }
      if (bucketStats != null) {
        bucketStats.bucketMiss();
      }
      int newImageWidth = Math.max(bucket.myMinWidth, w);
      int newImageHeight = Math.max(bucket.myMinHeight, h);
      //noinspection UndesirableClassUsage
      image = new BufferedImage(newImageWidth, newImageHeight, type);
      // Set acceleration priority to 0.9 out of 1.0. We reserve 1.0 for the shared buffers
      // that we paint to screen.
      image.setAccelerationPriority(0.9f);
      long estimatedSize = newImageWidth * newImageHeight * 4;
      myTotalAllocatedBytes.add(estimatedSize);
      myTotalInUseBytes.add(estimatedSize);

    }

    ImageImpl pooledImage = new ImageImpl(w, h, image);
    final BufferedImage imagePointer = image;
    FinalizablePhantomReference<ImagePool.Image> reference =
      new FinalizablePhantomReference<ImagePool.Image>(pooledImage, myFinalizableReferenceQueue) {
      @Override
      public void finalizeReferent() {
        // This method might be called twice if the user has manually called the free() method. The second call will have no effect.
        if (myReferences.remove(this)) {
          boolean accepted = bucket.offer(new SoftReference<>(imagePointer));
          if (bucketStats != null) {
            if (accepted) {
              bucketStats.returnedImageAccepted();
            }
            else {
              bucketStats.returnedImageRejected();
            }
          }
          if (LOG.isDebugEnabled()) {
            LOG.debug(String.format("%s image (%dx%d-%d) in bucket (%dx%d)\n",
                                    accepted ? "Released" : "Rejected",
                                    w, h, type, bucket.myMinWidth, bucket.myMinHeight));
          }

          long estimatedSize = imagePointer.getWidth() * imagePointer.getHeight() * 4;
          if (!accepted) {
            myTotalAllocatedBytes.add(-estimatedSize);
          }
          else {
            myTotalInUseBytes.add(-estimatedSize);
          }
          if (freedCallback != null) {
            freedCallback.accept(imagePointer);
          }
        }
      }
    };
    pooledImage.myOwnReference = reference;
    myReferences.add(reference);

    return pooledImage;
  }

    ImagePoolImpl(@NotNull int[] bucketSizes, @NotNull BiFunction<Integer, Integer, Function<Integer, Integer>> bucketSizingPolicy);

}