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

import static com.bumptech.glide.gifdecoder.GifDecoder.TOTAL_ITERATION_COUNT_FOREVER;

import android.content.Context;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.ColorFilter;
import android.graphics.Paint;
import android.graphics.PixelFormat;
import android.graphics.Rect;
import android.graphics.drawable.Animatable;
import android.graphics.drawable.Drawable;
import android.support.annotation.VisibleForTesting;
import android.view.Gravity;
import com.bumptech.glide.Glide;
import com.bumptech.glide.gifdecoder.GifDecoder;
import com.bumptech.glide.load.Transformation;
import com.bumptech.glide.load.engine.bitmap_recycle.BitmapPool;
import com.bumptech.glide.util.Preconditions;
import java.nio.ByteBuffer;

/**
 * An animated {@link android.graphics.drawable.Drawable} that plays the frames of an animated GIF.
 */
public class GifDrawable extends Drawable implements GifFrameLoader.FrameCallback,
    Animatable {
  /**
   * A constant indicating that an animated drawable should loop continuously.
   */
  public static final int LOOP_FOREVER = -1;
  /**
   * A constant indicating that an animated drawable should loop for its default number of times.
   * For animated GIFs, this constant indicates the GIF should use the netscape loop count if
   * present.
   */
  public static final int LOOP_INTRINSIC = 0;

  private final GifState state;
  /**
   * True if the drawable is currently animating.
   */
  private boolean isRunning;
  /**
   * True if the drawable should animate while visible.
   */
  private boolean isStarted;
  /**
   * True if the drawable's resources have been recycled.
   */
  private boolean isRecycled;
  /**
   * True if the drawable is currently visible. Default to true because on certain platforms (at
   * least 4.1.1), setVisible is not called on {@link android.graphics.drawable.Drawable Drawables}
   * during {@link android.widget.ImageView#setImageDrawable(android.graphics.drawable.Drawable)}.
   * See issue #130.
   */
  private boolean isVisible = true;
  /**
   * The number of times we've looped over all the frames in the GIF.
   */
  private int loopCount;
  /**
   * The number of times to loop through the GIF animation.
   */
  private int maxLoopCount = LOOP_FOREVER;

  private boolean applyGravity;
  private Paint paint;
  private Rect destRect;

  /**
   * Constructor for GifDrawable.
   *
   * @param context             A context.
   * @param bitmapPool          A {@link com.bumptech.glide.load.engine.bitmap_recycle.BitmapPool}
   *                            that can be used to return the first frame when this drawable is
   *                            recycled.
   * @param frameTransformation An {@link com.bumptech.glide.load.Transformation} that can be
   *                            applied to each frame.
   * @param targetFrameWidth    The desired width of the frames displayed by this drawable (the
   *                            width of the view or
   *                            {@link com.bumptech.glide.request.target.Target}
   *                            this drawable is being loaded into).
   * @param targetFrameHeight   The desired height of the frames displayed by this drawable (the
   *                            height of the view or
   *                            {@link com.bumptech.glide.request.target.Target}
   *                            this drawable is being loaded into).
   * @param gifDecoder          The decoder to use to decode GIF data.
   * @param firstFrame          The decoded and transformed first frame of this GIF.
   * @see #setFrameTransformation(com.bumptech.glide.load.Transformation, android.graphics.Bitmap)
   */
  public GifDrawable(Context context, GifDecoder gifDecoder, BitmapPool bitmapPool,
      Transformation<Bitmap> frameTransformation, int targetFrameWidth, int targetFrameHeight,
      Bitmap firstFrame) {
    this(
        new GifState(
            bitmapPool,
            new GifFrameLoader(
                // TODO(b/27524013): Factor out this call to Glide.get()
                Glide.get(context),
                gifDecoder,
                targetFrameWidth,
                targetFrameHeight,
                frameTransformation,
                firstFrame)));
  }

  GifDrawable(GifState state) {
    this.state = Preconditions.checkNotNull(state);
  }

  @VisibleForTesting
  GifDrawable(GifFrameLoader frameLoader, BitmapPool bitmapPool, Paint paint) {
    this(new GifState(bitmapPool, frameLoader));
    this.paint = paint;
  }

  public int getSize() {
    return state.frameLoader.getSize();
  }

  public Bitmap getFirstFrame() {
    return state.frameLoader.getFirstFrame();
  }

  public void setFrameTransformation(Transformation<Bitmap> frameTransformation,
      Bitmap firstFrame) {
    state.frameLoader.setFrameTransformation(frameTransformation, firstFrame);
  }

  public Transformation<Bitmap> getFrameTransformation() {
    return state.frameLoader.getFrameTransformation();
  }

  public ByteBuffer getBuffer() {
    return state.frameLoader.getBuffer();
  }

  public int getFrameCount() {
    return state.frameLoader.getFrameCount();
  }

  /**
   * Returns the current frame index in the range 0..{@link #getFrameCount()} - 1, or -1 if no frame
   * is displayed.
   */
  public int getFrameIndex() {
    return state.frameLoader.getCurrentIndex();
  }

  private void resetLoopCount() {
    loopCount = 0;
  }

  /**
   * Starts the animation from the first frame. Can only be called while animation is not running.
   */
  public void startFromFirstFrame() {
    Preconditions.checkArgument(!isRunning, "You cannot restart a currently running animation.");
    state.frameLoader.setNextStartFromFirstFrame();
    start();
  }

  @Override
  public void start() {
    isStarted = true;
    resetLoopCount();
    if (isVisible) {
      startRunning();
    }
  }

  @Override
  public void stop() {
    isStarted = false;
    stopRunning();
  }

  private void startRunning() {
    Preconditions.checkArgument(!isRecycled, "You cannot start a recycled Drawable. Ensure that"
        + "you clear any references to the Drawable when clearing the corresponding request.");
    // If we have only a single frame, we don't want to decode it endlessly.
    if (state.frameLoader.getFrameCount() == 1) {
      invalidateSelf();
    } else if (!isRunning) {
      isRunning = true;
      state.frameLoader.subscribe(this);
      invalidateSelf();
    }
  }

  private void stopRunning() {
    isRunning = false;
    state.frameLoader.unsubscribe(this);
  }

  @Override
  public boolean setVisible(boolean visible, boolean restart) {
    Preconditions.checkArgument(!isRecycled, "Cannot change the visibility of a recycled resource."
        + " Ensure that you unset the Drawable from your View before changing the View's"
        + " visibility.");
    isVisible = visible;
    if (!visible) {
      stopRunning();
    } else if (isStarted) {
      startRunning();
    }
    return super.setVisible(visible, restart);
  }

  @Override
  public int getIntrinsicWidth() {
    return state.frameLoader.getWidth();
  }

  @Override
  public int getIntrinsicHeight() {
    return state.frameLoader.getHeight();
  }

  @Override
  public boolean isRunning() {
    return isRunning;
  }

  // For testing.
  void setIsRunning(boolean isRunning) {
    this.isRunning = isRunning;
  }

  @Override
  protected void onBoundsChange(Rect bounds) {
    super.onBoundsChange(bounds);
    applyGravity = true;
  }

  @Override
  public void draw(Canvas canvas) {
    if (isRecycled) {
      return;
    }

    if (applyGravity) {
      Gravity.apply(GifState.GRAVITY, getIntrinsicWidth(), getIntrinsicHeight(), getBounds(),
          getDestRect());
      applyGravity = false;
    }

    Bitmap currentFrame = state.frameLoader.getCurrentFrame();
    canvas.drawBitmap(currentFrame, null, getDestRect(), getPaint());
  }

  @Override
  public void setAlpha(int i) {
    getPaint().setAlpha(i);
  }

  @Override
  public void setColorFilter(ColorFilter colorFilter) {
    getPaint().setColorFilter(colorFilter);
  }

  private Rect getDestRect() {
    if (destRect == null) {
      destRect = new Rect();
    }
    return destRect;
  }

  private Paint getPaint() {
    if (paint == null) {
      paint = new Paint(Paint.FILTER_BITMAP_FLAG);
    }
    return paint;
  }

  @Override
  public int getOpacity() {
    // We can't tell, so default to transparent to be safe.
    return PixelFormat.TRANSPARENT;
  }

  @Override
  public void onFrameReady() {
    if (getCallback() == null) {
      stop();
      invalidateSelf();
      return;
    }

    invalidateSelf();

    if (getFrameIndex() == getFrameCount() - 1) {
      loopCount++;
    }

    if (maxLoopCount != LOOP_FOREVER && loopCount >= maxLoopCount) {
      stop();
    }
  }

  @Override
  public ConstantState getConstantState() {
    return state;
  }

  /**
   * Clears any resources for loading frames that are currently held on to by this object.
   */
  public void recycle() {
    isRecycled = true;
    state.frameLoader.clear();
  }

  // For testing.
  boolean isRecycled() {
    return isRecycled;
  }

  public void setLoopCount(int loopCount) {
    if (loopCount <= 0 && loopCount != LOOP_FOREVER && loopCount != LOOP_INTRINSIC) {
      throw new IllegalArgumentException("Loop count must be greater than 0, or equal to "
          + "GlideDrawable.LOOP_FOREVER, or equal to GlideDrawable.LOOP_INTRINSIC");
    }

    if (loopCount == LOOP_INTRINSIC) {
      int intrinsicCount = state.frameLoader.getLoopCount();
      maxLoopCount =
          (intrinsicCount == TOTAL_ITERATION_COUNT_FOREVER) ? LOOP_FOREVER : intrinsicCount;
    } else {
      maxLoopCount = loopCount;
    }
  }

  static class GifState extends ConstantState {
    static final int GRAVITY = Gravity.FILL;
    final BitmapPool bitmapPool;
    final GifFrameLoader frameLoader;

    public GifState(BitmapPool bitmapPool, GifFrameLoader frameLoader) {
      this.bitmapPool = bitmapPool;
      this.frameLoader = frameLoader;
    }

    @Override
    public Drawable newDrawable(Resources res) {
      return newDrawable();
    }

    @Override
    public Drawable newDrawable() {
      return new GifDrawable(this);
    }

    @Override
    public int getChangingConfigurations() {
      return 0;
    }
  }
}
