/*
 * Copyright (C) 2012-2014 Dominik Schürmann <dominik@dominikschuermann.de>
 * Copyright (C) 2014 Vincent Breitmoser <v.breitmoser@mugenguild.com>
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

package org.sufficientlysecure.keychain.service;

import android.app.Service;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.os.IBinder;
import android.os.Message;
import android.os.Messenger;
import android.os.RemoteException;

import com.textuality.keybase.lib.Proof;
import com.textuality.keybase.lib.prover.Prover;

import org.json.JSONObject;
import org.spongycastle.openpgp.PGPUtil;
import org.sufficientlysecure.keychain.Constants;
import org.sufficientlysecure.keychain.R;
import org.sufficientlysecure.keychain.keyimport.HkpKeyserver;
import org.sufficientlysecure.keychain.keyimport.Keyserver;
import org.sufficientlysecure.keychain.keyimport.ParcelableKeyRing;
import org.sufficientlysecure.keychain.operations.CertifyOperation;
import org.sufficientlysecure.keychain.operations.DeleteOperation;
import org.sufficientlysecure.keychain.operations.EditKeyOperation;
import org.sufficientlysecure.keychain.operations.ImportExportOperation;
import org.sufficientlysecure.keychain.operations.PromoteKeyOperation;
import org.sufficientlysecure.keychain.operations.SignEncryptOperation;
import org.sufficientlysecure.keychain.operations.results.CertifyResult;
import org.sufficientlysecure.keychain.operations.results.ConsolidateResult;
import org.sufficientlysecure.keychain.operations.results.DecryptVerifyResult;
import org.sufficientlysecure.keychain.operations.results.DeleteResult;
import org.sufficientlysecure.keychain.operations.results.ExportResult;
import org.sufficientlysecure.keychain.operations.results.ImportKeyResult;
import org.sufficientlysecure.keychain.operations.results.OperationResult;
import org.sufficientlysecure.keychain.operations.results.OperationResult.OperationLog;
import org.sufficientlysecure.keychain.operations.results.PromoteKeyResult;
import org.sufficientlysecure.keychain.operations.results.SignEncryptResult;
import org.sufficientlysecure.keychain.pgp.CanonicalizedPublicKeyRing;
import org.sufficientlysecure.keychain.pgp.PgpDecryptVerify;
import org.sufficientlysecure.keychain.pgp.PgpDecryptVerifyInputParcel;
import org.sufficientlysecure.keychain.pgp.Progressable;
import org.sufficientlysecure.keychain.pgp.SignEncryptParcel;
import org.sufficientlysecure.keychain.pgp.exception.PgpGeneralException;
import org.sufficientlysecure.keychain.pgp.exception.PgpGeneralMsgIdException;
import org.sufficientlysecure.keychain.provider.ProviderHelper;
import org.sufficientlysecure.keychain.service.input.CryptoInputParcel;
import org.sufficientlysecure.keychain.service.ServiceProgressHandler.MessageStatus;
import org.sufficientlysecure.keychain.util.Log;
import org.sufficientlysecure.keychain.util.ParcelableFileCache;

import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.SynchronousQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;

import de.measite.minidns.Client;
import de.measite.minidns.DNSMessage;
import de.measite.minidns.Question;
import de.measite.minidns.Record;
import de.measite.minidns.record.Data;
import de.measite.minidns.record.TXT;

/**
 * This Service contains all important long lasting operations for OpenKeychain. It receives Intents with
 * data from the activities or other apps, executes them, and stops itself after doing them.
 */
public class KeychainService extends Service implements Progressable {

    /* extras that can be given by intent */
    public static final String EXTRA_MESSENGER = "messenger";
    public static final String EXTRA_DATA = "data";

    /* possible actions */

    public static final String ACTION_VERIFY_KEYBASE_PROOF = Constants.INTENT_PREFIX + "VERIFY_KEYBASE_PROOF";

    public static final String ACTION_EDIT_KEYRING = Constants.INTENT_PREFIX + "EDIT_KEYRING";

