/*
 *     PowerSwitch by Max Rosin & Markus Ressel
 *     Copyright (C) 2015  Markus Ressel
 *
 *     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/>.
 */

// Constants and functions for Tasker *extensions* to the plugin protocol
// See Also: http://tasker.dinglisch.net/plugins.html

// Release Notes

// v1.1 20140202
// added function variableNameValid()
// fixed some javadoc entries (thanks to David Stone)

// v1.2 20140211
// added ACTION_EDIT_EVENT

// v1.3 20140227
// added REQUESTED_TIMEOUT_MS_NONE, REQUESTED_TIMEOUT_MS_MAX and REQUESTED_TIMEOUT_MS_NEVER
// requestTimeoutMS(): added range check

// v1.4 20140516
// support for data pass through in REQUEST_QUERY intent
// some javadoc entries fixed (thanks again David :-))

// v1.5 20141120
// added RESULT_CODE_FAILED_PLUGIN_FIRST
// added Setting.VARNAME_ERROR_MESSAGE

// v1.6 20150213
// added Setting.getHintTimeoutMS()
// added Host.addHintTimeoutMS()

// Constants and functions for Tasker *extensions* to the plugin protocol
// See Also: http://tasker.dinglisch.net/plugins.html

// Release Notes

// v1.1 20140202
// added function variableNameValid()
// fixed some javadoc entries (thanks to David Stone)

// v1.2 20140211
// added ACTION_EDIT_EVENT

// v1.3 20140227
// added REQUESTED_TIMEOUT_MS_NONE, REQUESTED_TIMEOUT_MS_MAX and REQUESTED_TIMEOUT_MS_NEVER
// requestTimeoutMS(): added range check

// v1.4 20140516
// support for data pass through in REQUEST_QUERY intent
// some javadoc entries fixed (thanks again David :-))

// v1.5 20141120
// added RESULT_CODE_FAILED_PLUGIN_FIRST
// added Setting.VARNAME_ERROR_MESSAGE

// v1.6 20150213
// added Setting.getHintTimeoutMS()
// added Host.addHintTimeoutMS()

// Constants and functions for Tasker *extensions* to the plugin protocol
// See Also: http://tasker.dinglisch.net/plugins.html

// Release Notes

// v1.1 20140202
// added function variableNameValid()
// fixed some javadoc entries (thanks to David Stone)

// v1.2 20140211
// added ACTION_EDIT_EVENT

// v1.3 20140227
// added REQUESTED_TIMEOUT_MS_NONE, REQUESTED_TIMEOUT_MS_MAX and REQUESTED_TIMEOUT_MS_NEVER
// requestTimeoutMS(): added range check

// v1.4 20140516
// support for data pass through in REQUEST_QUERY intent
// some javadoc entries fixed (thanks again David :-))

// v1.5 20141120
// added RESULT_CODE_FAILED_PLUGIN_FIRST
// added Setting.VARNAME_ERROR_MESSAGE

// v1.6 20150213
// added Setting.getHintTimeoutMS()
// added Host.addHintTimeoutMS()

// Constants and functions for Tasker *extensions* to the plugin protocol
// See Also: http://tasker.dinglisch.net/plugins.html

// Release Notes

// v1.1 20140202
// added function variableNameValid()
// fixed some javadoc entries (thanks to David Stone)

// v1.2 20140211
// added ACTION_EDIT_EVENT

// v1.3 20140227
// added REQUESTED_TIMEOUT_MS_NONE, REQUESTED_TIMEOUT_MS_MAX and REQUESTED_TIMEOUT_MS_NEVER
// requestTimeoutMS(): added range check

// v1.4 20140516
// support for data pass through in REQUEST_QUERY intent
// some javadoc entries fixed (thanks again David :-))

// v1.5 20141120
// added RESULT_CODE_FAILED_PLUGIN_FIRST
// added Setting.VARNAME_ERROR_MESSAGE

// v1.6 20150213
// added Setting.getHintTimeoutMS()
// added Host.addHintTimeoutMS()

// Constants and functions for Tasker *extensions* to the plugin protocol
// See Also: http://tasker.dinglisch.net/plugins.html

// Release Notes

// v1.1 20140202
// added function variableNameValid()
// fixed some javadoc entries (thanks to David Stone)

// v1.2 20140211
// added ACTION_EDIT_EVENT

// v1.3 20140227
// added REQUESTED_TIMEOUT_MS_NONE, REQUESTED_TIMEOUT_MS_MAX and REQUESTED_TIMEOUT_MS_NEVER
// requestTimeoutMS(): added range check

// v1.4 20140516
// support for data pass through in REQUEST_QUERY intent
// some javadoc entries fixed (thanks again David :-))

// v1.5 20141120
// added RESULT_CODE_FAILED_PLUGIN_FIRST
// added Setting.VARNAME_ERROR_MESSAGE

// v1.6 20150213
// added Setting.getHintTimeoutMS()
// added Host.addHintTimeoutMS()

package eu.power_switch.api.taskerplugin;

import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.net.Uri;
import android.os.Bundle;
import android.util.Log;

