/*
 * Copyright (C) 2016 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.settingslib.drawable;

import android.annotation.ColorInt;
import android.annotation.DrawableRes;
import android.annotation.NonNull;
import android.app.admin.DevicePolicyManager;
import android.content.Context;
import android.content.res.ColorStateList;
import android.graphics.Bitmap;
import android.graphics.BitmapShader;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.ColorFilter;
import android.graphics.Matrix;
import android.graphics.Paint;
import android.graphics.PixelFormat;
import android.graphics.PorterDuff;
import android.graphics.PorterDuffColorFilter;
import android.graphics.PorterDuffXfermode;
import android.graphics.Rect;
import android.graphics.RectF;
import android.graphics.Shader;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.os.UserHandle;

import com.android.settingslib.R;

/**
 * Converts the user avatar icon to a circularly clipped one with an optional badge and frame
 */
public class UserIconDrawable extends Drawable implements Drawable.Callback {

    private Drawable mUserDrawable;
    private Bitmap mUserIcon;
    private Bitmap mBitmap; // baked representation. Required for transparent border around badge
    private final Paint mIconPaint = new Paint();
    private final Paint mPaint = new Paint();
    private final Matrix mIconMatrix = new Matrix();
    private float mIntrinsicRadius;
    private float mDisplayRadius;
    private float mPadding = 0;
    private int mSize = 0; // custom "intrinsic" size for this drawable if non-zero
    private boolean mInvalidated = true;
    private ColorStateList mTintColor = null;
    private PorterDuff.Mode mTintMode = PorterDuff.Mode.SRC_ATOP;

    private float mFrameWidth;
    private float mFramePadding;
    private ColorStateList mFrameColor = null;
    private Paint mFramePaint;

    private Drawable mBadge;
    private Paint mClearPaint;
    private float mBadgeRadius;
    private float mBadgeMargin;

    /**
     * Gets the system default managed-user badge as a drawable. This drawable is tint-able.
     * For badging purpose, consider
     * {@link android.content.pm.PackageManager#getUserBadgedDrawableForDensity(Drawable, UserHandle, Rect, int)}.
     *
     * @param context
     * @return drawable containing just the badge
     */
    public static Drawable getManagedUserDrawable(Context context) {
        return getDrawableForDisplayDensity
                (context, com.android.internal.R.drawable.ic_corp_user_badge);
    }

    private static Drawable getDrawableForDisplayDensity(
            Context context, @DrawableRes int drawable) {
        int density = context.getResources().getDisplayMetrics().densityDpi;
        return context.getResources().getDrawableForDensity(
                drawable, density, context.getTheme());
    }

    /**
     * Gets the preferred list-item size of this drawable.
     * @param context
     * @return size in pixels
     */
    public static int getSizeForList(Context context) {
        return (int) context.getResources().getDimension(R.dimen.circle_avatar_size);
    }

    public UserIconDrawable() {
        this(0);
    }

    /**
     * Use this constructor if the drawable is intended to be placed in listviews
     * @param intrinsicSize if 0, the intrinsic size will come from the icon itself
     */
    public UserIconDrawable(int intrinsicSize) {
        super();
        mIconPaint.setAntiAlias(true);
        mIconPaint.setFilterBitmap(true);
        mPaint.setFilterBitmap(true);
        mPaint.setAntiAlias(true);
        if (intrinsicSize > 0) {
            setBounds(0, 0, intrinsicSize, intrinsicSize);
            setIntrinsicSize(intrinsicSize);
        }
        setIcon(null);
    }

    public UserIconDrawable setIcon(Bitmap icon) {
        if (mUserDrawable != null) {
            mUserDrawable.setCallback(null);
            mUserDrawable = null;
        }
        mUserIcon = icon;
        if (mUserIcon == null) {
            mIconPaint.setShader(null);
            mBitmap = null;
        } else {
            mIconPaint.setShader(new BitmapShader(icon, Shader.TileMode.CLAMP,
                    Shader.TileMode.CLAMP));
        }
        onBoundsChange(getBounds());
        return this;
    }

    public UserIconDrawable setIconDrawable(Drawable icon) {
        if (mUserDrawable != null) {
            mUserDrawable.setCallback(null);
        }
        mUserIcon = null;
        mUserDrawable = icon;
        if (mUserDrawable == null) {
            mBitmap = null;
        } else {
            mUserDrawable.setCallback(this);
        }
        onBoundsChange(getBounds());
        return this;
    }