    public static final String ACTION_PROMOTE_KEYRING = Constants.INTENT_PREFIX + "PROMOTE_KEYRING";

    public static final String ACTION_IMPORT_KEYRING = Constants.INTENT_PREFIX + "IMPORT_KEYRING";
    public static final String ACTION_EXPORT_KEYRING = Constants.INTENT_PREFIX + "EXPORT_KEYRING";

    public static final String ACTION_UPLOAD_KEYRING = Constants.INTENT_PREFIX + "UPLOAD_KEYRING";

    public static final String ACTION_DELETE = Constants.INTENT_PREFIX + "DELETE";

    public static final String ACTION_CONSOLIDATE = Constants.INTENT_PREFIX + "CONSOLIDATE";

    public static final String ACTION_CANCEL = Constants.INTENT_PREFIX + "CANCEL";

    /* keys for data bundle */

    // keybase proof
    public static final String KEYBASE_REQUIRED_FINGERPRINT = "keybase_required_fingerprint";
    public static final String KEYBASE_PROOF = "keybase_proof";

    // save keyring
    public static final String EDIT_KEYRING_PARCEL = "save_parcel";
    public static final String EDIT_KEYRING_PASSPHRASE = "passphrase";
    public static final String EXTRA_CRYPTO_INPUT = "crypto_input";

    // delete keyring(s)
    public static final String DELETE_KEY_LIST = "delete_list";
    public static final String DELETE_IS_SECRET = "delete_is_secret";

    // import key
    public static final String IMPORT_KEY_LIST = "import_key_list";
    public static final String IMPORT_KEY_SERVER = "import_key_server";

    // export key
    public static final String EXPORT_FILENAME = "export_filename";
    public static final String EXPORT_URI = "export_uri";
    public static final String EXPORT_SECRET = "export_secret";
    public static final String EXPORT_ALL = "export_all";
    public static final String EXPORT_KEY_RING_MASTER_KEY_ID = "export_key_ring_id";

    // upload key
    public static final String UPLOAD_KEY_SERVER = "upload_key_server";

    // promote key
    public static final String PROMOTE_MASTER_KEY_ID = "promote_master_key_id";
    public static final String PROMOTE_CARD_AID = "promote_card_aid";
    public static final String PROMOTE_SUBKEY_IDS = "promote_fingerprints";

    // consolidate
    public static final String CONSOLIDATE_RECOVERY = "consolidate_recovery";

    Messenger mMessenger;

    // this attribute can possibly merged with the one above? not sure...
    private AtomicBoolean mActionCanceled = new AtomicBoolean(false);


    private KeyImportAccumulator mKeyImportAccumulator;

    private KeychainService mKeychainService;

    @Override
    public IBinder onBind(Intent intent) {
        return null;
    }