import java.net.URISyntaxException;
import java.security.SecureRandom;
import java.util.regex.Pattern;

/**
 * Helper class for Tasker plugin specific tasks
 * <p/>
 * Created by Markus on 24.02.2016.
 */
public class TaskerPlugin {

// Constants and functions for Tasker *extensions* to the plugin protocol
// See Also: http://tasker.dinglisch.net/plugins.html

// Release Notes

// v1.1 20140202
// added function variableNameValid()
// fixed some javadoc entries (thanks to David Stone)

// v1.2 20140211
// added ACTION_EDIT_EVENT

// v1.3 20140227
// added REQUESTED_TIMEOUT_MS_NONE, REQUESTED_TIMEOUT_MS_MAX and REQUESTED_TIMEOUT_MS_NEVER
// requestTimeoutMS(): added range check

// v1.4 20140516
// support for data pass through in REQUEST_QUERY intent
// some javadoc entries fixed (thanks again David :-))

// v1.5 20141120
// added RESULT_CODE_FAILED_PLUGIN_FIRST
// added Setting.VARNAME_ERROR_MESSAGE

// v1.6 20150213
// added Setting.getHintTimeoutMS()
// added Host.addHintTimeoutMS()

    //@formatter:off

    /**
     * @see Setting#hostSupportsVariableReturn(Bundle)
     */
    public final static int EXTRA_HOST_CAPABILITY_SETTING_RETURN_VARIABLES = 2;
    /**
     * @see Condition#hostSupportsVariableReturn(Bundle)
     */
    public final static int EXTRA_HOST_CAPABILITY_CONDITION_RETURN_VARIABLES = 4;
    /**
     * @see Setting#hostSupportsOnFireVariableReplacement(Bundle)
     */
    public final static int EXTRA_HOST_CAPABILITY_SETTING_FIRE_VARIABLE_REPLACEMENT = 8;
    public final static int EXTRA_HOST_CAPABILITY_SETTING_SYNCHRONOUS_EXECUTION = 32;
    public final static int EXTRA_HOST_CAPABILITY_REQUEST_QUERY_DATA_PASS_THROUGH = 64;
    public final static String VARIABLE_PREFIX = "%";
    // when generating non-repeating integers, look this far back for repeats
    // see getPositiveNonRepeatingRandomInteger()
    private final static int RANDOM_HISTORY_SIZE = 100;
    private final static String VARIABLE_NAME_START_EXPRESSION = "[\\w&&[^_]]";
    private final static String VARIABLE_NAME_MID_EXPRESSION = "[\\w0-9]+";
    private final static String VARIABLE_NAME_END_EXPRESSION = "[\\w0-9&&[^_]]";
    private final static String VARIABLE_NAME_MAIN_PART_MATCH_EXPRESSION =
            VARIABLE_NAME_START_EXPRESSION + VARIABLE_NAME_MID_EXPRESSION + VARIABLE_NAME_END_EXPRESSION;
    public final static String VARIABLE_NAME_MATCH_EXPRESSION = VARIABLE_PREFIX + "+" +
            VARIABLE_NAME_MAIN_PART_MATCH_EXPRESSION;
    private final static String TAG = "TaskerPlugin";
    private final static String BASE_KEY = "net.dinglisch.android.tasker";
    /**
     * Action that the EditActivity for an event plugin should be launched by
     */
    public final static String ACTION_EDIT_EVENT = BASE_KEY + ".ACTION_EDIT_EVENT";
    private final static String EXTRAS_PREFIX = BASE_KEY + ".extras.";
    private final static int FIRST_ON_FIRE_VARIABLES_TASKER_VERSION = 80;
    /**
     * @see #addVariableBundle(Bundle, Bundle)
     * @see Host#getVariablesBundle(Bundle)
     */
    private final static String EXTRA_VARIABLES_BUNDLE = EXTRAS_PREFIX + "VARIABLES";
    /**
     * Host capabilities, passed to plugin with edit intents
     */
    private final static String EXTRA_HOST_CAPABILITIES = EXTRAS_PREFIX + "HOST_CAPABILITIES";
    /**
     * @see Setting#hostSupportsVariableReturn(Bundle)
     */
    private final static int EXTRA_HOST_CAPABILITY_RELEVANT_VARIABLES = 16;
    public final static int EXTRA_HOST_CAPABILITY_ALL =
            EXTRA_HOST_CAPABILITY_SETTING_RETURN_VARIABLES |
                    EXTRA_HOST_CAPABILITY_CONDITION_RETURN_VARIABLES |
                    EXTRA_HOST_CAPABILITY_SETTING_FIRE_VARIABLE_REPLACEMENT |
                    EXTRA_HOST_CAPABILITY_RELEVANT_VARIABLES |
                    EXTRA_HOST_CAPABILITY_SETTING_SYNCHRONOUS_EXECUTION |
                    EXTRA_HOST_CAPABILITY_REQUEST_QUERY_DATA_PASS_THROUGH;
    /**
     * Miscellaneous operational hints going one way or the other
     *
     * @see Setting#hostSupportsVariableReturn(Bundle)
     */

