/*
* Copyright (C) 2006 The Android Open Source Project
*
* Changes to accomodate repurposing for CircularProgressBarDrawable
* Copyright (C) 2014 Chiller Labs
*
* 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.chillerlabs.circularprogressbar;

import android.content.res.Resources;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.ColorFilter;
import android.graphics.Matrix;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.PixelFormat;
import android.graphics.Rect;
import android.graphics.RectF;
import android.graphics.SweepGradient;
import android.graphics.drawable.Drawable;

public class CircularProgressBarDrawable extends Drawable {

    private CircularProgressBarState mCircularProgressBarState;

    private final Paint mFillPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
    private ColorFilter mColorFilter;   // optional, set by the caller
    private int mAlpha = 0xFF;  // modified by the caller

    private final RectF mRect = new RectF();

    private boolean mRectIsDirty;   // internal state
    private boolean mMutated;
    private Path mRingPath;
    private boolean mPathIsDirty = true;
    private float startingAngle;

    public void setThicknessRatio(float thicknessRatio) {
        mCircularProgressBarState.mThicknessRatio = thicknessRatio;
    }

    public void setUseLevel(boolean useLevel) {
        mCircularProgressBarState.mUseLevel = useLevel;
    }

    public void setStartingAngle(float startingAngle) {
        this.startingAngle = startingAngle;
    }

    public CircularProgressBarDrawable() {
        this(new CircularProgressBarState((int[]) null));

        setUseLevel(true);
        setThicknessRatio(8);
        mRectIsDirty = true;
        invalidateSelf();
        setStartingAngle(90);
    }

    private int modulateAlpha(int alpha) {
        int scale = mAlpha + (mAlpha >> 7);
        return alpha * scale >> 8;
    }

    /**
     * <p>Sets the colors used to draw the gradient. Each color is specified as an
     * ARGB integer and the array must contain at least 2 colors.</p>
     * <p><strong>Note</strong>: changing orientation will affect all instances
     * of a drawable loaded from a resource. It is recommended to invoke
     * {@link #mutate()} before changing the orientation.</p>
     *
     * @param colors 2 or more ARGB colors
     *
     * @see #mutate()
     * @see #setColor(int)
     */
    public void setColors(int[] colors) {
        mCircularProgressBarState.setColors(colors);
        mRectIsDirty = true;
        invalidateSelf();
    }

    public void setColorResources(Resources resources, int... colorResources) {
        int[] colors = new int[colorResources.length];

        for (int i = 0; i < colorResources.length; i++) {
            colors[i] = resources.getColor(colorResources[i]);
        }

        setColors(colors);
    }

    @Override
    public void draw(Canvas canvas) {
        if (!ensureValidRect()) {
            // nothing to draw
            return;
        }

        // remember the alpha values, in case we temporarily overwrite them
        // when we modulate them with mAlpha
        final int prevFillAlpha = mFillPaint.getAlpha();

        // compute the modulate alpha values
        final int currFillAlpha = modulateAlpha(prevFillAlpha);

        final CircularProgressBarState st = mCircularProgressBarState;

        /*  Drawing with a layer is slower than direct drawing, but it
            allows us to apply paint effects like alpha and colorfilter to
            the result of multiple separate draws. In our case, if the user
            asks for a non-opaque alpha value (via setAlpha), and we're
            stroking, then we need to apply the alpha AFTER we've drawn
            both the fill and the stroke.
        */
        /*  since we're not using a layer, apply the dither/filter to our
            individual paints
        */

        mFillPaint.setAlpha(currFillAlpha);
        mFillPaint.setColorFilter(mColorFilter);
        if (mColorFilter != null && !mCircularProgressBarState.mHasSolidColor) {
            mFillPaint.setColor(mAlpha << 24);
        }

        Path path = buildRing(st);
        canvas.drawPath(path, mFillPaint);

        mFillPaint.setAlpha(prevFillAlpha);
    }

    private Path buildRing(CircularProgressBarState st) {
        if (mRingPath != null && (!st.mUseLevel || !mPathIsDirty)) return mRingPath;
        mPathIsDirty = false;

        float sweep = st.mUseLevel ? (360.0f * getLevel() / 10000.0f) : 360f;

        RectF bounds = new RectF(mRect);

        float x = bounds.width() / 2.0f;
        float y = bounds.height() / 2.0f;

        float thickness = bounds.width() / st.mThicknessRatio;
        float innerRadius = (bounds.width() / 2) - thickness;

        RectF innerBounds = new RectF(bounds);
        innerBounds.inset(x - innerRadius, y - innerRadius);

        bounds = new RectF(innerBounds);
        bounds.inset(-thickness, -thickness);

        if (mRingPath == null) {
            mRingPath = new Path();
        } else {
            mRingPath.reset();
        }

        final Path ringPath = mRingPath;
        // arcTo treats the sweep angle mod 360, so check for that, since we
        // think 360 means draw the entire oval
        if (Math.abs(sweep) < 360) {
            ringPath.setFillType(Path.FillType.EVEN_ODD);

            double startingAngleRadians = Math.toRadians(startingAngle);
            final float startingAngleCosine = (float) Math.cos(startingAngleRadians);
            final float startingAngleSine = (float) Math.sin(startingAngleRadians) * -1;

            // inner top
            final float innerX = startingAngleCosine * innerRadius + x;
            final float innerY = startingAngleSine * innerRadius + y;
            ringPath.moveTo(innerX, innerY);

            // outer top
            final float outerX = startingAngleCosine * (innerRadius + thickness) + x;
            final float outerY = startingAngleSine * (innerRadius + thickness) + y;
            ringPath.lineTo(outerX, outerY);

            // outer arc
            ringPath.arcTo(bounds, -startingAngle, -sweep, false);

            // inner arc
            ringPath.arcTo(innerBounds, -startingAngle - sweep, sweep, false);
            ringPath.close();

        } else {
            // add the entire ovals
            ringPath.addOval(bounds, Path.Direction.CW);
            ringPath.addOval(innerBounds, Path.Direction.CCW);
        }

        return ringPath;
    }

    /**
     * <p>Changes this drawbale to use a single color instead of a gradient.</p>
     * <p><strong>Note</strong>: changing color will affect all instances
     * of a drawable loaded from a resource. It is recommended to invoke
     * {@link #mutate()} before changing the color.</p>
     *
     * @param argb The color used to fill the shape
     *
     * @see #mutate()
     * @see #setColors(int[])
     */
    public void setColor(int argb) {
        mCircularProgressBarState.setSolidColor(argb);
        mFillPaint.setColor(argb);
        invalidateSelf();
    }

    public void setColorResource(Resources resources, int color) {
        setColor(resources.getColor(color));
    }

    @Override
    public int getChangingConfigurations() {
        return super.getChangingConfigurations() | mCircularProgressBarState.mChangingConfigurations;
    }

    @Override
    public void setAlpha(int alpha) {
        if (alpha != mAlpha) {
            mAlpha = alpha;
            invalidateSelf();
        }
    }

    @Override
    public int getAlpha() {
        return mAlpha;
    }

    @Override
    public void setColorFilter(ColorFilter cf) {
        if (cf != mColorFilter) {
            mColorFilter = cf;
            invalidateSelf();
        }
    }

    @Override
    public int getOpacity() {
        return PixelFormat.TRANSLUCENT;
    }

    @Override
    protected void onBoundsChange(Rect r) {
        super.onBoundsChange(r);
        mRingPath = null;
        mPathIsDirty = true;
        mRectIsDirty = true;
    }

    @Override
    protected boolean onLevelChange(int level) {
        super.onLevelChange(level);
        mRectIsDirty = true;
        mPathIsDirty = true;
        invalidateSelf();
        return true;
    }

    /**
     * This checks mRectIsDirty, and if it is true, recomputes both our drawing
     * rectangle (mRect) and the gradient itself, since it depends on our
     * rectangle too.
     * @return true if the resulting rectangle is not empty, false otherwise
     */
    private boolean ensureValidRect() {
        if (mRectIsDirty) {
            mRectIsDirty = false;

            Rect bounds = getBounds();
            float inset = 0;

            final CircularProgressBarState st = mCircularProgressBarState;

            mRect.set(bounds.left + inset, bounds.top + inset,
                    bounds.right - inset, bounds.bottom - inset);

            final int[] colors = st.mColors;
            if (colors != null) {
                RectF r = mRect;
                float x0, y0;

                x0 = r.left + (r.right - r.left) * st.mCenterX;
                y0 = r.top + (r.bottom - r.top) * st.mCenterY;

                final SweepGradient sweepGradient = new SweepGradient(x0, y0, colors, null);
                Matrix flipMatrix = new Matrix();
                flipMatrix.setScale(1, -1);
                flipMatrix.postTranslate(0, (r.bottom - r.top));
                flipMatrix.postRotate(-startingAngle, x0, y0);
                sweepGradient.setLocalMatrix(flipMatrix);
                mFillPaint.setShader(sweepGradient);

                // If we don't have a solid color, the alpha channel must be
                // maxed out so that alpha modulation works correctly.
                if (!st.mHasSolidColor) {
                    mFillPaint.setColor(Color.BLACK);
                }
            }
        }
        return !mRect.isEmpty();
    }

    @Override
    public int getIntrinsicWidth() {
        return mCircularProgressBarState.mWidth;
    }

    @Override
    public int getIntrinsicHeight() {
        return mCircularProgressBarState.mHeight;
    }

    @Override
    public ConstantState getConstantState() {
        mCircularProgressBarState.mChangingConfigurations = getChangingConfigurations();
        return mCircularProgressBarState;
    }

    @Override
    public Drawable mutate() {
        if (!mMutated && super.mutate() == this) {
            mCircularProgressBarState = new CircularProgressBarState(mCircularProgressBarState);
            initializeWithState(mCircularProgressBarState);
            mMutated = true;
        }
        return this;
    }

    public static class CircularProgressBarState extends ConstantState {
        public int mChangingConfigurations;
        public int[] mColors;
        public float[] mPositions;
        public boolean mHasSolidColor;
        public int mSolidColor;
        public Rect mPadding;
        public int mWidth = -1;
        public int mHeight = -1;
        public float mThicknessRatio;
        private float mCenterX = 0.5f;
        private float mCenterY = 0.5f;
        private float mGradientRadius = 0.5f;
        private boolean mUseLevel;

        CircularProgressBarState(int[] colors) {
            setColors(colors);
        }

        public CircularProgressBarState(CircularProgressBarState state) {
            mChangingConfigurations = state.mChangingConfigurations;
            if (state.mColors != null) {
                mColors = state.mColors.clone();
            }
            if (state.mPositions != null) {
                mPositions = state.mPositions.clone();
            }
            mHasSolidColor = state.mHasSolidColor;
            mSolidColor = state.mSolidColor;

            if (state.mPadding != null) {
                mPadding = new Rect(state.mPadding);
            }
            mWidth = state.mWidth;
            mHeight = state.mHeight;
            mThicknessRatio = state.mThicknessRatio;
            mCenterX = state.mCenterX;
            mCenterY = state.mCenterY;
            mGradientRadius = state.mGradientRadius;
            mUseLevel = state.mUseLevel;
        }

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

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

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

        public void setColors(int[] colors) {
            mHasSolidColor = false;
            mColors = colors;
        }

        public void setSolidColor(int argb) {
            mHasSolidColor = true;
            mSolidColor = argb;
            mColors = null;
        }
    }

    private CircularProgressBarDrawable(CircularProgressBarState state) {
        mCircularProgressBarState = state;
        initializeWithState(state);
        mRectIsDirty = true;
        mMutated = false;
    }

    private void initializeWithState(CircularProgressBarState state) {
        if (state.mHasSolidColor) {
            mFillPaint.setColor(state.mSolidColor);
        } else if (state.mColors == null) {
            // If we don't have a solid color and we don't have a gradient,
            // the app is stroking the shape, set the color to the default
            // value of state.mSolidColor
            mFillPaint.setColor(0);
        } else {
            // Otherwise, make sure the fill alpha is maxed out.
            mFillPaint.setColor(Color.BLACK);
        }

    }
}