package com.bumptech.glide.load.resource.gif;

import static com.bumptech.glide.request.RequestOptions.diskCacheStrategyOf;
import static com.bumptech.glide.request.RequestOptions.signatureOf;

import android.graphics.Bitmap;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import android.os.SystemClock;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.annotation.VisibleForTesting;
import com.bumptech.glide.Glide;
import com.bumptech.glide.RequestBuilder;
import com.bumptech.glide.RequestManager;
import com.bumptech.glide.gifdecoder.GifDecoder;
import com.bumptech.glide.load.Key;
import com.bumptech.glide.load.Transformation;
import com.bumptech.glide.load.engine.DiskCacheStrategy;
import com.bumptech.glide.load.engine.bitmap_recycle.BitmapPool;
import com.bumptech.glide.request.RequestOptions;
import com.bumptech.glide.request.target.SimpleTarget;
import com.bumptech.glide.request.transition.Transition;
import com.bumptech.glide.signature.ObjectKey;
import com.bumptech.glide.util.Preconditions;
import com.bumptech.glide.util.Synthetic;
import com.bumptech.glide.util.Util;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.List;

class GifFrameLoader {
  private final GifDecoder gifDecoder;
  private final Handler handler;
  private final List<FrameCallback> callbacks = new ArrayList<>();
  @SuppressWarnings("WeakerAccess") @Synthetic final RequestManager requestManager;
  private final BitmapPool bitmapPool;

  private boolean isRunning;
  private boolean isLoadPending;
  private boolean startFromFirstFrame;
  private RequestBuilder<Bitmap> requestBuilder;
  private DelayTarget current;
  private boolean isCleared;
  private DelayTarget next;
  private Bitmap firstFrame;
  private Transformation<Bitmap> transformation;
  private DelayTarget pendingTarget;
  @Nullable
  private GifFrameLoader.OnEveryFrameListener onEveryFrameListener;

  public interface FrameCallback {
    void onFrameReady();
  }

  GifFrameLoader(
      Glide glide,
      GifDecoder gifDecoder,
      int width,
      int height,
      Transformation<Bitmap> transformation,
      Bitmap firstFrame) {
    this(
        glide.getBitmapPool(),
        Glide.with(glide.getContext()),
        gifDecoder,
        null /*handler*/,
        getRequestBuilder(Glide.with(glide.getContext()), width, height),
        transformation,
        firstFrame);
  }

  @SuppressWarnings("PMD.ConstructorCallsOverridableMethod")
  GifFrameLoader(
      BitmapPool bitmapPool,
      RequestManager requestManager,
      GifDecoder gifDecoder,
      Handler handler,
      RequestBuilder<Bitmap> requestBuilder,
      Transformation<Bitmap> transformation,
      Bitmap firstFrame) {
    this.requestManager = requestManager;
    if (handler == null) {
      handler = new Handler(Looper.getMainLooper(), new FrameLoaderCallback());
    }
    this.bitmapPool = bitmapPool;
    this.handler = handler;
    this.requestBuilder = requestBuilder;

    this.gifDecoder = gifDecoder;

    setFrameTransformation(transformation, firstFrame);
  }

  void setFrameTransformation(Transformation<Bitmap> transformation, Bitmap firstFrame) {
    this.transformation = Preconditions.checkNotNull(transformation);
    this.firstFrame = Preconditions.checkNotNull(firstFrame);
    requestBuilder = requestBuilder.apply(new RequestOptions().transform(transformation));
  }

  Transformation<Bitmap> getFrameTransformation() {
    return transformation;
  }

  Bitmap getFirstFrame() {
    return firstFrame;
  }

  void subscribe(FrameCallback frameCallback) {
    if (isCleared) {
      throw new IllegalStateException("Cannot subscribe to a cleared frame loader");
    }
    if (callbacks.contains(frameCallback)) {
      throw new IllegalStateException("Cannot subscribe twice in a row");
    }
    boolean start = callbacks.isEmpty();
    callbacks.add(frameCallback);
    if (start) {
      start();
    }
  }

  void unsubscribe(FrameCallback frameCallback) {
    callbacks.remove(frameCallback);
    if (callbacks.isEmpty()) {
      stop();
    }
  }

  int getWidth() {
    return getCurrentFrame().getWidth();
  }

  int getHeight() {
    return getCurrentFrame().getHeight();
  }

  int getSize() {
    return gifDecoder.getByteSize() + getFrameSize();
  }

  int getCurrentIndex() {
    return current != null ? current.index : -1;
  }

  private int getFrameSize() {
    return Util.getBitmapByteSize(getCurrentFrame().getWidth(), getCurrentFrame().getHeight(),
        getCurrentFrame().getConfig());
  }

  ByteBuffer getBuffer() {
    return gifDecoder.getData().asReadOnlyBuffer();
  }

  int getFrameCount() {
    return gifDecoder.getFrameCount();
  }

  int getLoopCount() {
    return gifDecoder.getTotalIterationCount();
  }

  private void start() {
    if (isRunning) {
      return;
    }
    isRunning = true;
    isCleared = false;

    loadNextFrame();
  }

  private void stop() {
    isRunning = false;
  }

