/*
 * Copyright (c) 2016 - 2019 Rui Zhao <[email protected]>
 *
 * This file is part of Easer.
 *
 * Easer 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.
 *
 * Easer 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 Easer.  If not, see <http://www.gnu.org/licenses/>.
 */

package ryey.easer.core;

import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.os.Bundle;
import android.os.Handler;
import android.os.IBinder;
import android.os.Message;
import android.os.Messenger;
import android.os.ParcelUuid;
import android.os.RemoteException;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.collection.ArrayMap;
import androidx.collection.ArraySet;

import com.orhanobut.logger.Logger;

import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.Objects;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.Callable;

import ryey.easer.commons.local_skill.operationskill.OperationData;
import ryey.easer.remote_plugin.RemoteOperationData;

/**
 * This class is meant to help the communication to remote plugins.
 * It communicates with {@link RemotePluginRegistryService}, exposing relevant methods.
 *
 * TODO: having a `SkillInfoSetSnapshot`?
 */
public class RemotePluginCommunicationHelper {

    /**
     * Constants of this class are used only internally in Easer (? at least I believe), not in plugins.
     * They can be changed freely without side-effects.
     */
    public static class C {

        public static final String EXTRA_MESSAGE_ID = "ryey.easer.IPC.EXTRA.MESSAGE_ID";

        public static final int MSG_FIND_PLUGIN = 0;
        public static final int MSG_FIND_PLUGIN_RESPONSE = 0;
        public static final String EXTRA_PLUGIN_ID = "ryey.easer.IPC.EXTRA.PLUGIN_ID";
        public static final String EXTRA_PLUGIN_INFO = "ryey.easer.IPC.EXTRA.PLUGIN_INFO";

        public static final int MSG_CURRENT_OPERATION_PLUGIN_LIST = 1;
        public static final int MSG_CURRENT_OPERATION_PLUGIN_LIST_RESPONSE = 1;
        public static final String EXTRA_PLUGIN_LIST = "ryey.easer.IPC.EXTRA.PLUGIN_LIST";

        public static final int MSG_PARSE_OPERATION_DATA = 2;
        public static final int MSG_PARSE_OPERATION_DATA_RESPONSE = 2;
        public static final String EXTRA_RAW_DATA = "ryey.easer.IPC.EXTRA.RAW_DATA";
        public static final String EXTRA_PLUGIN_DATA = "ryey.easer.IPC.EXTRA.PLUGIN_DATA";

        public static final int MSG_TRIGGER_OPERATION = 3;
        public static final int MSG_TRIGGER_OPERATION_RESPONSE = 3;
        public static final String EXTRA_SUCCESS = "ryey.easer.IPC.EXTRA_SUCCESS";

        public static final int MSG_EDIT_OPERATION_DATA = 4;
        public static final int MSG_EDIT_OPERATION_DATA_RESPONSE = 4;
        public static final String EXTRA_PLUGIN_PACKAGE = "ryey.easer.IPC.EXTRA.PLUGIN_PACKAGE";
        public static final String EXTRA_PLUGIN_EDIT_DATA_ACTIVITY = "ryey.easer.IPC.EXTRA.PLUGIN_EDIT_DATA_ACTIVITY";
    }

    private Context context;

    @Nullable
    private Messenger outMessenger;
    private IncomingHandler handler = new IncomingHandler(new WeakReference<>(this));
    public final Messenger inMessenger = new Messenger(handler);
    private ServiceConnection serviceConnection = new ServiceConnection() {
        @Override
        public void onServiceConnected(ComponentName componentName, IBinder iBinder) {
            Logger.d("[RemotePluginCommunicationHelper:%s] onServiceConnected", context);
            outMessenger = new Messenger(iBinder);
            delayedTaskUntilConnectedWrapper.onConnected(outMessenger);
        }

        @Override
        public void onServiceDisconnected(ComponentName componentName) {
            delayedTaskUntilConnectedWrapper.onDisconnected();
            outMessenger = null;
        }
    };

