/*
* Copyright (C) 2014 Samsung System LSI
* 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.bluetooth.map;

import android.content.BroadcastReceiver;
import android.content.ContentResolver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.database.ContentObserver;
import android.net.Uri;
import android.util.Log;

import com.android.bluetooth.mapapi.BluetoothMapContract;

import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Objects;

/**
 * Class to construct content observers for for email applications on the system.
 *
 *
 */

public class BluetoothMapAppObserver {

    private static final String TAG = "BluetoothMapAppObserver";

    private static final boolean D = BluetoothMapService.DEBUG;
    private static final boolean V = BluetoothMapService.VERBOSE;
    /*  */
    private LinkedHashMap<BluetoothMapAccountItem, ArrayList<BluetoothMapAccountItem>> mFullList;
    private LinkedHashMap<String, ContentObserver> mObserverMap =
            new LinkedHashMap<String, ContentObserver>();
    private ContentResolver mResolver;
    private Context mContext;
    private BroadcastReceiver mReceiver;
    private PackageManager mPackageManager = null;
    BluetoothMapAccountLoader mLoader;
    BluetoothMapService mMapService = null;
    private boolean mRegisteredReceiver = false;

    public BluetoothMapAppObserver(final Context context, BluetoothMapService mapService) {
        mContext = context;
        mMapService = mapService;
        mResolver = context.getContentResolver();
        mLoader = new BluetoothMapAccountLoader(mContext);
        mFullList = mLoader.parsePackages(false); /* Get the current list of apps */
        createReceiver();
        initObservers();
    }


    private BluetoothMapAccountItem getApp(String authoritiesName) {
        if (V) {
            Log.d(TAG, "getApp(): Looking for " + authoritiesName);
        }
        for (BluetoothMapAccountItem app : mFullList.keySet()) {
            if (V) {
                Log.d(TAG, "  Comparing: " + app.getProviderAuthority());
            }
            if (app.getProviderAuthority().equals(authoritiesName)) {
                if (V) {
                    Log.d(TAG, "  found " + app.mBase_uri_no_account);
                }
                return app;
            }
        }
        if (V) {
            Log.d(TAG, "  NOT FOUND!");
        }
        return null;
    }

    private void handleAccountChanges(String packageNameWithProvider) {

        if (D) {
            Log.d(TAG, "handleAccountChanges (packageNameWithProvider: " + packageNameWithProvider
                    + "\n");
        }
        //String packageName = packageNameWithProvider.replaceFirst("\\.[^\\.]+$", "");
        BluetoothMapAccountItem app = getApp(packageNameWithProvider);
        if (app != null) {
            ArrayList<BluetoothMapAccountItem> newAccountList = mLoader.parseAccounts(app);
            ArrayList<BluetoothMapAccountItem> oldAccountList = mFullList.get(app);
            ArrayList<BluetoothMapAccountItem> addedAccountList =
                    (ArrayList<BluetoothMapAccountItem>) newAccountList.clone();
            // Same as oldAccountList.clone
            ArrayList<BluetoothMapAccountItem> removedAccountList = mFullList.get(app);
            if (oldAccountList == null) {
                oldAccountList = new ArrayList<BluetoothMapAccountItem>();
            }
            if (removedAccountList == null) {
                removedAccountList = new ArrayList<BluetoothMapAccountItem>();
            }

            mFullList.put(app, newAccountList);
            for (BluetoothMapAccountItem newAcc : newAccountList) {
                for (BluetoothMapAccountItem oldAcc : oldAccountList) {
                    if (Objects.equals(newAcc.getId(), oldAcc.getId())) {
                        // For each match remove from both removed and added lists
                        removedAccountList.remove(oldAcc);
                        addedAccountList.remove(newAcc);
                        if (!newAcc.getName().equals(oldAcc.getName()) && newAcc.mIsChecked) {
                            // Name Changed and the acc is visible - Change Name in SDP record
                            mMapService.updateMasInstances(
                                    BluetoothMapService.UPDATE_MAS_INSTANCES_ACCOUNT_RENAMED);
                            if (V) {
                                Log.d(TAG, "    UPDATE_MAS_INSTANCES_ACCOUNT_RENAMED");
                            }
                        }
                        if (newAcc.mIsChecked != oldAcc.mIsChecked) {
                            // Visibility changed
                            if (newAcc.mIsChecked) {
                                // account added - create SDP record
                                mMapService.updateMasInstances(
                                        BluetoothMapService.UPDATE_MAS_INSTANCES_ACCOUNT_ADDED);
                                if (V) {
                                    Log.d(TAG, "UPDATE_MAS_INSTANCES_ACCOUNT_ADDED "
                                            + "isChecked changed");
                                }
                            } else {
                                // account removed - remove SDP record
                                mMapService.updateMasInstances(
                                        BluetoothMapService.UPDATE_MAS_INSTANCES_ACCOUNT_REMOVED);
                                if (V) {
                                    Log.d(TAG, "    UPDATE_MAS_INSTANCES_ACCOUNT_REMOVED "
                                            + "isChecked changed");
                                }
                            }
                        }
                        break;
                    }
                }
            }
            // Notify on any removed accounts
            for (BluetoothMapAccountItem removedAcc : removedAccountList) {
                mMapService.updateMasInstances(
                        BluetoothMapService.UPDATE_MAS_INSTANCES_ACCOUNT_REMOVED);
                if (V) {
                    Log.d(TAG, "    UPDATE_MAS_INSTANCES_ACCOUNT_REMOVED " + removedAcc);
                }
            }
            // Notify on any new accounts
            for (BluetoothMapAccountItem addedAcc : addedAccountList) {
                mMapService.updateMasInstances(
                        BluetoothMapService.UPDATE_MAS_INSTANCES_ACCOUNT_ADDED);
                if (V) {
                    Log.d(TAG, "    UPDATE_MAS_INSTANCES_ACCOUNT_ADDED " + addedAcc);
                }
            }

        } else {
            Log.e(TAG, "Received change notification on package not registered for notifications!");

        }
    }