    /**
     * This is run on the main thread, we need to spawn a runnable which runs on another thread for the actual operation
     */
    @Override
    public int onStartCommand(final Intent intent, int flags, int startId) {
        mKeychainService = this;

        if (ACTION_CANCEL.equals(intent.getAction())) {
            mActionCanceled.set(true);
            return START_NOT_STICKY;
        }

        Runnable actionRunnable = new Runnable() {
            @Override
            public void run() {
                // We have not been cancelled! (yet)
                mActionCanceled.set(false);

                Bundle extras = intent.getExtras();
                if (extras == null) {
                    Log.e(Constants.TAG, "Extras bundle is null!");
                    return;
                }

                if (!(extras.containsKey(EXTRA_MESSENGER) || extras.containsKey(EXTRA_DATA) || (intent
                        .getAction() == null))) {
                    Log.e(Constants.TAG,
                            "Extra bundle must contain a messenger, a data bundle, and an action!");
                    return;
                }

                Uri dataUri = intent.getData();

                mMessenger = (Messenger) extras.get(EXTRA_MESSENGER);
                Bundle data = extras.getBundle(EXTRA_DATA);
                if (data == null) {
                    Log.e(Constants.TAG, "data extra is null!");
                    return;
                }

                Log.logDebugBundle(data, "EXTRA_DATA");

                ProviderHelper providerHelper = new ProviderHelper(mKeychainService);

                String action = intent.getAction();

                // executeServiceMethod action from extra bundle
                switch (action) {
                    case ACTION_CONSOLIDATE: {

                        // Operation
                        ConsolidateResult result;
                        if (data.containsKey(CONSOLIDATE_RECOVERY) && data.getBoolean(CONSOLIDATE_RECOVERY)) {
                            result = providerHelper.consolidateDatabaseStep2(mKeychainService);
                        } else {
                            result = providerHelper.consolidateDatabaseStep1(mKeychainService);
                        }

                        // Result
                        sendMessageToHandler(MessageStatus.OKAY, result);

                        break;
                    }
                    case ACTION_VERIFY_KEYBASE_PROOF: {

                        try {
                            Proof proof = new Proof(new JSONObject(data.getString(KEYBASE_PROOF)));
                            setProgress(R.string.keybase_message_fetching_data, 0, 100);

                            Prover prover = Prover.findProverFor(proof);

                            if (prover == null) {
                                sendProofError(getString(R.string.keybase_no_prover_found) + ": " + proof
                                        .getPrettyName());
                                return;
                            }

                            if (!prover.fetchProofData()) {
                                sendProofError(prover.getLog(), getString(R.string.keybase_problem_fetching_evidence));
                                return;
                            }
                            String requiredFingerprint = data.getString(KEYBASE_REQUIRED_FINGERPRINT);
                            if (!prover.checkFingerprint(requiredFingerprint)) {
                                sendProofError(getString(R.string.keybase_key_mismatch));
                                return;
                            }

                            String domain = prover.dnsTxtCheckRequired();
                            if (domain != null) {
                                DNSMessage dnsQuery = new Client().query(new Question(domain, Record.TYPE.TXT));
                                if (dnsQuery == null) {
                                    sendProofError(prover.getLog(), getString(R.string.keybase_dns_query_failure));
                                    return;
                                }
                                Record[] records = dnsQuery.getAnswers();
                                List<List<byte[]>> extents = new ArrayList<List<byte[]>>();
                                for (Record r : records) {
                                    Data d = r.getPayload();
                                    if (d instanceof TXT) {
                                        extents.add(((TXT) d).getExtents());
                                    }
                                }
                                if (!prover.checkDnsTxt(extents)) {
                                    sendProofError(prover.getLog(), null);
                                    return;
                                }
                            }

                            byte[] messageBytes = prover.getPgpMessage().getBytes();
                            if (prover.rawMessageCheckRequired()) {
                                InputStream messageByteStream = PGPUtil.getDecoderStream(new ByteArrayInputStream
                                        (messageBytes));
                                if (!prover.checkRawMessageBytes(messageByteStream)) {
                                    sendProofError(prover.getLog(), null);
                                    return;
                                }
                            }

                            PgpDecryptVerify op = new PgpDecryptVerify(mKeychainService, providerHelper,
                                    mKeychainService);

                            PgpDecryptVerifyInputParcel input = new PgpDecryptVerifyInputParcel(messageBytes)
                                    .setSignedLiteralData(true)
                                    .setRequiredSignerFingerprint(requiredFingerprint);

                            DecryptVerifyResult decryptVerifyResult = op.execute(input, new CryptoInputParcel());

                            if (!decryptVerifyResult.success()) {
                                OperationLog log = decryptVerifyResult.getLog();
                                OperationResult.LogEntryParcel lastEntry = null;
                                for (OperationResult.LogEntryParcel entry : log) {
                                    lastEntry = entry;
                                }
                                sendProofError(getString(lastEntry.mType.getMsgId()));
                                return;
                            }

                            if (!prover.validate(new String(decryptVerifyResult.getOutputBytes()))) {
                                sendProofError(getString(R.string.keybase_message_payload_mismatch));
                                return;
                            }

                            Bundle resultData = new Bundle();
                            resultData.putString(ServiceProgressHandler.DATA_MESSAGE, "OK");

                            // these help the handler construct a useful human-readable message
                            resultData.putString(ServiceProgressHandler.KEYBASE_PROOF_URL, prover.getProofUrl());
                            resultData.putString(ServiceProgressHandler.KEYBASE_PRESENCE_URL, prover.getPresenceUrl());
                            resultData.putString(ServiceProgressHandler.KEYBASE_PRESENCE_LABEL, prover
                                    .getPresenceLabel());
                            sendMessageToHandler(MessageStatus.OKAY, resultData);
                        } catch (Exception e) {
                            sendErrorToHandler(e);
                        }

                        break;
                    }
                    case ACTION_DELETE: {

                        // Input
                        long[] masterKeyIds = data.getLongArray(DELETE_KEY_LIST);
                        boolean isSecret = data.getBoolean(DELETE_IS_SECRET);

                        // Operation
                        DeleteOperation op = new DeleteOperation(mKeychainService, providerHelper, mKeychainService);
                        DeleteResult result = op.execute(masterKeyIds, isSecret);

                        // Result
                        sendMessageToHandler(MessageStatus.OKAY, result);

                        break;
                    }
                    case ACTION_EDIT_KEYRING: {

                        // Input
                        SaveKeyringParcel saveParcel = data.getParcelable(EDIT_KEYRING_PARCEL);
                        CryptoInputParcel cryptoInput = data.getParcelable(EXTRA_CRYPTO_INPUT);

                        // Operation
                        EditKeyOperation op = new EditKeyOperation(mKeychainService, providerHelper,
                                mKeychainService, mActionCanceled);
                        OperationResult result = op.execute(saveParcel, cryptoInput);

                        // Result
                        sendMessageToHandler(MessageStatus.OKAY, result);

                        break;
                    }
                    case ACTION_PROMOTE_KEYRING: {

                        // Input
                        long keyRingId = data.getLong(PROMOTE_MASTER_KEY_ID);
                        byte[] cardAid = data.getByteArray(PROMOTE_CARD_AID);
                        long[] subKeyIds = data.getLongArray(PROMOTE_SUBKEY_IDS);

                        // Operation
                        PromoteKeyOperation op = new PromoteKeyOperation(
                                mKeychainService, providerHelper, mKeychainService,
                                mActionCanceled);
                        PromoteKeyResult result = op.execute(keyRingId, cardAid, subKeyIds);

                        // Result
                        sendMessageToHandler(MessageStatus.OKAY, result);

                        break;
                    }
                    case ACTION_EXPORT_KEYRING: {

                        // Input
                        boolean exportSecret = data.getBoolean(EXPORT_SECRET, false);
                        String outputFile = data.getString(EXPORT_FILENAME);
                        Uri outputUri = data.getParcelable(EXPORT_URI);

                        boolean exportAll = data.getBoolean(EXPORT_ALL);
                        long[] masterKeyIds = exportAll ? null : data.getLongArray(EXPORT_KEY_RING_MASTER_KEY_ID);

                        // Operation
                        ImportExportOperation importExportOperation = new ImportExportOperation(
                                mKeychainService, providerHelper, mKeychainService);
                        ExportResult result;
                        if (outputFile != null) {
                            result = importExportOperation.exportToFile(masterKeyIds, exportSecret, outputFile);
                        } else {
                            result = importExportOperation.exportToUri(masterKeyIds, exportSecret, outputUri);
                        }

                        // Result
                        sendMessageToHandler(MessageStatus.OKAY, result);

                        break;
                    }
                    case ACTION_IMPORT_KEYRING: {

                        // Input
                        String keyServer = data.getString(IMPORT_KEY_SERVER);
                        ArrayList<ParcelableKeyRing> keyList = data.getParcelableArrayList(IMPORT_KEY_LIST);

                        // either keyList or cache must be null, no guarantees otherwise
                        if (keyList == null) {// import from file, do serially
                            serialKeyImport(null, keyServer, providerHelper);
                        } else {
                            // if there is more than one key with the same fingerprint, we do a serial import to prevent
                            // https://github.com/open-keychain/open-keychain/issues/1221
                            HashSet<String> keyFingerprintSet = new HashSet<>();
                            for (int i = 0; i < keyList.size(); i++) {
                                keyFingerprintSet.add(keyList.get(i).mExpectedFingerprint);
                            }
                            if (keyFingerprintSet.size() == keyList.size()) {
                                // all keys have unique fingerprints
                                multiThreadedKeyImport(keyList.iterator(), keyList.size(), keyServer);
                            } else {
                                serialKeyImport(keyList, keyServer, providerHelper);
                            }
                        }

                        break;
                    }
                    case ACTION_UPLOAD_KEYRING: {
                        try {

                            // Input
                            String keyServer = data.getString(UPLOAD_KEY_SERVER);
                            // and dataUri!

                            // Operation
                            HkpKeyserver server = new HkpKeyserver(keyServer);

                            CanonicalizedPublicKeyRing keyring = providerHelper.getCanonicalizedPublicKeyRing(dataUri);
                            ImportExportOperation importExportOperation = new ImportExportOperation(mKeychainService,
                                    providerHelper, mKeychainService);

                            try {
                                importExportOperation.uploadKeyRingToServer(server, keyring);
                            } catch (Keyserver.AddKeyException e) {
                                throw new PgpGeneralException("Unable to export key to selected server");
                            }

                            sendMessageToHandler(MessageStatus.OKAY);
                        } catch (Exception e) {
                            sendErrorToHandler(e);
                        }
                        break;
                    }
                }
                if (!intent.getAction().equals(ACTION_IMPORT_KEYRING)) {
                    // import keyring handles stopping service on its own
                    stopSelf();
                }
            }
        };

        Thread actionThread = new Thread(actionRunnable);
        actionThread.start();
        
        return START_NOT_STICKY;
    }

