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

import android.app.ActivityManager;
import android.content.ComponentName;
import android.content.Context;
import android.nfc.cardemulation.NfcFServiceInfo;
import android.util.Log;
import android.util.proto.ProtoOutputStream;

import java.io.FileDescriptor;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.HashMap;

public class RegisteredT3tIdentifiersCache {
    static final String TAG = "RegisteredT3tIdentifiersCache";

    static final boolean DBG = false;

    // All NFC-F services that have registered
    List<NfcFServiceInfo> mServices = new ArrayList<NfcFServiceInfo>();

    final HashMap<String, NfcFServiceInfo> mForegroundT3tIdentifiersCache =
            new HashMap<String, NfcFServiceInfo>();

    ComponentName mEnabledForegroundService;

    final class T3tIdentifier {
        public final String systemCode;
        public final String nfcid2;
        public final String t3tPmm;

        T3tIdentifier(String systemCode, String nfcid2, String t3tPmm) {
            this.systemCode = systemCode;
            this.nfcid2 = nfcid2;
            this.t3tPmm = t3tPmm;
        }

        @Override
        public boolean equals(Object o) {
            if (this == o) return true;
            if (o == null || getClass() != o.getClass()) return false;

            T3tIdentifier that = (T3tIdentifier) o;
            if (!systemCode.equalsIgnoreCase(that.systemCode)) return false;
            if (!nfcid2.equalsIgnoreCase(that.nfcid2)) return false;

            return true;
        }

        @Override
        public int hashCode() {
            int result = systemCode.hashCode();
            result = 31 * result + nfcid2.hashCode();
            return result;
        }
    }

    final Context mContext;
    final SystemCodeRoutingManager mRoutingManager;

    final Object mLock = new Object();

    boolean mNfcEnabled = false;

    public RegisteredT3tIdentifiersCache(Context context) {
        Log.d(TAG, "RegisteredT3tIdentifiersCache");
        mContext = context;
        mRoutingManager = new SystemCodeRoutingManager();
    }

    public NfcFServiceInfo resolveNfcid2(String nfcid2) {
        synchronized (mLock) {
            if (DBG) Log.d(TAG, "resolveNfcid2: resolving NFCID " + nfcid2);
            NfcFServiceInfo resolveInfo;
            resolveInfo = mForegroundT3tIdentifiersCache.get(nfcid2);
            Log.d(TAG,
                    "Resolved to: " + (resolveInfo == null ? "null" : resolveInfo.toString()));
            return resolveInfo;
        }
    }

    void generateForegroundT3tIdentifiersCacheLocked() {
        if (DBG) Log.d(TAG, "generateForegroundT3tIdentifiersCacheLocked");
        mForegroundT3tIdentifiersCache.clear();
        if (mEnabledForegroundService != null) {
            for (NfcFServiceInfo service : mServices) {
                if (mEnabledForegroundService.equals(service.getComponent())) {
                    if (!service.getSystemCode().equalsIgnoreCase("NULL") &&
                            !service.getNfcid2().equalsIgnoreCase("NULL")) {
                        mForegroundT3tIdentifiersCache.put(service.getNfcid2(), service);
                    }
                    break;
                }
            }
        }

        if (DBG) {
            Log.d(TAG, "mForegroundT3tIdentifiersCache: size=" +
                    mForegroundT3tIdentifiersCache.size());
            for (Map.Entry<String, NfcFServiceInfo> entry :
                    mForegroundT3tIdentifiersCache.entrySet()) {
                Log.d(TAG, "    " + entry.getKey() +
                        "/" + entry.getValue().getComponent().toString());
            }
        }

        updateRoutingLocked(false);
    }