    /**
     * Adds a new content observer to the list of content observers.
     * The key for the observer is the uri as string
     * @param app app item for the package that supports MAP email
     */

    public void registerObserver(BluetoothMapAccountItem app) {
        Uri uri = BluetoothMapContract.buildAccountUri(app.getProviderAuthority());
        if (V) {
            Log.d(TAG, "registerObserver for URI " + uri.toString() + "\n");
        }
        ContentObserver observer = new ContentObserver(null) {
            @Override
            public void onChange(boolean selfChange) {
                onChange(selfChange, null);
            }

            @Override
            public void onChange(boolean selfChange, Uri uri) {
                if (V) {
                    Log.d(TAG,
                            "onChange on thread: " + Thread.currentThread().getId() + " Uri: " + uri
                                    + " selfchange: " + selfChange);
                }
                if (uri != null) {
                    handleAccountChanges(uri.getHost());
                } else {
                    Log.e(TAG, "Unable to handle change as the URI is NULL!");
                }

            }
        };
        mObserverMap.put(uri.toString(), observer);
        //False "notifyForDescendents" : Get notified whenever a change occurs to the exact URI.
        mResolver.registerContentObserver(uri, false, observer);
    }

    public void unregisterObserver(BluetoothMapAccountItem app) {
        Uri uri = BluetoothMapContract.buildAccountUri(app.getProviderAuthority());
        if (V) {
            Log.d(TAG, "unregisterObserver(" + uri.toString() + ")\n");
        }
        mResolver.unregisterContentObserver(mObserverMap.get(uri.toString()));
        mObserverMap.remove(uri.toString());
    }

    private void initObservers() {
        if (D) {
            Log.d(TAG, "initObservers()");
        }
        for (BluetoothMapAccountItem app : mFullList.keySet()) {
            registerObserver(app);
        }
    }

    private void deinitObservers() {
        if (D) {
            Log.d(TAG, "deinitObservers()");
        }
        for (BluetoothMapAccountItem app : mFullList.keySet()) {
            unregisterObserver(app);
        }
    }

