/*
 * Copyright (C) 2017 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.wallpaper.model;

import android.app.Activity;
import android.app.WallpaperManager;
import android.content.Context;
import android.content.Intent;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.content.res.Resources;
import android.net.Uri;
import android.os.Parcel;
import android.service.wallpaper.WallpaperService;
import android.text.TextUtils;
import android.util.AttributeSet;
import android.util.Log;

import androidx.annotation.Nullable;

import com.android.wallpaper.R;
import com.android.wallpaper.asset.Asset;
import com.android.wallpaper.asset.LiveWallpaperThumbAsset;
import com.android.wallpaper.module.InjectorProvider;
import com.android.wallpaper.module.LiveWallpaperInfoFactory;
import com.android.wallpaper.util.ActivityUtils;

import org.xmlpull.v1.XmlPullParserException;

import java.io.IOException;
import java.text.Collator;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.Iterator;
import java.util.List;
import java.util.Set;

/**
 * Represents a live wallpaper from the system.
 */
public class LiveWallpaperInfo extends WallpaperInfo {
    public static final Creator<LiveWallpaperInfo> CREATOR =
            new Creator<LiveWallpaperInfo>() {
                @Override
                public LiveWallpaperInfo createFromParcel(Parcel in) {
                    return new LiveWallpaperInfo(in);
                }

                @Override
                public LiveWallpaperInfo[] newArray(int size) {
                    return new LiveWallpaperInfo[size];
                }
            };

    public static final String TAG_NAME = "live-wallpaper";

    private static final String TAG = "LiveWallpaperInfo";
    public static final String ATTR_ID = "id";
    public static final String ATTR_PACKAGE = "package";
    public static final String ATTR_SERVICE = "service";

    /**
     * Creates a new {@link LiveWallpaperInfo} from an XML {@link AttributeSet}
     * @param context used to construct the {@link android.app.WallpaperInfo} associated with the
     *                new {@link LiveWallpaperInfo}
     * @param categoryId Id of the category the new wallpaper will belong to
     * @param attrs {@link AttributeSet} to parse
     * @return a newly created {@link LiveWallpaperInfo} or {@code null} if one couldn't be created.
     */
    @Nullable
    public static LiveWallpaperInfo fromAttributeSet(Context context, String categoryId,
            AttributeSet attrs) {
        String wallpaperId = attrs.getAttributeValue(null, ATTR_ID);
        if (TextUtils.isEmpty(wallpaperId)) {
            Log.w(TAG, "Live wallpaper declaration without id in category " + categoryId);
            return null;
        }
        String packageName = attrs.getAttributeValue(null, ATTR_PACKAGE);
        String serviceName = attrs.getAttributeValue(null, ATTR_SERVICE);
        return fromPackageAndServiceName(context, categoryId, wallpaperId, packageName,
                serviceName);
    }

    /**
     * Creates a new {@link LiveWallpaperInfo} from its individual components
     * @return a newly created {@link LiveWallpaperInfo} or {@code null} if one couldn't be created.
     */
    @Nullable
    public static LiveWallpaperInfo fromPackageAndServiceName(Context context, String categoryId,
            String wallpaperId, String packageName, String serviceName) {
        if (TextUtils.isEmpty(serviceName)) {
            Log.w(TAG, "Live wallpaper declaration without service: " + wallpaperId);
            return null;
        }

        Intent intent = new Intent(WallpaperService.SERVICE_INTERFACE);
        if (TextUtils.isEmpty(packageName)) {
            String [] parts = serviceName.split("/");
            if (parts != null && parts.length == 2) {
                packageName = parts[0];
                serviceName = parts[1];
            } else {
                Log.w(TAG, "Live wallpaper declaration with invalid service: " + wallpaperId);
                return null;
            }
        }
        intent.setClassName(packageName, serviceName);
        List<ResolveInfo> resolveInfos = context.getPackageManager().queryIntentServices(intent,
                PackageManager.GET_META_DATA);
        if (resolveInfos.isEmpty()) {
            Log.w(TAG, "Couldn't find live wallpaper for " + serviceName);
            return null;
        }
        android.app.WallpaperInfo wallpaperInfo;
        try {
            wallpaperInfo = new android.app.WallpaperInfo(context, resolveInfos.get(0));
        } catch (XmlPullParserException | IOException e) {
            Log.w(TAG, "Skipping wallpaper " + resolveInfos.get(0).serviceInfo, e);
            return null;
        }

        return new LiveWallpaperInfo(wallpaperInfo, false, categoryId);
    }