    private DelayedTaskUntilConnectedWrapper delayedTaskUntilConnectedWrapper = new DelayedTaskUntilConnectedWrapper();
    private void doAfterConnect(Callable<Void> task) {
        delayedTaskUntilConnectedWrapper.doAfterConnected(task);
    }

    private final AsyncHelper.CallbackStore<OnFindPluginResultCallback> onPluginFoundCallbackCallbackStore = new AsyncHelper.CallbackStore<>(new ArrayMap<>());
    private final AsyncHelper.CallbackStore<OnOperationPluginListObtainedCallback> onOperationPluginListObtainedCallbackCallbackStore = new AsyncHelper.CallbackStore<>(new ArrayMap<>());
    private final AsyncHelper.CallbackStore<OnEditDataIntentObtainedCallback> onEditDataIntentObtainedCallbackCallbackStore = new AsyncHelper.CallbackStore<>(new ArrayMap<>());
    private final AsyncHelper.CallbackStore<OnOperationDataParsedCallback> onOperationDataParsedCallbackCallbackStore = new AsyncHelper.CallbackStore<>(new ArrayMap<>());
    private final AsyncHelper.TimedCallbackStore<SkillHelper.OperationHelper.OnOperationLoadResultCallback> onOperationLoadResultCallbackCallbackStore = new LoadProfileCallbackStore();

    public RemotePluginCommunicationHelper(@NonNull Context context) {
        this.context = context;
    }

    public void begin() {
        Logger.d("[RemotePluginCommunicationHelper] begin()");
        Intent intent = new Intent(context, RemotePluginRegistryService.class);
        context.bindService(intent, serviceConnection, Context.BIND_AUTO_CREATE);
    }

//    @Deprecated
//    public void onConnectedParseOperationData(final String id, final String rawData, final OnOperationDataParsedCallback onOperationDataParsedCallback) {
//        ParcelUuid uuid = onOperationDataParsedCallbackCallbackStore.putCallback(onOperationDataParsedCallback);
//        doAfterConnect(new Callable<Void>() {
//            @Override
//            public Void call() throws Exception {
//                Message message = Message.obtain();
//                message.what = C.MSG_PARSE_OPERATION_DATA;
//                message.replyTo = inMessenger;
//                Bundle bundle = new Bundle();
//                bundle.putString(C.EXTRA_PLUGIN_ID, id);
//                bundle.putString(C.EXTRA_RAW_DATA, rawData);
//                bundle.putParcelable(C.EXTRA_MESSAGE_ID, uuid);
//                message.setData(bundle);
//                try {
//                    outMessenger.send(message);
//                } catch (RemoteException e) {
//                    e.printStackTrace();
//                }
//                return null;
//            }
//        });
//    }

    synchronized public void asyncFindPlugin(final String id, OnFindPluginResultCallback onFindPluginResultCallback) {
        ParcelUuid uuid = onPluginFoundCallbackCallbackStore.putCallback(onFindPluginResultCallback);
        doAfterConnect(new Callable<Void>() {
            @Override
            public Void call() throws Exception {
                Message message = Message.obtain();
                message.what = C.MSG_FIND_PLUGIN;
                message.replyTo = inMessenger;
                Bundle bundle = new Bundle();
                bundle.putString(C.EXTRA_PLUGIN_ID, id);
                bundle.putParcelable(C.EXTRA_MESSAGE_ID, uuid);
                message.setData(bundle);
                try {
                    outMessenger.send(message);
                } catch (RemoteException e) {
                    e.printStackTrace();
                }
                return null;
            }
        });
    }