    private void createReceiver() {
        if (D) {
            Log.d(TAG, "createReceiver()\n");
        }
        IntentFilter intentFilter = new IntentFilter();
        intentFilter.addAction(Intent.ACTION_PACKAGE_ADDED);
        intentFilter.addAction(Intent.ACTION_PACKAGE_REMOVED);
        intentFilter.addDataScheme("package");
        mReceiver = new BroadcastReceiver() {
            @Override
            public void onReceive(Context context, Intent intent) {
                if (D) {
                    Log.d(TAG, "onReceive\n");
                }
                String action = intent.getAction();

                if (Intent.ACTION_PACKAGE_ADDED.equals(action)) {
                    Uri data = intent.getData();
                    String packageName = data.getEncodedSchemeSpecificPart();
                    if (D) {
                        Log.d(TAG, "The installed package is: " + packageName);
                    }

                    BluetoothMapUtils.TYPE msgType = BluetoothMapUtils.TYPE.NONE;
                    ResolveInfo resolveInfo = null;
                    Intent[] searchIntents = new Intent[2];
                    //Array <Intent> searchIntents = new Array <Intent>();
                    searchIntents[0] = new Intent(BluetoothMapContract.PROVIDER_INTERFACE_EMAIL);
                    searchIntents[1] = new Intent(BluetoothMapContract.PROVIDER_INTERFACE_IM);
                    // Find all installed packages and filter out those that support Bluetooth Map.

                    mPackageManager = mContext.getPackageManager();

                    for (Intent searchIntent : searchIntents) {
                        List<ResolveInfo> resInfos =
                                mPackageManager.queryIntentContentProviders(searchIntent, 0);
                        if (resInfos != null) {
                            if (D) {
                                Log.d(TAG,
                                        "Found " + resInfos.size() + " application(s) with intent "
                                                + searchIntent.getAction());
                            }
                            for (ResolveInfo rInfo : resInfos) {
                                if (rInfo != null) {
                                    // Find out if package contain Bluetooth MAP support
                                    if (packageName.equals(rInfo.providerInfo.packageName)) {
                                        resolveInfo = rInfo;
                                        if (Objects.equals(searchIntent.getAction(),
                                                BluetoothMapContract.PROVIDER_INTERFACE_EMAIL)) {
                                            msgType = BluetoothMapUtils.TYPE.EMAIL;
                                        } else if (Objects.equals(searchIntent.getAction(),
                                                BluetoothMapContract.PROVIDER_INTERFACE_IM)) {
                                            msgType = BluetoothMapUtils.TYPE.IM;
                                        }
                                        break;
                                    }
                                }
                            }
                        }
                    }
                    // if application found with Bluetooth MAP support add to list
                    if (resolveInfo != null) {
                        if (D) {
                            Log.d(TAG, "Found " + resolveInfo.providerInfo.packageName
                                    + " application of type " + msgType);
                        }
                        BluetoothMapAccountItem app =
                                mLoader.createAppItem(resolveInfo, false, msgType);
                        if (app != null) {
                            registerObserver(app);
                            // Add all accounts to mFullList
                            ArrayList<BluetoothMapAccountItem> newAccountList =
                                    mLoader.parseAccounts(app);
                            mFullList.put(app, newAccountList);
                        }
                    }

                } else if (Intent.ACTION_PACKAGE_REMOVED.equals(action)) {
                    Uri data = intent.getData();
                    String packageName = data.getEncodedSchemeSpecificPart();
                    if (D) {
                        Log.d(TAG, "The removed package is: " + packageName);
                    }
                    BluetoothMapAccountItem app = getApp(packageName);
                    /* Find the object and remove from fullList */
                    if (app != null) {
                        unregisterObserver(app);
                        mFullList.remove(app);
                    }
                }
            }
        };
        if (!mRegisteredReceiver) {
            try {
                mContext.registerReceiver(mReceiver, intentFilter);
                mRegisteredReceiver = true;
            } catch (Exception e) {
                Log.e(TAG, "Unable to register MapAppObserver receiver", e);
            }
        }
    }

    private void removeReceiver() {
        if (D) {
            Log.d(TAG, "removeReceiver()\n");
        }
        if (mRegisteredReceiver) {
            try {
                mRegisteredReceiver = false;
                mContext.unregisterReceiver(mReceiver);
            } catch (Exception e) {
                Log.e(TAG, "Unable to unregister mapAppObserver receiver", e);
            }
        }
    }

    /**
     * Method to get a list of the accounts (across all apps) that are set to be shared
     * through MAP.
     * @return Arraylist<BluetoothMapAccountItem> containing all enabled accounts
     */
    public ArrayList<BluetoothMapAccountItem> getEnabledAccountItems() {
        if (D) {
            Log.d(TAG, "getEnabledAccountItems()\n");
        }
        ArrayList<BluetoothMapAccountItem> list = new ArrayList<BluetoothMapAccountItem>();
        for (BluetoothMapAccountItem app : mFullList.keySet()) {
            if (app != null) {
                ArrayList<BluetoothMapAccountItem> accountList = mFullList.get(app);
                if (accountList != null) {
                    for (BluetoothMapAccountItem acc : accountList) {
                        if (acc.mIsChecked) {
                            list.add(acc);
                        }
                    }
                } else {
                    Log.w(TAG, "getEnabledAccountItems() - No AccountList enabled\n");
                }
            } else {
                Log.w(TAG, "getEnabledAccountItems() - No Account in App enabled\n");
            }
        }
        return list;
    }

    /**
     * Method to get a list of the accounts (across all apps).
     * @return Arraylist<BluetoothMapAccountItem> containing all accounts
     */
    public ArrayList<BluetoothMapAccountItem> getAllAccountItems() {
        if (D) {
            Log.d(TAG, "getAllAccountItems()\n");
        }
        ArrayList<BluetoothMapAccountItem> list = new ArrayList<BluetoothMapAccountItem>();
        for (BluetoothMapAccountItem app : mFullList.keySet()) {
            ArrayList<BluetoothMapAccountItem> accountList = mFullList.get(app);
            list.addAll(accountList);
        }
        return list;
    }


    /**
     * Cleanup all resources - must be called to avoid leaks.
     */
    public void shutdown() {
        deinitObservers();
        removeReceiver();
    }
}