  void clear() {
    callbacks.clear();
    recycleFirstFrame();
    stop();
    if (current != null) {
      requestManager.clear(current);
      current = null;
    }
    if (next != null) {
      requestManager.clear(next);
      next = null;
    }
    if (pendingTarget != null) {
      requestManager.clear(pendingTarget);
      pendingTarget = null;
    }
    gifDecoder.clear();
    isCleared = true;
  }

  Bitmap getCurrentFrame() {
    return current != null ? current.getResource() : firstFrame;
  }

  private void loadNextFrame() {
    if (!isRunning || isLoadPending) {
      return;
    }
    if (startFromFirstFrame) {
      Preconditions.checkArgument(
          pendingTarget == null, "Pending target must be null when starting from the first frame");
      gifDecoder.resetFrameIndex();
      startFromFirstFrame = false;
    }
    if (pendingTarget != null) {
      DelayTarget temp = pendingTarget;
      pendingTarget = null;
      onFrameReady(temp);
      return;
    }
    isLoadPending = true;
    // Get the delay before incrementing the pointer because the delay indicates the amount of time
    // we want to spend on the current frame.
    int delay = gifDecoder.getNextDelay();
    long targetTime = SystemClock.uptimeMillis() + delay;

    gifDecoder.advance();
    next = new DelayTarget(handler, gifDecoder.getCurrentFrameIndex(), targetTime);
    requestBuilder.apply(signatureOf(getFrameSignature())).load(gifDecoder).into(next);
  }

  private void recycleFirstFrame() {
    if (firstFrame != null) {
      bitmapPool.put(firstFrame);
      firstFrame = null;
    }
  }

  void setNextStartFromFirstFrame() {
    Preconditions.checkArgument(!isRunning, "Can't restart a running animation");
    startFromFirstFrame = true;
    if (pendingTarget != null) {
      requestManager.clear(pendingTarget);
      pendingTarget = null;
    }
  }

  @VisibleForTesting
  void setOnEveryFrameReadyListener(@Nullable OnEveryFrameListener onEveryFrameListener) {
    this.onEveryFrameListener = onEveryFrameListener;
  }

  @VisibleForTesting
  void onFrameReady(DelayTarget delayTarget) {
    if (onEveryFrameListener != null) {
      onEveryFrameListener.onFrameReady();
    }
    isLoadPending = false;
    if (isCleared) {
      handler.obtainMessage(FrameLoaderCallback.MSG_CLEAR, delayTarget).sendToTarget();
      return;
    }
    // If we're not running, notifying here will recycle the frame that we might currently be
    // showing, which breaks things (see #2526). We also can't discard this frame because we've
    // already incremented the frame pointer and can't decode the same frame again. Instead we'll
    // just hang on to this next frame until start() or clear() are called.
    if (!isRunning) {
      pendingTarget = delayTarget;
      return;
    }

    if (delayTarget.getResource() != null) {
      recycleFirstFrame();
      DelayTarget previous = current;
      current = delayTarget;
      // The callbacks may unregister when onFrameReady is called, so iterate in reverse to avoid
      // concurrent modifications.
      for (int i = callbacks.size() - 1; i >= 0; i--) {
        FrameCallback cb = callbacks.get(i);
        cb.onFrameReady();
      }
      if (previous != null) {
        handler.obtainMessage(FrameLoaderCallback.MSG_CLEAR, previous).sendToTarget();
      }
    }

    loadNextFrame();
  }

  private class FrameLoaderCallback implements Handler.Callback {
    static final int MSG_DELAY = 1;
    static final int MSG_CLEAR = 2;

    @Synthetic
    FrameLoaderCallback() { }

    @Override
    public boolean handleMessage(Message msg) {
      if (msg.what == MSG_DELAY) {
        GifFrameLoader.DelayTarget target = (DelayTarget) msg.obj;
        onFrameReady(target);
        return true;
      } else if (msg.what == MSG_CLEAR) {
        GifFrameLoader.DelayTarget target = (DelayTarget) msg.obj;
        requestManager.clear(target);
      }
      return false;
    }
  }

  @VisibleForTesting
  static class DelayTarget extends SimpleTarget<Bitmap> {
    private final Handler handler;
    @Synthetic final int index;
    private final long targetTime;
    private Bitmap resource;

    DelayTarget(Handler handler, int index, long targetTime) {
      this.handler = handler;
      this.index = index;
      this.targetTime = targetTime;
    }

    Bitmap getResource() {
      return resource;
    }

    @Override
    public void onResourceReady(@NonNull Bitmap resource,
        @Nullable Transition<? super Bitmap> transition) {
      this.resource = resource;
      Message msg = handler.obtainMessage(FrameLoaderCallback.MSG_DELAY, this);
      handler.sendMessageAtTime(msg, targetTime);
    }
  }

  private static RequestBuilder<Bitmap> getRequestBuilder(
      RequestManager requestManager, int width, int height) {
    return requestManager
        .asBitmap()
        .apply(
            diskCacheStrategyOf(DiskCacheStrategy.NONE)
                .useAnimationPool(true)
                .skipMemoryCache(true)
                .override(width, height));
  }

  private static Key getFrameSignature() {
    // Some devices seem to have crypto bugs that throw exceptions when you create a new UUID.
    // See #1510.
    return new ObjectKey(Math.random());
  }

  @VisibleForTesting
  interface OnEveryFrameListener {
    void onFrameReady();
  }
}
