/*
 * Copyright (C) 2021 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.launcher3.icons;

import static android.content.res.Configuration.UI_MODE_NIGHT_MASK;
import static android.content.res.Configuration.UI_MODE_NIGHT_YES;
import static android.content.res.Resources.ID_NULL;

import static com.android.launcher3.icons.GraphicsUtils.getExpectedBitmapSize;
import static com.android.launcher3.icons.IconProvider.ICON_TYPE_CALENDAR;
import static com.android.launcher3.icons.IconProvider.ICON_TYPE_CLOCK;

import android.content.Context;
import android.content.res.Resources;
import android.content.res.TypedArray;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Rect;
import android.graphics.drawable.AdaptiveIconDrawable;
import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.InsetDrawable;
import android.os.Process;
import android.os.UserHandle;
import android.util.Log;

import androidx.annotation.Nullable;

import com.android.launcher3.icons.BitmapInfo.Extender;
import com.android.launcher3.icons.cache.BaseIconCache;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;

/**
 * Class to handle monochrome themed app icons
 */
@SuppressWarnings("NewApi")
public class ThemedIconDrawable extends FastBitmapDrawable {

    public static final String TAG = "ThemedIconDrawable";

    final ThemedBitmapInfo bitmapInfo;
    final int colorFg, colorBg;

    // The foreground/monochrome icon for the app
    private final Drawable mMonochromeIcon;
    private final AdaptiveIconDrawable mBgWrapper;
    private final Rect mBadgeBounds;

    protected ThemedIconDrawable(ThemedConstantState constantState) {
        super(constantState.mBitmap, constantState.colorFg, constantState.mIsDisabled);
        bitmapInfo = constantState.bitmapInfo;
        colorBg = constantState.colorBg;
        colorFg = constantState.colorFg;

        mMonochromeIcon = bitmapInfo.mThemeData.loadMonochromeDrawable(colorFg);
        mBgWrapper = new AdaptiveIconDrawable(new ColorDrawable(colorBg), null);
        mBadgeBounds = bitmapInfo.mUserBadge == null ? null :
                new Rect(0, 0, bitmapInfo.mUserBadge.getWidth(), bitmapInfo.mUserBadge.getHeight());

    }

    @Override
    protected void onBoundsChange(Rect bounds) {
        super.onBoundsChange(bounds);
        mBgWrapper.setBounds(bounds);
        mMonochromeIcon.setBounds(bounds);
    }

    @Override
    protected void drawInternal(Canvas canvas, Rect bounds) {
        int count = canvas.save();
        canvas.scale(bitmapInfo.mNormalizationScale, bitmapInfo.mNormalizationScale,
                bounds.exactCenterX(), bounds.exactCenterY());
        mPaint.setColor(colorBg);
        canvas.drawPath(mBgWrapper.getIconMask(), mPaint);
        mMonochromeIcon.draw(canvas);
        canvas.restoreToCount(count);
        if (mBadgeBounds != null) {
            canvas.drawBitmap(bitmapInfo.mUserBadge, mBadgeBounds, getBounds(), mPaint);
        }
    }

    @Override
    public boolean isThemed() {
        return true;
    }

    @Override
    public ConstantState getConstantState() {
        return new ThemedConstantState(bitmapInfo, colorBg, colorFg, mIsDisabled);
    }

    static class ThemedConstantState extends FastBitmapConstantState {

        final ThemedBitmapInfo bitmapInfo;
        final int colorFg, colorBg;

        public ThemedConstantState(ThemedBitmapInfo bitmapInfo,
                int colorBg, int colorFg, boolean isDisabled) {
            super(bitmapInfo.icon, bitmapInfo.color, isDisabled);
            this.bitmapInfo = bitmapInfo;
            this.colorBg = colorBg;
            this.colorFg = colorFg;
        }

        @Override
        public FastBitmapDrawable newDrawable() {
            return new ThemedIconDrawable(this);
        }
    }

    public static class ThemedBitmapInfo extends BitmapInfo {

        final ThemeData mThemeData;
        final float mNormalizationScale;
        final Bitmap mUserBadge;

        public ThemedBitmapInfo(Bitmap icon, int color, ThemeData themeData,
                float normalizationScale, Bitmap userBadge) {
            super(icon, color);
            mThemeData = themeData;
            mNormalizationScale = normalizationScale;
            mUserBadge = userBadge;
        }

        @Override
        public FastBitmapDrawable newThemedIcon(Context context) {
            int[] colors = getColors(context);
            FastBitmapDrawable drawable = new ThemedConstantState(this, colors[0], colors[1], false)
                    .newDrawable();
            drawable.mDisabledAlpha = GraphicsUtils.getFloat(context, R.attr.disabledIconAlpha, 1f);
            return drawable;
        }