    private void sendProofError(List<String> log, String label) {
        String msg = null;
        label = (label == null) ? "" : label + ": ";
        for (String m : log) {
            Log.e(Constants.TAG, label + m);
            msg = m;
        }
        sendProofError(label + msg);
    }

    private void sendProofError(String msg) {
        Bundle bundle = new Bundle();
        bundle.putString(ServiceProgressHandler.DATA_ERROR, msg);
        sendMessageToHandler(MessageStatus.OKAY, bundle);
    }

    private void sendErrorToHandler(Exception e) {
        // TODO: Implement a better exception handling here
        // contextualize the exception, if necessary
        String message;
        if (e instanceof PgpGeneralMsgIdException) {
            e = ((PgpGeneralMsgIdException) e).getContextualized(mKeychainService);
            message = e.getMessage();
        } else {
            message = e.getMessage();
        }
        Log.d(Constants.TAG, "KeychainService Exception: ", e);

        Bundle data = new Bundle();
        data.putString(ServiceProgressHandler.DATA_ERROR, message);
        sendMessageToHandler(MessageStatus.EXCEPTION, null, data);
    }

    private void sendMessageToHandler(MessageStatus status, Integer arg2, Bundle data) {

        Message msg = Message.obtain();
        assert msg != null;
        msg.arg1 = status.ordinal();
        if (arg2 != null) {
            msg.arg2 = arg2;
        }
        if (data != null) {
            msg.setData(data);
        }

        try {
            mMessenger.send(msg);
        } catch (RemoteException e) {
            Log.w(Constants.TAG, "Exception sending message, Is handler present?", e);
        } catch (NullPointerException e) {
            Log.w(Constants.TAG, "Messenger is null!", e);
        }
    }

