/*
 * Copyright (C) 2013 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.android.mail.bitmap;

import android.animation.ValueAnimator;
import android.animation.ValueAnimator.AnimatorUpdateListener;
import android.graphics.Canvas;
import android.graphics.ColorFilter;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;

import com.android.mail.utils.LogUtils;

/**
 * A drawable that wraps two other drawables and allows flipping between them. The flipping
 * animation is a 2D rotation around the y axis.
 *
 * <p/>
 * The 3 durations are: (best viewed in documentation form)
 * <pre>
 * &lt;pre&gt;[_][]|[][_]&lt;post&gt;
 *   |       |       |
 *   V       V       V
 * &lt;pre>&lt;   flip  &gt;&lt;post&gt;
 * </pre>
 */
public class FlipDrawable extends Drawable implements Drawable.Callback {

    /**
     * The inner drawables.
     */
    protected final Drawable mFront;
    protected final Drawable mBack;

    protected final int mFlipDurationMs;
    protected final int mPreFlipDurationMs;
    protected final int mPostFlipDurationMs;
    private final ValueAnimator mFlipAnimator;

    private static final float END_VALUE = 2f;

    /**
     * From 0f to END_VALUE. Determines the flip progress between mFront and mBack. 0f means
     * mFront is fully shown, while END_VALUE means mBack is fully shown.
     */
    private float mFlipFraction = 0f;

    /**
     * True if flipping towards front, false if flipping towards back.
     */
    private boolean mFlipToSide = true;

    /**
     * Create a new FlipDrawable. The front is fully shown by default.
     *
     * <p/>
     * The 3 durations are: (best viewed in documentation form)
     * <pre>
     * &lt;pre&gt;[_][]|[][_]&lt;post&gt;
     *   |       |       |
     *   V       V       V
     * &lt;pre>&lt;   flip  &gt;&lt;post&gt;
     * </pre>
     *
     * @param front              The front drawable.
     * @param back               The back drawable.
     * @param flipDurationMs     The duration of the actual flip. This duration includes both
     *                           animating away one side and showing the other.
     * @param preFlipDurationMs  The duration before the actual flip begins. Subclasses can use this
     *                           to add flourish.
     * @param postFlipDurationMs The duration after the actual flip begins. Subclasses can use this
     *                           to add flourish.
     */
    public FlipDrawable(final Drawable front, final Drawable back, final int flipDurationMs,
            final int preFlipDurationMs, final int postFlipDurationMs) {
        if (front == null || back == null) {
            throw new IllegalArgumentException("Front and back drawables must not be null.");
        }
        mFront = front;
        mBack = back;

        mFront.setCallback(this);
        mBack.setCallback(this);

        mFlipDurationMs = flipDurationMs;
        mPreFlipDurationMs = preFlipDurationMs;
        mPostFlipDurationMs = postFlipDurationMs;

        mFlipAnimator = ValueAnimator.ofFloat(0f, END_VALUE)
                .setDuration(mPreFlipDurationMs + mFlipDurationMs + mPostFlipDurationMs);
        mFlipAnimator.addUpdateListener(new AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(final ValueAnimator animation) {
                final float old = mFlipFraction;
                //noinspection ConstantConditions
                mFlipFraction = (Float) animation.getAnimatedValue();
                if (old != mFlipFraction) {
                    invalidateSelf();
                }
            }
        });