    private final static String EXTRA_HINTS_BUNDLE = EXTRAS_PREFIX + "HINTS";
    private final static String BUNDLE_KEY_HINT_PREFIX = ".hints.";
    private final static String BUNDLE_KEY_HINT_TIMEOUT_MS = BUNDLE_KEY_HINT_PREFIX + "TIMEOUT";
    /**
     * @see #hostSupportsRelevantVariables(Bundle)
     * @see #addRelevantVariableList(Intent, String[])
     * @see #getRelevantVariableList(Bundle)
     */
    private final static String BUNDLE_KEY_RELEVANT_VARIABLES = BASE_KEY + ".RELEVANT_VARIABLES";
    private static Pattern VARIABLE_NAME_MATCH_PATTERN = null;
    // state tracking for random number sequence
    private static int[] lastRandomsSeen = null;
    private static int randomInsertPointer = 0;
    private static SecureRandom sr = null;

    //@formatter:on

    public static boolean hostSupportsRelevantVariables(Bundle extrasFromHost) {
        return hostSupports(extrasFromHost, EXTRA_HOST_CAPABILITY_RELEVANT_VARIABLES);
    }

    /**
     * Specifies to localHost which variables might be used by the plugin.
     * <p/>
     * Used in EditActivity, before setResult().
     *
     * @param intentToHost  the intent being returned to the localHost
     * @param variableNames array of relevant variable names
     */
    public static void addRelevantVariableList(Intent intentToHost, String[] variableNames) {
        intentToHost.putExtra(BUNDLE_KEY_RELEVANT_VARIABLES, variableNames);
    }

    // ----------------------------- SETTING PLUGIN ONLY --------------------------------- //

    /**
     * Validate a variable name.
     * <p/>
     * The basic requirement for variables from a plugin is that they must be all lower-case.
     *
     * @param varName name to check
     */
    public static boolean variableNameValid(String varName) {

        boolean validFlag = false;

        if (varName == null) {
            Log.d(TAG, "variableNameValid: null name");
        } else {
            if (VARIABLE_NAME_MATCH_PATTERN == null) {
                VARIABLE_NAME_MATCH_PATTERN = Pattern.compile(VARIABLE_NAME_MATCH_EXPRESSION, 0);
            }

            if (VARIABLE_NAME_MATCH_PATTERN.matcher(varName).matches()) {

                if (variableNameIsLocal(varName)) {
                    validFlag = true;
                } else {
                    Log.d(TAG, "variableNameValid: name not local: " + varName);
                }
            } else {
                Log.d(TAG, "variableNameValid: invalid name: " + varName);
            }
        }

        return validFlag;
    }

    // ----------------------------- CONDITION/EVENT PLUGIN ONLY --------------------------------- //

    /**
     * Allows the plugin/localHost to indicate to each other a set of variables which they are referencing.
     * The localHost may use this to e.g. show a variable selection list in it's UI.
     * The localHost should use this if it previously indicated to the plugin that it supports relevant vars
     *
     * @param fromHostIntentExtras usually from getIntent().getExtras()
     * @return variableNames an array of relevant variable names
     */
    public static String[] getRelevantVariableList(Bundle fromHostIntentExtras) {

        String[] relevantVars = (String[]) getBundleValueSafe(fromHostIntentExtras, BUNDLE_KEY_RELEVANT_VARIABLES, String[].class, "getRelevantVariableList");

        if (relevantVars == null) {
            relevantVars = new String[0];
        }

        return relevantVars;
    }

    // ----------------------------- EVENT PLUGIN ONLY --------------------------------- //

    /**
     * Used by: plugin QueryReceiver, FireReceiver
     * <p/>
     * Add a bundle of variable name/value pairs.
     * <p/>
     * Names must be valid Tasker local variable names.
     * Values must be String, String [] or ArrayList<String>
     * Null values cause deletion of possible already-existing variables.
     *
     * @param resultExtras the result extras from the receiver onReceive (from a call to getResultExtras())
     * @param variables    the variables to send
     * @see Setting#hostSupportsVariableReturn(Bundle)
     * @see #variableNameValid(String)
     */
    public static void addVariableBundle(Bundle resultExtras, Bundle variables) {
        resultExtras.putBundle(EXTRA_VARIABLES_BUNDLE, variables);
    }
    // ---------------------------------- HOST  ----------------------------------------- //

    private static Object getBundleValueSafe(Bundle b, String key, Class<?> expectedClass, String funcName) {
        Object value = null;

        if (b != null) {
            if (b.containsKey(key)) {
                Object obj = b.get(key);
                if (obj == null) {
                    Log.w(TAG, funcName + ": " + key + ": null value");
                } else if (obj.getClass() != expectedClass) {
                    Log.w(TAG, funcName + ": " + key + ": expected " + expectedClass.getClass()
                            .getName() + ", got " + obj.getClass().getName());
                } else {
                    value = obj;
                }
            }
        }
        return value;
    }

    // ---------------------------------- HELPER FUNCTIONS -------------------------------- //