        @Nullable
        public byte[] toByteArray() {
            if (isNullOrLowRes()) {
                return null;
            }
            String resName = mThemeData.mResources.getResourceName(mThemeData.mResID);
            ByteArrayOutputStream out = new ByteArrayOutputStream(
                    getExpectedBitmapSize(icon) + 3 + resName.length());
            try {
                DataOutputStream dos = new DataOutputStream(out);
                dos.writeByte(TYPE_THEMED);
                dos.writeFloat(mNormalizationScale);
                dos.writeUTF(resName);
                icon.compress(Bitmap.CompressFormat.PNG, 100, dos);

                dos.flush();
                dos.close();
                return out.toByteArray();
            } catch (IOException e) {
                Log.w(TAG, "Could not write bitmap");
                return null;
            }
        }

        static ThemedBitmapInfo decode(byte[] data, int color,
                BitmapFactory.Options decodeOptions, UserHandle user, BaseIconCache iconCache,
                Context context) {
            try (DataInputStream dis = new DataInputStream(new ByteArrayInputStream(data))) {
                dis.readByte(); // type
                float normalizationScale = dis.readFloat();

                String resName = dis.readUTF();
                int resId = context.getResources()
                        .getIdentifier(resName, "drawable", context.getPackageName());
                if (resId == ID_NULL) {
                    return null;
                }

                Bitmap userBadgeBitmap = null;
                if (!Process.myUserHandle().equals(user)) {
                    try (BaseIconFactory iconFactory = iconCache.getIconFactory()) {
                        userBadgeBitmap = iconFactory.getUserBadgeBitmap(user);
                    }
                }

                ThemeData themeData = new ThemeData(context.getResources(), resId);
                Bitmap icon = BitmapFactory.decodeStream(dis, null, decodeOptions);
                return new ThemedBitmapInfo(icon, color, themeData, normalizationScale,
                        userBadgeBitmap);
            } catch (IOException e) {
                return null;
            }
        }
    }

    public static class ThemeData {

        final Resources mResources;
        final int mResID;

        public ThemeData(Resources resources, int resID) {
            mResources = resources;
            mResID = resID;
        }

        Drawable loadMonochromeDrawable(int accentColor) {
            Drawable d = mResources.getDrawable(mResID).mutate();
            d.setTint(accentColor);
            d = new InsetDrawable(d, .2f);
            return d;
        }

        public Drawable wrapDrawable(Drawable original, int iconType) {
            if (!(original instanceof AdaptiveIconDrawable)) {
                return original;
            }
            AdaptiveIconDrawable aid = (AdaptiveIconDrawable) original;
            String resourceType = mResources.getResourceTypeName(mResID);
            if (iconType == ICON_TYPE_CALENDAR && "array".equals(resourceType)) {
                TypedArray ta = mResources.obtainTypedArray(mResID);
                int id = ta.getResourceId(IconProvider.getDay(), ID_NULL);
                ta.recycle();
                return id == ID_NULL ? original
                        : new ThemedAdaptiveIcon(aid, new ThemeData(mResources, id));
            } else if (iconType == ICON_TYPE_CLOCK && "array".equals(resourceType)) {
                ((ClockDrawableWrapper) original).mThemeData = this;
                return original;
            } else if ("drawable".equals(resourceType)) {
                return new ThemedAdaptiveIcon(aid, this);
            } else {
                return original;
            }
        }
    }

    static class ThemedAdaptiveIcon extends AdaptiveIconDrawable implements Extender {

        protected final ThemeData mThemeData;

        public ThemedAdaptiveIcon(AdaptiveIconDrawable parent, ThemeData themeData) {
            super(parent.getBackground(), parent.getForeground());
            mThemeData = themeData;
        }

        @Override
        public BitmapInfo getExtendedInfo(Bitmap bitmap, int color, BaseIconFactory iconFactory,
                float normalizationScale, UserHandle user) {
            Bitmap userBadge = Process.myUserHandle().equals(user)
                    ? null : iconFactory.getUserBadgeBitmap(user);
            return new ThemedBitmapInfo(bitmap, color, mThemeData, normalizationScale, userBadge);
        }

        @Override
        public void drawForPersistence(Canvas canvas) {
            draw(canvas);
        }

        @Override
        public Drawable getThemedDrawable(Context context) {
            int[] colors = getColors(context);
            Drawable bg = new ColorDrawable(colors[0]);
            float inset = getExtraInsetFraction() / (1 + 2 * getExtraInsetFraction());
            Drawable fg = new InsetDrawable(mThemeData.loadMonochromeDrawable(colors[1]), inset);
            return new AdaptiveIconDrawable(bg, fg);
        }
    }

    /**
     * Get an int array representing background and foreground colors for themed icons
     */
    public static int[] getColors(Context context) {
        Resources res = context.getResources();
        int[] colors = new int[2];
        if ((res.getConfiguration().uiMode & UI_MODE_NIGHT_MASK) == UI_MODE_NIGHT_YES) {
            colors[0] = res.getColor(android.R.color.system_neutral1_800);
            colors[1] = res.getColor(android.R.color.system_accent1_100);
        } else {
            colors[0] = res.getColor(android.R.color.system_accent1_100);
            colors[1] = res.getColor(android.R.color.system_neutral2_700);
        }
        return colors;
    }
}
