/*
 * Copyright (c) 2015-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.imagepipeline.producers;

import javax.annotation.concurrent.GuardedBy;

import java.util.Map;
import java.util.concurrent.Executor;

import android.graphics.Bitmap;

import com.facebook.common.internal.ImmutableMap;
import com.facebook.common.internal.Preconditions;
import com.facebook.common.internal.VisibleForTesting;
import com.facebook.common.references.CloseableReference;
import com.facebook.imagepipeline.decoder.CloseableImageCopier;
import com.facebook.imagepipeline.image.CloseableBitmap;
import com.facebook.imagepipeline.image.CloseableImage;
import com.facebook.imagepipeline.request.Postprocessor;
import com.facebook.imagepipeline.request.RepeatedPostprocessor;
import com.facebook.imagepipeline.request.RepeatedPostprocessorRunner;

/**
 * Runs a caller-supplied post-processor object.
 *
 * <p>Post-processors are only supported for static bitmaps. If the request is for an animated
 * image, the post-processor step will be skipped without warning.
 */
public class PostprocessorProducer implements Producer<CloseableReference<CloseableImage>> {

  @VisibleForTesting static final String NAME = "PostprocessorProducer";
  @VisibleForTesting static final String BITMAP_COPIED_EVENT = "bitmap_copied";
  @VisibleForTesting static final String POSTPROCESSOR = "Postprocessor";

  private final Producer<CloseableReference<CloseableImage>> mNextProducer;
  private final CloseableImageCopier mCloseableImageCopier;
  private final Executor mExecutor;

  public PostprocessorProducer(
      Producer<CloseableReference<CloseableImage>> nextProducer,
      CloseableImageCopier closeableImageCopier,
      Executor executor) {
    mNextProducer = Preconditions.checkNotNull(nextProducer);
    mCloseableImageCopier = Preconditions.checkNotNull(closeableImageCopier);
    mExecutor = Preconditions.checkNotNull(executor);
  }

  @Override
  public void produceResults(
      final Consumer<CloseableReference<CloseableImage>> consumer,
      ProducerContext context) {
    final ProducerListener listener = context.getListener();
    final Postprocessor postprocessor = context.getImageRequest().getPostprocessor();
    Consumer<CloseableReference<CloseableImage>> postprocessorConsumer;
    if (postprocessor instanceof RepeatedPostprocessor) {
      postprocessorConsumer =
          new RepeatedPostprocessorConsumer(
              consumer,
              listener,
              context.getId(),
              (RepeatedPostprocessor) postprocessor,
              context);
    } else {
      postprocessorConsumer =
          new SingleUsePostprocessorConsumer(consumer, listener, context.getId(), postprocessor);
    }
    mNextProducer.produceResults(postprocessorConsumer, context);
  }

  private abstract class AbstractPostprocessorConsumer extends DelegatingConsumer<
      CloseableReference<CloseableImage>,
      CloseableReference<CloseableImage>> {
    private final ProducerListener mListener;
    private final String mRequestId;
    private final Postprocessor mPostprocessor;

    private AbstractPostprocessorConsumer(
        Consumer<CloseableReference<CloseableImage>> consumer,
        ProducerListener listener,
        String requestId,
        Postprocessor postprocessor) {
      super(consumer);
      mListener = listener;
      mRequestId = requestId;
      mPostprocessor = postprocessor;
    }

    protected void copyAndPostprocessBitmap(
        CloseableReference<CloseableImage> sourceImageRef,
        boolean isLast) {
      mListener.onProducerStart(mRequestId, NAME);
      CloseableReference<CloseableImage> destRef = null;
      try {
        try {
          destRef = mCloseableImageCopier.copyCloseableImage(sourceImageRef);
          mListener.onProducerEvent(mRequestId, NAME, BITMAP_COPIED_EVENT);
          postprocessBitmap(destRef, mPostprocessor);
        } catch (Throwable t) {
          mListener.onProducerFinishWithFailure(
              mRequestId, NAME, t, getExtraMap(mListener, mRequestId, mPostprocessor));
          getConsumer().onFailure(t);
          return;
        }
        mListener.onProducerFinishWithSuccess(
            mRequestId, NAME, getExtraMap(mListener, mRequestId, mPostprocessor));
        getConsumer().onNewResult(destRef, isLast);
      } finally {
        CloseableReference.closeSafely(destRef);
      }
    }

    private void postprocessBitmap(
        CloseableReference<CloseableImage> destinationCloseableImageRef,
        Postprocessor postprocessor) {
      Bitmap destinationBitmap =
          ((CloseableBitmap) destinationCloseableImageRef.get()).getUnderlyingBitmap();
      postprocessor.process(destinationBitmap);
    }

    private Map<String, String> getExtraMap(
        ProducerListener listener,
        String requestId,
        Postprocessor postprocessor) {
      if (!listener.requiresExtraMap(requestId)) {
        return null;
      }
      return ImmutableMap.of(POSTPROCESSOR, postprocessor.getName());
    }
  }

