/*
 * 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.camera;

import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.AnimatorSet;
import android.animation.ValueAnimator;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Matrix;
import android.graphics.drawable.Drawable;
import android.os.AsyncTask;
import android.util.AttributeSet;
import android.view.View;
import android.widget.ImageButton;
import android.widget.ImageView;

import com.android.camera.util.Gusterpolator;
import com.android.camera2.R;

/*
 * A toggle button that supports two or more states with images rendererd on top
 * for each state.
 * The button is initialized in an XML layout file with an array reference of
 * image ids (e.g. imageIds="@array/camera_flashmode_icons").
 * Each image in the referenced array represents a single integer state.
 * Every time the user touches the button it gets set to next state in line,
 * with the corresponding image drawn onto the face of the button.
 * State wraps back to 0 on user touch when button is already at n-1 state.
 */
public class MultiToggleImageButton extends ImageButton {
    /*
     * Listener interface for button state changes.
     */
    public interface OnStateChangeListener {
        /*
         * @param view the MultiToggleImageButton that received the touch event
         * @param state the new state the button is in
         */
        public abstract void stateChanged(View view, int state);
    }

    public static final int ANIM_DIRECTION_VERTICAL = 0;
    public static final int ANIM_DIRECTION_HORIZONTAL = 1;

    private static final int ANIM_DURATION_MS = 250;
    private static final int UNSET = -1;

    private OnStateChangeListener mOnStateChangeListener;
    private OnStateChangeListener mOnStatePreChangeListener;
    private int mState = UNSET;
    private int[] mImageIds;
    private int[] mDescIds;
    private int mLevel;
    private boolean mClickEnabled = true;
    private int mParentSize;
    private int mAnimDirection;
    private Matrix mMatrix = new Matrix();
    private ValueAnimator mAnimator;

    public MultiToggleImageButton(Context context) {
        super(context);
        init();
    }

    public MultiToggleImageButton(Context context, AttributeSet attrs) {
        super(context, attrs);
        init();
        parseAttributes(context, attrs);
        setState(0);
    }

    public MultiToggleImageButton(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
        init();
        parseAttributes(context, attrs);
        setState(0);
    }

    /*
     * Set the state change listener.
     *
     * @param onStateChangeListener The listener to set.
     */
    public void setOnStateChangeListener(OnStateChangeListener onStateChangeListener) {
        mOnStateChangeListener = onStateChangeListener;
    }

    /**
     * Set the listener that will be invoked right after the click event before
     * all the operations required to change the state of the button.  This
     * listener is useful if the client doesn't want to wait until the state
     * change is completed to perform certain tasks.
     *
     * @param onStatePreChangeListener The listener to set.
     */
    public void setOnPreChangeListener(OnStateChangeListener onStatePreChangeListener) {
        mOnStatePreChangeListener = onStatePreChangeListener;
    }

    /*
     * Get the current button state.
     *
     */
    public int getState() {
        return mState;
    }

    /*
     * Set the current button state, thus causing the state change listener to
     * get called.
     *
     * @param state the desired state
     */
    public void setState(int state) {
        setState(state, true);
    }

    /*
     * Set the current button state.
     *
     * @param state the desired state
     * @param callListener should the state change listener be called?
     */
    public void setState(final int state, final boolean callListener) {
        setStateAnimatedInternal(state, callListener);
    }