        reset(true);
    }

    @Override
    protected void onBoundsChange(final Rect bounds) {
        super.onBoundsChange(bounds);
        if (bounds.isEmpty()) {
            mFront.setBounds(0, 0, 0, 0);
            mBack.setBounds(0, 0, 0, 0);
        } else {
            mFront.setBounds(bounds);
            mBack.setBounds(bounds);
        }
    }

    @Override
    public void draw(final Canvas canvas) {
        final Rect bounds = getBounds();
        if (!isVisible() || bounds.isEmpty()) {
            return;
        }

        final Drawable inner = getSideShown() /* == front */ ? mFront : mBack;

        final float totalDurationMs = mPreFlipDurationMs + mFlipDurationMs + mPostFlipDurationMs;

        final float scaleX;
        if (mFlipFraction / 2 <= mPreFlipDurationMs / totalDurationMs) {
            // During pre-flip.
            scaleX = 1;
        } else if (mFlipFraction / 2 >= (totalDurationMs - mPostFlipDurationMs) / totalDurationMs) {
            // During post-flip.
            scaleX = 1;
        } else {
            // During flip.
            final float flipFraction = mFlipFraction / 2;
            final float flipMiddle = (mPreFlipDurationMs / totalDurationMs
                    + (totalDurationMs - mPostFlipDurationMs) / totalDurationMs) / 2;
            final float distFraction = Math.abs(flipFraction - flipMiddle);
            final float multiplier = 1 / (flipMiddle - (mPreFlipDurationMs / totalDurationMs));
            scaleX = distFraction * multiplier;
        }

        canvas.save();
        // The flip is a simple 1 dimensional scale.
        canvas.scale(scaleX, 1, bounds.exactCenterX(), bounds.exactCenterY());
        inner.draw(canvas);
        canvas.restore();
    }

    @Override
    public void setAlpha(final int alpha) {
        mFront.setAlpha(alpha);
        mBack.setAlpha(alpha);
    }

    @Override
    public void setColorFilter(final ColorFilter cf) {
        mFront.setColorFilter(cf);
        mBack.setColorFilter(cf);
    }

    @Override
    public int getOpacity() {
        return resolveOpacity(mFront.getOpacity(), mBack.getOpacity());
    }

    @Override
    protected boolean onLevelChange(final int level) {
        return mFront.setLevel(level) || mBack.setLevel(level);
    }

    @Override
    public void invalidateDrawable(final Drawable who) {
        invalidateSelf();
    }

    @Override
    public void scheduleDrawable(final Drawable who, final Runnable what, final long when) {
        scheduleSelf(what, when);
    }

    @Override
    public void unscheduleDrawable(final Drawable who, final Runnable what) {
        unscheduleSelf(what);
    }

    /**
     * Stop animating the flip and reset to one side.
     * @param side Pass true if reset to front, false if reset to back.
     */
    public void reset(final boolean side) {
        final float old = mFlipFraction;
        mFlipAnimator.cancel();
        mFlipFraction = side ? 0f : 2f;
        mFlipToSide = side;
        if (mFlipFraction != old) {
            invalidateSelf();
        }
    }

    /**
     * Returns true if the front is shown. Returns false if the back is shown.
     */
    public boolean getSideShown() {
        final float totalDurationMs = mPreFlipDurationMs + mFlipDurationMs + mPostFlipDurationMs;
        final float middleFraction = (mPreFlipDurationMs / totalDurationMs
                + (totalDurationMs - mPostFlipDurationMs) / totalDurationMs) / 2;
        return mFlipFraction / 2 < middleFraction;
    }

    /**
     * Returns true if the front is being flipped towards. Returns false if the back is being
     * flipped towards.
     */
    public boolean getSideFlippingTowards() {
        return mFlipToSide;
    }

    /**
     * Starts an animated flip to the other side. If a flip animation is currently started,
     * it will be reversed.
     */
    public void flip() {
        mFlipToSide = !mFlipToSide;
        if (mFlipAnimator.isStarted()) {
            mFlipAnimator.reverse();
        } else {
            if (!mFlipToSide /* front to back */) {
                mFlipAnimator.start();
            } else /* back to front */ {
                mFlipAnimator.reverse();
            }
        }
    }

    /**
     * Start an animated flip to a side. This works regardless of whether a flip animation is
     * currently started.
     * @param side Pass true if flip to front, false if flip to back.
     */
    public void flipTo(final boolean side) {
        if (mFlipToSide != side) {
            flip();
        }
    }

    /**
     * Returns whether flipping is in progress.
     */
    public boolean isFlipping() {
        return mFlipAnimator.isStarted();
    }
}