  private class SingleUsePostprocessorConsumer extends AbstractPostprocessorConsumer {
    private SingleUsePostprocessorConsumer(
        Consumer<CloseableReference<CloseableImage>> consumer,
        ProducerListener listener,
        String requestId,
        Postprocessor postprocessor) {
      super(consumer, listener, requestId, postprocessor);
    }

    @Override
    protected void onNewResultImpl(
        final CloseableReference<CloseableImage> newResult, final boolean isLast) {
      if (!isLast) {
        return;
      }
      if (!mCloseableImageCopier.isCloseableImageCopyable(newResult)) {
        getConsumer().onNewResult(newResult, true);
        return;
      }

      final CloseableReference<CloseableImage> clonedResult = newResult.clone();
      mExecutor.execute(
          new Runnable() {
            @Override
            public void run() {
              try {
                copyAndPostprocessBitmap(clonedResult, isLast);
              } finally {
                CloseableReference.closeSafely(clonedResult);
              }
            }
          });
    }
  }

  private class RepeatedPostprocessorConsumer
      extends AbstractPostprocessorConsumer implements RepeatedPostprocessorRunner {
    @GuardedBy("this")
    private CloseableReference<CloseableImage> mOriginalImageRef;
    @GuardedBy("this")
    private boolean mIsDirty;
    @GuardedBy("this")
    private boolean mIsPostProcessingRunning;

    private RepeatedPostprocessorConsumer(
        Consumer<CloseableReference<CloseableImage>> consumer,
        ProducerListener listener,
        String requestId,
        RepeatedPostprocessor repeatedPostprocessor,
        ProducerContext context) {
      super(consumer, listener, requestId, repeatedPostprocessor);
      repeatedPostprocessor.setCallback(RepeatedPostprocessorConsumer.this);
      context.addCallbacks(
          new BaseProducerContextCallbacks() {
            @Override
            public void onCancellationRequested() {
              closeOriginalImage();
              getConsumer().onCancellation();
            }
          });
      mIsDirty = false;
      mIsPostProcessingRunning = false;
    }

    @Override
    protected void onNewResultImpl(
        CloseableReference<CloseableImage> newResult, boolean isLast) {
      if (!isLast) {
        return;
      }
      if (!mCloseableImageCopier.isCloseableImageCopyable(newResult)) {
        getConsumer().onNewResult(newResult, true);
        return;
      }

      synchronized (RepeatedPostprocessorConsumer.this) {
        mOriginalImageRef = newResult.clone();
      }
      maybeExecuteCopyAndPostprocessBitmap();
    }

    @Override
    public synchronized void update() {
      maybeExecuteCopyAndPostprocessBitmap();
    }

    private void maybeExecuteCopyAndPostprocessBitmap() {
      boolean shouldExecutePostProcessing = false;
      synchronized (RepeatedPostprocessorConsumer.this) {
        mIsDirty = true;
        if (!mIsPostProcessingRunning && CloseableReference.isValid(mOriginalImageRef)) {
          mIsPostProcessingRunning = true;
          shouldExecutePostProcessing = true;
        }
      }
      if (shouldExecutePostProcessing) {
        executeCopyAndPostprocessBitmap();
      }
    }

    private void executeCopyAndPostprocessBitmap() {
      mExecutor.execute(
          new Runnable() {
            @Override
            public void run() {
              CloseableReference<CloseableImage> closeableImageRef = null;
              synchronized (RepeatedPostprocessorConsumer.this) {
                mIsDirty = false;
                if (CloseableReference.isValid(mOriginalImageRef)) {
                  closeableImageRef = mOriginalImageRef.clone();
                }
              }
              if (closeableImageRef != null) {
                try {
                  copyAndPostprocessBitmap(closeableImageRef, /* isLast */false);
                } finally {
                  CloseableReference.closeSafely(closeableImageRef);
                }
              }
              boolean shouldExecutePostprocessing;
              synchronized (RepeatedPostprocessorConsumer.this) {
                shouldExecutePostprocessing = mIsDirty;
                mIsPostProcessingRunning = mIsDirty;
              }
              if (shouldExecutePostprocessing) {
                executeCopyAndPostprocessBitmap();
              }
            }
          });
    }

    private synchronized void closeOriginalImage() {
      CloseableReference.closeSafely(mOriginalImageRef);
      mOriginalImageRef = null;
    }
  }
}