    synchronized public void asyncCurrentOperationPluginList(@NonNull OnOperationPluginListObtainedCallback onOperationPluginListObtainedCallback) {
        ParcelUuid uuid = onOperationPluginListObtainedCallbackCallbackStore.putCallback(onOperationPluginListObtainedCallback);
        doAfterConnect(new Callable<Void>() {
            @Override
            public Void call() throws Exception {
                Message message = Message.obtain();
                message.what = C.MSG_CURRENT_OPERATION_PLUGIN_LIST;
                message.replyTo = inMessenger;
                Bundle bundle = new Bundle();
                bundle.putParcelable(C.EXTRA_MESSAGE_ID, uuid);
                message.setData(bundle);
                try {
                    outMessenger.send(message);
                } catch (RemoteException e) {
                    e.printStackTrace();
                }
                return null;
            }
        });
    }

    public void asyncTriggerOperation(final UUID jobId, final String skillId, final RemoteOperationData data, SkillHelper.OperationHelper.OnOperationLoadResultCallback callback) {
        final ParcelUuid parcelUuid = new ParcelUuid(jobId);
        onOperationLoadResultCallbackCallbackStore.putCallback(parcelUuid, callback);
        doAfterConnect(new Callable<Void>() {
            @Override
            public Void call() throws Exception {
                Message message = Message.obtain();
                message.what = C.MSG_TRIGGER_OPERATION;
                message.replyTo = inMessenger;
                message.getData().putParcelable(C.EXTRA_MESSAGE_ID, parcelUuid);
                message.getData().putString(C.EXTRA_PLUGIN_ID, skillId);
                message.getData().putParcelable(C.EXTRA_PLUGIN_DATA, data);
                try {
                    outMessenger.send(message);
                    callback.onResult(jobId, true); //TODO: listen remote result
                } catch (RemoteException e) {
                    e.printStackTrace();
                    callback.onResult(jobId, false);
                }
                return null;
            }
        });
    }

    public void asyncRemoteEditOperationData(final String id, OnEditDataIntentObtainedCallback onEditDataIntentObtainedCallback) {
        ParcelUuid uuid = onEditDataIntentObtainedCallbackCallbackStore.putCallback(onEditDataIntentObtainedCallback);
        doAfterConnect(new Callable<Void>() {
            @Override
            public Void call() throws Exception {
                Message message = Message.obtain();
                message.what = C.MSG_EDIT_OPERATION_DATA;
                message.replyTo = inMessenger;
                Bundle bundle = new Bundle();
                bundle.putString(C.EXTRA_PLUGIN_ID, id);
                bundle.putParcelable(C.EXTRA_MESSAGE_ID, uuid);
                message.setData(bundle);
                outMessenger.send(message);
                return null;
            }
        });
    }

    public void end() {
        context.unbindService(serviceConnection);
    }

    static class IncomingHandler extends Handler {
        WeakReference<RemotePluginCommunicationHelper> ref;

        IncomingHandler(WeakReference<RemotePluginCommunicationHelper> reference) {
            ref = reference;
        }

