/*
 * Copyright (C) 2019 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.customization.model.theme;

import static com.android.customization.model.ResourceConstants.OVERLAY_CATEGORY_COLOR;
import static com.android.customization.model.ResourceConstants.OVERLAY_CATEGORY_FONT;
import static com.android.customization.model.ResourceConstants.OVERLAY_CATEGORY_ICON_ANDROID;
import static com.android.customization.model.ResourceConstants.OVERLAY_CATEGORY_SHAPE;
import static com.android.customization.model.ResourceConstants.PATH_SIZE;

import android.content.Context;
import android.content.pm.PackageManager;
import android.content.res.Configuration;
import android.content.res.Resources;
import android.graphics.Path;
import android.graphics.Typeface;
import android.graphics.drawable.AdaptiveIconDrawable;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.ShapeDrawable;
import android.graphics.drawable.shapes.PathShape;
import android.text.TextUtils;
import android.util.Log;
import android.view.View;
import android.widget.ImageView;
import android.widget.TextView;

import androidx.annotation.ColorInt;
import androidx.annotation.Dimension;
import androidx.annotation.Nullable;
import androidx.core.graphics.PathParser;

import com.android.customization.model.CustomizationManager;
import com.android.customization.model.CustomizationOption;
import com.android.customization.model.theme.ThemeBundle.PreviewInfo.ShapeAppIcon;
import com.android.customization.widget.DynamicAdaptiveIconDrawable;
import com.android.wallpaper.R;
import com.android.wallpaper.asset.Asset;
import com.android.wallpaper.asset.BitmapCachingAsset;
import com.android.wallpaper.model.WallpaperInfo;
import com.android.wallpaper.util.ResourceUtils;

import org.json.JSONException;
import org.json.JSONObject;

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;

/**
 * Represents a Theme component available in the system as a "persona" bundle.
 * Note that in this context a Theme is not related to Android's Styles, but it's rather an
 * abstraction representing a series of overlays to be applied to the system.
 */
public class ThemeBundle implements CustomizationOption<ThemeBundle> {

    private static final String TAG = "ThemeBundle";
    private final static String EMPTY_JSON = "{}";
    private final static String TIMESTAMP_FIELD = "_applied_timestamp";

    private final String mTitle;
    private final PreviewInfo mPreviewInfo;
    private final boolean mIsDefault;
    protected final Map<String, String> mPackagesByCategory;
    private WallpaperInfo mOverrideWallpaper;
    private Asset mOverrideWallpaperAsset;
    private CharSequence mContentDescription;

    protected ThemeBundle(String title, Map<String, String> overlayPackages,
            boolean isDefault, PreviewInfo previewInfo) {
        mTitle = title;
        mIsDefault = isDefault;
        mPreviewInfo = previewInfo;
        mPackagesByCategory = Collections.unmodifiableMap(removeNullValues(overlayPackages));
    }

    @Override
    public String getTitle() {
        return mTitle;
    }

    @Override
    public void bindThumbnailTile(View view) {
        Resources res = view.getContext().getResources();

        ((TextView) view.findViewById(R.id.theme_option_font)).setTypeface(
                mPreviewInfo.headlineFontFamily);
        if (mPreviewInfo.shapeDrawable != null) {
            ((ShapeDrawable) mPreviewInfo.shapeDrawable).getPaint().setColor(
                    mPreviewInfo.resolveAccentColor(res));
            ((ImageView) view.findViewById(R.id.theme_option_shape)).setImageDrawable(
                    mPreviewInfo.shapeDrawable);
        }
        if (!mPreviewInfo.icons.isEmpty()) {
            Drawable icon = mPreviewInfo.icons.get(0).getConstantState().newDrawable().mutate();
            icon.setTint(ResourceUtils.getColorAttr(
                    view.getContext(), android.R.attr.textColorSecondary));
            ((ImageView) view.findViewById(R.id.theme_option_icon)).setImageDrawable(
                    icon);
        }
        view.setContentDescription(getContentDescription(view.getContext()));
    }

    @Override
    public boolean isActive(CustomizationManager<ThemeBundle> manager) {
        ThemeManager themeManager = (ThemeManager) manager;

        if (mIsDefault) {
            String serializedOverlays = themeManager.getStoredOverlays();
            return TextUtils.isEmpty(serializedOverlays) || EMPTY_JSON.equals(serializedOverlays);
        } else {
            Map<String, String> currentOverlays = themeManager.getCurrentOverlays();
            return mPackagesByCategory.equals(currentOverlays);
        }
    }

    @Override
    public int getLayoutResId() {
        return R.layout.theme_option;
    }

    /**
     * This is similar to #equals() but it only compares this theme's packages with the other, that
     * is, it will return true if applying this theme has the same effect of applying the given one.
     */
    public boolean isEquivalent(ThemeBundle other) {
        if (other == null) {
            return false;
        }
        if (mIsDefault) {
            return other.isDefault() || TextUtils.isEmpty(other.getSerializedPackages())
                    || EMPTY_JSON.equals(other.getSerializedPackages());
        }
        // Map#equals ensures keys and values are compared.
        return mPackagesByCategory.equals(other.mPackagesByCategory);
    }

