/*
 * Copyright (c) 2017-present, Facebook, Inc.
 * All rights reserved.
 *
 * This source code is licensed under the BSD-style license found in the
 * LICENSE file in the root directory of this source tree. An additional grant
 * of patent rights can be found in the PATENTS file in the same directory.
 */
package com.facebook.fresco.animation.bitmap.preparation;

import android.graphics.Bitmap;
import android.util.SparseArray;
import com.facebook.common.logging.FLog;
import com.facebook.common.references.CloseableReference;
import com.facebook.fresco.animation.backend.AnimationBackend;
import com.facebook.fresco.animation.bitmap.BitmapAnimationBackend;
import com.facebook.fresco.animation.bitmap.BitmapFrameCache;
import com.facebook.fresco.animation.bitmap.BitmapFrameRenderer;
import com.facebook.imagepipeline.bitmaps.PlatformBitmapFactory;
import java.util.concurrent.ExecutorService;

/**
 * Default bitmap frame preparer that uses the given {@link ExecutorService} to schedule jobs.
 * An instance of this class can be shared between multiple animated images.
 */
public class DefaultBitmapFramePreparer
    implements BitmapFramePreparer {

  private static final Class<?> TAG = DefaultBitmapFramePreparer.class;

  private final PlatformBitmapFactory mPlatformBitmapFactory;
  private final BitmapFrameRenderer mBitmapFrameRenderer;
  private final Bitmap.Config mBitmapConfig;
  private final ExecutorService mExecutorService;
  private final SparseArray<Runnable> mPendingFrameDecodeJobs;

  public DefaultBitmapFramePreparer(
      PlatformBitmapFactory platformBitmapFactory,
      BitmapFrameRenderer bitmapFrameRenderer,
      Bitmap.Config bitmapConfig,
      ExecutorService executorService) {
    mPlatformBitmapFactory = platformBitmapFactory;
    mBitmapFrameRenderer = bitmapFrameRenderer;
    mBitmapConfig = bitmapConfig;
    mExecutorService = executorService;
    mPendingFrameDecodeJobs = new SparseArray<>();
  }

  @Override
  public boolean prepareFrame(
      BitmapFrameCache bitmapFrameCache,
      AnimationBackend animationBackend,
      int frameNumber) {
    // Create a unique ID to identify the frame for the given backend.
    int frameId = getUniqueId(animationBackend, frameNumber);
    synchronized (mPendingFrameDecodeJobs) {
      // Check if already scheduled.
      if (mPendingFrameDecodeJobs.get(frameId) != null) {
        FLog.v(TAG, "Already scheduled decode job for frame %d", frameNumber);
        return true;
      }
      // Check if already cached.
      if (bitmapFrameCache.contains(frameNumber)) {
        FLog.v(TAG, "Frame %d is cached already.", frameNumber);
        return true;
      }
      Runnable frameDecodeRunnable = new FrameDecodeRunnable(
          animationBackend,
          bitmapFrameCache,
          frameNumber,
          frameId);
      mPendingFrameDecodeJobs.put(frameId, frameDecodeRunnable);
      mExecutorService.execute(frameDecodeRunnable);
    }
    return true;
  }

  private static int getUniqueId(AnimationBackend backend, int frameNumber) {
    int result = backend.hashCode();
    result = 31 * result + frameNumber;
    return result;
  }

  private class FrameDecodeRunnable implements Runnable {

    private final BitmapFrameCache mBitmapFrameCache;
    private final AnimationBackend mAnimationBackend;
    private final int mFrameNumber;
    private final int mHashCode;

    public FrameDecodeRunnable(
        AnimationBackend animationBackend,
        BitmapFrameCache bitmapFrameCache,
        int frameNumber,
        int hashCode) {
      mAnimationBackend = animationBackend;
      mBitmapFrameCache = bitmapFrameCache;
      mFrameNumber = frameNumber;
      mHashCode = hashCode;
    }

    @Override
    public void run() {
      try {
        // If we have a cached frame already, we don't need to do anything.
        if (mBitmapFrameCache.contains(mFrameNumber)) {
          FLog.v(TAG, "Frame %d is cached already.", mFrameNumber);
          return;
        }

        // Prepare the frame.
        if (prepareFrameAndCache(mFrameNumber, BitmapAnimationBackend.FRAME_TYPE_REUSED)) {
          FLog.v(TAG, "Prepared frame frame %d.", mFrameNumber);
        } else {
          FLog.e(TAG, "Could not prepare frame %d.", mFrameNumber);
        }
      } finally {
        synchronized (mPendingFrameDecodeJobs) {
          mPendingFrameDecodeJobs.remove(mHashCode);
        }
      }
    }

    private boolean prepareFrameAndCache(
        int frameNumber,
        @BitmapAnimationBackend.FrameType int frameType) {
      CloseableReference<Bitmap> bitmapReference = null;
      boolean created;
      int nextFrameType;

      try {
        switch (frameType) {
          case BitmapAnimationBackend.FRAME_TYPE_REUSED:
            bitmapReference =
                mBitmapFrameCache.getBitmapToReuseForFrame(
                    frameNumber,
                    mAnimationBackend.getIntrinsicWidth(),
                    mAnimationBackend.getIntrinsicHeight());
            nextFrameType = BitmapAnimationBackend.FRAME_TYPE_CREATED;
            break;

          case BitmapAnimationBackend.FRAME_TYPE_CREATED:
            bitmapReference =
                mPlatformBitmapFactory.createBitmap(
                    mAnimationBackend.getIntrinsicWidth(),
                    mAnimationBackend.getIntrinsicHeight(),
                    mBitmapConfig);
            nextFrameType = BitmapAnimationBackend.FRAME_TYPE_UNKNOWN;
            break;
          default:
            return false;
        }
        // Try to render and cache the frame
        created = renderFrameAndCache(frameNumber, bitmapReference, frameType);
      } finally {
        CloseableReference.closeSafely(bitmapReference);
      }

      if (created || nextFrameType == BitmapAnimationBackend.FRAME_TYPE_UNKNOWN) {
        return created;
      } else {
        return prepareFrameAndCache(frameNumber, nextFrameType);
      }
    }

    private boolean renderFrameAndCache(
        int frameNumber,
        CloseableReference<Bitmap> bitmapReference,
        @BitmapAnimationBackend.FrameType int frameType) {
      // Check if the bitmap is valid
      if (!CloseableReference.isValid(bitmapReference)) {
        return false;
      }
      // Try to render the frame
      if (!mBitmapFrameRenderer.renderFrame(frameNumber, bitmapReference.get())) {
        return false;
      }
      FLog.v(TAG, "Frame %d ready.", mFrameNumber);
      // Cache the frame
      synchronized (mPendingFrameDecodeJobs) {
        mBitmapFrameCache.onFramePrepared(mFrameNumber, bitmapReference, frameType);
      }
      return true;
    }
  }
}

@@ -5125,24 +5125,44 @@
 PE_CREATED:%0A
+            try %7B%0A  
             
@@ -5191,24 +5191,26 @@
             
+  
 mPlatformBit
@@ -5234,16 +5234,18 @@
 Bitmap(%0A
+  
         
@@ -5307,32 +5307,34 @@
                 
+ 
+ 
 mAnimationBacken
@@ -5373,24 +5373,26 @@
             
+  
 mBitmapConfi
@@ -5395,16 +5395,296 @@
 onfig);%0A
+            %7D catch (RuntimeException e) %7B%0A              // Failed to create the bitmap for the frame, return and report that we could not%0A              // prepare the frame.%0A              FLog.w(TAG, %22Failed to create frame bitmap%22, e);%0A              return false;%0A            %7D%0A
         

Handle pin bitmap error in DefaultBitmapFramePreparer