/*
 * 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.module;

import static android.app.WallpaperManager.FLAG_LOCK;
import static android.app.WallpaperManager.FLAG_SYSTEM;

import android.annotation.Nullable;
import android.annotation.SuppressLint;
import android.app.WallpaperManager;
import android.content.ContentProviderClient;
import android.content.Context;
import android.database.Cursor;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Point;
import android.graphics.Rect;
import android.graphics.drawable.BitmapDrawable;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Bundle;
import android.os.ParcelFileDescriptor;
import android.os.RemoteException;
import android.util.Log;

import com.android.wallpaper.R;
import com.android.wallpaper.asset.BitmapUtils;
import com.android.wallpaper.model.CreativeCategory;
import com.android.wallpaper.model.LiveWallpaperMetadata;
import com.android.wallpaper.model.WallpaperInfoContract;
import com.android.wallpaper.model.WallpaperMetadata;
import com.android.wallpaper.picker.customization.data.content.WallpaperClient;
import com.android.wallpaper.picker.customization.shared.model.WallpaperDestination;
import com.android.wallpaper.util.DisplayUtils;

import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;

/**
 * Default implementation of {@link WallpaperRefresher} which refreshes wallpaper metadata
 * asynchronously.
 */
@SuppressLint("ServiceCast")
public class DefaultWallpaperRefresher implements WallpaperRefresher {

    private static final String TAG = "DefaultWPRefresher";

    private final Context mAppContext;
    private final WallpaperPreferences mWallpaperPreferences;
    private final WallpaperManager mWallpaperManager;
    private final WallpaperStatusChecker mWallpaperStatusChecker;

    private final DisplayUtils mDisplayUtils;

    private final WallpaperClient mWallpaperClient;

    private final Executor mExecutor = Executors.newCachedThreadPool();


    /**
     * @param context The application's context.
     */
    public DefaultWallpaperRefresher(Context context) {
        mAppContext = context.getApplicationContext();

        Injector injector = InjectorProvider.getInjector();
        mWallpaperPreferences = injector.getPreferences(mAppContext);
        mWallpaperStatusChecker = injector.getWallpaperStatusChecker(context);
        mDisplayUtils = injector.getDisplayUtils(mAppContext);
        mWallpaperClient = injector.getWallpaperClient(mAppContext);

        // Retrieve WallpaperManager using Context#getSystemService instead of
        // WallpaperManager#getInstance so it can be mocked out in test.
        mWallpaperManager = (WallpaperManager) context.getSystemService(Context.WALLPAPER_SERVICE);
    }

    @Override
    public void refresh(RefreshListener listener) {
        GetWallpaperMetadataAsyncTask task = new GetWallpaperMetadataAsyncTask(listener);
        task.executeOnExecutor(mExecutor);
    }