    public PreviewInfo getPreviewInfo() {
        return mPreviewInfo;
    }

    public void setOverrideThemeWallpaper(WallpaperInfo homeWallpaper) {
        mOverrideWallpaper = homeWallpaper;
        mOverrideWallpaperAsset = null;
    }

    private Asset getOverrideWallpaperAsset(Context context) {
        if (mOverrideWallpaperAsset == null) {
            mOverrideWallpaperAsset = new BitmapCachingAsset(context,
                    mOverrideWallpaper.getThumbAsset(context));
        }
        return mOverrideWallpaperAsset;
    }

    boolean isDefault() {
        return mIsDefault;
    }

    public Map<String, String> getPackagesByCategory() {
        return mPackagesByCategory;
    }

    public String getSerializedPackages() {
        return getJsonPackages(false).toString();
    }

    public String getSerializedPackagesWithTimestamp() {
        return getJsonPackages(true).toString();
    }

    JSONObject getJsonPackages(boolean insertTimestamp) {
        if (isDefault()) {
            return new JSONObject();
        }
        JSONObject json = new JSONObject(mPackagesByCategory);
        // Remove items with null values to avoid deserialization issues.
        removeNullValues(json);
        if (insertTimestamp) {
            try {
                json.put(TIMESTAMP_FIELD, System.currentTimeMillis());
            } catch (JSONException e) {
                Log.e(TAG, "Couldn't add timestamp to serialized themebundle");
            }
        }
        return json;
    }

    private void removeNullValues(JSONObject json) {
        Iterator<String> keys = json.keys();
        Set<String> keysToRemove = new HashSet<>();
        while(keys.hasNext()) {
            String key = keys.next();
            if (json.isNull(key)) {
                keysToRemove.add(key);
            }
        }
        for (String key : keysToRemove) {
            json.remove(key);
        }
    }

    private Map<String, String> removeNullValues(Map<String, String> map) {
        return map.entrySet()
                .stream()
                .filter(entry -> entry.getValue() != null)
                .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
    }

    protected CharSequence getContentDescription(Context context) {
        if (mContentDescription == null) {
            CharSequence defaultName = context.getString(R.string.default_theme_title);
            if (isDefault()) {
                mContentDescription = defaultName;
            } else {
                PackageManager pm = context.getPackageManager();
                CharSequence fontName = getOverlayName(pm, OVERLAY_CATEGORY_FONT);
                CharSequence iconName = getOverlayName(pm, OVERLAY_CATEGORY_ICON_ANDROID);
                CharSequence shapeName = getOverlayName(pm, OVERLAY_CATEGORY_SHAPE);
                CharSequence colorName = getOverlayName(pm, OVERLAY_CATEGORY_COLOR);
                mContentDescription = context.getString(R.string.theme_description,
                        TextUtils.isEmpty(fontName) ? defaultName : fontName,
                        TextUtils.isEmpty(iconName) ? defaultName : iconName,
                        TextUtils.isEmpty(shapeName) ? defaultName : shapeName,
                        TextUtils.isEmpty(colorName) ? defaultName : colorName);
            }
        }
        return mContentDescription;
    }

    private CharSequence getOverlayName(PackageManager pm, String overlayCategoryFont) {
        try {
            return pm.getApplicationInfo(
                    mPackagesByCategory.get(overlayCategoryFont), 0).loadLabel(pm);
        } catch (PackageManager.NameNotFoundException e) {
            return "";
        }
    }

    public static class PreviewInfo {
        public final Typeface bodyFontFamily;
        public final Typeface headlineFontFamily;
        @ColorInt public final int colorAccentLight;
        @ColorInt public final int colorAccentDark;
        public final List<Drawable> icons;
        public final Drawable shapeDrawable;
        public final List<ShapeAppIcon> shapeAppIcons;
        @Dimension public final int bottomSheeetCornerRadius;

        /** A class to represent an App icon and its name. */
        public static class ShapeAppIcon {
            private Drawable mIconDrawable;
            private CharSequence mAppName;

            public ShapeAppIcon(Drawable icon, CharSequence appName) {
                mIconDrawable = icon;
                mAppName = appName;
            }

            /** Returns a copy of app icon drawable. */
            public Drawable getDrawableCopy() {
                return mIconDrawable.getConstantState().newDrawable().mutate();
            }

            /** Returns the app name. */
            public CharSequence getAppName() {
                return mAppName;
            }
        }