        @Override
        public void handleMessage(@NonNull Message msg) {
            Logger.d("[RemotePluginCommunicationHelper][handleMessage] %s", msg);
            if (msg.what == C.MSG_FIND_PLUGIN_RESPONSE) {
                msg.getData().setClassLoader(RemotePluginInfo.class.getClassLoader());
                RemotePluginInfo info = msg.getData().getParcelable(C.EXTRA_PLUGIN_INFO);
                ParcelUuid uuid = msg.getData().getParcelable(C.EXTRA_MESSAGE_ID);
                assert uuid != null;
                OnFindPluginResultCallback callback = ref.get().onPluginFoundCallbackCallbackStore.extractCallBack(uuid);
                if (callback != null)
                    callback.onFindPluginResult(info);
            } else if (msg.what == C.MSG_CURRENT_OPERATION_PLUGIN_LIST_RESPONSE) {
                msg.getData().setClassLoader(RemoteOperationPluginInfo.class.getClassLoader()); // Required (for strange reason); otherwise ClassNotFound
                ArrayList<RemoteOperationPluginInfo> infoList = msg.getData().getParcelableArrayList(C.EXTRA_PLUGIN_LIST);
                Set<RemoteOperationPluginInfo> infoSet = new ArraySet<>(infoList);
                ParcelUuid uuid = msg.getData().getParcelable(C.EXTRA_MESSAGE_ID);
                assert uuid != null;
                OnOperationPluginListObtainedCallback callback = ref.get().onOperationPluginListObtainedCallbackCallbackStore.extractCallBack(uuid);
                if (callback != null)
                    callback.onListObtained(infoSet);
            } else if (msg.what == C.MSG_PARSE_OPERATION_DATA_RESPONSE) {
                OperationData operationData = msg.getData().getParcelable(C.EXTRA_PLUGIN_DATA);
                assert operationData != null;
                ParcelUuid uuid = msg.getData().getParcelable(C.EXTRA_MESSAGE_ID);
                OnOperationDataParsedCallback callback = ref.get().onOperationDataParsedCallbackCallbackStore.extractCallBack(uuid);
                if (callback != null)
                    callback.onOperationDataParsed(operationData);
            } else if (msg.what == C.MSG_EDIT_OPERATION_DATA_RESPONSE) {
                Bundle bundle = msg.getData();
                String packageName = bundle.getString(C.EXTRA_PLUGIN_PACKAGE);
                String activityName = bundle.getString(C.EXTRA_PLUGIN_EDIT_DATA_ACTIVITY);
                assert packageName != null;
                assert activityName != null;
                Intent editDataIntent = new Intent();
                editDataIntent.setComponent(new ComponentName(packageName, activityName));
                ParcelUuid uuid = bundle.getParcelable(C.EXTRA_MESSAGE_ID);
                OnEditDataIntentObtainedCallback callback = ref.get().onEditDataIntentObtainedCallbackCallbackStore.extractCallBack(uuid);
                if (callback != null)
                    callback.onEditDataIntentObtained(editDataIntent);
            } else if (msg.what == C.MSG_TRIGGER_OPERATION_RESPONSE) {
                Bundle bundle = msg.getData();
                ParcelUuid uuid = Objects.requireNonNull(bundle.getParcelable(C.EXTRA_MESSAGE_ID));
                boolean success = bundle.getBoolean(C.EXTRA_SUCCESS);
                SkillHelper.OperationHelper.OnOperationLoadResultCallback callback = ref.get().onOperationLoadResultCallbackCallbackStore.extractCallBack(uuid);
                if (callback != null)
                    callback.onResult(uuid.getUuid(), success);
            }
        }
    }

    public interface OnOperationPluginListObtainedCallback {
        void onListObtained(@NonNull Set<RemoteOperationPluginInfo> operationPluginInfos);
    }

    public interface OnFindPluginResultCallback {
        void onFindPluginResult(@Nullable RemotePluginInfo info);
    }

    public interface OnOperationDataParsedCallback {
        void onOperationDataParsed(@NonNull OperationData data);
    }

    public interface OnEditDataIntentObtainedCallback {
        void onEditDataIntentObtained(@NonNull Intent editDataIntent);
    }

    static class DelayedTaskUntilConnectedWrapper extends AsyncHelper.DelayedWhenSatisfied {
        void onConnected(Messenger outMessenger) {
            super.onSatisfied();
        }

        void onDisconnected() {
            super.onUnsatisfied();
        }

        void doAfterConnected(Callable<Void> task) {
            super.doAfter(task);
        }
    }

    private static class LoadProfileCallbackStore extends AsyncHelper.TimedCallbackStore<SkillHelper.OperationHelper.OnOperationLoadResultCallback> {

        LoadProfileCallbackStore() {
            super(new ArrayMap<>(), 5000);
        }

        @Override
        protected void onTimeout(ParcelUuid uuid, SkillHelper.OperationHelper.OnOperationLoadResultCallback callback) {
            callback.onResult(uuid.getUuid(), null);
        }
    }

}