    public android.app.WallpaperInfo getInfo() {
        return mInfo;
    }

    public LiveWallpaperThumbAsset getThumbAsset() {
        return mThumbAsset;
    }

    public boolean isVisibleTitle() {
        return mVisibleTitle;
    }

    @Nullable
    public String getCollectionId() {
        return mCollectionId;
    }

    protected android.app.WallpaperInfo mInfo;
    protected LiveWallpaperThumbAsset mThumbAsset;
    protected boolean mVisibleTitle;
    @Nullable private final String mCollectionId;

    /**
     * Constructs a LiveWallpaperInfo wrapping the given system WallpaperInfo object, representing
     * a particular live wallpaper.
     *
     * @param info
     */
    public LiveWallpaperInfo(android.app.WallpaperInfo info) {
        this(info, true, null);
    }

    /**
     * Constructs a LiveWallpaperInfo wrapping the given system WallpaperInfo object, representing
     * a particular live wallpaper.
     */
    public LiveWallpaperInfo(android.app.WallpaperInfo info, boolean visibleTitle,
            @Nullable String collectionId) {
        mInfo = info;
        mVisibleTitle = visibleTitle;
        mCollectionId = collectionId;
    }

    protected LiveWallpaperInfo(Parcel in) {
        super(in);
        mInfo = in.readParcelable(android.app.WallpaperInfo.class.getClassLoader());
        mVisibleTitle = in.readInt() == 1;
        mCollectionId = in.readString();
    }

    /**
     * Returns all live wallpapers found on the device, excluding those residing in APKs described by
     * the package names in excludedPackageNames.
     */
    public static List<WallpaperInfo> getAll(Context context,
                                             @Nullable Set<String> excludedPackageNames) {
        List<ResolveInfo> resolveInfos = getAllOnDevice(context);
        List<WallpaperInfo> wallpaperInfos = new ArrayList<>();
        LiveWallpaperInfoFactory factory =
                InjectorProvider.getInjector().getLiveWallpaperInfoFactory(context);
        for (int i = 0; i < resolveInfos.size(); i++) {
            ResolveInfo resolveInfo = resolveInfos.get(i);
            android.app.WallpaperInfo wallpaperInfo;
            try {
                wallpaperInfo = new android.app.WallpaperInfo(context, resolveInfo);
            } catch (XmlPullParserException | IOException e) {
                Log.w(TAG, "Skipping wallpaper " + resolveInfo.serviceInfo, e);
                continue;
            }

            if (excludedPackageNames != null && excludedPackageNames.contains(
                    wallpaperInfo.getPackageName())) {
                continue;
            }

            wallpaperInfos.add(factory.getLiveWallpaperInfo(wallpaperInfo));
        }

        return wallpaperInfos;
    }

    /**
     * Returns the live wallpapers having the given service names, found within the APK with the
     * given package name.
     */
    public static List<WallpaperInfo> getFromSpecifiedPackage(
            Context context, String packageName, @Nullable List<String> serviceNames,
            boolean shouldShowTitle, String collectionId) {
        List<ResolveInfo> resolveInfos;
        if (serviceNames != null) {
            resolveInfos = getAllContainingServiceNames(context, serviceNames);
        } else {
            resolveInfos = getAllOnDevice(context);
        }
        List<WallpaperInfo> wallpaperInfos = new ArrayList<>();
        LiveWallpaperInfoFactory factory =
                InjectorProvider.getInjector().getLiveWallpaperInfoFactory(context);

        for (int i = 0; i < resolveInfos.size(); i++) {
            ResolveInfo resolveInfo = resolveInfos.get(i);
            if (resolveInfo == null) {
                Log.e(TAG, "Found a null resolve info");
                continue;
            }

            android.app.WallpaperInfo wallpaperInfo;
            try {
                wallpaperInfo = new android.app.WallpaperInfo(context, resolveInfo);
            } catch (XmlPullParserException e) {
                Log.w(TAG, "Skipping wallpaper " + resolveInfo.serviceInfo, e);
                continue;
            } catch (IOException e) {
                Log.w(TAG, "Skipping wallpaper " + resolveInfo.serviceInfo, e);
                continue;
            }

            if (!packageName.equals(wallpaperInfo.getPackageName())) {
                continue;
            }

            wallpaperInfos.add(
                    factory.getLiveWallpaperInfo(wallpaperInfo, shouldShowTitle, collectionId));
        }

        return wallpaperInfos;
    }

