/*
 * Copyright (C) 2017 Schürmann & Breitmoser GbR
 *
 * 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.remote;

import android.app.Service;
import android.content.Context;
import android.content.Intent;
import android.os.Binder;
import android.os.Bundle;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.IBinder;
import android.os.Message;
import android.os.Messenger;
import android.os.RemoteException;

import org.openintents.openpgp.util.OpenPgpApi;
import org.sufficientlysecure.keychain.Constants;
import org.sufficientlysecure.keychain.service.input.CryptoInputParcel;
import timber.log.Timber;

import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;

/**
 * This service caches CryptoInputParcels, which contain sensitive data like passphrases.
 * This way, they are not exposed to the client app using the API.
 */
public class CryptoInputParcelCacheService extends Service {

    public static final String ACTION_ADD = Constants.INTENT_PREFIX + "ADD";
    public static final String ACTION_GET = Constants.INTENT_PREFIX + "GET";

    public static final String EXTRA_CRYPTO_INPUT_PARCEL = "crypto_input_parcel";
    public static final String EXTRA_UUID1 = "uuid1";
    public static final String EXTRA_UUID2 = "uuid2";
    public static final String EXTRA_MESSENGER = "messenger";

    private static final int MSG_GET_OKAY = 1;
    private static final int MSG_GET_NOT_FOUND = 2;

    Context mContext;

    private static final UUID NULL_UUID = new UUID(0, 0);

    private ConcurrentHashMap<UUID, CryptoInputParcel> mCache = new ConcurrentHashMap<>();

    public static class InputParcelNotFound extends Exception {
        public InputParcelNotFound() {
        }

        public InputParcelNotFound(String name) {
            super(name);
        }
    }

    public static void addCryptoInputParcel(Context context, Intent data, CryptoInputParcel inputParcel) {
        UUID mTicket = addCryptoInputParcel(context, inputParcel);

        // prevent crashes with ClassNotFoundException when unmarshalling: org.openintents.openpgp.AutocryptPeerUpdate
        data.setExtrasClassLoader(CryptoInputParcelCacheService.class.getClassLoader());

        // And write out the UUID most and least significant bits.
        data.setExtrasClassLoader(CryptoInputParcelCacheService.class.getClassLoader());
        data.putExtra(OpenPgpApi.EXTRA_CALL_UUID1, mTicket.getMostSignificantBits());
        data.putExtra(OpenPgpApi.EXTRA_CALL_UUID2, mTicket.getLeastSignificantBits());
    }

    public static CryptoInputParcel getCryptoInputParcel(Context context, Intent data) {
        if (!data.getExtras().containsKey(OpenPgpApi.EXTRA_CALL_UUID1)
                || !data.getExtras().containsKey(OpenPgpApi.EXTRA_CALL_UUID2)) {
            return null;
        }
        long mostSig = data.getLongExtra(OpenPgpApi.EXTRA_CALL_UUID1, 0);
        long leastSig = data.getLongExtra(OpenPgpApi.EXTRA_CALL_UUID2, 0);
        UUID uuid = new UUID(mostSig, leastSig);
        try {
            return getCryptoInputParcel(context, uuid);
        } catch (InputParcelNotFound inputParcelNotFound) {
            return null;
        }
    }

    private static UUID addCryptoInputParcel(Context context, CryptoInputParcel inputParcel) {
        UUID uuid = UUID.randomUUID();

        Intent intent = new Intent(context, CryptoInputParcelCacheService.class);
        intent.setAction(ACTION_ADD);
        intent.putExtra(EXTRA_CRYPTO_INPUT_PARCEL, inputParcel);
        intent.putExtra(EXTRA_UUID1, uuid.getMostSignificantBits());
        intent.putExtra(EXTRA_UUID2, uuid.getLeastSignificantBits());
        context.startService(intent);
        return uuid;
    }