        private PreviewInfo(Context context, Typeface bodyFontFamily, Typeface headlineFontFamily,
                int colorAccentLight, int colorAccentDark, List<Drawable> icons,
                Drawable shapeDrawable, @Dimension int cornerRadius,
                List<ShapeAppIcon> shapeAppIcons) {
            this.bodyFontFamily = bodyFontFamily;
            this.headlineFontFamily = headlineFontFamily;
            this.colorAccentLight = colorAccentLight;
            this.colorAccentDark = colorAccentDark;
            this.icons = icons;
            this.shapeDrawable = shapeDrawable;
            this.bottomSheeetCornerRadius = cornerRadius;
            this.shapeAppIcons = shapeAppIcons;
        }

        /**
         * Returns the accent color to be applied corresponding with the current configuration's
         * UI mode.
         * @return one of {@link #colorAccentDark} or {@link #colorAccentLight}
         */
        @ColorInt
        public int resolveAccentColor(Resources res) {
            return (res.getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK)
                    == Configuration.UI_MODE_NIGHT_YES ? colorAccentDark : colorAccentLight;
        }
    }

    public static class Builder {
        protected String mTitle;
        private Typeface mBodyFontFamily;
        private Typeface mHeadlineFontFamily;
        @ColorInt private int mColorAccentLight = -1;
        @ColorInt private int mColorAccentDark = -1;
        private List<Drawable> mIcons = new ArrayList<>();
        private String mPathString;
        private Path mShapePath;
        private boolean mIsDefault;
        @Dimension private int mCornerRadius;
        protected Map<String, String> mPackages = new HashMap<>();
        private List<ShapeAppIcon> mAppIcons = new ArrayList<>();

        public ThemeBundle build(Context context) {
            return new ThemeBundle(mTitle, mPackages, mIsDefault, createPreviewInfo(context));
        }

        public PreviewInfo createPreviewInfo(Context context) {
            ShapeDrawable shapeDrawable = null;
            List<ShapeAppIcon> shapeIcons = new ArrayList<>();
            Path path = mShapePath;
            if (!TextUtils.isEmpty(mPathString)) {
                path = PathParser.createPathFromPathData(mPathString);
            }
            if (path != null) {
                PathShape shape = new PathShape(path, PATH_SIZE, PATH_SIZE);
                shapeDrawable = new ShapeDrawable(shape);
                shapeDrawable.setIntrinsicHeight((int) PATH_SIZE);
                shapeDrawable.setIntrinsicWidth((int) PATH_SIZE);
                for (ShapeAppIcon icon : mAppIcons) {
                    Drawable drawable = icon.mIconDrawable;
                    if (drawable instanceof AdaptiveIconDrawable) {
                        AdaptiveIconDrawable adaptiveIcon = (AdaptiveIconDrawable) drawable;
                        shapeIcons.add(new ShapeAppIcon(
                                new DynamicAdaptiveIconDrawable(adaptiveIcon.getBackground(),
                                        adaptiveIcon.getForeground(), path),
                                icon.getAppName()));
                    } else if (drawable instanceof DynamicAdaptiveIconDrawable) {
                        shapeIcons.add(icon);
                    }
                    // TODO: add iconloader library's legacy treatment helper methods for
                    //  non-adaptive icons
                }
            }
            return new PreviewInfo(context, mBodyFontFamily, mHeadlineFontFamily, mColorAccentLight,
                    mColorAccentDark, mIcons, shapeDrawable, mCornerRadius, shapeIcons);
        }

        public Map<String, String> getPackages() {
            return Collections.unmodifiableMap(mPackages);
        }

        public String getTitle() {
            return mTitle;
        }

        public Builder setTitle(String title) {
            mTitle = title;
            return this;
        }

        public Builder setBodyFontFamily(@Nullable Typeface bodyFontFamily) {
            mBodyFontFamily = bodyFontFamily;
            return this;
        }

        public Builder setHeadlineFontFamily(@Nullable Typeface headlineFontFamily) {
            mHeadlineFontFamily = headlineFontFamily;
            return this;
        }

        public Builder setColorAccentLight(@ColorInt int colorAccentLight) {
            mColorAccentLight = colorAccentLight;
            return this;
        }

        public Builder setColorAccentDark(@ColorInt int colorAccentDark) {
            mColorAccentDark = colorAccentDark;
            return this;
        }

        public Builder addIcon(Drawable icon) {
            mIcons.add(icon);
            return this;
        }

        public Builder addOverlayPackage(String category, String packageName) {
            mPackages.put(category, packageName);
            return this;
        }

        public Builder setShapePath(String path) {
            mPathString = path;
            return this;
        }

        public Builder setShapePath(Path path) {
            mShapePath = path;
            return this;
        }

        public Builder asDefault() {
            mIsDefault = true;
            return this;
        }

        public Builder setShapePreviewIcons(List<ShapeAppIcon> appIcons) {
            mAppIcons.clear();
            mAppIcons.addAll(appIcons);
            return this;
        }

        public Builder setBottomSheetCornerRadius(@Dimension int radius) {
            mCornerRadius = radius;
            return this;
        }
    }
}