    public UserIconDrawable setBadge(Drawable badge) {
        mBadge = badge;
        if (mBadge != null) {
            if (mClearPaint == null) {
                mClearPaint = new Paint();
                mClearPaint.setAntiAlias(true);
                mClearPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR));
                mClearPaint.setStyle(Paint.Style.FILL);
            }
            // update metrics
            onBoundsChange(getBounds());
        } else {
            invalidateSelf();
        }
        return this;
    }

    public UserIconDrawable setBadgeIfManagedUser(Context context, int userId) {
        Drawable badge = null;
        if (userId != UserHandle.USER_NULL) {
            boolean isManaged = context.getSystemService(DevicePolicyManager.class)
                    .getProfileOwnerAsUser(userId) != null;
            if (isManaged) {
                badge = getDrawableForDisplayDensity(
                        context, com.android.internal.R.drawable.ic_corp_badge_case);
            }
        }
        return setBadge(badge);
    }

    public void setBadgeRadius(float radius) {
        mBadgeRadius = radius;
        onBoundsChange(getBounds());
    }

    public void setBadgeMargin(float margin) {
        mBadgeMargin = margin;
        onBoundsChange(getBounds());
    }

    /**
     * Sets global padding of icon/frame. Doesn't effect the badge.
     * @param padding
     */
    public void setPadding(float padding) {
        mPadding = padding;
        onBoundsChange(getBounds());
    }

    private void initFramePaint() {
        if (mFramePaint == null) {
            mFramePaint = new Paint();
            mFramePaint.setStyle(Paint.Style.STROKE);
            mFramePaint.setAntiAlias(true);
        }
    }

    public void setFrameWidth(float width) {
        initFramePaint();
        mFrameWidth = width;
        mFramePaint.setStrokeWidth(width);
        onBoundsChange(getBounds());
    }

    public void setFramePadding(float padding) {
        initFramePaint();
        mFramePadding = padding;
        onBoundsChange(getBounds());
    }

    public void setFrameColor(int color) {
        initFramePaint();
        mFramePaint.setColor(color);
        invalidateSelf();
    }

    public void setFrameColor(ColorStateList colorList) {
        initFramePaint();
        mFrameColor = colorList;
        invalidateSelf();
    }

    /**
     * This sets the "intrinsic" size of this drawable. Useful for views which use the drawable's
     * intrinsic size for layout. It is independent of the bounds.
     * @param size if 0, the intrinsic size will be set to the displayed icon's size
     */
    public void setIntrinsicSize(int size) {
        mSize = size;
    }

    @Override
    public void draw(Canvas canvas) {
        if (mInvalidated) {
            rebake();
        }
        if (mBitmap != null) {
            if (mTintColor == null) {
                mPaint.setColorFilter(null);
            } else {
                int color = mTintColor.getColorForState(getState(), mTintColor.getDefaultColor());
                if (shouldUpdateColorFilter(color, mTintMode)) {
                    mPaint.setColorFilter(new PorterDuffColorFilter(color, mTintMode));
                }
            }

            canvas.drawBitmap(mBitmap, 0, 0, mPaint);
        }
    }

    private boolean shouldUpdateColorFilter(@ColorInt int color, PorterDuff.Mode mode) {
        ColorFilter colorFilter = mPaint.getColorFilter();
        if (colorFilter instanceof PorterDuffColorFilter) {
            PorterDuffColorFilter porterDuffColorFilter = (PorterDuffColorFilter) colorFilter;
            int currentColor = porterDuffColorFilter.getColor();
            PorterDuff.Mode currentMode = porterDuffColorFilter.getMode();
            return currentColor != color || currentMode != mode;
        } else {
            return true;
        }
    }

    @Override
    public void setAlpha(int alpha) {
        mPaint.setAlpha(alpha);
        super.invalidateSelf();
    }

    @Override
    public void setColorFilter(ColorFilter colorFilter) {
    }

    @Override
    public void setTintList(ColorStateList tintList) {
        mTintColor = tintList;
        super.invalidateSelf();
    }

    @Override
    public void setTintMode(@NonNull PorterDuff.Mode mode) {
        mTintMode = mode;
        super.invalidateSelf();
    }

    @Override
    public ConstantState getConstantState() {
        return new BitmapDrawable(mBitmap).getConstantState();
    }

    /**
     * This 'bakes' the current state of this icon into a bitmap and removes/recycles the source
     * bitmap/drawable. Use this when no more changes will be made and an intrinsic size is set.
     * This effectively turns this into a static drawable.
     */
    public UserIconDrawable bake() {
        if (mSize <= 0) {
            throw new IllegalStateException("Baking requires an explicit intrinsic size");
        }
        onBoundsChange(new Rect(0, 0, mSize, mSize));
        rebake();
        mFrameColor = null;
        mFramePaint = null;
        mClearPaint = null;
        if (mUserDrawable != null) {
            mUserDrawable.setCallback(null);
            mUserDrawable = null;
        } else if (mUserIcon != null) {
            mUserIcon.recycle();
            mUserIcon = null;
        }
        return this;
    }

    private void rebake() {
        mInvalidated = false;

        if (mBitmap == null || (mUserDrawable == null && mUserIcon == null)) {
            return;
        }

        final Canvas canvas = new Canvas(mBitmap);
        canvas.drawColor(0, PorterDuff.Mode.CLEAR);

        if(mUserDrawable != null) {
            mUserDrawable.draw(canvas);
        } else if (mUserIcon != null) {
            int saveId = canvas.save();
            canvas.concat(mIconMatrix);
            canvas.drawCircle(mUserIcon.getWidth() * 0.5f, mUserIcon.getHeight() * 0.5f,
                    mIntrinsicRadius, mIconPaint);
            canvas.restoreToCount(saveId);
        }
        if (mFrameColor != null) {
            mFramePaint.setColor(mFrameColor.getColorForState(getState(), Color.TRANSPARENT));
        }
        if ((mFrameWidth + mFramePadding) > 0.001f) {
            float radius = mDisplayRadius - mPadding - mFrameWidth * 0.5f;
            canvas.drawCircle(getBounds().exactCenterX(), getBounds().exactCenterY(),
                    radius, mFramePaint);
        }

        if ((mBadge != null) && (mBadgeRadius > 0.001f)) {
            final float badgeDiameter = mBadgeRadius * 2f;
            final float badgeTop = mBitmap.getHeight() - badgeDiameter;
            float badgeLeft = mBitmap.getWidth() - badgeDiameter;

            mBadge.setBounds((int) badgeLeft, (int) badgeTop,
                    (int) (badgeLeft + badgeDiameter), (int) (badgeTop + badgeDiameter));

            final float borderRadius = mBadge.getBounds().width() * 0.5f + mBadgeMargin;
            canvas.drawCircle(badgeLeft + mBadgeRadius, badgeTop + mBadgeRadius,
                    borderRadius, mClearPaint);
            mBadge.draw(canvas);
        }
    }

    @Override
    protected void onBoundsChange(Rect bounds) {
        if (bounds.isEmpty() || (mUserIcon == null && mUserDrawable == null)) {
            return;
        }

        // re-create bitmap if applicable
        float newDisplayRadius = Math.min(bounds.width(), bounds.height()) * 0.5f;
        int size = (int) (newDisplayRadius * 2);
        if (mBitmap == null || size != ((int) (mDisplayRadius * 2))) {
            mDisplayRadius = newDisplayRadius;
            if (mBitmap != null) {
                mBitmap.recycle();
            }
            mBitmap = Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888);
        }

        // update metrics
        mDisplayRadius = Math.min(bounds.width(), bounds.height()) * 0.5f;
        final float iconRadius = mDisplayRadius - mFrameWidth - mFramePadding - mPadding;
        RectF dstRect = new RectF(bounds.exactCenterX() - iconRadius,
                                  bounds.exactCenterY() - iconRadius,
                                  bounds.exactCenterX() + iconRadius,
                                  bounds.exactCenterY() + iconRadius);
        if (mUserDrawable != null) {
            Rect rounded = new Rect();
            dstRect.round(rounded);
            mIntrinsicRadius = Math.min(mUserDrawable.getIntrinsicWidth(),
                                        mUserDrawable.getIntrinsicHeight()) * 0.5f;
            mUserDrawable.setBounds(rounded);
        } else if (mUserIcon != null) {
            // Build square-to-square transformation matrix
            final float iconCX = mUserIcon.getWidth() * 0.5f;
            final float iconCY = mUserIcon.getHeight() * 0.5f;
            mIntrinsicRadius = Math.min(iconCX, iconCY);
            RectF srcRect = new RectF(iconCX - mIntrinsicRadius, iconCY - mIntrinsicRadius,
                                      iconCX + mIntrinsicRadius, iconCY + mIntrinsicRadius);
            mIconMatrix.setRectToRect(srcRect, dstRect, Matrix.ScaleToFit.FILL);
        }

        invalidateSelf();
    }

    @Override
    public void invalidateSelf() {
        super.invalidateSelf();
        mInvalidated = true;
    }

    @Override
    public boolean isStateful() {
        return mFrameColor != null && mFrameColor.isStateful();
    }

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

    @Override
    public int getIntrinsicWidth() {
        return (mSize <= 0 ? (int) mIntrinsicRadius * 2 : mSize);
    }

    @Override
    public int getIntrinsicHeight() {
        return getIntrinsicWidth();
    }

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

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

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