/*
 * Copyright (C) 2011 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 android.nfc;

import android.app.Activity;
import android.app.Application;
import android.compat.annotation.UnsupportedAppUsage;
import android.nfc.NfcAdapter.ReaderCallback;
import android.os.Binder;
import android.os.Bundle;
import android.os.RemoteException;
import android.util.Log;

import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;

/**
 * Manages NFC API's that are coupled to the life-cycle of an Activity.
 *
 * <p>Uses {@link Application#registerActivityLifecycleCallbacks} to hook
 * into activity life-cycle events such as onPause() and onResume().
 *
 * @hide
 */
public final class NfcActivityManager extends IAppCallback.Stub
        implements Application.ActivityLifecycleCallbacks {
    static final String TAG = NfcAdapter.TAG;
    static final Boolean DBG = false;

    @UnsupportedAppUsage
    final NfcAdapter mAdapter;

    // All objects in the lists are protected by this
    final List<NfcApplicationState> mApps;  // Application(s) that have NFC state. Usually one
    final List<NfcActivityState> mActivities;  // Activities that have NFC state

    /**
     * NFC State associated with an {@link Application}.
     */
    class NfcApplicationState {
        int refCount = 0;
        final Application app;
        public NfcApplicationState(Application app) {
            this.app = app;
        }
        public void register() {
            refCount++;
            if (refCount == 1) {
                this.app.registerActivityLifecycleCallbacks(NfcActivityManager.this);
            }
        }
        public void unregister() {
            refCount--;
            if (refCount == 0) {
                this.app.unregisterActivityLifecycleCallbacks(NfcActivityManager.this);
            } else if (refCount < 0) {
                Log.e(TAG, "-ve refcount for " + app);
            }
        }
    }

    NfcApplicationState findAppState(Application app) {
        for (NfcApplicationState appState : mApps) {
            if (appState.app == app) {
                return appState;
            }
        }
        return null;
    }

    void registerApplication(Application app) {
        NfcApplicationState appState = findAppState(app);
        if (appState == null) {
            appState = new NfcApplicationState(app);
            mApps.add(appState);
        }
        appState.register();
    }

    void unregisterApplication(Application app) {
        NfcApplicationState appState = findAppState(app);
        if (appState == null) {
            Log.e(TAG, "app was not registered " + app);
            return;
        }
        appState.unregister();
    }

    /**
     * NFC state associated with an {@link Activity}
     */
    class NfcActivityState {
        boolean resumed = false;
        Activity activity;
        NfcAdapter.ReaderCallback readerCallback = null;
        int readerModeFlags = 0;
        Bundle readerModeExtras = null;
        Binder token;

        int mPollTech = NfcAdapter.FLAG_USE_ALL_TECH;
        int mListenTech = NfcAdapter.FLAG_USE_ALL_TECH;

        public NfcActivityState(Activity activity) {
            if (activity.isDestroyed()) {
                throw new IllegalStateException("activity is already destroyed");
            }
            // Check if activity is resumed right now, as we will not
            // immediately get a callback for that.
            resumed = activity.isResumed();

            this.activity = activity;
            this.token = new Binder();
            registerApplication(activity.getApplication());
        }
        public void destroy() {
            unregisterApplication(activity.getApplication());
            resumed = false;
            activity = null;
            readerCallback = null;
            readerModeFlags = 0;
            readerModeExtras = null;
            token = null;

            mPollTech = NfcAdapter.FLAG_USE_ALL_TECH;
            mListenTech = NfcAdapter.FLAG_USE_ALL_TECH;
        }
        @Override
        public String toString() {
            StringBuilder s = new StringBuilder("[");
            s.append(readerCallback);
            s.append("]");
            return s.toString();
        }
    }

    /** find activity state from mActivities */
    synchronized NfcActivityState findActivityState(Activity activity) {
        for (NfcActivityState state : mActivities) {
            if (state.activity == activity) {
                return state;
            }
        }
        return null;
    }

    /** find or create activity state from mActivities */
    synchronized NfcActivityState getActivityState(Activity activity) {
        NfcActivityState state = findActivityState(activity);
        if (state == null) {
            state = new NfcActivityState(activity);
            mActivities.add(state);
        }
        return state;
    }

    synchronized NfcActivityState findResumedActivityState() {
        for (NfcActivityState state : mActivities) {
            if (state.resumed) {
                return state;
            }
        }
        return null;
    }

    synchronized void destroyActivityState(Activity activity) {
        NfcActivityState activityState = findActivityState(activity);
        if (activityState != null) {
            activityState.destroy();
            mActivities.remove(activityState);
        }
    }

    public NfcActivityManager(NfcAdapter adapter) {
        mAdapter = adapter;
        mActivities = new LinkedList<NfcActivityState>();
        mApps = new ArrayList<NfcApplicationState>(1);  // Android VM usually has 1 app
    }

    public void enableReaderMode(Activity activity, ReaderCallback callback, int flags,
            Bundle extras) {
        boolean isResumed;
        Binder token;
        int pollTech, listenTech;
        synchronized (NfcActivityManager.this) {
            NfcActivityState state = getActivityState(activity);
            state.readerCallback = callback;
            state.readerModeFlags = flags;
            state.readerModeExtras = extras;
            pollTech = state.mPollTech;
            listenTech = state.mListenTech;
            token = state.token;
            isResumed = state.resumed;
        }
        if (isResumed) {
            if (listenTech != NfcAdapter.FLAG_USE_ALL_TECH
                    || pollTech != NfcAdapter.FLAG_USE_ALL_TECH) {
                throw new IllegalStateException(
                    "Cannot be used when alternative DiscoveryTechnology is set");
            } else {
                setReaderMode(token, flags, extras);
            }
        }
    }

    public void disableReaderMode(Activity activity) {
        boolean isResumed;
        Binder token;
        synchronized (NfcActivityManager.this) {
            NfcActivityState state = getActivityState(activity);
            state.readerCallback = null;
            state.readerModeFlags = 0;
            state.readerModeExtras = null;
            token = state.token;
            isResumed = state.resumed;
        }
        if (isResumed) {
            setReaderMode(token, 0, null);
        }

    }

    public void setReaderMode(Binder token, int flags, Bundle extras) {
        if (DBG) Log.d(TAG, "Setting reader mode");
        try {
            NfcAdapter.sService.setReaderMode(token, this, flags, extras);
        } catch (RemoteException e) {
            mAdapter.attemptDeadServiceRecovery(e);
        }
    }

    /**
     * Request or unrequest NFC service callbacks.
     * Makes IPC call - do not hold lock.
     */
    void requestNfcServiceCallback() {
        try {
            NfcAdapter.sService.setAppCallback(this);
        } catch (RemoteException e) {
            mAdapter.attemptDeadServiceRecovery(e);
        }
    }

    void verifyNfcPermission() {
        try {
            NfcAdapter.sService.verifyNfcPermission();
        } catch (RemoteException e) {
            mAdapter.attemptDeadServiceRecovery(e);
        }
    }

    @Override
    public void onTagDiscovered(Tag tag) throws RemoteException {
        NfcAdapter.ReaderCallback callback;
        synchronized (NfcActivityManager.this) {
            NfcActivityState state = findResumedActivityState();
            if (state == null) return;

            callback = state.readerCallback;
        }

        // Make callback without lock
        if (callback != null) {
            callback.onTagDiscovered(tag);
        }

    }
    /** Callback from Activity life-cycle, on main thread */
    @Override
    public void onActivityCreated(Activity activity, Bundle savedInstanceState) { /* NO-OP */ }

    /** Callback from Activity life-cycle, on main thread */
    @Override
    public void onActivityStarted(Activity activity) { /* NO-OP */ }

    /** Callback from Activity life-cycle, on main thread */
    @Override
    public void onActivityResumed(Activity activity) {
        int readerModeFlags = 0;
        Bundle readerModeExtras = null;
        Binder token;
        int pollTech;
        int listenTech;

        synchronized (NfcActivityManager.this) {
            NfcActivityState state = findActivityState(activity);
            if (DBG) Log.d(TAG, "onResume() for " + activity + " " + state);
            if (state == null) return;
            state.resumed = true;
            token = state.token;
            readerModeFlags = state.readerModeFlags;
            readerModeExtras = state.readerModeExtras;

            pollTech = state.mPollTech;
            listenTech = state.mListenTech;
        }
        if (readerModeFlags != 0) {
            setReaderMode(token, readerModeFlags, readerModeExtras);
        } else if (listenTech != NfcAdapter.FLAG_USE_ALL_TECH
                || pollTech != NfcAdapter.FLAG_USE_ALL_TECH) {
            changeDiscoveryTech(token, pollTech, listenTech);
        }
        requestNfcServiceCallback();
    }

    /** Callback from Activity life-cycle, on main thread */
    @Override
    public void onActivityPaused(Activity activity) {
        boolean readerModeFlagsSet;
        Binder token;
        int pollTech;
        int listenTech;

        synchronized (NfcActivityManager.this) {
            NfcActivityState state = findActivityState(activity);
            if (DBG) Log.d(TAG, "onPause() for " + activity + " " + state);
            if (state == null) return;
            state.resumed = false;
            token = state.token;
            readerModeFlagsSet = state.readerModeFlags != 0;

            pollTech = state.mPollTech;
            listenTech = state.mListenTech;
        }
        if (readerModeFlagsSet) {
            // Restore default p2p modes
            setReaderMode(token, 0, null);
        } else if (listenTech != NfcAdapter.FLAG_USE_ALL_TECH
                || pollTech != NfcAdapter.FLAG_USE_ALL_TECH) {
            changeDiscoveryTech(token,
                    NfcAdapter.FLAG_USE_ALL_TECH, NfcAdapter.FLAG_USE_ALL_TECH);
        }
    }

    /** Callback from Activity life-cycle, on main thread */
    @Override
    public void onActivityStopped(Activity activity) { /* NO-OP */ }

    /** Callback from Activity life-cycle, on main thread */
    @Override
    public void onActivitySaveInstanceState(Activity activity, Bundle outState) { /* NO-OP */ }

    /** Callback from Activity life-cycle, on main thread */
    @Override
    public void onActivityDestroyed(Activity activity) {
        synchronized (NfcActivityManager.this) {
            NfcActivityState state = findActivityState(activity);
            if (DBG) Log.d(TAG, "onDestroy() for " + activity + " " + state);
            if (state != null) {
                // release all associated references
                destroyActivityState(activity);
            }
        }
    }

    /** setDiscoveryTechnology() implementation */
    public void setDiscoveryTech(Activity activity, int pollTech, int listenTech) {
        boolean isResumed;
        Binder token;
        boolean readerModeFlagsSet;
        synchronized (NfcActivityManager.this) {
            NfcActivityState state = getActivityState(activity);
            readerModeFlagsSet = state.readerModeFlags != 0;
            state.mListenTech = listenTech;
            state.mPollTech = pollTech;
            token = state.token;
            isResumed = state.resumed;
        }
        if (!readerModeFlagsSet && isResumed) {
            changeDiscoveryTech(token, pollTech, listenTech);
        } else if (readerModeFlagsSet) {
            throw new IllegalStateException("Cannot be used when the Reader Mode is enabled");
        }
    }

    /** resetDiscoveryTechnology() implementation */
    public void resetDiscoveryTech(Activity activity) {
        boolean isResumed;
        Binder token;
        boolean readerModeFlagsSet;
        synchronized (NfcActivityManager.this) {
            NfcActivityState state = getActivityState(activity);
            state.mListenTech = NfcAdapter.FLAG_USE_ALL_TECH;
            state.mPollTech = NfcAdapter.FLAG_USE_ALL_TECH;
            token = state.token;
            isResumed = state.resumed;
        }
        if (isResumed) {
            changeDiscoveryTech(token, NfcAdapter.FLAG_USE_ALL_TECH, NfcAdapter.FLAG_USE_ALL_TECH);
        }

    }

    private void changeDiscoveryTech(Binder token, int pollTech, int listenTech) {
        try {
            NfcAdapter.sService.updateDiscoveryTechnology(token, pollTech, listenTech);
        } catch (RemoteException e) {
            mAdapter.attemptDeadServiceRecovery(e);
        }
    }

}