    /**
     * Set the current button state via an animated transition.
     *
     * @param state
     * @param callListener
     */
    private void setStateAnimatedInternal(final int state, final boolean callListener) {
        if(callListener && mOnStatePreChangeListener != null) {
            mOnStatePreChangeListener.stateChanged(MultiToggleImageButton.this, mState);
        }

        if (mState == state || mState == UNSET) {
            setStateInternal(state, callListener);
            return;
        }

        if (mImageIds == null) {
            return;
        }

        new AsyncTask<Integer, Void, Bitmap>() {
            @Override
            protected Bitmap doInBackground(Integer... params) {
                return combine(params[0], params[1]);
            }

            @Override
            protected void onPostExecute(Bitmap bitmap) {
                if (bitmap == null) {
                    setStateInternal(state, callListener);
                } else {
                    setImageBitmap(bitmap);

                    int offset;
                    if (mAnimDirection == ANIM_DIRECTION_VERTICAL) {
                        offset = (mParentSize+getHeight())/2;
                    } else if (mAnimDirection == ANIM_DIRECTION_HORIZONTAL) {
                        offset = (mParentSize+getWidth())/2;
                    } else {
                        return;
                    }

                    mAnimator.setFloatValues(-offset, 0.0f);
                    AnimatorSet s = new AnimatorSet();
                    s.play(mAnimator);
                    s.addListener(new AnimatorListenerAdapter() {
                        @Override
                        public void onAnimationStart(Animator animation) {
                            setClickEnabled(false);
                        }

                        @Override
                        public void onAnimationEnd(Animator animation) {
                            setStateInternal(state, callListener);
                            setClickEnabled(true);
                        }
                    });
                    s.start();
                }
            }
        }.execute(mState, state);
    }

    /**
     * Enable or disable click reactions for this button
     * without affecting visual state.
     * For most cases you'll want to use {@link #setEnabled(boolean)}.
     * @param enabled True if click enabled, false otherwise.
     */
    public void setClickEnabled(boolean enabled) {
        mClickEnabled = enabled;
    }

    private void setStateInternal(int state, boolean callListener) {
        mState = state;
        if (mImageIds != null) {
            setImageByState(mState);
        }

        if (mDescIds != null) {
            String oldContentDescription = String.valueOf(getContentDescription());
            String newContentDescription = getResources().getString(mDescIds[mState]);
            if (oldContentDescription != null && !oldContentDescription.isEmpty()
                    && !oldContentDescription.equals(newContentDescription)) {
                setContentDescription(newContentDescription);
                String announceChange = getResources().getString(
                    R.string.button_change_announcement, newContentDescription);
                announceForAccessibility(announceChange);
            }
        }
        super.setImageLevel(mLevel);

        if (callListener && mOnStateChangeListener != null) {
            mOnStateChangeListener.stateChanged(MultiToggleImageButton.this, getState());
        }
    }

    private void nextState() {
        int state = mState + 1;
        if (state >= mImageIds.length) {
            state = 0;
        }
        setState(state);
    }