    private void sendMessageToHandler(MessageStatus status, OperationResult data) {
        Bundle bundle = new Bundle();
        bundle.putParcelable(OperationResult.EXTRA_RESULT, data);
        sendMessageToHandler(status, null, bundle);
    }

    private void sendMessageToHandler(MessageStatus status, Bundle data) {
        sendMessageToHandler(status, null, data);
    }

    private void sendMessageToHandler(MessageStatus status) {
        sendMessageToHandler(status, null, null);
    }

    /**
     * Set progress of ProgressDialog by sending message to handler on UI thread
     */
    @Override
    public void setProgress(String message, int progress, int max) {
        Log.d(Constants.TAG, "Send message by setProgress with progress=" + progress + ", max="
                + max);

        Bundle data = new Bundle();
        if (message != null) {
            data.putString(ServiceProgressHandler.DATA_MESSAGE, message);
        }
        data.putInt(ServiceProgressHandler.DATA_PROGRESS, progress);
        data.putInt(ServiceProgressHandler.DATA_PROGRESS_MAX, max);

        sendMessageToHandler(MessageStatus.UPDATE_PROGRESS, null, data);
    }

    @Override
    public void setProgress(int resourceId, int progress, int max) {
        setProgress(getString(resourceId), progress, max);
    }

    @Override
    public void setProgress(int progress, int max) {
        setProgress(null, progress, max);
    }

