/*
 * Copyright (C) 2014 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 android.support.design.widget;

import android.content.res.Resources;
import android.graphics.Canvas;
import android.graphics.LinearGradient;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.PixelFormat;
import android.graphics.RadialGradient;
import android.graphics.Rect;
import android.graphics.RectF;
import android.graphics.Shader;
import android.graphics.drawable.Drawable;
import android.support.design.R;
import android.support.v7.graphics.drawable.DrawableWrapper;

/**
 * A {@link android.graphics.drawable.Drawable} which wraps another drawable and
 * draws a shadow around it.
 */
class ShadowDrawableWrapper extends DrawableWrapper {
    // used to calculate content padding
    static final double COS_45 = Math.cos(Math.toRadians(45));

    static final float SHADOW_MULTIPLIER = 1.5f;

    static final float SHADOW_TOP_SCALE = 0.25f;
    static final float SHADOW_HORIZ_SCALE = 0.5f;
    static final float SHADOW_BOTTOM_SCALE = 1f;

    final Paint mCornerShadowPaint;
    final Paint mEdgeShadowPaint;

    final RectF mContentBounds;

    float mCornerRadius;

    Path mCornerShadowPath;

    // updated value with inset
    float mMaxShadowSize;
    // actual value set by developer
    float mRawMaxShadowSize;

    // multiplied value to account for shadow offset
    float mShadowSize;
    // actual value set by developer
    float mRawShadowSize;

    private boolean mDirty = true;

    private final int mShadowStartColor;
    private final int mShadowMiddleColor;
    private final int mShadowEndColor;

    private boolean mAddPaddingForCorners = true;

    private float mRotation;

    /**
     * If shadow size is set to a value above max shadow, we print a warning
     */
    private boolean mPrintedShadowClipWarning = false;