    private static Object getExtraValueSafe(Intent i, String key, Class<?> expectedClass, String funcName) {
        return (i.hasExtra(key)) ?
                getBundleValueSafe(i.getExtras(), key, expectedClass, funcName) :
                null;
    }

    private static boolean hostSupports(Bundle extrasFromHost, int capabilityFlag) {
        Integer flags = (Integer) getBundleValueSafe(extrasFromHost, EXTRA_HOST_CAPABILITIES, Integer.class, "hostSupports");
        return
                (flags != null) &&
                        ((flags & capabilityFlag) > 0)
                ;
    }

    public static int getPackageVersionCode(PackageManager pm, String packageName) {

        int code = -1;

        if (pm != null) {
            try {
                PackageInfo pi = pm.getPackageInfo(packageName, 0);
                if (pi != null) {
                    code = pi.versionCode;
                }
            } catch (Exception e) {
                Log.e(TAG, "getPackageVersionCode: exception getting package info");
            }
        }

        return code;
    }

    private static boolean variableNameIsLocal(String varName) {

        int digitCount = 0;
        int length = varName.length();

        for (int x = 0; x < length; x++) {
            char ch = varName.charAt(x);

            if (Character.isUpperCase(ch)) {
                return false;
            } else if (Character.isDigit(ch)) {
                digitCount++;
            }
        }

        return digitCount != (varName.length() - 1);

    }

    /**
     * Generate a sequence of secure random positive integers which is guaranteed not to repeat
     * in the last 100 calls to this function.
     *
     * @return a random positive integer
     */
    public static int getPositiveNonRepeatingRandomInteger() {

        // initialize on first call
        if (sr == null) {
            sr = new SecureRandom();
            lastRandomsSeen = new int[RANDOM_HISTORY_SIZE];

            for (int x = 0; x < lastRandomsSeen.length; x++) {
                lastRandomsSeen[x] = -1;
            }
        }

        int toReturn;
        do {
            // pick a number
            toReturn = sr.nextInt(Integer.MAX_VALUE);

            // check we havn't see it recently
            for (int seen : lastRandomsSeen) {
                if (seen == toReturn) {
                    toReturn = -1;
                    break;
                }
            }
        }
        while (toReturn == -1);

        // update history
        lastRandomsSeen[randomInsertPointer] = toReturn;
        randomInsertPointer = (randomInsertPointer + 1) % lastRandomsSeen.length;

        return toReturn;
    }

    public static class Setting {

        /**
         * Variable name into which a description of any error that occurred can be placed
         * for the user to process.
         * <p/>
         * Should *only* be set when the BroadcastReceiver result code indicates a failure.
         * <p/>
         * Note that the user needs to have configured the task to continue after failure of the plugin
         * action otherwise they will not be able to make use of the error message.
         * <p/>
         * For use with #addRelevantVariableList(Intent, String[]) and #addVariableBundle(Bundle, Bundle)
         */
        public final static String VARNAME_ERROR_MESSAGE = VARIABLE_PREFIX + "errmsg";
        /**
         * @see #requestTimeoutMS(android.content.Intent, int)
         */

        public final static int REQUESTED_TIMEOUT_MS_NONE = 0;
        /**
         * @see #requestTimeoutMS(android.content.Intent, int)
         */

        public final static int REQUESTED_TIMEOUT_MS_MAX = 3599000;
        /**
         * @see #requestTimeoutMS(android.content.Intent, int)
         */

        public final static int REQUESTED_TIMEOUT_MS_NEVER = REQUESTED_TIMEOUT_MS_MAX + 1000;
        /**
         * @see #signalFinish(Context, Intent, int, Bundle)
         * @see Host#getSettingResultCode(Intent)
         */
        public final static String EXTRA_RESULT_CODE = EXTRAS_PREFIX + "RESULT_CODE";
        /**
         * @see #signalFinish(Context, Intent, int, Bundle)
         * @see Host#getSettingResultCode(Intent)
         */

        public final static int RESULT_CODE_OK = Activity.RESULT_OK;
        public final static int RESULT_CODE_OK_MINOR_FAILURES = Activity.RESULT_FIRST_USER;
        public final static int RESULT_CODE_FAILED = Activity.RESULT_FIRST_USER + 1;
        public final static int RESULT_CODE_PENDING = Activity.RESULT_FIRST_USER + 2;
        public final static int RESULT_CODE_UNKNOWN = Activity.RESULT_FIRST_USER + 3;
        /**
         * If a plugin wants to define it's own error codes, start numbering them here.
         * The code will be placed in an error variable (%err in the case of Tasker) for
         * the user to process after the plugin action.
         */