    @Override
    public void setPreventCancel() {
        sendMessageToHandler(MessageStatus.PREVENT_CANCEL);
    }

    public void serialKeyImport(ArrayList<ParcelableKeyRing> keyList, final String keyServer,
                                ProviderHelper providerHelper) {
        Log.d(Constants.TAG, "serial key import starting");
        ParcelableFileCache<ParcelableKeyRing> cache =
                new ParcelableFileCache<>(mKeychainService, "key_import.pcl");

        // Operation
        ImportExportOperation importExportOperation = new ImportExportOperation(
                mKeychainService, providerHelper, mKeychainService,
                mActionCanceled);
        // Either list or cache must be null, no guarantees otherwise.
        ImportKeyResult result = keyList != null
                ? importExportOperation.importKeyRings(keyList, keyServer)
                : importExportOperation.importKeyRings(cache, keyServer);

        ContactSyncAdapterService.requestSync();
        // Result
        sendMessageToHandler(MessageStatus.OKAY, result);

        stopSelf();
    }

    public void multiThreadedKeyImport(Iterator<ParcelableKeyRing> keyListIterator, int totKeys, final String
            keyServer) {
        Log.d(Constants.TAG, "Multi-threaded key import starting");
        if (keyListIterator != null) {
            mKeyImportAccumulator = new KeyImportAccumulator(totKeys, mKeychainService);
            setProgress(0, totKeys);

            final int maxThreads = 200;
            ExecutorService importExecutor = new ThreadPoolExecutor(0, maxThreads,
                    30L, TimeUnit.SECONDS,
                    new SynchronousQueue<Runnable>());

            while (keyListIterator.hasNext()) {

                final ParcelableKeyRing pkRing = keyListIterator.next();

                Runnable importOperationRunnable = new Runnable() {

                    @Override
                    public void run() {
                        ImportKeyResult result = null;
                        try {
                            ImportExportOperation importExportOperation = new ImportExportOperation(
                                    mKeychainService,
                                    new ProviderHelper(mKeychainService),
                                    mKeyImportAccumulator.getImportProgressable(),
                                    mActionCanceled);

                            ArrayList<ParcelableKeyRing> list = new ArrayList<>();
                            list.add(pkRing);

                            result = importExportOperation.importKeyRings(list,
                                    keyServer);
                        } finally {
                            // in the off-chance that importKeyRings does something to crash the
                            // thread before it can call singleKeyRingImportCompleted, our imported
                            // key count will go wrong. This will cause the service to never die,
                            // and the progress dialog to stay displayed. The finally block was
                            // originally meant to ensure singleKeyRingImportCompleted was called,
                            // and checks for null were to be introduced, but in such a scenario,
                            // knowing an uncaught error exists in importKeyRings is more important.

                            // if a null gets passed, something wrong is happening. We want a crash.

                            mKeyImportAccumulator.singleKeyRingImportCompleted(result);
                        }
                    }
                };

                importExecutor.execute(importOperationRunnable);
            }
        }
    }

    /**
     * Used to accumulate the results of individual key imports
     */
    private class KeyImportAccumulator {
        private OperationResult.OperationLog mImportLog = new OperationResult.OperationLog();
        private int mTotalKeys;
        private int mImportedKeys = 0;
        private Progressable mInternalProgressable;
        ArrayList<Long> mImportedMasterKeyIds = new ArrayList<Long>();
        private int mBadKeys = 0;
        private int mNewKeys = 0;
        private int mUpdatedKeys = 0;
        private int mSecret = 0;
        private int mResultType = 0;