    public ShadowDrawableWrapper(Resources resources, Drawable content, float radius,
            float shadowSize, float maxShadowSize) {
        super(content);

        mShadowStartColor = resources.getColor(R.color.design_fab_shadow_start_color);
        mShadowMiddleColor = resources.getColor(R.color.design_fab_shadow_mid_color);
        mShadowEndColor = resources.getColor(R.color.design_fab_shadow_end_color);

        mCornerShadowPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG);
        mCornerShadowPaint.setStyle(Paint.Style.FILL);
        mCornerRadius = Math.round(radius);
        mContentBounds = new RectF();
        mEdgeShadowPaint = new Paint(mCornerShadowPaint);
        mEdgeShadowPaint.setAntiAlias(false);
        setShadowSize(shadowSize, maxShadowSize);
    }

    /**
     * Casts the value to an even integer.
     */
    private static int toEven(float value) {
        int i = Math.round(value);
        return (i % 2 == 1) ? i - 1 : i;
    }

    public void setAddPaddingForCorners(boolean addPaddingForCorners) {
        mAddPaddingForCorners = addPaddingForCorners;
        invalidateSelf();
    }

    @Override
    public void setAlpha(int alpha) {
        super.setAlpha(alpha);
        mCornerShadowPaint.setAlpha(alpha);
        mEdgeShadowPaint.setAlpha(alpha);
    }

    @Override
    protected void onBoundsChange(Rect bounds) {
        mDirty = true;
    }

    void setShadowSize(float shadowSize, float maxShadowSize) {
        if (shadowSize < 0 || maxShadowSize < 0) {
            throw new IllegalArgumentException("invalid shadow size");
        }
        shadowSize = toEven(shadowSize);
        maxShadowSize = toEven(maxShadowSize);
        if (shadowSize > maxShadowSize) {
            shadowSize = maxShadowSize;
            if (!mPrintedShadowClipWarning) {
                mPrintedShadowClipWarning = true;
            }
        }
        if (mRawShadowSize == shadowSize && mRawMaxShadowSize == maxShadowSize) {
            return;
        }
        mRawShadowSize = shadowSize;
        mRawMaxShadowSize = maxShadowSize;
        mShadowSize = Math.round(shadowSize * SHADOW_MULTIPLIER);
        mMaxShadowSize = maxShadowSize;
        mDirty = true;
        invalidateSelf();
    }

    @Override
    public boolean getPadding(Rect padding) {
        int vOffset = (int) Math.ceil(calculateVerticalPadding(mRawMaxShadowSize, mCornerRadius,
                mAddPaddingForCorners));
        int hOffset = (int) Math.ceil(calculateHorizontalPadding(mRawMaxShadowSize, mCornerRadius,
                mAddPaddingForCorners));
        padding.set(hOffset, vOffset, hOffset, vOffset);
        return true;
    }

    public static float calculateVerticalPadding(float maxShadowSize, float cornerRadius,
            boolean addPaddingForCorners) {
        if (addPaddingForCorners) {
            return (float) (maxShadowSize * SHADOW_MULTIPLIER + (1 - COS_45) * cornerRadius);
        } else {
            return maxShadowSize * SHADOW_MULTIPLIER;
        }
    }

    public static float calculateHorizontalPadding(float maxShadowSize, float cornerRadius,
            boolean addPaddingForCorners) {
        if (addPaddingForCorners) {
            return (float) (maxShadowSize + (1 - COS_45) * cornerRadius);
        } else {
            return maxShadowSize;
        }
    }

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

    public void setCornerRadius(float radius) {
        radius = Math.round(radius);
        if (mCornerRadius == radius) {
            return;
        }
        mCornerRadius = radius;
        mDirty = true;
        invalidateSelf();
    }

    @Override
    public void draw(Canvas canvas) {
        if (mDirty) {
            buildComponents(getBounds());
            mDirty = false;
        }
        drawShadow(canvas);

        super.draw(canvas);
    }

    final void setRotation(float rotation) {
        if (mRotation != rotation) {
            mRotation = rotation;
            invalidateSelf();
        }
    }

    private void drawShadow(Canvas canvas) {
        final int rotateSaved = canvas.save();
        canvas.rotate(mRotation, mContentBounds.centerX(), mContentBounds.centerY());

        final float edgeShadowTop = -mCornerRadius - mShadowSize;
        final float shadowOffset = mCornerRadius;
        final boolean drawHorizontalEdges = mContentBounds.width() - 2 * shadowOffset > 0;
        final boolean drawVerticalEdges = mContentBounds.height() - 2 * shadowOffset > 0;

        final float shadowOffsetTop = mRawShadowSize - (mRawShadowSize * SHADOW_TOP_SCALE);
        final float shadowOffsetHorizontal = mRawShadowSize - (mRawShadowSize * SHADOW_HORIZ_SCALE);
        final float shadowOffsetBottom = mRawShadowSize - (mRawShadowSize * SHADOW_BOTTOM_SCALE);

        final float shadowScaleHorizontal = shadowOffset / (shadowOffset + shadowOffsetHorizontal);
        final float shadowScaleTop = shadowOffset / (shadowOffset + shadowOffsetTop);
        final float shadowScaleBottom = shadowOffset / (shadowOffset + shadowOffsetBottom);

        // LT
        int saved = canvas.save();
        canvas.translate(mContentBounds.left + shadowOffset, mContentBounds.top + shadowOffset);
        canvas.scale(shadowScaleHorizontal, shadowScaleTop);
        canvas.drawPath(mCornerShadowPath, mCornerShadowPaint);
        if (drawHorizontalEdges) {
            // TE
            canvas.scale(1f / shadowScaleHorizontal, 1f);
            canvas.drawRect(0, edgeShadowTop,
                    mContentBounds.width() - 2 * shadowOffset, -mCornerRadius,
                    mEdgeShadowPaint);
        }
        canvas.restoreToCount(saved);
        // RB
        saved = canvas.save();
        canvas.translate(mContentBounds.right - shadowOffset, mContentBounds.bottom - shadowOffset);
        canvas.scale(shadowScaleHorizontal, shadowScaleBottom);
        canvas.rotate(180f);
        canvas.drawPath(mCornerShadowPath, mCornerShadowPaint);
        if (drawHorizontalEdges) {
            // BE
            canvas.scale(1f / shadowScaleHorizontal, 1f);
            canvas.drawRect(0, edgeShadowTop,
                    mContentBounds.width() - 2 * shadowOffset, -mCornerRadius + mShadowSize,
                    mEdgeShadowPaint);
        }
        canvas.restoreToCount(saved);
        // LB
        saved = canvas.save();
        canvas.translate(mContentBounds.left + shadowOffset, mContentBounds.bottom - shadowOffset);
        canvas.scale(shadowScaleHorizontal, shadowScaleBottom);
        canvas.rotate(270f);
        canvas.drawPath(mCornerShadowPath, mCornerShadowPaint);
        if (drawVerticalEdges) {
            // LE
            canvas.scale(1f / shadowScaleBottom, 1f);
            canvas.drawRect(0, edgeShadowTop,
                    mContentBounds.height() - 2 * shadowOffset, -mCornerRadius, mEdgeShadowPaint);
        }
        canvas.restoreToCount(saved);
        // RT
        saved = canvas.save();
        canvas.translate(mContentBounds.right - shadowOffset, mContentBounds.top + shadowOffset);
        canvas.scale(shadowScaleHorizontal, shadowScaleTop);
        canvas.rotate(90f);
        canvas.drawPath(mCornerShadowPath, mCornerShadowPaint);
        if (drawVerticalEdges) {
            // RE
            canvas.scale(1f / shadowScaleTop, 1f);
            canvas.drawRect(0, edgeShadowTop,
                    mContentBounds.height() - 2 * shadowOffset, -mCornerRadius, mEdgeShadowPaint);
        }
        canvas.restoreToCount(saved);

        canvas.restoreToCount(rotateSaved);
    }

    private void buildShadowCorners() {
        RectF innerBounds = new RectF(-mCornerRadius, -mCornerRadius, mCornerRadius, mCornerRadius);
        RectF outerBounds = new RectF(innerBounds);
        outerBounds.inset(-mShadowSize, -mShadowSize);

        if (mCornerShadowPath == null) {
            mCornerShadowPath = new Path();
        } else {
            mCornerShadowPath.reset();
        }
        mCornerShadowPath.setFillType(Path.FillType.EVEN_ODD);
        mCornerShadowPath.moveTo(-mCornerRadius, 0);
        mCornerShadowPath.rLineTo(-mShadowSize, 0);
        // outer arc
        mCornerShadowPath.arcTo(outerBounds, 180f, 90f, false);
        // inner arc
        mCornerShadowPath.arcTo(innerBounds, 270f, -90f, false);
        mCornerShadowPath.close();

        float shadowRadius = -outerBounds.top;
        if (shadowRadius > 0f) {
            float startRatio = mCornerRadius / shadowRadius;
            float midRatio = startRatio + ((1f - startRatio) / 2f);
            mCornerShadowPaint.setShader(new RadialGradient(0, 0, shadowRadius,
                    new int[]{0, mShadowStartColor, mShadowMiddleColor, mShadowEndColor},
                    new float[]{0f, startRatio, midRatio, 1f},
                    Shader.TileMode.CLAMP));
        }

        // we offset the content shadowSize/2 pixels up to make it more realistic.
        // this is why edge shadow shader has some extra space
        // When drawing bottom edge shadow, we use that extra space.
        mEdgeShadowPaint.setShader(new LinearGradient(0, innerBounds.top, 0, outerBounds.top,
                new int[]{mShadowStartColor, mShadowMiddleColor, mShadowEndColor},
                new float[]{0f, .5f, 1f}, Shader.TileMode.CLAMP));
        mEdgeShadowPaint.setAntiAlias(false);
    }

    private void buildComponents(Rect bounds) {
        // Card is offset SHADOW_MULTIPLIER * maxShadowSize to account for the shadow shift.
        // We could have different top-bottom offsets to avoid extra gap above but in that case
        // center aligning Views inside the CardView would be problematic.
        final float verticalOffset = mRawMaxShadowSize * SHADOW_MULTIPLIER;
        mContentBounds.set(bounds.left + mRawMaxShadowSize, bounds.top + verticalOffset,
                bounds.right - mRawMaxShadowSize, bounds.bottom - verticalOffset);

        getWrappedDrawable().setBounds((int) mContentBounds.left, (int) mContentBounds.top,
                (int) mContentBounds.right, (int) mContentBounds.bottom);

        buildShadowCorners();
    }

    public float getCornerRadius() {
        return mCornerRadius;
    }

    public void setShadowSize(float size) {
        setShadowSize(size, mRawMaxShadowSize);
    }

    public void setMaxShadowSize(float size) {
        setShadowSize(mRawShadowSize, size);
    }

    public float getShadowSize() {
        return mRawShadowSize;
    }

    public float getMaxShadowSize() {
        return mRawMaxShadowSize;
    }

    public float getMinWidth() {
        final float content = 2 *
                Math.max(mRawMaxShadowSize, mCornerRadius + mRawMaxShadowSize / 2);
        return content + mRawMaxShadowSize * 2;
    }

    public float getMinHeight() {
        final float content = 2 * Math.max(mRawMaxShadowSize, mCornerRadius
                + mRawMaxShadowSize * SHADOW_MULTIPLIER / 2);
        return content + (mRawMaxShadowSize * SHADOW_MULTIPLIER) * 2;
    }
}