    /**
     * Retrieves the current wallpaper's thumbnail and metadata off the UI thread.
     */
    private class GetWallpaperMetadataAsyncTask extends
            AsyncTask<Void, Void, List<WallpaperMetadata>> {

        private final RefreshListener mListener;
        private final WallpaperManager mWallpaperManager;

        private long mCurrentHomeWallpaperHashCode;
        private long mCurrentLockWallpaperHashCode;
        private String mSystemWallpaperServiceName;

        @SuppressLint("ServiceCast")
        public GetWallpaperMetadataAsyncTask(RefreshListener listener) {
            mListener = listener;
            mWallpaperManager = WallpaperManager.getInstance(mAppContext);
        }

        @Override
        protected List<WallpaperMetadata> doInBackground(Void... unused) {
            List<WallpaperMetadata> wallpaperMetadatas = new ArrayList<>();

            boolean isHomeScreenStatic = mWallpaperManager.getWallpaperInfo(FLAG_SYSTEM) == null;
            if (!isHomeScreenMetadataCurrent() || (isHomeScreenStatic
                    && isHomeScreenAttributionsEmpty())) {
                mWallpaperPreferences.clearHomeWallpaperMetadata();
                setFallbackHomeScreenWallpaperMetadata();
            }

            boolean isLockScreenWallpaperCurrentlySet =
                    mWallpaperStatusChecker.isLockWallpaperSet();

            if (mWallpaperManager.getWallpaperInfo() == null) {
                wallpaperMetadatas.add(new WallpaperMetadata(
                        mWallpaperPreferences.getHomeWallpaperAttributions(),
                        mWallpaperPreferences.getHomeWallpaperActionUrl(),
                        mWallpaperPreferences.getHomeWallpaperCollectionId(),
                        /* wallpaperComponent= */ null,
                        getCurrentWallpaperCropHints(FLAG_SYSTEM)));
            } else {
                android.app.WallpaperInfo info = mWallpaperManager.getWallpaperInfo();
                Uri previewUri = getCreativePreviewUri(mAppContext, info,
                        WallpaperDestination.HOME);
                wallpaperMetadatas.add(new LiveWallpaperMetadata(info, previewUri));
            }

            // Return only home metadata if pre-N device or lock screen wallpaper is not explicitly
            // set.
            if (!isLockScreenWallpaperCurrentlySet) {
                return wallpaperMetadatas;
            }

            boolean isLockScreenStatic = mWallpaperManager.getWallpaperInfo(FLAG_LOCK) == null;
            if (!isLockScreenMetadataCurrent() || (isLockScreenStatic
                    && isLockScreenAttributionsEmpty())) {
                mWallpaperPreferences.clearLockWallpaperMetadata();
                setFallbackLockScreenWallpaperMetadata();
            }

            if (isLockScreenStatic) {
                wallpaperMetadatas.add(new WallpaperMetadata(
                        mWallpaperPreferences.getLockWallpaperAttributions(),
                        mWallpaperPreferences.getLockWallpaperActionUrl(),
                        mWallpaperPreferences.getLockWallpaperCollectionId(),
                        /* wallpaperComponent= */ null,
                        getCurrentWallpaperCropHints(FLAG_LOCK)));
            } else {
                android.app.WallpaperInfo info = mWallpaperManager.getWallpaperInfo(FLAG_LOCK);
                Uri previewUri = getCreativePreviewUri(mAppContext, info,
                        WallpaperDestination.LOCK);
                wallpaperMetadatas.add(new LiveWallpaperMetadata(info, previewUri));
            }

            return wallpaperMetadatas;
        }

        @Override
        protected void onPostExecute(List<WallpaperMetadata> metadatas) {
            if (metadatas.size() > 2) {
                Log.e(TAG,
                        "Got more than 2 WallpaperMetadata objects - only home and (optionally) "
                                + "lock are permitted.");
                return;
            }

            mListener.onRefreshed(metadatas.get(0), metadatas.size() > 1 ? metadatas.get(1) : null,
                    mWallpaperPreferences.getWallpaperPresentationMode());
        }

        /**
         * Sets fallback wallpaper attributions to WallpaperPreferences when the saved metadata did
         * not match the system wallpaper. For live wallpapers, loads the label (title) but for
         * image wallpapers loads a generic title string.
         */
        private void setFallbackHomeScreenWallpaperMetadata() {
            android.app.WallpaperInfo wallpaperComponent = mWallpaperManager.getWallpaperInfo();
            if (wallpaperComponent == null) { // Image wallpaper
                mWallpaperPreferences.setHomeWallpaperAttributions(
                        Arrays.asList(mAppContext.getResources()
                                .getString(R.string.fallback_wallpaper_title)));

                mWallpaperPreferences.setHomeWallpaperManagerId(
                        mWallpaperManager.getWallpaperId(FLAG_SYSTEM));
            } else { // Live wallpaper
                mWallpaperPreferences.setHomeWallpaperAttributions(Arrays.asList(
                        wallpaperComponent.loadLabel(mAppContext.getPackageManager()).toString()));
                mWallpaperPreferences.setHomeWallpaperServiceName(mSystemWallpaperServiceName);
            }

            // Disable rotation wallpaper when setting fallback home screen wallpaper
            // Daily rotation wallpaper only rotates the home screen wallpaper
            mWallpaperPreferences.setWallpaperPresentationMode(
                    WallpaperPreferences.PRESENTATION_MODE_STATIC);
            mWallpaperPreferences.clearDailyRotations();
        }

        /**
         * Sets fallback lock screen wallpaper attributions to WallpaperPreferences. This should be
         * called when the saved lock screen wallpaper metadata does not match the currently set
         * lock screen wallpaper.
         */
        private void setFallbackLockScreenWallpaperMetadata() {
            mWallpaperPreferences.setLockWallpaperAttributions(
                    Arrays.asList(mAppContext.getResources()
                            .getString(R.string.fallback_wallpaper_title)));
            mWallpaperPreferences.setLockWallpaperManagerId(mWallpaperManager.getWallpaperId(
                    FLAG_LOCK));
        }

        /**
         * Returns whether the home screen metadata saved in WallpaperPreferences corresponds to the
         * current system wallpaper.
         */
        private boolean isHomeScreenMetadataCurrent() {
            return (mWallpaperManager.getWallpaperInfo() == null)
                    ? isHomeScreenImageWallpaperCurrent()
                    : isHomeScreenLiveWallpaperCurrent();
        }

        /**
         * Returns whether the home screen attributions saved in WallpaperPreferences is empty.
         */
        private boolean isHomeScreenAttributionsEmpty() {
            List<String> homeScreenAttributions =
                    mWallpaperPreferences.getHomeWallpaperAttributions();
            return homeScreenAttributions.get(0) == null
                    && homeScreenAttributions.get(1) == null
                    && homeScreenAttributions.get(2) == null;
        }

        private long getCurrentHomeWallpaperHashCode() {
            if (mCurrentHomeWallpaperHashCode == 0) {
                BitmapDrawable wallpaperDrawable = (BitmapDrawable) mWallpaperManager.getDrawable();
                // wallpaperDrawable should always be non-null, unless if there's a error in
                // WallpaperManager's state, in which case we'll consider the hashcode as unset.
                Bitmap wallpaperBitmap = wallpaperDrawable != null ? wallpaperDrawable.getBitmap()
                        : null;
                mCurrentHomeWallpaperHashCode =
                        wallpaperBitmap != null ? BitmapUtils.generateHashCode(wallpaperBitmap) : 0;

                // Manually request that WallpaperManager loses its reference to the current
                // wallpaper bitmap, which can occupy a large memory allocation for the lifetime of
                // the app.
                mWallpaperManager.forgetLoadedWallpaper();
            }
            return mCurrentHomeWallpaperHashCode;
        }

        private long getCurrentLockWallpaperHashCode() {
            if (mCurrentLockWallpaperHashCode == 0
                    && mWallpaperStatusChecker.isLockWallpaperSet()) {
                Bitmap wallpaperBitmap = getLockWallpaperBitmap();
                // If isLockWallpaperSet() returned true, wallpaperBitmap should always be
                // non-null, unless if there's a error in WallpaperManager, in which case we'll
                // consider the hashcode as unset.
                mCurrentLockWallpaperHashCode =
                        wallpaperBitmap != null ? BitmapUtils.generateHashCode(wallpaperBitmap) : 0;
            }
            return mCurrentLockWallpaperHashCode;
        }

        /**
         * Returns the lock screen wallpaper currently set on the device as a Bitmap, or null if no
         * lock screen wallpaper is set.
         */
        private Bitmap getLockWallpaperBitmap() {
            Bitmap lockBitmap = null;

            ParcelFileDescriptor pfd = mWallpaperManager.getWallpaperFile(FLAG_LOCK);
            // getWallpaperFile returns null if the lock screen isn't explicitly set, so need this
            // check.
            if (pfd != null) {
                InputStream fileStream = null;
                try {
                    fileStream = new FileInputStream(pfd.getFileDescriptor());
                    lockBitmap = BitmapFactory.decodeStream(fileStream);
                    pfd.close();
                    return lockBitmap;
                } catch (IOException e) {
                    Log.e(TAG, "IO exception when closing the file descriptor.");
                } finally {
                    if (fileStream != null) {
                        try {
                            fileStream.close();
                        } catch (IOException e) {
                            Log.e(TAG,
                                    "IO exception when closing input stream for lock screen WP.");
                        }
                    }
                }
            }

            return lockBitmap;
        }

        /**
         * Returns whether the image wallpaper set to the system matches the metadata in
         * WallpaperPreferences.
         */
        private boolean isHomeScreenImageWallpaperCurrent() {
            return mWallpaperPreferences.getHomeWallpaperManagerId()
                    == mWallpaperManager.getWallpaperId(FLAG_SYSTEM);
        }

        /**
         * Returns whether the live wallpaper set to the system's home screen matches the metadata
         * in WallpaperPreferences.
         */
        private boolean isHomeScreenLiveWallpaperCurrent() {
            mSystemWallpaperServiceName = mWallpaperManager.getWallpaperInfo().getServiceName();
            String homeWallpaperServiceName = mWallpaperPreferences.getHomeWallpaperServiceName();
            return mSystemWallpaperServiceName.equals(homeWallpaperServiceName);
        }

        /**
         * Returns whether the lock screen metadata saved in WallpaperPreferences corresponds to the
         * current lock screen wallpaper.
         */
        private boolean isLockScreenMetadataCurrent() {
            return (mWallpaperManager.getWallpaperInfo(FLAG_LOCK) == null)
                    ? isLockScreenImageWallpaperCurrent()
                    : isLockScreenLiveWallpaperCurrent();
        }

        /**
         * Returns whether the image wallpaper set for the lock screen matches the metadata in
         * WallpaperPreferences.
         */
        private boolean isLockScreenImageWallpaperCurrent() {
            // Check for lock wallpaper image same-ness only when there is no stored lock wallpaper
            // hash code. Otherwise if there is a lock wallpaper hash code stored in
            // {@link WallpaperPreferences}, then check hash codes.
            long savedLockWallpaperHash = mWallpaperPreferences.getLockWallpaperHashCode();

            if (savedLockWallpaperHash == 0) {
                return mWallpaperPreferences.getLockWallpaperManagerId()
                        == mWallpaperManager.getWallpaperId(FLAG_LOCK);
            } else {
                return savedLockWallpaperHash == getCurrentLockWallpaperHashCode();
            }
        }

        /**
         * Returns whether the live wallpaper for the home screen matches the metadata in
         * WallpaperPreferences.
         */
        private boolean isLockScreenLiveWallpaperCurrent() {
            String currentServiceName = mWallpaperManager.getWallpaperInfo(FLAG_LOCK)
                    .getServiceName();
            String storedServiceName = mWallpaperPreferences.getLockWallpaperServiceName();
            return currentServiceName.equals(storedServiceName);
        }


        /**
         * Returns whether the lock screen attributions saved in WallpaperPreferences are empty.
         */
        private boolean isLockScreenAttributionsEmpty() {
            List<String> attributions = mWallpaperPreferences.getLockWallpaperAttributions();
            return attributions.get(0) == null
                    && attributions.get(1) == null
                    && attributions.get(2) == null;
        }

        private Map<Point, Rect> getCurrentWallpaperCropHints(
                @WallpaperManager.SetWallpaperFlags int which) {
            List<Point> displaySizes = mDisplayUtils
                    .getInternalDisplaySizes(/* allDimensions= */ true);
            return mWallpaperClient.getCurrentCropHints(displaySizes, which);
        }
    }