        public final static int RESULT_CODE_FAILED_PLUGIN_FIRST = Activity.RESULT_FIRST_USER + 9;
        /**
         * @see #setVariableReplaceKeys(Bundle, String[])
         */
        private final static String BUNDLE_KEY_VARIABLE_REPLACE_STRINGS = EXTRAS_PREFIX + "VARIABLE_REPLACE_KEYS";
        /**
         * @see #requestTimeoutMS(android.content.Intent, int)
         */
        private final static String EXTRA_REQUESTED_TIMEOUT = EXTRAS_PREFIX + "REQUESTED_TIMEOUT";
        /**
         * @see #signalFinish(Context, Intent, int, Bundle)
         * @see Host#addCompletionIntent(Intent, Intent)
         */
        private final static String EXTRA_PLUGIN_COMPLETION_INTENT = EXTRAS_PREFIX + "COMPLETION_INTENT";

        /**
         * Used by: plugin EditActivity.
         * <p/>
         * Indicates to plugin that localHost will replace variables in specified bundle keys.
         * <p/>
         * Replacement takes place every time the setting is fired, before the bundle is
         * passed to the plugin FireReceiver.
         *
         * @param extrasFromHost intent extras from the intent received by the edit activity
         * @see #setVariableReplaceKeys(Bundle, String[])
         */
        public static boolean hostSupportsOnFireVariableReplacement(Bundle extrasFromHost) {
            return hostSupports(extrasFromHost, EXTRA_HOST_CAPABILITY_SETTING_FIRE_VARIABLE_REPLACEMENT);
        }

        /**
         * Used by: plugin EditActivity.
         * <p/>
         * Description as above.
         * <p/>
         * This version also includes backwards compatibility with pre 4.2 Tasker versions.
         * At some point this function will be deprecated.
         *
         * @param editActivity the plugin edit activity, needed to test calling Tasker version
         * @see #setVariableReplaceKeys(Bundle, String[])
         */

        public static boolean hostSupportsOnFireVariableReplacement(Activity editActivity) {

            boolean supportedFlag = hostSupportsOnFireVariableReplacement(editActivity.getIntent().getExtras());

            if (!supportedFlag) {
                String callerPackage = editActivity.getCallingActivity().getPackageName();

                // Tasker only supporteed this from 1.0.10
                supportedFlag =
                        (callerPackage.startsWith(BASE_KEY)) &&
                                (getPackageVersionCode(editActivity.getPackageManager(), callerPackage) > FIRST_ON_FIRE_VARIABLES_TASKER_VERSION)
                ;
            }

            return supportedFlag;
        }

        public static boolean hostSupportsSynchronousExecution(Bundle extrasFromHost) {
            return hostSupports(extrasFromHost, EXTRA_HOST_CAPABILITY_SETTING_SYNCHRONOUS_EXECUTION);
        }

        /**
         * Request the localHost to wait the specified number of milliseconds before continuing.
         * Note that the localHost may choose to ignore the request.
         * <p/>
         * Maximum value is REQUESTED_TIMEOUT_MS_MAX.
         * Also available are REQUESTED_TIMEOUT_MS_NONE (continue immediately without waiting
         * for the plugin to finish) and REQUESTED_TIMEOUT_MS_NEVER (wait forever for
         * a result).
         * <p/>
         * Used in EditActivity, before setResult().
         *
         * @param intentToHost the intent being returned to the localHost
         * @param timeoutMS
         */
        public static void requestTimeoutMS(Intent intentToHost, int timeoutMS) {
            if (timeoutMS < 0) {
                Log.w(TAG, "requestTimeoutMS: ignoring negative timeout (" + timeoutMS + ")");
            } else {
                if (
                        (timeoutMS > REQUESTED_TIMEOUT_MS_MAX) &&
                                (timeoutMS != REQUESTED_TIMEOUT_MS_NEVER)
                        ) {
                    Log.w(TAG, "requestTimeoutMS: requested timeout " + timeoutMS + " exceeds maximum, setting to max (" + REQUESTED_TIMEOUT_MS_MAX + ")");
                    timeoutMS = REQUESTED_TIMEOUT_MS_MAX;
                }
                intentToHost.putExtra(EXTRA_REQUESTED_TIMEOUT, timeoutMS);
            }
        }

        /**
         * Used by: plugin EditActivity
         * <p/>
         * Indicates to localHost which bundle keys should be replaced.
         *
         * @param resultBundleToHost the bundle being returned to the localHost
         * @param listOfKeyNames     which bundle keys to replace variables in when setting fires
         * @see #hostSupportsOnFireVariableReplacement(Bundle)
         */
        public static void setVariableReplaceKeys(Bundle resultBundleToHost, String[] listOfKeyNames) {

            StringBuilder builder = new StringBuilder();

            if (listOfKeyNames != null) {

                for (String keyName : listOfKeyNames) {

                    if (keyName.contains(" ")) {
                        Log.w(TAG, "setVariableReplaceKeys: ignoring bad keyName containing space: " + keyName);
                    } else {
                        if (builder.length() > 0) {
                            builder.append(' ');
                        }

                        builder.append(keyName);
                    }

                    if (builder.length() > 0) {
                        resultBundleToHost.putString(BUNDLE_KEY_VARIABLE_REPLACE_STRINGS, builder.toString());
                    }
                }
            }
        }

