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

import static androidx.lifecycle.Lifecycle.Event.ON_DESTROY;
import static androidx.lifecycle.Lifecycle.Event.ON_START;
import static androidx.lifecycle.Lifecycle.Event.ON_STOP;

import android.content.Context;
import android.os.Looper;
import android.provider.Settings;
import android.telephony.SubscriptionInfo;
import android.telephony.SubscriptionManager;
import android.util.Log;

import androidx.annotation.Keep;
import androidx.annotation.VisibleForTesting;
import androidx.lifecycle.Lifecycle;
import androidx.lifecycle.LifecycleObserver;
import androidx.lifecycle.OnLifecycleEvent;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

/**
 * A proxy to the subscription manager
 */
public class ProxySubscriptionManager implements LifecycleObserver {

    private static final String LOG_TAG = "ProxySubscriptionManager";

    private static final int LISTENER_END_OF_LIFE = -1;
    private static final int LISTENER_IS_INACTIVE = 0;
    private static final int LISTENER_IS_ACTIVE = 1;

    /**
     * Interface for monitor active subscriptions list changing
     */
    public interface OnActiveSubscriptionChangedListener {
        /**
         * When active subscriptions list get changed
         */
        void onChanged();
        /**
         * get Lifecycle of listener
         *
         * @return Returns Lifecycle.
         */
        default Lifecycle getLifecycle() {
            return null;
        }
    }

    /**
     * Get proxy instance to subscription manager
     *
     * @return proxy to subscription manager
     */
    public static ProxySubscriptionManager getInstance(Context context) {
        if (sSingleton != null) {
            return sSingleton;
        }
        sSingleton = new ProxySubscriptionManager(context.getApplicationContext());
        return sSingleton;
    }

    private static ProxySubscriptionManager sSingleton;

    private ProxySubscriptionManager(Context context) {
        final Looper looper = context.getMainLooper();

        ActiveSubscriptionsListener subscriptionMonitor = new ActiveSubscriptionsListener(
                looper, context) {
            public void onChanged() {
                notifySubscriptionInfoMightChanged();
            }
        };
        GlobalSettingsChangeListener airplaneModeMonitor = new GlobalSettingsChangeListener(
                looper, context, Settings.Global.AIRPLANE_MODE_ON) {
            public void onChanged(String field) {
                subscriptionMonitor.clearCache();
                notifySubscriptionInfoMightChanged();
            }
        };

        init(context, subscriptionMonitor, airplaneModeMonitor);
    }

    @Keep
    @VisibleForTesting
    protected void init(Context context, ActiveSubscriptionsListener activeSubscriptionsListener,
            GlobalSettingsChangeListener airplaneModeOnSettingsChangeListener) {

        mActiveSubscriptionsListeners =
                new ArrayList<OnActiveSubscriptionChangedListener>();
        mPendingNotifyListeners =
                new ArrayList<OnActiveSubscriptionChangedListener>();

        mSubscriptionMonitor = activeSubscriptionsListener;
        mAirplaneModeMonitor = airplaneModeOnSettingsChangeListener;

        mSubscriptionMonitor.start();
    }

    private Lifecycle mLifecycle;
    private ActiveSubscriptionsListener mSubscriptionMonitor;
    private GlobalSettingsChangeListener mAirplaneModeMonitor;

    private List<OnActiveSubscriptionChangedListener> mActiveSubscriptionsListeners;
    private List<OnActiveSubscriptionChangedListener> mPendingNotifyListeners;

    @Keep
    @VisibleForTesting
    protected void notifySubscriptionInfoMightChanged() {
        // create a merged list for processing all listeners
        List<OnActiveSubscriptionChangedListener> listeners =
                new ArrayList<OnActiveSubscriptionChangedListener>(mPendingNotifyListeners);
        listeners.addAll(mActiveSubscriptionsListeners);

        mActiveSubscriptionsListeners.clear();
        mPendingNotifyListeners.clear();
        processStatusChangeOnListeners(listeners);
    }

    /**
     * Lifecycle for data within proxy
     *
     * @param lifecycle life cycle to reference
     */
    public void setLifecycle(Lifecycle lifecycle) {
        if (mLifecycle == lifecycle) {
            return;
        }
        if (mLifecycle != null) {
            mLifecycle.removeObserver(this);
        }
        if (lifecycle != null) {
            lifecycle.addObserver(this);
        }
        mLifecycle = lifecycle;
        mAirplaneModeMonitor.notifyChangeBasedOn(lifecycle);
    }

    @OnLifecycleEvent(ON_START)
    void onStart() {
        mSubscriptionMonitor.start();

        // callback notify those listener(s) which back to active state
        List<OnActiveSubscriptionChangedListener> listeners = mPendingNotifyListeners;
        mPendingNotifyListeners = new ArrayList<OnActiveSubscriptionChangedListener>();
        processStatusChangeOnListeners(listeners);
    }

    @OnLifecycleEvent(ON_STOP)
    void onStop() {
        mSubscriptionMonitor.stop();
    }