        /**
         * meant to be used with a service due to stopSelf() in singleKeyRingImportCompleted. Remove this if
         * generalising.
         *
         * @param totalKeys            total number of keys to be imported
         * @param externalProgressable the external progressable to be updated every time a key is imported
         */
        public KeyImportAccumulator(int totalKeys, Progressable externalProgressable) {
            mTotalKeys = totalKeys;
            // ignore updates from ImportExportOperation for now
            mInternalProgressable = new Progressable() {
                @Override
                public void setProgress(String message, int current, int total) {

                }

                @Override
                public void setProgress(int resourceId, int current, int total) {

                }

                @Override
                public void setProgress(int current, int total) {

                }

                @Override
                public void setPreventCancel() {

                }
            };
        }

        private synchronized void singleKeyRingImportCompleted(ImportKeyResult result) {
            // increase imported key count and accumulate log and bad, new etc. key counts from result
            mKeyImportAccumulator.accumulateKeyImport(result);

            setProgress(mKeyImportAccumulator.getImportedKeys(), mKeyImportAccumulator.getTotalKeys());

            if (mKeyImportAccumulator.isImportFinished()) {
                ContactSyncAdapterService.requestSync();

                sendMessageToHandler(ServiceProgressHandler.MessageStatus.OKAY,
                        mKeyImportAccumulator.getConsolidatedImportKeyResult());

                stopSelf();//we're done here
            }
        }

        public Progressable getImportProgressable() {
            return mInternalProgressable;
        }

        public int getTotalKeys() {
            return mTotalKeys;
        }

        public int getImportedKeys() {
            return mImportedKeys;
        }

        public synchronized void accumulateKeyImport(ImportKeyResult result) {
            mImportedKeys++;
            mImportLog.addAll(result.getLog().toList());//accumulates log
            mBadKeys += result.mBadKeys;
            mNewKeys += result.mNewKeys;
            mUpdatedKeys += result.mUpdatedKeys;
            mSecret += result.mSecret;

            long[] masterKeyIds = result.getImportedMasterKeyIds();
            for (long masterKeyId : masterKeyIds) {
                mImportedMasterKeyIds.add(masterKeyId);
            }

            // if any key import has been cancelled, set result type to cancelled
            // resultType is added to in getConsolidatedKayImport to account for remaining factors
            mResultType |= result.getResult() & ImportKeyResult.RESULT_CANCELLED;
        }

        /**
         * returns accumulated result of all imports so far
         */
        public ImportKeyResult getConsolidatedImportKeyResult() {

            // adding required information to mResultType
            // special case,no keys requested for import
            if (mBadKeys == 0 && mNewKeys == 0 && mUpdatedKeys == 0) {
                mResultType = ImportKeyResult.RESULT_FAIL_NOTHING;
            } else {
                if (mNewKeys > 0) {
                    mResultType |= ImportKeyResult.RESULT_OK_NEWKEYS;
                }
                if (mUpdatedKeys > 0) {
                    mResultType |= ImportKeyResult.RESULT_OK_UPDATED;
                }
                if (mBadKeys > 0) {
                    mResultType |= ImportKeyResult.RESULT_WITH_ERRORS;
                    if (mNewKeys == 0 && mUpdatedKeys == 0) {
                        mResultType |= ImportKeyResult.RESULT_ERROR;
                    }
                }
                if (mImportLog.containsWarnings()) {
                    mResultType |= ImportKeyResult.RESULT_WARNINGS;
                }
            }

            long masterKeyIds[] = new long[mImportedMasterKeyIds.size()];
            for (int i = 0; i < masterKeyIds.length; i++) {
                masterKeyIds[i] = mImportedMasterKeyIds.get(i);
            }

            return new ImportKeyResult(mResultType, mImportLog, mNewKeys, mUpdatedKeys, mBadKeys,
                    mSecret, masterKeyIds);
        }

        public boolean isImportFinished() {
            return mTotalKeys == mImportedKeys;
        }
    }
}