        /**
         * Used by: plugin FireReceiver
         * <p/>
         * Indicates to plugin whether the localHost will process variables which it passes back
         *
         * @param extrasFromHost intent extras from the intent received by the FireReceiver
         * @see #signalFinish(Context, Intent, int, Bundle)
         */
        public static boolean hostSupportsVariableReturn(Bundle extrasFromHost) {
            return hostSupports(extrasFromHost, EXTRA_HOST_CAPABILITY_SETTING_RETURN_VARIABLES);
        }

        /**
         * Used by: plugin FireReceiver
         * <p/>
         * Tell the localHost that the plugin has finished execution.
         * <p/>
         * This should only be used if RESULT_CODE_PENDING was returned by FireReceiver.onReceive().
         *
         * @param originalFireIntent the intent received from the localHost (via onReceive())
         * @param resultCode         level of success in performing the settings
         * @param vars               any variables that the plugin wants to set in the localHost
         * @see #hostSupportsSynchronousExecution(Bundle)
         */
        public static boolean signalFinish(Context context, Intent originalFireIntent, int resultCode, Bundle vars) {

            String errorPrefix = "signalFinish: ";

            boolean okFlag = false;

            String completionIntentString = (String) getExtraValueSafe(originalFireIntent, Setting.EXTRA_PLUGIN_COMPLETION_INTENT, String.class, "signalFinish");

            if (completionIntentString != null) {

                Uri completionIntentUri = null;
                try {
                    completionIntentUri = Uri.parse(completionIntentString);
                }
                // 	should only throw NullPointer but don't particularly trust it
                catch (Exception e) {
                    Log.w(TAG, errorPrefix + "couldn't parse " + completionIntentString);
                }

                if (completionIntentUri != null) {
                    try {
                        Intent completionIntent = Intent.parseUri(completionIntentString, Intent.URI_INTENT_SCHEME);

                        completionIntent.putExtra(EXTRA_RESULT_CODE, resultCode);

                        if (vars != null) {
                            completionIntent.putExtra(EXTRA_VARIABLES_BUNDLE, vars);
                        }

                        context.sendBroadcast(completionIntent);

                        okFlag = true;
                    } catch (URISyntaxException e) {
                        Log.w(TAG, errorPrefix + "bad URI: " + completionIntentUri);
                    }
                }
            }

            return okFlag;
        }

        /**
         * Check for a hint on the timeout value the localHost is using.
         * Used by: plugin FireReceiver.
         * Requires Tasker 4.7+
         *
         * @param extrasFromHost intent extras from the intent received by the FireReceiver
         * @return timeoutMS the hosts timeout setting for the action or -1 if no hint is available.
         * @see #REQUESTED_TIMEOUT_MS_NONE, REQUESTED_TIMEOUT_MS_MAX, REQUESTED_TIMEOUT_MS_NEVER
         */
        public static int getHintTimeoutMS(Bundle extrasFromHost) {

            int timeoutMS = -1;

            Bundle hintsBundle = (Bundle) TaskerPlugin.getBundleValueSafe(extrasFromHost, EXTRA_HINTS_BUNDLE, Bundle.class, "getHintTimeoutMS");

            if (hintsBundle != null) {

                Integer val = (Integer) getBundleValueSafe(hintsBundle, BUNDLE_KEY_HINT_TIMEOUT_MS, Integer.class, "getHintTimeoutMS");

                if (val != null) {
                    timeoutMS = val;
                }
            }

            return timeoutMS;
        }
    }

    public static class Condition {

        /**
         * Used by: plugin QueryReceiver
         * <p/>
         * Indicates to plugin whether the localHost will process variables which it passes back
         *
         * @param extrasFromHost intent extras from the intent received by the QueryReceiver
         * @see #addVariableBundle(Bundle, Bundle)
         */
        public static boolean hostSupportsVariableReturn(Bundle extrasFromHost) {
            return hostSupports(extrasFromHost, EXTRA_HOST_CAPABILITY_CONDITION_RETURN_VARIABLES);
        }
    }

    public static class Event {

        public final static String PASS_THROUGH_BUNDLE_MESSAGE_ID_KEY = BASE_KEY + ".MESSAGE_ID";

        private final static String EXTRA_REQUEST_QUERY_PASS_THROUGH_DATA = EXTRAS_PREFIX + "PASS_THROUGH_DATA";

        /**
         * @param extrasFromHost intent extras from the intent received by the QueryReceiver
         * @see #addPassThroughData(Intent, Bundle)
         */
        public static boolean hostSupportsRequestQueryDataPassThrough(Bundle extrasFromHost) {
            return hostSupports(extrasFromHost, EXTRA_HOST_CAPABILITY_REQUEST_QUERY_DATA_PASS_THROUGH);
        }