    void updateRoutingLocked(boolean force) {
        if (DBG) Log.d(TAG, "updateRoutingLocked");
        if (!mNfcEnabled) {
            Log.d(TAG, "Not updating routing table because NFC is off.");
            return;
        }

        List<T3tIdentifier> t3tIdentifiers = new ArrayList<T3tIdentifier>();

        // Sending an empty table will de-register all entries
        if (force) {
            mRoutingManager.configureRouting(t3tIdentifiers);
        }
        Iterator<Map.Entry<String, NfcFServiceInfo>> it;
        // Register foreground service
        it = mForegroundT3tIdentifiersCache.entrySet().iterator();
        while (it.hasNext()) {
            Map.Entry<String, NfcFServiceInfo> entry =
                    (Map.Entry<String, NfcFServiceInfo>) it.next();
            t3tIdentifiers.add(new T3tIdentifier(
                    entry.getValue().getSystemCode(), entry.getValue().getNfcid2(), entry.getValue().getT3tPmm()));
        }
        mRoutingManager.configureRouting(t3tIdentifiers);
    }

    public void onSecureNfcToggled() {
        synchronized(mLock) {
            updateRoutingLocked(true);
      }
    }

    public void onServicesUpdated(int userId, List<NfcFServiceInfo> services) {
        if (DBG) Log.d(TAG, "onServicesUpdated");
        synchronized (mLock) {
            if (ActivityManager.getCurrentUser() == userId) {
                // Rebuild our internal data-structures
                mServices = services;
            } else {
                Log.d(TAG, "Ignoring update because it's not for the current user.");
            }
        }
    }

    public void onEnabledForegroundNfcFServiceChanged(ComponentName component) {
        if (DBG) Log.d(TAG, "Enabled foreground service changed.");
        synchronized (mLock) {
            if (component != null) {
                if (mEnabledForegroundService != null) {
                    return;
                }
                mEnabledForegroundService = component;
            } else {
                if (mEnabledForegroundService == null) {
                    return;
                }
                mEnabledForegroundService = null;
            }
            generateForegroundT3tIdentifiersCacheLocked();
        }
    }

    public void onNfcEnabled() {
        synchronized (mLock) {
            mNfcEnabled = true;
        }
    }

    public void onNfcDisabled() {
        synchronized (mLock) {
            mNfcEnabled = false;
            mForegroundT3tIdentifiersCache.clear();
            mEnabledForegroundService = null;
        }
        mRoutingManager.onNfccRoutingTableCleared();
    }

    public void onUserSwitched() {
        synchronized (mLock) {
            mForegroundT3tIdentifiersCache.clear();
            updateRoutingLocked(false);
            mEnabledForegroundService = null;
        }
    }

    public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
        pw.println("T3T Identifier cache entries: ");
        for (Map.Entry<String, NfcFServiceInfo> entry : mForegroundT3tIdentifiersCache.entrySet()) {
            pw.println("    NFCID2: " + entry.getKey());
            pw.println("    NfcFServiceInfo: ");
            entry.getValue().dump(fd, pw, args);
        }
        pw.println("");
        mRoutingManager.dump(fd, pw, args);
        pw.println("");
    }

    /**
     * Dump debugging information as a RegisteredT3tIdentifiersCacheProto
     *
     * Note:
     * See proto definition in frameworks/base/core/proto/android/nfc/card_emulation.proto
     * When writing a nested message, must call {@link ProtoOutputStream#start(long)} before and
     * {@link ProtoOutputStream#end(long)} after.
     * Never reuse a proto field number. When removing a field, mark it as reserved.
     */
    void dumpDebug(ProtoOutputStream proto) {
        for (NfcFServiceInfo serviceInfo : mForegroundT3tIdentifiersCache.values()) {
            long token = proto.start(
                    RegisteredT3tIdentifiersCacheProto.T3T_IDENTIFIER_CACHE_ENTRIES);
            serviceInfo.dumpDebug(proto);
            proto.end(token);
        }
        long token = proto.start(RegisteredT3tIdentifiersCacheProto.ROUTING_MANAGER);
        mRoutingManager.dumpDebug(proto);
        proto.end(token);
    }
}
