/**
 * Copyright (c) 2015-present, Facebook, Inc.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */

package com.facebook.react.flat;

import javax.annotation.Nullable;

import android.graphics.Bitmap;

import com.facebook.common.executors.UiThreadImmediateExecutorService;
import com.facebook.common.references.CloseableReference;
import com.facebook.datasource.DataSource;
import com.facebook.datasource.DataSubscriber;
import com.facebook.imagepipeline.core.ImagePipeline;
import com.facebook.imagepipeline.core.ImagePipelineFactory;
import com.facebook.imagepipeline.image.CloseableBitmap;
import com.facebook.imagepipeline.image.CloseableImage;
import com.facebook.imagepipeline.request.ImageRequest;
import com.facebook.infer.annotation.Assertions;
import com.facebook.react.views.image.ImageLoadEvent;

/**
 * Helper class for DrawImage that helps manage fetch requests through ImagePipeline.
 *
 * Request states this class can be in:
 * 1) mDataSource == null, mImageRef == null : request has not be started, was canceled or failed.
 * 2) mDataSource != null, mImageRef == null : request is in progress.
 * 3) mDataSource == null, mImageRef != null : request successfully finished.
 * 4) mDataSource != null, mImageRef != null : invalid state (should never happen)
 */
/* package */ final class PipelineRequestHelper
    implements DataSubscriber<CloseableReference<CloseableImage>> {

  private final ImageRequest mImageRequest;
  private @Nullable BitmapUpdateListener mBitmapUpdateListener;
  private @Nullable DataSource<CloseableReference<CloseableImage>> mDataSource;
  private @Nullable CloseableReference<CloseableImage> mImageRef;
  private int mAttachCounter;

  /* package */ PipelineRequestHelper(ImageRequest imageRequest) {
    mImageRequest = imageRequest;
  }

  /* package */ void attach(BitmapUpdateListener listener) {
    mBitmapUpdateListener = listener;

    mAttachCounter++;
    if (mAttachCounter != 1) {
      // this is a secondary attach, ignore it, only updating Bitmap boundaries if needed.
      Bitmap bitmap = getBitmap();
      if (bitmap != null) {
        listener.onSecondaryAttach(bitmap);
      }
      return;
    }

    listener.onImageLoadEvent(ImageLoadEvent.ON_LOAD_START);

    Assertions.assertCondition(mDataSource == null);
    Assertions.assertCondition(mImageRef == null);

    // Submit the request
    ImagePipeline imagePipeline = ImagePipelineFactory.getInstance().getImagePipeline();
    mDataSource = imagePipeline.fetchDecodedImage(mImageRequest, RCTImageView.getCallerContext());
    mDataSource.subscribe(this, UiThreadImmediateExecutorService.getInstance());
  }

  /**
   * Returns whether detach() was primary, false otherwise.
   */
  /* package */ void detach() {
    --mAttachCounter;
    if (mAttachCounter != 0) {
      // this is a secondary detach, ignore it
      return;
    }

    if (mDataSource != null) {
      mDataSource.close();
      mDataSource = null;
    }

    if (mImageRef != null) {
      mImageRef.close();
      mImageRef = null;
    }

    mBitmapUpdateListener = null;
  }

  /**
   * Returns an unsafe bitmap reference. Do not assign the result of this method to anything other
   * than a local variable, or it will no longer work with the reference count goes to zero.
   */
  /* package */ @Nullable Bitmap getBitmap() {
    if (mImageRef == null) {
      return null;
    }

    CloseableImage closeableImage = mImageRef.get();
    if (!(closeableImage instanceof CloseableBitmap)) {
      mImageRef.close();
      mImageRef = null;
      return null;
    }

    return ((CloseableBitmap) closeableImage).getUnderlyingBitmap();
  }

  /* package */ boolean isDetached() {
    return mAttachCounter == 0;
  }

  @Override
  public void onNewResult(DataSource<CloseableReference<CloseableImage>> dataSource) {
    if (!dataSource.isFinished()) {
      // only interested in final image, no need to close the dataSource
      return;
    }

    try {
      if (mDataSource != dataSource) {
        // Shouldn't ever happen, but let's be safe (dataSource got closed by callback still fired?)
        return;
      }

      mDataSource = null;

      CloseableReference<CloseableImage> imageReference = dataSource.getResult();
      if (imageReference == null) {
        // Shouldn't ever happen, but let's be safe (dataSource got closed by callback still fired?)
        return;
      }

      CloseableImage image = imageReference.get();
      if (!(image instanceof CloseableBitmap)) {
        // only bitmaps are supported
        imageReference.close();
        return;
      }

      mImageRef = imageReference;

      Bitmap bitmap = getBitmap();
      if (bitmap == null) {
        // Shouldn't ever happen, but let's be safe.
        return;
      }

      BitmapUpdateListener listener = Assertions.assumeNotNull(mBitmapUpdateListener);
      listener.onBitmapReady(bitmap);
      listener.onImageLoadEvent(ImageLoadEvent.ON_LOAD);
      listener.onImageLoadEvent(ImageLoadEvent.ON_LOAD_END);
    } finally {
      dataSource.close();
    }
  }

  @Override
  public void onFailure(DataSource<CloseableReference<CloseableImage>> dataSource) {
    if (mDataSource == dataSource) {
      Assertions.assumeNotNull(mBitmapUpdateListener).onImageLoadEvent(ImageLoadEvent.ON_ERROR);
      Assertions.assumeNotNull(mBitmapUpdateListener).onImageLoadEvent(ImageLoadEvent.ON_LOAD_END);
      mDataSource = null;
    }

    dataSource.close();
  }

  @Override
  public void onCancellation(DataSource<CloseableReference<CloseableImage>> dataSource) {
    if (mDataSource == dataSource) {
      mDataSource = null;
    }

    dataSource.close();
  }

  @Override
  public void onProgressUpdate(DataSource<CloseableReference<CloseableImage>> dataSource) {
  }
}