    @OnLifecycleEvent(ON_DESTROY)
    void onDestroy() {
        mSubscriptionMonitor.close();
        mAirplaneModeMonitor.close();

        if (mLifecycle != null) {
            mLifecycle.removeObserver(this);
            mLifecycle = null;

            sSingleton = null;
        }
    }

    /**
     * Get SubscriptionManager
     *
     * @return a SubscriptionManager
     */
    public SubscriptionManager get() {
        return mSubscriptionMonitor.getSubscriptionManager();
    }

    /**
     * Get current max. number active subscription info(s) been setup within device
     *
     * @return max. number of active subscription info(s)
     */
    public int getActiveSubscriptionInfoCountMax() {
        return mSubscriptionMonitor.getActiveSubscriptionInfoCountMax();
    }

    /**
     * Get a list of active subscription info
     *
     * @return A list of active subscription info
     */
    public List<SubscriptionInfo> getActiveSubscriptionsInfo() {
        return mSubscriptionMonitor.getActiveSubscriptionsInfo();
    }

    /**
     * Get an active subscription info with given subscription ID
     *
     * @param subId target subscription ID
     * @return A subscription info which is active list
     */
    public SubscriptionInfo getActiveSubscriptionInfo(int subId) {
        return mSubscriptionMonitor.getActiveSubscriptionInfo(subId);
    }

    /**
     * Get a list of accessible subscription info
     *
     * @return A list of accessible subscription info
     */
    public List<SubscriptionInfo> getAccessibleSubscriptionsInfo() {
        return mSubscriptionMonitor.getAccessibleSubscriptionsInfo();
    }

    /**
     * Get an accessible subscription info with given subscription ID
     *
     * @param subId target subscription ID
     * @return A subscription info which is accessible list
     */
    public SubscriptionInfo getAccessibleSubscriptionInfo(int subId) {
        return mSubscriptionMonitor.getAccessibleSubscriptionInfo(subId);
    }

    /**
     * Clear data cached within proxy
     */
    public void clearCache() {
        mSubscriptionMonitor.clearCache();
    }

    /**
     * Add listener to active subscriptions monitor list.
     * Note: listener only take place when change happens.
     *       No immediate callback performed after the invoke of this method.
     *
     * @param listener listener to active subscriptions change
     */
    @Keep
    public void addActiveSubscriptionsListener(OnActiveSubscriptionChangedListener listener) {
        removeSpecificListenerAndCleanList(listener, mPendingNotifyListeners);
        removeSpecificListenerAndCleanList(listener, mActiveSubscriptionsListeners);
        if ((listener == null) || (getListenerState(listener) == LISTENER_END_OF_LIFE)) {
            return;
        }
        mActiveSubscriptionsListeners.add(listener);
    }

    /**
     * Remove listener from active subscriptions monitor list
     *
     * @param listener listener to active subscriptions change
     */
    @Keep
    public void removeActiveSubscriptionsListener(OnActiveSubscriptionChangedListener listener) {
        removeSpecificListenerAndCleanList(listener, mPendingNotifyListeners);
        removeSpecificListenerAndCleanList(listener, mActiveSubscriptionsListeners);
    }

    private int getListenerState(OnActiveSubscriptionChangedListener listener) {
        Lifecycle lifecycle = listener.getLifecycle();
        if (lifecycle == null) {
            return LISTENER_IS_ACTIVE;
        }
        Lifecycle.State lifecycleState = lifecycle.getCurrentState();
        if (lifecycleState == Lifecycle.State.DESTROYED) {
            Log.d(LOG_TAG, "Listener dead detected - " + listener);
            return LISTENER_END_OF_LIFE;
        }
        return lifecycleState.isAtLeast(Lifecycle.State.STARTED) ?
                LISTENER_IS_ACTIVE : LISTENER_IS_INACTIVE;
    }

    private void removeSpecificListenerAndCleanList(OnActiveSubscriptionChangedListener listener,
            List<OnActiveSubscriptionChangedListener> list) {
        // also drop listener(s) which is end of life
        list.removeIf(it -> (it == listener) || (getListenerState(it) == LISTENER_END_OF_LIFE));
    }

    private void processStatusChangeOnListeners(
            List<OnActiveSubscriptionChangedListener> listeners) {
        // categorize listener(s), and end of life listener(s) been ignored
        Map<Integer, List<OnActiveSubscriptionChangedListener>> categorizedListeners =
                listeners.stream()
                .collect(Collectors.groupingBy(it -> getListenerState(it)));

        // have inactive listener(s) in pending list
        categorizedListeners.computeIfPresent(LISTENER_IS_INACTIVE, (category, list) -> {
            mPendingNotifyListeners.addAll(list);
            return list;
        });

        // get active listener(s)
        categorizedListeners.computeIfPresent(LISTENER_IS_ACTIVE, (category, list) -> {
            mActiveSubscriptionsListeners.addAll(list);
            // notify each one of them
            list.stream().forEach(it -> it.onChanged());
            return list;
        });
    }
}
