/*
 * Copyright (C) 2020 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.systemui.plugin.globalactions.wallet;

import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.res.Resources;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.Icon;
import android.os.Handler;
import android.os.UserHandle;
import android.os.Looper;
import android.service.quickaccesswallet.GetWalletCardsError;
import android.service.quickaccesswallet.GetWalletCardsRequest;
import android.service.quickaccesswallet.GetWalletCardsResponse;
import android.service.quickaccesswallet.QuickAccessWalletClient;
import android.service.quickaccesswallet.SelectWalletCardRequest;
import android.service.quickaccesswallet.WalletCard;
import android.service.quickaccesswallet.WalletServiceEvent;
import android.text.TextUtils;
import android.util.Log;
import android.view.View;
import android.widget.FrameLayout;

import com.android.systemui.plugin.globalactions.wallet.WalletPopupMenu.OverflowItem;
import com.android.systemui.plugins.GlobalActionsPanelPlugin;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

public class WalletPanelViewController implements
        GlobalActionsPanelPlugin.PanelViewController,
        WalletCardCarousel.OnSelectionListener,
        QuickAccessWalletClient.OnWalletCardsRetrievedCallback,
        QuickAccessWalletClient.WalletServiceEventListener {

    private static final String TAG = "WalletPanelViewCtrl";
    private static final int MAX_CARDS = 10;
    private static final long SELECTION_DELAY_MILLIS = TimeUnit.SECONDS.toMillis(30);
    private static final String PREFS_WALLET_VIEW_HEIGHT = "wallet_view_height";
    private static final String PREFS_HAS_CARDS = "has_cards";
    private static final String SETTINGS_PKG = "com.android.settings";
    private static final String SETTINGS_ACTION = SETTINGS_PKG + ".GLOBAL_ACTIONS_PANEL_SETTINGS";
    private final Context mSysuiContext;
    private final Context mPluginContext;
    private final QuickAccessWalletClient mWalletClient;
    private final WalletView mWalletView;
    private final WalletCardCarousel mWalletCardCarousel;
    private final GlobalActionsPanelPlugin.Callbacks mPluginCallbacks;
    private final ExecutorService mExecutor;
    private final Handler mHandler;
    private final Runnable mSelectionRunnable = this::selectCard;
    private final SharedPreferences mPrefs;
    private boolean mIsDeviceLocked;
    private boolean mIsDismissed;
    private boolean mHasRegisteredListener;
    private String mSelectedCardId;

    public WalletPanelViewController(
            Context sysuiContext,
            Context pluginContext,
            QuickAccessWalletClient walletClient,
            GlobalActionsPanelPlugin.Callbacks pluginCallbacks,
            boolean isDeviceLocked) {
        mSysuiContext = sysuiContext;
        mPluginContext = pluginContext;
        mWalletClient = walletClient;
        mPrefs = mSysuiContext.getSharedPreferences(TAG, Context.MODE_PRIVATE);
        mPluginCallbacks = pluginCallbacks;
        mIsDeviceLocked = isDeviceLocked;
        mWalletView = new WalletView(pluginContext);
        mWalletView.setMinimumHeight(getExpectedMinHeight());
        mWalletView.setLayoutParams(
                new FrameLayout.LayoutParams(
                        FrameLayout.LayoutParams.MATCH_PARENT,
                        FrameLayout.LayoutParams.WRAP_CONTENT));
        mWalletCardCarousel = mWalletView.getCardCarousel();
        mWalletCardCarousel.setSelectionListener(this);
        mHandler = new Handler(Looper.myLooper());
        mExecutor = Executors.newSingleThreadExecutor();
        if (!mPrefs.getBoolean(PREFS_HAS_CARDS, false)) {
            // The empty state view is shown preemptively when cards were not returned last time
            // to decrease perceived latency.
            showEmptyStateView();
        }
    }

    /**
     * Implements {@link GlobalActionsPanelPlugin.PanelViewController}. Returns the {@link View}
     * containing the Quick Access Wallet.
     */
    @Override
    public View getPanelContent() {
        return mWalletView;
    }

    /**
     * Implements {@link GlobalActionsPanelPlugin.PanelViewController}. Invoked when the view
     * containing the Quick Access Wallet is dismissed.
     */
    @Override
    public void onDismissed() {
        if (mIsDismissed) {
            return;
        }
        mIsDismissed = true;
        mSelectedCardId = null;
        mHandler.removeCallbacks(mSelectionRunnable);
        mWalletClient.notifyWalletDismissed();
        mWalletClient.removeWalletServiceEventListener(this);
        mWalletView.animateDismissal();
    }

    /**
     * Implements {@link GlobalActionsPanelPlugin.PanelViewController}. Invoked when the device is
     * either locked or unlocked while the wallet is visible.
     */
    @Override
    public void onDeviceLockStateChanged(boolean deviceLocked) {
        if (mIsDismissed || mIsDeviceLocked == deviceLocked || !mIsDeviceLocked) {
            // Disregard repeat events and events after unlock
            return;
        }
        mIsDeviceLocked = deviceLocked;
        // Cards are re-queried because the wallet application may wish to change card art, icons,
        // text, or other attributes depending on the lock state of the device.
        queryWalletCards();
    }

    /**
     * Query wallet cards from the client and display them on screen.
     */
    void queryWalletCards() {
        if (mIsDismissed) {
            return;
        }
        if (!mHasRegisteredListener) {
            // Listener is registered even when device is locked. Should only be registered once.
            mWalletClient.addWalletServiceEventListener(this);
            mHasRegisteredListener = true;
        }
        if (mIsDeviceLocked && !mWalletClient.isWalletFeatureAvailableWhenDeviceLocked()) {
            mWalletView.hide();
            return;
        }
        mWalletView.show();
        mWalletView.hideErrorMessage();
        int cardWidthPx = mWalletCardCarousel.getCardWidthPx();
        int cardHeightPx = mWalletCardCarousel.getCardHeightPx();
        int iconSizePx = mWalletView.getIconSizePx();
        GetWalletCardsRequest request =
                new GetWalletCardsRequest(cardWidthPx, cardHeightPx, iconSizePx, MAX_CARDS);
        mWalletClient.getWalletCards(mExecutor, request, this);
    }

    /**
     * Implements {@link QuickAccessWalletClient.OnWalletCardsRetrievedCallback}. Called when cards
     * are retrieved successfully from the service. This is called on {@link #mExecutor}.
     */
    @Override
    public void onWalletCardsRetrieved(GetWalletCardsResponse response) {
        if (mIsDismissed) {
            return;
        }
        List<WalletCard> walletCards = response.getWalletCards();
        List<WalletCardViewInfo> data = new ArrayList<>(walletCards.size());
        for (WalletCard card : walletCards) {
            data.add(new QAWalletCardViewInfo(card));
        }

        // Get on main thread for UI updates
        mWalletView.post(() -> {
            if (mIsDismissed) {
                return;
            }
            if (data.isEmpty()) {
                showEmptyStateView();
            } else {
                mWalletView.showCardCarousel(data, response.getSelectedIndex(), getOverflowItems());
            }
            // The empty state view will not be shown preemptively next time if cards were returned
            mPrefs.edit().putBoolean(PREFS_HAS_CARDS, !data.isEmpty()).apply();
            removeMinHeightAndRecordHeightOnLayout();
        });
    }

    /**
     * Implements {@link QuickAccessWalletClient.OnWalletCardsRetrievedCallback}. Called when there
     * is an error during card retrieval. This will be run on the {@link #mExecutor}.
     */
    @Override
    public void onWalletCardRetrievalError(GetWalletCardsError error) {
        mWalletView.post(() -> {
            if (mIsDismissed) {
                return;
            }
            mWalletView.showErrorMessage(error.getMessage());
        });
    }

    /**
     * Implements {@link QuickAccessWalletClient.WalletServiceEventListener}. Called when the wallet
     * application propagates an event, such as an NFC tap, to the quick access wallet view.
     */
    @Override
    public void onWalletServiceEvent(WalletServiceEvent event) {
        if (mIsDismissed) {
            return;
        }
        switch (event.getEventType()) {
            case WalletServiceEvent.TYPE_NFC_PAYMENT_STARTED:
                mPluginCallbacks.dismissGlobalActionsMenu();
                onDismissed();
                break;
            case WalletServiceEvent.TYPE_WALLET_CARDS_UPDATED:
                queryWalletCards();
                break;
            default:
                Log.w(TAG, "onWalletServiceEvent: Unknown event type");
        }
    }

    /**
     * Implements {@link WalletCardCarousel.OnSelectionListener}. Called when the user selects a
     * card from the carousel by scrolling to it.
     */
    @Override
    public void onCardSelected(WalletCardViewInfo card) {
        if (mIsDismissed) {
            return;
        }
        mSelectedCardId = card.getCardId();
        selectCard();
    }

    private void selectCard() {
        mHandler.removeCallbacks(mSelectionRunnable);
        String selectedCardId = mSelectedCardId;
        if (mIsDismissed || selectedCardId == null) {
            return;
        }
        mWalletClient.selectWalletCard(new SelectWalletCardRequest(selectedCardId));
        // Re-selecting the card keeps the connection bound so we continue to get service events
        // even if the user keeps it open for a long time.
        mHandler.postDelayed(mSelectionRunnable, SELECTION_DELAY_MILLIS);
    }

    /**
     * Implements {@link WalletCardCarousel.OnSelectionListener}. Called when the user clicks on a
     * card.
     */
    @Override
    public void onCardClicked(WalletCardViewInfo card) {
        if (mIsDismissed) {
            return;
        }
        PendingIntent pendingIntent = ((QAWalletCardViewInfo) card).mWalletCard.getPendingIntent();
        startPendingIntent(pendingIntent);
    }

    private OverflowItem[] getOverflowItems() {
        CharSequence walletLabel = mWalletClient.getShortcutShortLabel();
        Intent walletIntent = mWalletClient.createWalletIntent();
        CharSequence settingsLabel = mPluginContext.getString(R.string.settings);
        Intent settingsIntent = new Intent(SETTINGS_ACTION).setPackage(SETTINGS_PKG);
        OverflowItem settings = new OverflowItem(settingsLabel, () -> startIntent(settingsIntent));
        if (!TextUtils.isEmpty(walletLabel) && walletIntent != null) {
            OverflowItem wallet = new OverflowItem(walletLabel, () -> startIntent(walletIntent));
            return new OverflowItem[]{wallet, settings};
        } else {
            return new OverflowItem[]{settings};
        }
    }

    private void showEmptyStateView() {
        Drawable logo = mWalletClient.getLogo();
        CharSequence logoContentDesc = mWalletClient.getServiceLabel();
        CharSequence label = mWalletClient.getShortcutLongLabel();
        Intent intent = mWalletClient.createWalletIntent();
        if (logo == null
                || TextUtils.isEmpty(logoContentDesc)
                || TextUtils.isEmpty(label)
                || intent == null) {
            Log.w(TAG, "QuickAccessWalletService manifest entry mis-configured");
            // Issue is not likely to be resolved until manifest entries are enabled.
            // Hide wallet feature until then.
            mWalletView.hide();
            mPrefs.edit().putInt(PREFS_WALLET_VIEW_HEIGHT, 0).apply();
        } else {
            mWalletView.showEmptyStateView(logo, logoContentDesc, label, v -> startIntent(intent));
        }
    }

    private void startIntent(Intent intent) {
        PendingIntent pendingIntent = PendingIntent.getActivity(mSysuiContext, 0, intent,
                PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_ONE_SHOT);
        startPendingIntent(pendingIntent);
    }

    private void startPendingIntent(PendingIntent pendingIntent) {
        mPluginCallbacks.startPendingIntentDismissingKeyguard(pendingIntent);
        mPluginCallbacks.dismissGlobalActionsMenu();
        onDismissed();
    }

    /**
     * The total view height depends on whether cards are shown or not. Since it is not known at
     * construction time whether cards will be available, the best we can do is set the height to
     * whatever it was the last time. Setting the height correctly ahead of time is important
     * because Home Controls are shown below the wallet and may be displayed before card data is
     * loaded, causing the home controls to jump down when card data arrives.
     */
    private int getExpectedMinHeight() {
        int expectedHeight = mPrefs.getInt(PREFS_WALLET_VIEW_HEIGHT, -1);
        if (expectedHeight == -1) {
            Resources res = mPluginContext.getResources();
            expectedHeight = res.getDimensionPixelSize(R.dimen.min_wallet_empty_height);
        }
        return expectedHeight;
    }

    private void removeMinHeightAndRecordHeightOnLayout() {
        mWalletView.setMinimumHeight(0);
        mWalletView.addOnLayoutChangeListener(new View.OnLayoutChangeListener() {
            @Override
            public void onLayoutChange(View v, int left, int top, int right, int bottom,
                    int oldLeft, int oldTop, int oldRight, int oldBottom) {
                mWalletView.removeOnLayoutChangeListener(this);
                mPrefs.edit().putInt(PREFS_WALLET_VIEW_HEIGHT, bottom - top).apply();
            }
        });
    }

    private class QAWalletCardViewInfo implements WalletCardViewInfo {

        private final WalletCard mWalletCard;
        private final Drawable mCardDrawable;
        private final Drawable mIconDrawable;

        /**
         * Constructor is called on background executor, so it is safe to load drawables
         * synchronously.
         */
        QAWalletCardViewInfo(WalletCard walletCard) {
            mWalletCard = walletCard;
            Icon cardImage = mWalletCard.getCardImage();
            if (cardImage.getType() == Icon.TYPE_URI) {
                // Do not allow icon created with content URI.
                mCardDrawable = null;
            } else {
                mCardDrawable =
                    mWalletCard.getCardImage().loadDrawable(mPluginContext);
            }
            Icon icon = mWalletCard.getCardIcon();
            mIconDrawable = icon == null ? null : icon.loadDrawable(mPluginContext);
        }

        @Override
        public String getCardId() {
            return mWalletCard.getCardId();
        }

        @Override
        public Drawable getCardDrawable() {
            return mCardDrawable;
        }

        @Override
        public CharSequence getContentDescription() {
            return mWalletCard.getContentDescription();
        }

        @Override
        public Drawable getIcon() {
            return mIconDrawable;
        }

        @Override
        public CharSequence getText() {
            return mWalletCard.getCardLabel();
        }
    }
}