    private static CryptoInputParcel getCryptoInputParcel(Context context, UUID uuid) throws InputParcelNotFound {
        Intent intent = new Intent(context, CryptoInputParcelCacheService.class);
        intent.setAction(ACTION_GET);

        final Object mutex = new Object();
        final Message returnMessage = Message.obtain();

        HandlerThread handlerThread = new HandlerThread("getParcelableThread");
        handlerThread.start();
        Handler returnHandler = new Handler(handlerThread.getLooper()) {
            @Override
            public void handleMessage(Message message) {
                // copy over result to handle after mutex.wait
                returnMessage.what = message.what;
                returnMessage.copyFrom(message);
                synchronized (mutex) {
                    mutex.notify();
                }
                // quit handlerThread
                getLooper().quit();
            }
        };

        // Create a new Messenger for the communication back
        Messenger messenger = new Messenger(returnHandler);
        intent.putExtra(EXTRA_UUID1, uuid.getMostSignificantBits());
        intent.putExtra(EXTRA_UUID2, uuid.getLeastSignificantBits());
        intent.putExtra(EXTRA_MESSENGER, messenger);
        // send intent to this service
        context.startService(intent);

        // Wait on mutex until parcelable is returned to handlerThread. Note that this local
        // variable is used in the handler closure above, so it does make sense here!
        // noinspection SynchronizationOnLocalVariableOrMethodParameter
        synchronized (mutex) {
            try {
                mutex.wait(3000);
            } catch (InterruptedException e) {
                // don't care
            }
        }

        switch (returnMessage.what) {
            case MSG_GET_OKAY:
                Bundle returnData = returnMessage.getData();
                returnData.setClassLoader(context.getClassLoader());
                return returnData.getParcelable(EXTRA_CRYPTO_INPUT_PARCEL);
            case MSG_GET_NOT_FOUND:
                throw new InputParcelNotFound();
            default:
                Timber.e("timeout!");
                throw new InputParcelNotFound("should not happen!");
        }
    }

    /**
     * Executed when service is started by intent
     */
    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {

        if (intent == null || intent.getAction() == null) {
            return START_NOT_STICKY;
        }

        String action = intent.getAction();
        switch (action) {
            case ACTION_ADD: {
                long uuid1 = intent.getLongExtra(EXTRA_UUID1, 0);
                long uuid2 = intent.getLongExtra(EXTRA_UUID2, 0);
                UUID uuid = new UUID(uuid1, uuid2);
                CryptoInputParcel inputParcel = intent.getParcelableExtra(EXTRA_CRYPTO_INPUT_PARCEL);
                mCache.put(uuid, inputParcel);

                break;
            }
            case ACTION_GET: {
                long uuid1 = intent.getLongExtra(EXTRA_UUID1, 0);
                long uuid2 = intent.getLongExtra(EXTRA_UUID2, 0);
                UUID uuid = new UUID(uuid1, uuid2);
                Messenger messenger = intent.getParcelableExtra(EXTRA_MESSENGER);

                Message msg = Message.obtain();
                // UUID.equals isn't well documented; we use compareTo instead.
                if (NULL_UUID.compareTo(uuid) == 0) {
                    msg.what = MSG_GET_NOT_FOUND;
                } else {
                    CryptoInputParcel inputParcel = mCache.get(uuid);
                    mCache.remove(uuid);
                    msg.what = MSG_GET_OKAY;
                    Bundle bundle = new Bundle();
                    bundle.putParcelable(EXTRA_CRYPTO_INPUT_PARCEL, inputParcel);
                    msg.setData(bundle);
                }

                try {
                    messenger.send(msg);
                } catch (RemoteException e) {
                    Timber.e(e, "CryptoInputParcelCacheService: Sending message failed");
                }
                break;
            }
            default: {
                Timber.e("CryptoInputParcelCacheService: Intent or Intent Action not supported!");
                break;
            }
        }

        if (mCache.size() <= 0) {
            // stop whole service if cache is empty
            Timber.d("CryptoInputParcelCacheService: No passphrases remaining in memory, stopping service!");
            stopSelf();
        }

        return START_NOT_STICKY;
    }

    @Override
    public void onCreate() {
        super.onCreate();
        mContext = this;
        Timber.d("CryptoInputParcelCacheService, onCreate()");
    }

    @Override
    public void onDestroy() {
        super.onDestroy();
        Timber.d("CryptoInputParcelCacheService, onDestroy()");
    }

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

    public class CryptoInputParcelCacheServiceBinder extends Binder {
        public CryptoInputParcelCacheService getService() {
            return CryptoInputParcelCacheService.this;
        }
    }

    private final IBinder mBinder = new CryptoInputParcelCacheServiceBinder();

}