    // Queries a live wallpaper for its preview Uri, and returns it if it exists.
    private static @Nullable Uri getCreativePreviewUri(Context context,
            android.app.WallpaperInfo info,
            WallpaperDestination destination) {
        Bundle metaData = info.getServiceInfo().metaData;
        String uri = metaData.getString(
                CreativeCategory.KEY_WALLPAPER_SAVE_CREATIVE_WALLPAPER_CURRENT);
        if (uri == null) {
            return null;
        }
        Uri currentAssetsUri = Uri.parse(uri);
        try (ContentProviderClient client = context.getContentResolver()
                .acquireContentProviderClient(currentAssetsUri.getAuthority())) {
            if (client == null) {
                return null;
            }
            try (Cursor cursor = client.query(currentAssetsUri, null, null, null, null)) {
                if (cursor == null || !cursor.moveToFirst()) {
                    return null;
                }
                do {
                    String dest = cursor.getString(
                            cursor.getColumnIndex(WallpaperInfoContract.CURRENT_DESTINATION));
                    Uri previewUri = Uri.parse(cursor.getString(
                            cursor.getColumnIndex(
                                    WallpaperInfoContract.CURRENT_CONFIG_PREVIEW_URI)));
                    if ((dest.equals("home") && destination == WallpaperDestination.HOME)
                            || (dest.equals("lock") && destination == WallpaperDestination.LOCK)) {
                        return previewUri;
                    }
                } while (cursor.moveToNext());
            } catch (RemoteException e) {
                Log.w(TAG, "Error retrieving current creative asset id: ", e);
            }
        }
        return null;
    }
}