    /**
     * Returns ResolveInfo objects for all live wallpaper services with the specified fully qualified
     * service names, keeping order intact.
     */
    private static List<ResolveInfo> getAllContainingServiceNames(Context context,
                                                                  List<String> serviceNames) {
        final PackageManager pm = context.getPackageManager();

        List<ResolveInfo> allResolveInfos = pm.queryIntentServices(
                new Intent(WallpaperService.SERVICE_INTERFACE),
                PackageManager.GET_META_DATA);

        // Filter ALL live wallpapers for only those in the list of specified service names.
        // Prefer this approach so we can make only one call to PackageManager (expensive!) rather than
        // one call per live wallpaper.
        ResolveInfo[] specifiedResolveInfos = new ResolveInfo[serviceNames.size()];
        for (ResolveInfo resolveInfo : allResolveInfos) {
            int index = serviceNames.indexOf(resolveInfo.serviceInfo.name);
            if (index != -1) {
                specifiedResolveInfos[index] = resolveInfo;
            }
        }

        return Arrays.asList(specifiedResolveInfos);
    }

    /**
     * Returns ResolveInfo objects for all live wallpaper services installed on the device. System
     * wallpapers are listed first, unsorted, with other installed wallpapers following sorted
     * in alphabetical order.
     */
    private static List<ResolveInfo> getAllOnDevice(Context context) {
        final PackageManager pm = context.getPackageManager();
        final String packageName = context.getPackageName();

        List<ResolveInfo> resolveInfos = pm.queryIntentServices(
                new Intent(WallpaperService.SERVICE_INTERFACE),
                PackageManager.GET_META_DATA);

        List<ResolveInfo> wallpaperInfos = new ArrayList<>();

        // Remove the "Rotating Image Wallpaper" live wallpaper, which is owned by this package,
        // and separate system wallpapers to sort only non-system ones.
        Iterator<ResolveInfo> iter = resolveInfos.iterator();
        while (iter.hasNext()) {
            ResolveInfo resolveInfo = iter.next();
            if (packageName.equals(resolveInfo.serviceInfo.packageName)) {
                iter.remove();
            } else if (isSystemApp(resolveInfo.serviceInfo.applicationInfo)) {
                wallpaperInfos.add(resolveInfo);
                iter.remove();
            }
        }

        if (resolveInfos.isEmpty()) {
            return wallpaperInfos;
        }

        // Sort non-system wallpapers alphabetically and append them to system ones
        Collections.sort(resolveInfos, new Comparator<ResolveInfo>() {
            final Collator mCollator = Collator.getInstance();

            @Override
            public int compare(ResolveInfo info1, ResolveInfo info2) {
                return mCollator.compare(info1.loadLabel(pm), info2.loadLabel(pm));
            }
        });
        wallpaperInfos.addAll(resolveInfos);

        return wallpaperInfos;
    }

    /**
     * @return whether the given app is a system app
     */
    public static boolean isSystemApp(ApplicationInfo appInfo) {
        return (appInfo.flags & (ApplicationInfo.FLAG_SYSTEM
                | ApplicationInfo.FLAG_UPDATED_SYSTEM_APP)) != 0;
    }

    public void setVisibleTitle(boolean visibleTitle) {
        mVisibleTitle = visibleTitle;
    }

    @Override
    public String getTitle(Context context) {
        if (mVisibleTitle) {
            CharSequence labelCharSeq = mInfo.loadLabel(context.getPackageManager());
            return labelCharSeq == null ? null : labelCharSeq.toString();
        }
        return null;
    }