        /**
         * Specify a bundle of data (probably representing whatever change happened in the condition)
         * which will be included in the QUERY_CONDITION broadcast sent by the localHost for each
         * event instance of the plugin.
         * <p/>
         * The minimal purpose is to enable the plugin to associate a QUERY_CONDITION to the
         * with the REQUEST_QUERY that caused it.
         * <p/>
         * Note that for security reasons it is advisable to also store a message ID with the bundle
         * which can be compared to known IDs on receipt. The localHost cannot validate the source of
         * REQUEST_QUERY intents so fake data may be passed. Replay attacks are also possible.
         * addPassThroughMesssageID() can be used to add an ID if the plugin doesn't wish to add it's
         * own ID to the pass through bundle.
         * <p/>
         * Note also that there are several situations where REQUEST_QUERY will not result in a
         * QUERY_CONDITION intent (e.g. event throttling by the localHost), so plugin-local data
         * indexed with a message ID needs to be timestamped and eventually timed-out.
         * <p/>
         * This function can be called multiple times, each time all keys in data will be added to
         * that of previous calls.
         *
         * @param requestQueryIntent intent being sent to the localHost
         * @param data               the data to be passed-through
         * @see #hostSupportsRequestQueryDataPassThrough(Bundle)
         * @see #retrievePassThroughData(Intent)
         * @see #addPassThroughMessageID
         */
        public static void addPassThroughData(Intent requestQueryIntent, Bundle data) {

            Bundle passThroughBundle = retrieveOrCreatePassThroughBundle(requestQueryIntent);

            passThroughBundle.putAll(data);
        }

        /**
         * Retrieve the pass through data from a QUERY_REQUEST from the localHost which was generated
         * by a REQUEST_QUERY from the plugin.
         * <p/>
         * Note that if addPassThroughMessageID() was previously called, the data will contain an extra
         * key TaskerPlugin.Event.PASS_THOUGH_BUNDLE_MESSAGE_ID_KEY.
         *
         * @param queryConditionIntent QUERY_REQUEST sent from localHost
         * @return data previously added to the REQUEST_QUERY intent
         * @see #hostSupportsRequestQueryDataPassThrough(Bundle)
         * @see #addPassThroughData(Intent, Bundle)
         */
        public static Bundle retrievePassThroughData(Intent queryConditionIntent) {
            return (Bundle) getExtraValueSafe(
                    queryConditionIntent,
                    EXTRA_REQUEST_QUERY_PASS_THROUGH_DATA,
                    Bundle.class,
                    "retrievePassThroughData"
            );
        }

        /**
         * Add a message ID to a REQUEST_QUERY intent which will then be included in the corresponding
         * QUERY_CONDITION broadcast sent by the localHost for each event instance of the plugin.
         * <p/>
         * The minimal purpose is to enable the plugin to associate a QUERY_CONDITION to the
         * with the REQUEST_QUERY that caused it. It also allows the message to be verified
         * by the plugin to prevent e.g. replay attacks
         *
         * @param requestQueryIntent intent being sent to the localHost
         * @return an ID for the bundle so it can be identified and the caller verified when it is again received by the plugin
         * @see #hostSupportsRequestQueryDataPassThrough(Bundle)
         * @see #retrievePassThroughData(Intent)
         */
        public static int addPassThroughMessageID(Intent requestQueryIntent) {

            Bundle passThroughBundle = retrieveOrCreatePassThroughBundle(requestQueryIntent);

            int id = getPositiveNonRepeatingRandomInteger();

            passThroughBundle.putInt(PASS_THROUGH_BUNDLE_MESSAGE_ID_KEY, id);

            return id;
        }

        /*
         * Retrieve the pass through data from a QUERY_REQUEST from the localHost which was generated
         * by a REQUEST_QUERY from the plugin.
         *
         * @param queryConditionIntent QUERY_REQUEST sent from localHost
         * @return the ID which was passed through by the localHost, or -1 if no ID was found
         * @see #hostSupportsRequestQueryDataPassThrough(Bundle)
         * @see #addPassThroughData(Intent,Bundle)
        */
        public static int retrievePassThroughMessageID(Intent queryConditionIntent) {

            int toReturn = -1;

            Bundle passThroughData = Event.retrievePassThroughData(queryConditionIntent);

            if (passThroughData != null) {
                Integer id = (Integer) getBundleValueSafe(
                        passThroughData,
                        PASS_THROUGH_BUNDLE_MESSAGE_ID_KEY,
                        Integer.class,
                        "retrievePassThroughMessageID"
                );

                if (id != null) {
                    toReturn = id;
                }
            }

            return toReturn;
        }

        // internal use
        private static Bundle retrieveOrCreatePassThroughBundle(Intent requestQueryIntent) {

            Bundle passThroughBundle;

            if (requestQueryIntent.hasExtra(EXTRA_REQUEST_QUERY_PASS_THROUGH_DATA)) {
                passThroughBundle = requestQueryIntent.getBundleExtra(EXTRA_REQUEST_QUERY_PASS_THROUGH_DATA);
            } else {
                passThroughBundle = new Bundle();
                requestQueryIntent.putExtra(EXTRA_REQUEST_QUERY_PASS_THROUGH_DATA, passThroughBundle);
            }

            return passThroughBundle;
        }
    }

    public static class Host {