    protected void init() {
        this.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                if (mClickEnabled) {
                    nextState();
                }
            }
        });
        setScaleType(ImageView.ScaleType.MATRIX);

        mAnimator = ValueAnimator.ofFloat(0.0f, 0.0f);
        mAnimator.setDuration(ANIM_DURATION_MS);
        mAnimator.setInterpolator(Gusterpolator.INSTANCE);
        mAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                mMatrix.reset();
                if (mAnimDirection == ANIM_DIRECTION_VERTICAL) {
                    mMatrix.setTranslate(0.0f, (Float) animation.getAnimatedValue());
                } else if (mAnimDirection == ANIM_DIRECTION_HORIZONTAL) {
                    mMatrix.setTranslate((Float) animation.getAnimatedValue(), 0.0f);
                }

                setImageMatrix(mMatrix);
                invalidate();
            }
        });
    }

    private void parseAttributes(Context context, AttributeSet attrs) {
        TypedArray a = context.getTheme().obtainStyledAttributes(
            attrs,
            R.styleable.MultiToggleImageButton,
            0, 0);
        int imageIds = a.getResourceId(R.styleable.MultiToggleImageButton_imageIds, 0);
        if (imageIds > 0) {
            overrideImageIds(imageIds);
        }
        int descIds = a.getResourceId(R.styleable.MultiToggleImageButton_contentDescriptionIds, 0);
        if (descIds > 0) {
            overrideContentDescriptions(descIds);
        }
        a.recycle();
    }

    /**
     * Override the image ids of this button.
     */
    public void overrideImageIds(int resId) {
        TypedArray ids = null;
        try {
            ids = getResources().obtainTypedArray(resId);
            mImageIds = new int[ids.length()];
            for (int i = 0; i < ids.length(); i++) {
                mImageIds[i] = ids.getResourceId(i, 0);
            }
        } finally {
            if (ids != null) {
                ids.recycle();
            }
        }

        if (mState >= 0 && mState < mImageIds.length) {
            setImageByState(mState);
        }
    }

    /**
     * Override the content descriptions of this button.
     */
    public void overrideContentDescriptions(int resId) {
        TypedArray ids = null;
        try {
            ids = getResources().obtainTypedArray(resId);
            mDescIds = new int[ids.length()];
            for (int i = 0; i < ids.length(); i++) {
                mDescIds[i] = ids.getResourceId(i, 0);
            }
        } finally {
            if (ids != null) {
                ids.recycle();
            }
        }
    }

    /**
     * Set size info (either width or height, as necessary) of the view containing
     * this button. Used for offset calculations during animation.
     * @param s The size.
     */
    public void setParentSize(int s) {
        mParentSize = s;
    }

    /**
     * Set the animation direction.
     * @param d Either ANIM_DIRECTION_VERTICAL or ANIM_DIRECTION_HORIZONTAL.
     */
    public void setAnimDirection(int d) {
        mAnimDirection = d;
    }

    @Override
    public void setImageLevel(int level) {
        super.setImageLevel(level);
        mLevel = level;
    }

    private void setImageByState(int state) {
        if (mImageIds != null) {
            setImageResource(mImageIds[state]);
        }
        super.setImageLevel(mLevel);
    }

    private Bitmap combine(int oldState, int newState) {
        // In some cases, a new set of image Ids are set via overrideImageIds()
        // and oldState or newState overrun the array.
        // check here for that.
        if (oldState >= mImageIds.length || newState >= mImageIds.length) {
            return null;
        }

        int width = getWidth();
        int height = getHeight();

        if (width <= 0 || height <= 0) {
            return null;
        }

        int[] enabledState = new int[] {android.R.attr.state_enabled};

        // new state
        Drawable newDrawable = getResources().getDrawable(mImageIds[newState]).mutate();
        newDrawable.setState(enabledState);

        // old state
        Drawable oldDrawable = getResources().getDrawable(mImageIds[oldState]).mutate();
        oldDrawable.setState(enabledState);

        // combine 'em
        Bitmap bitmap = null;
        if (mAnimDirection == ANIM_DIRECTION_VERTICAL) {
            int bitmapHeight = (height*2) + ((mParentSize - height)/2);
            int oldBitmapOffset = height + ((mParentSize - height)/2);
            bitmap = Bitmap.createBitmap(width, bitmapHeight, Bitmap.Config.ARGB_8888);
            Canvas canvas = new Canvas(bitmap);
            newDrawable.setBounds(0, 0, newDrawable.getIntrinsicWidth(), newDrawable.getIntrinsicHeight());
            oldDrawable.setBounds(0, oldBitmapOffset, oldDrawable.getIntrinsicWidth(), oldDrawable.getIntrinsicHeight()+oldBitmapOffset);
            newDrawable.draw(canvas);
            oldDrawable.draw(canvas);
        } else if (mAnimDirection == ANIM_DIRECTION_HORIZONTAL) {
            int bitmapWidth = (width*2) + ((mParentSize - width)/2);
            int oldBitmapOffset = width + ((mParentSize - width)/2);
            bitmap = Bitmap.createBitmap(bitmapWidth, height, Bitmap.Config.ARGB_8888);
            Canvas canvas = new Canvas(bitmap);
            newDrawable.setBounds(0, 0, newDrawable.getIntrinsicWidth(), newDrawable.getIntrinsicHeight());
            oldDrawable.setBounds(oldBitmapOffset, 0, oldDrawable.getIntrinsicWidth()+oldBitmapOffset, oldDrawable.getIntrinsicHeight());
            newDrawable.draw(canvas);
            oldDrawable.draw(canvas);
        }

        return bitmap;
    }
}