    @Override
    public List<String> getAttributions(Context context) {
        List<String> attributions = new ArrayList<>();
        PackageManager packageManager = context.getPackageManager();
        CharSequence labelCharSeq = mInfo.loadLabel(packageManager);
        attributions.add(labelCharSeq == null ? null : labelCharSeq.toString());

        try {
            CharSequence authorCharSeq = mInfo.loadAuthor(packageManager);
            if (authorCharSeq != null) {
                String author = authorCharSeq.toString();
                attributions.add(author);
            }
        } catch (Resources.NotFoundException e) {
            // No author specified, so no other attribution to add.
        }

        try {
            CharSequence descCharSeq = mInfo.loadDescription(packageManager);
            if (descCharSeq != null) {
                String desc = descCharSeq.toString();
                attributions.add(desc);
            }
        } catch (Resources.NotFoundException e) {
            // No description specified, so no other attribution to add.
        }

        return attributions;
    }

    @Override
    public String getActionUrl(Context context) {
        try {
            Uri wallpaperContextUri = mInfo.loadContextUri(context.getPackageManager());
            return wallpaperContextUri != null ? wallpaperContextUri.toString() : null;
        } catch (Resources.NotFoundException e) {
            return null;
        }
    }

    /**
     * Get an optional description for the action button if provided by this LiveWallpaper.
     */
    @Nullable
    public CharSequence getActionDescription(Context context) {
        try {
            return mInfo.loadContextDescription(context.getPackageManager());
        } catch (Resources.NotFoundException e) {
            return null;
        }
    }

    @Override
    public Asset getAsset(Context context) {
        return null;
    }

    @Override
    public Asset getThumbAsset(Context context) {
        if (mThumbAsset == null) {
            mThumbAsset = new LiveWallpaperThumbAsset(context, mInfo);
        }
        return mThumbAsset;
    }

    @Override
    public void showPreview(Activity srcActivity, InlinePreviewIntentFactory factory,
                            int requestCode, boolean isAssetIdPresent) {
        //Only use internal live picker if available, otherwise, default to the Framework one
        if (factory.shouldUseInternalLivePicker(srcActivity)) {
            srcActivity.startActivityForResult(factory.newIntent(srcActivity, this,
                    isAssetIdPresent), requestCode);
        } else {
            Intent preview = new Intent(WallpaperManager.ACTION_CHANGE_LIVE_WALLPAPER);
            preview.putExtra(WallpaperManager.EXTRA_LIVE_WALLPAPER_COMPONENT, mInfo.getComponent());
            ActivityUtils.startActivityForResultSafely(srcActivity, preview, requestCode);
        }
    }

    @Override
    public void writeToParcel(Parcel parcel, int i) {
        super.writeToParcel(parcel, i);
        parcel.writeParcelable(mInfo, 0 /* flags */);
        parcel.writeInt(mVisibleTitle ? 1 : 0);
        parcel.writeString(mCollectionId);
    }

    @Override
    public android.app.WallpaperInfo getWallpaperComponent() {
        return mInfo;
    }

    @Override
    public String getCollectionId(Context context) {
        return TextUtils.isEmpty(mCollectionId)
                ? context.getString(R.string.live_wallpaper_collection_id)
                : mCollectionId;
    }

    @Override
    public String getWallpaperId() {
        return mInfo.getServiceName();
    }

    /**
     * Returns true if this wallpaper is currently applied to either home or lock screen.
     */
    public boolean isApplied(@Nullable android.app.WallpaperInfo currentHomeWallpaper,
            @Nullable android.app.WallpaperInfo currentLockWallpaper) {
        android.app.WallpaperInfo component = getWallpaperComponent();
        if (component == null) {
            return false;
        }
        String serviceName = component.getServiceName();
        boolean isAppliedToHome = currentHomeWallpaper != null
                && TextUtils.equals(currentHomeWallpaper.getServiceName(), serviceName);
        boolean isAppliedToLock = currentLockWallpaper != null
                && TextUtils.equals(currentLockWallpaper.getServiceName(), serviceName);
        return isAppliedToHome || isAppliedToLock;
    }

    /**
     * Saves a wallpaper of type LiveWallpaperInfo at a particular destination.
     * The default implementation simply returns the current wallpaper, but this can be overridden
     * as per requirement.
     *
     * @param context context of the calling activity
     * @param destination destination of the wallpaper being saved
     * @return saved LiveWallpaperInfo object
     */
    public LiveWallpaperInfo saveWallpaper(Context context, int destination) {
        return this;
    }
}