        /**
         * Tell the plugin what capabilities the localHost support. This should be called when sending
         * intents to any EditActivity, FireReceiver or QueryReceiver.
         *
         * @param toPlugin the intent we're sending
         * @return capabilities one or more of the EXTRA_HOST_CAPABILITY_XXX flags
         */
        public static Intent addCapabilities(Intent toPlugin, int capabilities) {
            return toPlugin.putExtra(EXTRA_HOST_CAPABILITIES, capabilities);
        }

        /**
         * Add an intent to the fire intent before it goes to the plugin FireReceiver, which the plugin
         * can use to signal when it is finished. Only use if @code{pluginWantsSychronousExecution} is true.
         *
         * @param fireIntent       fire intent going to the plugin
         * @param completionIntent intent which will signal the localHost that the plugin is finished.
         *                         Implementation is localHost-dependent.
         */
        public static void addCompletionIntent(Intent fireIntent, Intent completionIntent) {
            fireIntent.putExtra(
                    Setting.EXTRA_PLUGIN_COMPLETION_INTENT,
                    completionIntent.toUri(Intent.URI_INTENT_SCHEME)
            );
        }

        /**
         * When a setting plugin is finished, it sends the localHost the intent which was passed to it
         * via @code{addCompletionIntent}.
         *
         * @param completionIntent intent returned from the plugin when it finished.
         * @return resultCode measure of plugin success, defaults to UNKNOWN
         */
        public static int getSettingResultCode(Intent completionIntent) {

            Integer val = (Integer) getExtraValueSafe(completionIntent, Setting.EXTRA_RESULT_CODE, Integer.class, "getSettingResultCode");

            return (val == null) ? Setting.RESULT_CODE_UNKNOWN : val;
        }

        /**
         * Extract a bundle of variables from an intent received from the FireReceiver. This
         * should be called if the localHost previously indicated to the plugin
         * that it supports setting variable return.
         *
         * @param resultExtras getResultExtras() from BroadcastReceiver:onReceive()
         * @return variables a bundle of variable name/value pairs
         * @see #addCapabilities(Intent, int)
         */

        public static Bundle getVariablesBundle(Bundle resultExtras) {
            return (Bundle) getBundleValueSafe(
                    resultExtras, EXTRA_VARIABLES_BUNDLE, Bundle.class, "getVariablesBundle"
            );
        }

        /**
         * Inform a setting plugin of the timeout value the localHost is using.
         *
         * @param toPlugin  the intent we're sending
         * @param timeoutMS the hosts timeout setting for the action. Note that this may differ from
         *                  that which the plugin requests.
         * @see \REQUESTED_TIMEOUT_MS_NONE, REQUESTED_TIMEOUT_MS_MAX, REQUESTED_TIMEOUT_MS_NEVER
         */
        public static void addHintTimeoutMS(Intent toPlugin, int timeoutMS) {
            getHintsBundle(toPlugin, "addHintTimeoutMS").putInt(BUNDLE_KEY_HINT_TIMEOUT_MS, timeoutMS);
        }

        private static Bundle getHintsBundle(Intent intent, String funcName) {

            Bundle hintsBundle = (Bundle) getExtraValueSafe(intent, EXTRA_HINTS_BUNDLE, Bundle.class, funcName);

            if (hintsBundle == null) {
                hintsBundle = new Bundle();
                intent.putExtra(EXTRA_HINTS_BUNDLE, hintsBundle);
            }

            return hintsBundle;
        }

        public static boolean haveRequestedTimeout(Bundle extrasFromPluginEditActivity) {
            return extrasFromPluginEditActivity.containsKey(Setting.EXTRA_REQUESTED_TIMEOUT);
        }

        public static int getRequestedTimeoutMS(Bundle extrasFromPluginEditActivity) {
            return
                    (Integer) getBundleValueSafe(
                            extrasFromPluginEditActivity, Setting.EXTRA_REQUESTED_TIMEOUT, Integer.class, "getRequestedTimeout"
                    )
                    ;
        }

        public static String[] getSettingVariableReplaceKeys(Bundle fromPluginEditActivity) {

            String spec = (String) getBundleValueSafe(fromPluginEditActivity, Setting.BUNDLE_KEY_VARIABLE_REPLACE_STRINGS, String.class, "getSettingVariableReplaceKeys");

            String[] replaceKeys = null;

            if (spec != null) {
                replaceKeys = spec.split(" ");
            }

            return replaceKeys;
        }

        public static boolean haveRelevantVariables(Bundle b) {
            return b.containsKey(BUNDLE_KEY_RELEVANT_VARIABLES);
        }

        public static void cleanRelevantVariables(Bundle b) {
            b.remove(BUNDLE_KEY_RELEVANT_VARIABLES);
        }

        public static void cleanHints(Bundle extras) {
            extras.remove(TaskerPlugin.EXTRA_HINTS_BUNDLE);
        }

        public static void cleanRequestedTimeout(Bundle extras) {
            extras.remove(Setting.EXTRA_REQUESTED_TIMEOUT);
        }

        public static void cleanSettingReplaceVariables(Bundle b) {
            b.remove(Setting.BUNDLE_KEY_VARIABLE_REPLACE_STRINGS);
        }
    }
}