/**
    Licensed to the Apache Software Foundation (ASF) under one
    or more contributor license agreements.  See the NOTICE file
    distributed with this work for additional information
    regarding copyright ownership.  The ASF licenses this file
    to you under the Apache License, Version 2.0 (the
    "License"); you may not use this file except in compliance
    with the License.  You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

    Unless required by applicable law or agreed to in writing,
    software distributed under the License is distributed on an
    "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
    KIND, either express or implied.  See the License for the
    specific language governing permissions and limitations
    under the License.
*/

var path = require('path');
var fs = require('fs-extra');
var ActionStack = require('cordova-common').ActionStack;
var dependencies = require('./util/dependencies');
var CordovaError = require('cordova-common').CordovaError;
var underscore = require('underscore');
var events = require('cordova-common').events;
var platform_modules = require('../platforms/platforms');
var promiseutil = require('../util/promise-util');
var HooksRunner = require('../hooks/HooksRunner');
var cordovaUtil = require('../cordova/util');
var npmUninstall = require('cordova-fetch').uninstall;

var superspawn = require('cordova-common').superspawn;
var PlatformJson = require('cordova-common').PlatformJson;
var PluginInfoProvider = require('cordova-common').PluginInfoProvider;
var variableMerge = require('../plugman/variable-merge');

// possible options: cli_variables, www_dir
// Returns a promise.
module.exports = uninstall;
function uninstall (platform, project_dir, id, plugins_dir, options) {
    project_dir = cordovaUtil.convertToRealPathSafe(project_dir);
    plugins_dir = cordovaUtil.convertToRealPathSafe(plugins_dir);

    options = options || {};
    options.is_top_level = true;
    options.pluginInfoProvider = options.pluginInfoProvider || new PluginInfoProvider();
    plugins_dir = plugins_dir || path.join(project_dir, 'cordova', 'plugins');

    // Allow `id` to be a path to a file.
    var xml_path = path.join(id, 'plugin.xml');
    if (fs.existsSync(xml_path)) {
        id = options.pluginInfoProvider.get(id).id;
    }

    return module.exports.uninstallPlatform(platform, project_dir, id, plugins_dir, options)
        .then(function () {
            return module.exports.uninstallPlugin(id, plugins_dir, options);
        });
}

// Returns a promise.
module.exports.uninstallPlatform = function (platform, project_dir, id, plugins_dir, options) {
    project_dir = cordovaUtil.convertToRealPathSafe(project_dir);
    plugins_dir = cordovaUtil.convertToRealPathSafe(plugins_dir);

    options = options || {};
    options.is_top_level = true;
    options.pluginInfoProvider = options.pluginInfoProvider || new PluginInfoProvider();
    plugins_dir = plugins_dir || path.join(project_dir, 'cordova', 'plugins');

    if (!platform_modules[platform]) {
        return Promise.reject(new CordovaError('Platform "' + platform + '" not supported.'));
    }

    var plugin_dir = path.join(plugins_dir, id);
    if (!fs.existsSync(plugin_dir)) {
        return Promise.reject(new CordovaError('Plugin "' + id + '" not found. Already uninstalled?'));
    }

    var current_stack = new ActionStack();

    return Promise.resolve().then(function () {
        if (options.platformVersion) {
            return Promise.resolve(options.platformVersion);
        }
        return Promise.resolve(superspawn.maybeSpawn(path.join(project_dir, 'cordova', 'version'), [], { chmod: true }));
    }).then(function (platformVersion) {
        options.platformVersion = platformVersion;
        return runUninstallPlatform(current_stack, platform, project_dir, plugin_dir, plugins_dir, options);
    });
};

// Returns a promise.
module.exports.uninstallPlugin = function (id, plugins_dir, options) {
    plugins_dir = cordovaUtil.convertToRealPathSafe(plugins_dir);

    options = options || {};
    options.pluginInfoProvider = options.pluginInfoProvider || new PluginInfoProvider();
    var pluginInfoProvider = options.pluginInfoProvider;

    var plugin_dir = path.join(plugins_dir, id);

    // @tests - important this event is checked spec/uninstall.spec.js
    events.emit('log', 'Removing "' + id + '"');

    // If already removed, skip.
    if (!fs.existsSync(plugin_dir)) {
        events.emit('verbose', 'Plugin "' + id + '" already removed (' + plugin_dir + ')');
        return Promise.resolve();
    }

    /*
     * Deletes plugin from plugins directory and node_modules directory.
     *
     * @param {String} id   the id of the plugin being removed
     *
     * @return {Promise||Error} Returns a empty promise or a promise of doing the npm uninstall
     */
    var doDelete = function (id) {
        var plugin_dir = path.join(plugins_dir, id);
        if (!fs.existsSync(plugin_dir)) {
            events.emit('verbose', 'Plugin "' + id + '" already removed (' + plugin_dir + ')');
            return Promise.resolve();
        }

        fs.removeSync(plugin_dir);
        events.emit('verbose', 'Deleted plugin "' + id + '"');

        // remove plugin from node_modules directory
        return npmUninstall(id, options.projectRoot, options);
    };

    // We've now lost the metadata for the plugins that have been uninstalled, so we can't use that info.
    // Instead, we list all dependencies of the target plugin, and check the remaining metadata to see if
    // anything depends on them, or if they're listed as top-level.
    // If neither, they can be deleted.
    var top_plugin_id = id;

    // Recursively remove plugins which were installed as dependents (that are not top-level)
    var toDelete = [];
    function findDependencies (pluginId) {
        var depPluginDir = path.join(plugin_dir, '..', pluginId);
        // Skip plugin check for dependencies if it does not exist (CB-7846).
        if (!fs.existsSync(depPluginDir)) {
            events.emit('verbose', 'Plugin "' + pluginId + '" does not exist (' + depPluginDir + ')');
            return;
        }
        var pluginInfo = pluginInfoProvider.get(depPluginDir);
        // TODO: Should remove dependencies in a separate step, since dependencies depend on platform.
        var deps = pluginInfo.getDependencies();
        deps.forEach(function (d) {
            if (toDelete.indexOf(d.id) === -1) {
                toDelete.push(d.id);
                findDependencies(d.id);
            }
        });
    }
    findDependencies(top_plugin_id);
    toDelete.push(top_plugin_id);

    // Okay, now we check if any of these are depended on, or top-level.
    // Find the installed platforms by whether they have a metadata file.
    var platforms = Object.keys(platform_modules).filter(function (platform) {
        return fs.existsSync(path.join(plugins_dir, platform + '.json'));
    });

    // Can have missing plugins on some platforms when not supported..
    var dependList = {};
    platforms.forEach(function (platform) {
        var platformJson = PlatformJson.load(plugins_dir, platform);
        var depsInfo = dependencies.generateDependencyInfo(platformJson, plugins_dir, pluginInfoProvider);
        var tlps = depsInfo.top_level_plugins;
        var deps;

        // Top-level deps must always be explicitely asked to remove by user
        tlps.forEach(function (plugin_id) {
            if (top_plugin_id === plugin_id) { return; }

            var i = toDelete.indexOf(plugin_id);
            if (i >= 0) { toDelete.splice(i, 1); }
        });

        toDelete.forEach(function (plugin) {
            deps = dependencies.dependents(plugin, depsInfo, platformJson, pluginInfoProvider);

            var i = deps.indexOf(top_plugin_id);
            if (i >= 0) { deps.splice(i, 1); } // remove current/top-level plugin as blocking uninstall

            if (deps.length) {
                dependList[plugin] = deps.join(', ');
            }
        });
    });

    var dependPluginIds = toDelete.filter(function (plugin_id) {
        return dependList[plugin_id];
    });
    var createMsg = function (plugin_id) {
        return 'Plugin "' + plugin_id + '" is required by (' + dependList[plugin_id] + ')';
    };
    var createMsg2 = function (plugin_id) {
        return createMsg(plugin_id) + ' and cannot be removed (hint: use -f or --force)';
    };
    if (!options.force && dependPluginIds.includes(top_plugin_id)) {
        var msg = createMsg2(top_plugin_id);
        return Promise.reject(new CordovaError(msg));
    }

    // action emmiting events.
    if (options.force) {
        dependPluginIds.forEach(function (plugin_id) {
            var msg = createMsg(plugin_id);
            events.emit('log', msg + ' but forcing removal.');
        });
    } else {
        dependPluginIds.forEach(function (plugin_id) {
            var msg = createMsg2(plugin_id);
            events.emit('warn', msg);
        });
    }
    var deletePluginIds = options.force ? toDelete : toDelete.filter(function (plugin_id) { return !dependList[plugin_id]; });
    var deleteExecList = deletePluginIds.map(function (plugin_id) {
        return function () { return doDelete(plugin_id); };
    });
    return deleteExecList.reduce(function (acc, deleteExec) {
        return acc.then(deleteExec);
    }, Promise.resolve());

};

// possible options: cli_variables, www_dir, is_top_level
// Returns a promise
function runUninstallPlatform (actions, platform, project_dir, plugin_dir, plugins_dir, options) {
    var pluginInfoProvider = options.pluginInfoProvider;
    // If this plugin is not really installed, return (CB-7004).
    if (!fs.existsSync(plugin_dir)) {
        return Promise.resolve(true);
    }

    var pluginInfo = pluginInfoProvider.get(plugin_dir);
    var plugin_id = pluginInfo.id;

    // Merge cli_variables and plugin.xml variables
    var variables = variableMerge.mergeVariables(plugin_dir, platform, options);

    // Deps info can be passed recusively
    var platformJson = PlatformJson.load(plugins_dir, platform);
    var depsInfo = options.depsInfo || dependencies.generateDependencyInfo(platformJson, plugins_dir, pluginInfoProvider);

    // Check that this plugin has no dependents.
    var dependents = dependencies.dependents(plugin_id, depsInfo, platformJson, pluginInfoProvider);

    if (options.is_top_level && dependents && dependents.length > 0) {
        var msg = 'The plugin \'' + plugin_id + '\' is required by (' + dependents.join(', ') + ')';
        if (options.force) {
            events.emit('warn', msg + ' but forcing removal');
        } else {
            return Promise.reject(new CordovaError(msg + ', skipping uninstallation. (try --force if trying to update)'));
        }
    }

    // Check how many dangling dependencies this plugin has.
    var deps = depsInfo.graph.getChain(plugin_id);
    var danglers = dependencies.danglers(plugin_id, depsInfo, platformJson, pluginInfoProvider);

    var promise;
    if (deps && deps.length && danglers && danglers.length) {

        // @tests - important this event is checked spec/uninstall.spec.js
        events.emit('log', 'Uninstalling ' + danglers.length + ' dependent plugins.');
        promise = promiseutil.Q_chainmap(danglers, function (dangler) {
            var dependent_path = path.join(plugins_dir, dangler);
            var opts = underscore.extend({}, options, {
                is_top_level: depsInfo.top_level_plugins.indexOf(dangler) > -1,
                depsInfo: depsInfo
            });

            return runUninstallPlatform(actions, platform, project_dir, dependent_path, plugins_dir, opts);
        });
    } else {
        promise = Promise.resolve();
    }

    var projectRoot = cordovaUtil.isCordova();

    if (projectRoot) {
        // CB-10708 This is the case when we're trying to uninstall plugin using plugman from specific
        // platform inside of the existing CLI project. This option is usually set by cordova-lib for CLI projects
        // but since we're running this code through plugman, we need to set it here implicitly
        options.usePlatformWww = true;
        options.cli_variables = variables;
    }

    return promise.then(function () {
        if (!projectRoot) return;

        var hooksRunner = new HooksRunner(projectRoot);
        var hooksRunnerOptions = {
            cordova: { platforms: [ platform ] },
            plugin: {
                id: pluginInfo.id,
                pluginInfo: pluginInfo,
                platform: platform,
                dir: plugin_dir
            }
        };
        return hooksRunner.fire('before_plugin_uninstall', hooksRunnerOptions);
    }).then(function () {
        return handleUninstall(actions, platform, pluginInfo, project_dir, options.www_dir, plugins_dir, options.is_top_level, options);
    });
}

// Returns a promise.
function handleUninstall (actions, platform, pluginInfo, project_dir, www_dir, plugins_dir, is_top_level, options) {
    events.emit('log', 'Uninstalling ' + pluginInfo.id + ' from ' + platform);

    // Set up platform to uninstall asset files/js modules
    // from <platform>/platform_www dir instead of <platform>/www.
    options.usePlatformWww = true;
    return platform_modules.getPlatformApi(platform, project_dir)
        .removePlugin(pluginInfo, options)
        .then(function (result) {
            // Remove plugin from installed list. This already done in platform,
            // but need to be duplicated here to remove plugin entry from project's
            // plugin list to manage dependencies properly.
            PlatformJson.load(plugins_dir, platform)
                .removePlugin(pluginInfo.id, is_top_level)
                .save();

            // CB-11022 propagate `removePlugin` result to the caller
            return Promise.resolve(result);
        });
}


/**
    Licensed to the Apache Software Foundation (ASF) under one
    or more contributor license agreements.  See the NOTICE file
    distributed with this work for additional information
    regarding copyright ownership.  The ASF licenses this file
    to you under the Apache License, Version 2.0 (the
    "License"); you may not use this file except in compliance
    with the License.  You may obtain a copy of the License at
    http://www.apache.org/licenses/LICENSE-2.0
    Unless required by applicable law or agreed to in writing,
    software distributed under the License is distributed on an
    "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
    KIND, either express or implied.  See the License for the
    specific language governing permissions and limitations
    under the License.
*/

var path = require('path');
var CordovaError = require('cordova-common').CordovaError;
var events = require('cordova-common').events;
var cordova_util = require('../util');
var platforms = require('../../platforms/platforms');

module.exports = getPlatformDetailsFromDir;
module.exports.platformFromName = platformFromName;

// Gets platform details from a directory
function getPlatformDetailsFromDir (dir, platformIfKnown) {
    var libDir = path.resolve(dir);
    var platform;
    var version;

    // console.log("getPlatformDetailsFromDir : ", dir, platformIfKnown, libDir);

    try {
        var pkgPath = path.join(libDir, 'package.json');
        var pkg = cordova_util.requireNoCache(pkgPath);
        platform = module.exports.platformFromName(pkg.name);
        version = pkg.version;
    } catch (e) {
        return Promise.reject(new CordovaError('The provided path does not seem to contain a valid package.json or a valid Cordova platform: ' + libDir));
    }

    // platform does NOT have to exist in 'platforms', but it should have a name, and a version
    if (!version || !platform) {
        return Promise.reject(new CordovaError('The provided path does not seem to contain a ' +
            'Cordova platform: ' + libDir));
    }

    return Promise.resolve({
        libDir: libDir,
        platform: platform,
        version: version
    });
}

/**
 * Removes the cordova- prefix from the platform's name for known platforms.
 * @param {string} name - platform name
 * @returns {string}
 */
function platformFromName (name) {
    var platName = name;
    var platMatch = /^cordova-([a-z0-9-]+)$/.exec(name);
    if (platMatch && (platMatch[1] in platforms)) {
        platName = platMatch[1];
        events.emit('verbose', 'Removing "cordova-" prefix from ' + name);
    }
    return platName;
}


/*
 *
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 *
*/

/**
 * Execute a cordova command.  It is up to the native side whether this action
 * is synchronous or asynchronous.  The native side can return:
 *      Synchronous: PluginResult object as a JSON string
 *      Asynchronous: Empty string ""
 * If async, the native side will cordova.callbackSuccess or cordova.callbackError,
 * depending upon the result of the action.
 *
 * @param {Function} success    The success callback
 * @param {Function} fail       The fail callback
 * @param {String} service      The name of the service to use
 * @param {String} action       Action to be run in cordova
 * @param {String[]} [args]     Zero or more arguments to pass to the method
 */
var cordova = require('cordova'),
    nativeApiProvider = require('cordova/android/nativeapiprovider'),
    utils = require('cordova/utils'),
    base64 = require('cordova/base64'),
    channel = require('cordova/channel'),
    jsToNativeModes = {
        PROMPT: 0,
        JS_OBJECT: 1
    },
    nativeToJsModes = {
        // Polls for messages using the JS->Native bridge.
        POLLING: 0,
        // For LOAD_URL to be viable, it would need to have a work-around for
        // the bug where the soft-keyboard gets dismissed when a message is sent.
        LOAD_URL: 1,
        // For the ONLINE_EVENT to be viable, it would need to intercept all event
        // listeners (both through addEventListener and window.ononline) as well
        // as set the navigator property itself.
        ONLINE_EVENT: 2,
        EVAL_BRIDGE: 3
    },
    jsToNativeBridgeMode,  // Set lazily.
    nativeToJsBridgeMode = nativeToJsModes.EVAL_BRIDGE,
    pollEnabled = false,
    bridgeSecret = -1;

var messagesFromNative = [];
var isProcessing = false;
var resolvedPromise = typeof Promise == 'undefined' ? null : Promise.resolve();
var nextTick = resolvedPromise ? function(fn) { resolvedPromise.then(fn); } : function(fn) { setTimeout(fn); };

function androidExec(success, fail, service, action, args) {
    if (bridgeSecret < 0) {
        // If we ever catch this firing, we'll need to queue up exec()s
        // and fire them once we get a secret. For now, I don't think
        // it's possible for exec() to be called since plugins are parsed but
        // not run until until after onNativeReady.
        throw new Error('exec() called without bridgeSecret');
    }
    // Set default bridge modes if they have not already been set.
    // By default, we use the failsafe, since addJavascriptInterface breaks too often
    if (jsToNativeBridgeMode === undefined) {
        androidExec.setJsToNativeBridgeMode(jsToNativeModes.JS_OBJECT);
    }

    // If args is not provided, default to an empty array
    args = args || [];

    // Process any ArrayBuffers in the args into a string.
    for (var i = 0; i < args.length; i++) {
        if (utils.typeName(args[i]) == 'ArrayBuffer') {
            args[i] = base64.fromArrayBuffer(args[i]);
        }
    }

    var callbackId = service + cordova.callbackId++,
        argsJson = JSON.stringify(args);
    if (success || fail) {
        cordova.callbacks[callbackId] = {success:success, fail:fail};
    }

    var msgs = nativeApiProvider.get().exec(bridgeSecret, service, action, callbackId, argsJson);
    // If argsJson was received by Java as null, try again with the PROMPT bridge mode.
    // This happens in rare circumstances, such as when certain Unicode characters are passed over the bridge on a Galaxy S2.  See CB-2666.
    if (jsToNativeBridgeMode == jsToNativeModes.JS_OBJECT && msgs === "@Null arguments.") {
        androidExec.setJsToNativeBridgeMode(jsToNativeModes.PROMPT);
        androidExec(success, fail, service, action, args);
        androidExec.setJsToNativeBridgeMode(jsToNativeModes.JS_OBJECT);
    } else if (msgs) {
        messagesFromNative.push(msgs);
        // Always process async to avoid exceptions messing up stack.
        nextTick(processMessages);
    }
}

androidExec.init = function() {
    //CB-11828
    //This failsafe checks the version of Android and if it's Jellybean, it switches it to
    //using the Online Event bridge for communicating from Native to JS
    //
    //It's ugly, but it's necessary.
    var check = navigator.userAgent.toLowerCase().match(/android\s[0-9].[0-9]/);
    var version_code = check && check[0].match(/4.[0-3].*/);
    if (version_code != null && nativeToJsBridgeMode == nativeToJsModes.EVAL_BRIDGE) {
      nativeToJsBridgeMode = nativeToJsModes.ONLINE_EVENT;
    }

    bridgeSecret = +prompt('', 'gap_init:' + nativeToJsBridgeMode);
    channel.onNativeReady.fire();
};

function pollOnceFromOnlineEvent() {
    pollOnce(true);
}

function pollOnce(opt_fromOnlineEvent) {
    if (bridgeSecret < 0) {
        // This can happen when the NativeToJsMessageQueue resets the online state on page transitions.
        // We know there's nothing to retrieve, so no need to poll.
        return;
    }
    var msgs = nativeApiProvider.get().retrieveJsMessages(bridgeSecret, !!opt_fromOnlineEvent);
    if (msgs) {
        messagesFromNative.push(msgs);
        // Process sync since we know we're already top-of-stack.
        processMessages();
    }
}

function pollingTimerFunc() {
    if (pollEnabled) {
        pollOnce();
        setTimeout(pollingTimerFunc, 50);
    }
}

function hookOnlineApis() {
    function proxyEvent(e) {
        cordova.fireWindowEvent(e.type);
    }
    // The network module takes care of firing online and offline events.
    // It currently fires them only on document though, so we bridge them
    // to window here (while first listening for exec()-releated online/offline
    // events).
    window.addEventListener('online', pollOnceFromOnlineEvent, false);
    window.addEventListener('offline', pollOnceFromOnlineEvent, false);
    cordova.addWindowEventHandler('online');
    cordova.addWindowEventHandler('offline');
    document.addEventListener('online', proxyEvent, false);
    document.addEventListener('offline', proxyEvent, false);
}

hookOnlineApis();

androidExec.jsToNativeModes = jsToNativeModes;
androidExec.nativeToJsModes = nativeToJsModes;

androidExec.setJsToNativeBridgeMode = function(mode) {
    if (mode == jsToNativeModes.JS_OBJECT && !window._cordovaNative) {
        mode = jsToNativeModes.PROMPT;
    }
    nativeApiProvider.setPreferPrompt(mode == jsToNativeModes.PROMPT);
    jsToNativeBridgeMode = mode;
};

androidExec.setNativeToJsBridgeMode = function(mode) {
    if (mode == nativeToJsBridgeMode) {
        return;
    }
    if (nativeToJsBridgeMode == nativeToJsModes.POLLING) {
        pollEnabled = false;
    }

    nativeToJsBridgeMode = mode;
    // Tell the native side to switch modes.
    // Otherwise, it will be set by androidExec.init()
    if (bridgeSecret >= 0) {
        nativeApiProvider.get().setNativeToJsBridgeMode(bridgeSecret, mode);
    }

    if (mode == nativeToJsModes.POLLING) {
        pollEnabled = true;
        setTimeout(pollingTimerFunc, 1);
    }
};

function buildPayload(payload, message) {
    var payloadKind = message.charAt(0);
    if (payloadKind == 's') {
        payload.push(message.slice(1));
    } else if (payloadKind == 't') {
        payload.push(true);
    } else if (payloadKind == 'f') {
        payload.push(false);
    } else if (payloadKind == 'N') {
        payload.push(null);
    } else if (payloadKind == 'n') {
        payload.push(+message.slice(1));
    } else if (payloadKind == 'A') {
        var data = message.slice(1);
        payload.push(base64.toArrayBuffer(data));
    } else if (payloadKind == 'S') {
        payload.push(window.atob(message.slice(1)));
    } else if (payloadKind == 'M') {
        var multipartMessages = message.slice(1);
        while (multipartMessages !== "") {
            var spaceIdx = multipartMessages.indexOf(' ');
            var msgLen = +multipartMessages.slice(0, spaceIdx);
            var multipartMessage = multipartMessages.substr(spaceIdx + 1, msgLen);
            multipartMessages = multipartMessages.slice(spaceIdx + msgLen + 1);
            buildPayload(payload, multipartMessage);
        }
    } else {
        payload.push(JSON.parse(message));
    }
}

// Processes a single message, as encoded by NativeToJsMessageQueue.java.
function processMessage(message) {
    var firstChar = message.charAt(0);
    if (firstChar == 'J') {
        // This is deprecated on the .java side. It doesn't work with CSP enabled.
        eval(message.slice(1));
    } else if (firstChar == 'S' || firstChar == 'F') {
        var success = firstChar == 'S';
        var keepCallback = message.charAt(1) == '1';
        var spaceIdx = message.indexOf(' ', 2);
        var status = +message.slice(2, spaceIdx);
        var nextSpaceIdx = message.indexOf(' ', spaceIdx + 1);
        var callbackId = message.slice(spaceIdx + 1, nextSpaceIdx);
        var payloadMessage = message.slice(nextSpaceIdx + 1);
        var payload = [];
        buildPayload(payload, payloadMessage);
        cordova.callbackFromNative(callbackId, success, status, payload, keepCallback);
    } else {
        console.log("processMessage failed: invalid message: " + JSON.stringify(message));
    }
}

function processMessages() {
    // Check for the reentrant case.
    if (isProcessing) {
        return;
    }
    if (messagesFromNative.length === 0) {
        return;
    }
    isProcessing = true;
    try {
        var msg = popMessageFromQueue();
        // The Java side can send a * message to indicate that it
        // still has messages waiting to be retrieved.
        if (msg == '*' && messagesFromNative.length === 0) {
            nextTick(pollOnce);
            return;
        }
        processMessage(msg);
    } finally {
        isProcessing = false;
        if (messagesFromNative.length > 0) {
            nextTick(processMessages);
        }
    }
}

function popMessageFromQueue() {
    var messageBatch = messagesFromNative.shift();
    if (messageBatch == '*') {
        return '*';
    }

    var spaceIdx = messageBatch.indexOf(' ');
    var msgLen = +messageBatch.slice(0, spaceIdx);
    var message = messageBatch.substr(spaceIdx + 1, msgLen);
    messageBatch = messageBatch.slice(spaceIdx + msgLen + 1);
    if (messageBatch) {
        messagesFromNative.unshift(messageBatch);
    }
    return message;
}

module.exports = androidExec;


/**
    Licensed to the Apache Software Foundation (ASF) under one
    or more contributor license agreements.  See the NOTICE file
    distributed with this work for additional information
    regarding copyright ownership.  The ASF licenses this file
    to you under the Apache License, Version 2.0 (the
    "License"); you may not use this file except in compliance
    with the License.  You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

    Unless required by applicable law or agreed to in writing,
    software distributed under the License is distributed on an
    "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
    KIND, either express or implied.  See the License for the
    specific language governing permissions and limitations
    under the License.
*/

var path = require('path');
var fs = require('fs-extra');
var ActionStack = require('cordova-common').ActionStack;
var DepGraph = require('dep-graph');
var semver = require('semver');
var PlatformJson = require('cordova-common').PlatformJson;
var CordovaError = require('cordova-common').CordovaError;
var platform_modules = require('../platforms/platforms');
var os = require('os');
var underscore = require('underscore');
var events = require('cordova-common').events;
var HooksRunner = require('../hooks/HooksRunner');
var isWindows = (os.platform().substr(0, 3) === 'win');
var pluginSpec = require('../cordova/plugin/plugin_spec_parser');
var cordovaUtil = require('../cordova/util');

var superspawn = require('cordova-common').superspawn;
var PluginInfo = require('cordova-common').PluginInfo;
var PluginInfoProvider = require('cordova-common').PluginInfoProvider;
var variableMerge = require('../plugman/variable-merge');

/* INSTALL FLOW
   ------------
   There are four functions install "flows" through. Here is an attempt at
   providing a high-level logic flow overview.
   1. module.exports (installPlugin)
     a) checks that the platform is supported
     b) converts oldIds into newIds (CPR -> npm)
     c) invokes possiblyFetch
   2. possiblyFetch
     a) checks that the plugin is fetched. if so, calls runInstall
     b) if not, invokes plugman.fetch, and when done, calls runInstall
   3. runInstall
     a) checks if the plugin is already installed. if so, calls back (done).
     b) if possible, will check the version of the project and make sure it is compatible with the plugin (checks <engine> tags)
     c) makes sure that any variables required by the plugin are specified. if they are not specified, plugman will throw or callback with an error.
     d) if dependencies are listed in the plugin, it will recurse for each dependent plugin, autoconvert IDs to newIDs and call possiblyFetch (2) on each one. When each dependent plugin is successfully installed, it will then proceed to call handleInstall (4)
   4. handleInstall
     a) queues up actions into a queue (asset, source-file, headers, etc)
     b) processes the queue
     c) calls back (done)
*/

// possible options: subdir, cli_variables, www_dir
// Returns a promise.
module.exports = function installPlugin (platform, project_dir, id, plugins_dir, options) {
    project_dir = cordovaUtil.convertToRealPathSafe(project_dir);
    plugins_dir = cordovaUtil.convertToRealPathSafe(plugins_dir);
    options = options || {};
    if (!options.hasOwnProperty('is_top_level')) options.is_top_level = true;

    plugins_dir = plugins_dir || path.join(project_dir, 'cordova', 'plugins');

    var current_stack = new ActionStack();
    return possiblyFetch(id, plugins_dir, options)
        .then(function (plugin_dir) {
            return module.exports.runInstall(current_stack, platform, project_dir, plugin_dir, plugins_dir, options);
        });
};

// possible options: subdir, cli_variables, www_dir, git_ref, is_top_level
// Returns a promise.
function possiblyFetch (id, plugins_dir, options) {
    var parsedSpec = pluginSpec.parse(id);

    // if plugin is a relative path, check if it already exists
    var plugin_src_dir = isAbsolutePath(id) ? id : path.join(plugins_dir, parsedSpec.id);

    // Check that the plugin has already been fetched.
    if (fs.existsSync(plugin_src_dir)) {
        return Promise.resolve(plugin_src_dir);
    }

    var opts = underscore.extend({}, options, {
        client: 'plugman'
    });
    // TODO: without runtime require below, we have a circular dependency.
    return require('./plugman').fetch(id, plugins_dir, opts);
}

function checkEngines (engines) {

    for (var i = 0; i < engines.length; i++) {
        var engine = engines[i];

        // This is a hack to allow plugins with <engine> tag to be installed with
        // engine with '-dev' or '-nightly' suffixes. It is required due to new semver range logic,
        // introduced in semver@3.x. For more details see https://github.com/npm/node-semver#prerelease-tags.
        //
        // This may lead to false-positive checks, when engine version with dropped
        // suffix is equal to one of range bounds, for example: 5.1.0-dev >= 5.1.0.
        // However this shouldn't be a problem, because this only should happen in dev workflow.
        engine.currentVersion = engine.currentVersion && engine.currentVersion.replace(/-dev|-nightly.*$/, '');
        if (semver.satisfies(engine.currentVersion, engine.minVersion, /* loose= */true) || engine.currentVersion === null) {
            continue; // engine ok!
        } else {
            var msg = 'Plugin doesn\'t support this project\'s ' + engine.name + ' version. ' +
                      engine.name + ': ' + engine.currentVersion +
                      ', failed version requirement: ' + engine.minVersion;
            events.emit('warn', msg);
            return Promise.reject(Object.assign(new Error(), { skip: true }));
        }
    }

    return Promise.resolve(true);
}

function cleanVersionOutput (version, name) {
    var out = version.trim();
    var rc_index = out.indexOf('rc');
    var dev_index = out.indexOf('dev');
    if (rc_index > -1) {
        out = out.substr(0, rc_index) + '-' + out.substr(rc_index);
    }

    // put a warning about using the dev branch
    if (dev_index > -1) {
        // some platform still lists dev branches as just dev, set to null and continue
        if (out === 'dev') {
            out = null;
        }
        events.emit('verbose', name + ' has been detected as using a development branch. Attemping to install anyways.');
    }

    // add extra period/digits to conform to semver - some version scripts will output
    // just a major or major minor version number
    var majorReg = /\d+/;
    var minorReg = /\d+\.\d+/;
    var patchReg = /\d+\.\d+\.\d+/;

    if (patchReg.test(out)) {

    } else if (minorReg.test(out)) {
        out = out.match(minorReg)[0] + '.0';
    } else if (majorReg.test(out)) {
        out = out.match(majorReg)[0] + '.0.0';
    }

    return out;
}

// exec engine scripts in order to get the current engine version
// Returns a promise for the array of engines.
function callEngineScripts (engines, project_dir) {

    return Promise.all(
        engines.map(function (engine) {
            // CB-5192; on Windows scriptSrc doesn't have file extension so we shouldn't check whether the script exists
            var scriptPath = engine.scriptSrc || null;
            if (scriptPath && (isWindows || fs.existsSync(engine.scriptSrc))) {

                if (!isWindows) { // not required on Windows
                    fs.chmodSync(engine.scriptSrc, '755');
                }
                return superspawn.spawn(scriptPath)
                    .then(stdout => {
                        engine.currentVersion = cleanVersionOutput(stdout, engine.name);
                        if (engine.currentVersion === '') {
                            events.emit('warn', engine.name + ' version check returned nothing (' + scriptPath + '), continuing anyways.');
                            engine.currentVersion = null;
                        }
                    }, () => {
                        events.emit('warn', engine.name + ' version check failed (' + scriptPath + '), continuing anyways.');
                        engine.currentVersion = null;
                    })
                    .then(_ => engine);
            } else {

                if (engine.currentVersion) {
                    engine.currentVersion = cleanVersionOutput(engine.currentVersion, engine.name);
                } else {
                    events.emit('warn', engine.name + ' version not detected (lacks script ' + scriptPath + ' ), continuing.');
                }

                return Promise.resolve(engine);
            }
        })
    );
}

// return only the engines we care about/need
function getEngines (pluginInfo, platform, project_dir, plugin_dir) {
    var engines = pluginInfo.getEngines();
    var defaultEngines = require('./util/default-engines')(project_dir);
    var uncheckedEngines = [];
    var cordovaEngineIndex, cordovaPlatformEngineIndex, theName, platformIndex, defaultPlatformIndex;
    // load in known defaults and update when necessary

    engines.forEach(function (engine) {
        theName = engine.name;

        // check to see if the engine is listed as a default engine
        if (defaultEngines[theName]) {
            // make sure engine is for platform we are installing on
            defaultPlatformIndex = defaultEngines[theName].platform.indexOf(platform);
            if (defaultPlatformIndex > -1 || defaultEngines[theName].platform === '*') {
                defaultEngines[theName].minVersion = defaultEngines[theName].minVersion ? defaultEngines[theName].minVersion : engine.version;
                defaultEngines[theName].currentVersion = defaultEngines[theName].currentVersion ? defaultEngines[theName].currentVersion : null;
                defaultEngines[theName].scriptSrc = defaultEngines[theName].scriptSrc ? defaultEngines[theName].scriptSrc : null;
                defaultEngines[theName].name = theName;

                // set the indices so we can pop the cordova engine when needed
                if (theName === 'cordova') cordovaEngineIndex = uncheckedEngines.length;
                if (theName === 'cordova-' + platform) cordovaPlatformEngineIndex = uncheckedEngines.length;

                uncheckedEngines.push(defaultEngines[theName]);
            }
        // check for other engines
        } else {
            if (typeof engine.platform === 'undefined' || typeof engine.scriptSrc === 'undefined') {
                throw new CordovaError('warn', 'engine.platform or engine.scriptSrc is not defined in custom engine "' +
                    theName + '" from plugin "' + pluginInfo.id + '" for ' + platform);
            }

            platformIndex = engine.platform.indexOf(platform);
            // CB-7183: security check for scriptSrc path escaping outside the plugin
            var scriptSrcPath = path.resolve(plugin_dir, engine.scriptSrc);
            if (scriptSrcPath.indexOf(plugin_dir) !== 0) {
                throw new Error('Security violation: scriptSrc ' + scriptSrcPath + ' is out of plugin dir ' + plugin_dir);
            }
            if (platformIndex > -1 || engine.platform === '*') {
                uncheckedEngines.push({ 'name': theName, 'platform': engine.platform, 'scriptSrc': scriptSrcPath, 'minVersion': engine.version });
            }
        }
    });

    // make sure we check for platform req's and not just cordova reqs
    if (cordovaEngineIndex && cordovaPlatformEngineIndex) uncheckedEngines.pop(cordovaEngineIndex);
    return uncheckedEngines;
}

// possible options: cli_variables, www_dir, is_top_level
// Returns a promise.
module.exports.runInstall = runInstall;
function runInstall (actions, platform, project_dir, plugin_dir, plugins_dir, options) {
    project_dir = cordovaUtil.convertToRealPathSafe(project_dir);
    plugin_dir = cordovaUtil.convertToRealPathSafe(plugin_dir);
    plugins_dir = cordovaUtil.convertToRealPathSafe(plugins_dir);
    options = options || {};
    options.graph = options.graph || new DepGraph();
    options.pluginInfoProvider = options.pluginInfoProvider || new PluginInfoProvider();

    var pluginInfoProvider = options.pluginInfoProvider;
    var pluginInfo = pluginInfoProvider.get(plugin_dir);
    var filtered_variables = {};
    var platformJson = PlatformJson.load(plugins_dir, platform);

    if (platformJson.isPluginInstalled(pluginInfo.id)) {
        if (options.is_top_level) {
            var msg = 'Plugin "' + pluginInfo.id + '" already installed on ' + platform + '.';
            if (platformJson.isPluginDependent(pluginInfo.id)) {
                msg += ' Making it top-level.';
                platformJson.makeTopLevel(pluginInfo.id).save();
            }
            events.emit('log', msg);
        } else {
            events.emit('log', 'Dependent plugin "' + pluginInfo.id + '" already installed on ' + platform + '.');
        }

        // CB-11022 return true always in this case since if the plugin is installed
        // we don't need to call prepare in any way
        return Promise.resolve(true);
    }
    events.emit('log', 'Installing "' + pluginInfo.id + '" for ' + platform);

    var theEngines = getEngines(pluginInfo, platform, project_dir, plugin_dir);

    var install = {
        actions: actions,
        platform: platform,
        project_dir: project_dir,
        plugins_dir: plugins_dir,
        top_plugin_id: pluginInfo.id,
        top_plugin_dir: plugin_dir
    };

    return Promise.resolve().then(function () {
        if (options.platformVersion) {
            return Promise.resolve(options.platformVersion);
        }
        return Promise.resolve(superspawn.maybeSpawn(path.join(project_dir, 'cordova', 'version'), [], { chmod: true }));
    }).then(function (platformVersion) {
        options.platformVersion = platformVersion;
        return callEngineScripts(theEngines, path.resolve(plugins_dir, '..'));
    }).then(function (engines) {
        return checkEngines(engines);
    }).then(function () {
        filtered_variables = variableMerge.mergeVariables(plugin_dir, platform, options);
        install.filtered_variables = filtered_variables;

        // Check for dependencies
        var dependencies = pluginInfo.getDependencies(platform);
        if (dependencies.length) {
            return installDependencies(install, dependencies, options);
        }
        return Promise.resolve(true);
    }
    ).then(
        function () {
            var install_plugin_dir = path.join(plugins_dir, pluginInfo.id);

            // may need to copy to destination...
            if (!fs.existsSync(install_plugin_dir)) {
                copyPlugin(plugin_dir, plugins_dir, options.link, pluginInfoProvider);
            }

            var projectRoot = cordovaUtil.isCordova();

            if (projectRoot) {
                // using unified hooksRunner
                var hookOptions = {
                    cordova: { platforms: [ platform ] },
                    plugin: {
                        id: pluginInfo.id,
                        pluginInfo: pluginInfo,
                        platform: install.platform,
                        dir: install.top_plugin_dir
                    },
                    nohooks: options.nohooks
                };

                // CB-10708 This is the case when we're trying to install plugin using plugman to specific
                // platform inside of the existing CLI project. In this case we need to put plugin's files
                // into platform_www but plugman CLI doesn't allow us to do that, so we set it here
                options.usePlatformWww = true;

                var hooksRunner = new HooksRunner(projectRoot);

                return hooksRunner.fire('before_plugin_install', hookOptions).then(function () {
                    return handleInstall(actions, pluginInfo, platform, project_dir, plugins_dir, install_plugin_dir, filtered_variables, options);
                }).then(function (installResult) {
                    return hooksRunner.fire('after_plugin_install', hookOptions)
                        // CB-11022 Propagate install result to caller to be able to avoid unnecessary prepare
                        .then(_ => installResult);
                });
            } else {
                return handleInstall(actions, pluginInfo, platform, project_dir, plugins_dir, install_plugin_dir, filtered_variables, options);
            }
        }
    ).catch(
        function (error) {

            if (error.skip) {
                events.emit('warn', 'Skipping \'' + pluginInfo.id + '\' for ' + platform);
            } else {
                events.emit('warn', 'Failed to install \'' + pluginInfo.id + '\': ' + error.stack);
                throw error;
            }
        }
    );
}

function installDependencies (install, dependencies, options) {
    events.emit('verbose', 'Dependencies detected, iterating through them...');

    var top_plugins = path.join(options.plugin_src_dir || install.top_plugin_dir, '..');

    // Add directory of top-level plugin to search path
    options.searchpath = options.searchpath || [];
    if (top_plugins !== install.plugins_dir && options.searchpath.indexOf(top_plugins) === -1) { options.searchpath.push(top_plugins); }

    // Search for dependency by Id is:
    // a) Look for {$top_plugins}/{$depId} directory
    // b) Scan the top level plugin directory {$top_plugins} for matching id (searchpath)
    // c) Fetch from registry

    return dependencies.reduce(function (soFar, dep) {
        return soFar.then(
            function () {
                dep.git_ref = dep.commit;

                if (dep.subdir) {
                    dep.subdir = path.normalize(dep.subdir);
                }

                // We build the dependency graph only to be able to detect cycles, getChain will throw an error if it detects one
                options.graph.add(install.top_plugin_id, dep.id);
                options.graph.getChain(install.top_plugin_id);

                return tryFetchDependency(dep, install, options)
                    .then(
                        function (url) {
                            dep.url = url;
                            return installDependency(dep, install, options);
                        }
                    );
            }
        );

    }, Promise.resolve(true));
}

function tryFetchDependency (dep, install, options) {

    // Handle relative dependency paths by expanding and resolving them.
    // The easy case of relative paths is to have a URL of '.' and a different subdir.
    // TODO: Implement the hard case of different repo URLs, rather than the special case of
    // same-repo-different-subdir.
    var relativePath;
    if (dep.url === '.') {

        // Look up the parent plugin's fetch metadata and determine the correct URL.
        var fetchdata = require('./util/metadata').get_fetch_metadata(install.top_plugin_dir);
        if (!fetchdata || !(fetchdata.source && fetchdata.source.type)) {

            relativePath = dep.subdir || dep.id;

            events.emit('warn', 'No fetch metadata found for plugin ' + install.top_plugin_id + '. checking for ' + relativePath + ' in ' + options.searchpath.join(','));

            return Promise.resolve(relativePath);
        }

        // Now there are two cases here: local directory, and git URL.
        if (fetchdata.source.type === 'local') {

            dep.url = fetchdata.source.path;

            return superspawn.spawn('git rev-parse --show-toplevel', { cwd: dep.url })
                .catch(err => {
                    if (err.code === 128) {
                        throw new Error('Plugin ' + dep.id + ' is not in git repository. All plugins must be in a git repository.');
                    } else {
                        throw new Error('Failed to locate git repository for ' + dep.id + ' plugin.');
                    }
                })
                .then(function (git_repo) {
                    // Clear out the subdir since the url now contains it
                    var url = path.join(git_repo, dep.subdir);
                    dep.subdir = '';
                    return Promise.resolve(url);
                }).catch(function () {
                    return Promise.resolve(dep.url);
                });

        } else if (fetchdata.source.type === 'git') {
            return Promise.resolve(fetchdata.source.url);
        } else if (fetchdata.source.type === 'dir') {

            // Note: With fetch() independant from install()
            // $md5 = md5(uri)
            // Need a Hash(uri) --> $tmpDir/cordova-fetch/git-hostname.com-$md5/
            // plugin[id].install.source --> searchpath that matches fetch uri

            // mapping to a directory of OS containing fetched plugins
            var tmpDir = fetchdata.source.url;
            tmpDir = tmpDir.replace('$tmpDir', os.tmpdir());

            var pluginSrc = '';
            if (dep.subdir.length) {
                // Plugin is relative to directory
                pluginSrc = path.join(tmpDir, dep.subdir);
            }

            // Try searchpath in dir, if that fails re-fetch
            if (!pluginSrc.length || !fs.existsSync(pluginSrc)) {
                pluginSrc = dep.id;

                // Add search path
                if (options.searchpath.indexOf(tmpDir) === -1) { options.searchpath.unshift(tmpDir); } // place at top of search
            }

            return Promise.resolve(pluginSrc);
        }
    }

    // Test relative to parent folder
    if (dep.url && !isAbsolutePath(dep.url)) {
        relativePath = path.resolve(install.top_plugin_dir, '../' + dep.url);

        if (fs.existsSync(relativePath)) {
            dep.url = relativePath;
        }
    }

    // CB-4770: registry fetching
    if (dep.url === undefined) {
        dep.url = dep.id;
    }

    return Promise.resolve(dep.url);
}

function installDependency (dep, install, options) {

    var opts;
    dep.install_dir = path.join(install.plugins_dir, dep.id);

    events.emit('verbose', 'Requesting plugin "' + (dep.version ? dep.id + '@' + dep.version : dep.id) + '".');

    if (fs.existsSync(dep.install_dir)) {
        var pluginInfo = new PluginInfo(dep.install_dir);
        var version_installed = pluginInfo.version;
        var version_required = dep.version;

        if (dep.version) {
            if (Number(dep.version.replace('.', ''))) {
                version_required = '^' + dep.version;
            }
        }
        // strip -dev from the installed plugin version so it properly passes
        // semver.satisfies
        var stripped_version;
        if (version_installed.includes('-dev')) {
            stripped_version = semver.inc(version_installed, 'patch');
        }
        if (options.force ||
            semver.satisfies(version_installed, version_required, /* loose= */true) ||
            semver.satisfies(stripped_version, version_required, /* loose= */true) ||
            version_required === null ||
            version_required === undefined ||
            version_required === '') {
            events.emit('log', 'Plugin dependency "' + (version_installed ? dep.id + '@' + version_installed : dep.id) + '" already fetched, using that version.');
        } else {
            var msg = 'Version of installed plugin: "' +
                dep.id + '@' + version_installed +
                '" does not satisfy dependency plugin requirement "' +
                dep.id + '@' + version_required +
                 '". Try --force to use installed plugin as dependency.';
            return Promise.resolve()
                .then(function () {
                    // Remove plugin
                    return fs.removeSync(path.join(install.plugins_dir, install.top_plugin_id));
                }).then(function () {
                    // Return promise chain and finally reject
                    return Promise.reject(new CordovaError(msg));
                });
        }
        opts = underscore.extend({}, options, {
            cli_variables: install.filtered_variables,
            is_top_level: false
        });
        return module.exports.runInstall(install.actions, install.platform, install.project_dir, dep.install_dir, install.plugins_dir, opts);

    } else {
        events.emit('verbose', 'Plugin dependency "' + dep.id + '" not fetched, retrieving then installing.');

        opts = underscore.extend({}, options, {
            cli_variables: install.filtered_variables,
            is_top_level: false,
            subdir: dep.subdir,
            git_ref: dep.git_ref,
            expected_id: dep.id
        });

        var dep_src = dep.url.length ? dep.url : (dep.version ? dep.id + '@' + dep.version : dep.id);
        return possiblyFetch(dep_src, install.plugins_dir, opts)
            .then(
                function (plugin_dir) {
                    return module.exports.runInstall(install.actions, install.platform, install.project_dir, plugin_dir, install.plugins_dir, opts);
                }
            );
    }
}

function handleInstall (actions, pluginInfo, platform, project_dir, plugins_dir, plugin_dir, filtered_variables, options) {

    // @tests - important this event is checked spec/install.spec.js
    events.emit('verbose', 'Install start for "' + pluginInfo.id + '" on ' + platform + '.');

    options.variables = filtered_variables;
    return platform_modules.getPlatformApi(platform, project_dir)
        .addPlugin(pluginInfo, options)
        .then(function (result) {
            events.emit('verbose', 'Install complete for ' + pluginInfo.id + ' on ' + platform + '.');
            // Add plugin to installed list. This already done in platform,
            // but need to be duplicated here to manage dependencies properly.
            PlatformJson.load(plugins_dir, platform)
                .addPlugin(pluginInfo.id, filtered_variables, options.is_top_level)
                .save();

            // WIN!
            // Log out plugin INFO element contents in case additional install steps are necessary
            var info_strings = pluginInfo.getInfo(platform) || [];
            info_strings.forEach(function (info) {
                events.emit('results', interp_vars(filtered_variables, info));
            });

            // Propagate value, returned by platform's addPlugin method to caller
            return Promise.resolve(result);
        });
}

function interp_vars (vars, text) {
    vars && Object.keys(vars).forEach(function (key) {
        var regExp = new RegExp('\\$' + key, 'g');
        text = text.replace(regExp, vars[key]);
    });
    return text;
}

function isAbsolutePath (_path) {
    // some valid abs paths: 'c:' '/' '\' and possibly ? 'file:' 'http:'
    return _path && (_path.charAt(0) === path.sep || _path.indexOf(':') > 0);
}

// Copy or link a plugin from plugin_dir to plugins_dir/plugin_id.
function copyPlugin (plugin_src_dir, plugins_dir, link, pluginInfoProvider) {
    var pluginInfo = new PluginInfo(plugin_src_dir);
    var dest = path.join(plugins_dir, pluginInfo.id);

    if (link) {
        events.emit('verbose', 'Symlinking from location "' + plugin_src_dir + '" to location "' + dest + '"');
        fs.removeSync(dest);
        fs.ensureSymlinkSync(plugin_src_dir, dest, 'junction');
    } else {
        events.emit('verbose', 'Copying from location "' + plugin_src_dir + '" to location "' + dest + '"');
        fs.copySync(plugin_src_dir, dest);
    }
    pluginInfo.dir = dest;
    pluginInfoProvider.put(pluginInfo);

    return dest;
}


/**
    Licensed to the Apache Software Foundation (ASF) under one
    or more contributor license agreements.  See the NOTICE file
    distributed with this work for additional information
    regarding copyright ownership.  The ASF licenses this file
    to you under the Apache License, Version 2.0 (the
    "License"); you may not use this file except in compliance
    with the License.  You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

    Unless required by applicable law or agreed to in writing,
    software distributed under the License is distributed on an
    "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
    KIND, either express or implied.  See the License for the
    specific language governing permissions and limitations
    under the License.
*/

var path = require('path');
var Q = require('q');

var AndroidProject = require('./lib/AndroidProject');
var AndroidStudio = require('./lib/AndroidStudio');
var PluginManager = require('cordova-common').PluginManager;

var CordovaLogger = require('cordova-common').CordovaLogger;
var selfEvents = require('cordova-common').events;

var PLATFORM = 'android';

function setupEvents(externalEventEmitter) {
    if (externalEventEmitter) {
        // This will make the platform internal events visible outside
        selfEvents.forwardEventsTo(externalEventEmitter);
        return externalEventEmitter;
    }

    // There is no logger if external emitter is not present,
    // so attach a console logger
    CordovaLogger.get().subscribe(selfEvents);
    return selfEvents;
}

/**
 * Class, that acts as abstraction over particular platform. Encapsulates the
 *   platform's properties and methods.
 *
 * Platform that implements own PlatformApi instance _should implement all
 *   prototype methods_ of this class to be fully compatible with cordova-lib.
 *
 * The PlatformApi instance also should define the following field:
 *
 * * platform: String that defines a platform name.
 */
function Api(platform, platformRootDir, events) {
    this.platform = PLATFORM;
    this.root = path.resolve(__dirname, '..');

    setupEvents(events);

    var self = this;

    this.locations = {
        root: self.root,
        www: path.join(self.root, 'assets/www'),
        res: path.join(self.root, 'res'),
        platformWww: path.join(self.root, 'platform_www'),
        configXml: path.join(self.root, 'res/xml/config.xml'),
        defaultConfigXml: path.join(self.root, 'cordova/defaults.xml'),
        strings: path.join(self.root, 'res/values/strings.xml'),
        manifest: path.join(self.root, 'AndroidManifest.xml'),
        build: path.join(self.root, 'build'),
        // NOTE: Due to platformApi spec we need to return relative paths here
        cordovaJs: 'bin/templates/project/assets/www/cordova.js',
        cordovaJsSrc: 'cordova-js-src'
    };

    // XXX Override some locations for Android Studio projects
    if(AndroidStudio.isAndroidStudioProject(self.root) === true) {
      selfEvents.emit('log', 'Android Studio project detected');
      this.android_studio = true;
      this.locations.configXml = path.join(self.root, 'app/src/main/res/xml/config.xml');
      this.locations.strings = path.join(self.root, 'app/src/main/res/xml/strings.xml');
      this.locations.manifest = path.join(self.root, 'app/src/main/AndroidManifest.xml');
      this.locations.www = path.join(self.root, 'app/src/main/assets/www');
      this.locations.res = path.join(self.root, 'app/src/main/res');
    }
}

/**
 * Installs platform to specified directory and creates a platform project.
 *
 * @param  {String}  destination Destination directory, where insatll platform to
 * @param  {ConfigParser}  [config] ConfgiParser instance, used to retrieve
 *   project creation options, such as package id and project name.
 * @param  {Object}  [options]  An options object. The most common options are:
 * @param  {String}  [options.customTemplate]  A path to custom template, that
 *   should override the default one from platform.
 * @param  {Boolean}  [options.link]  Flag that indicates that platform's
 *   sources will be linked to installed platform instead of copying.
 * @param {EventEmitter} [events] An EventEmitter instance that will be used for
 *   logging purposes. If no EventEmitter provided, all events will be logged to
 *   console
 *
 * @return {Promise<PlatformApi>} Promise either fulfilled with PlatformApi
 *   instance or rejected with CordovaError.
 */
Api.createPlatform = function (destination, config, options, events) {
    events = setupEvents(events);
    var result;
    try {
        result = require('../../lib/create')
        .create(destination, config, options, events)
        .then(function (destination) {
            var PlatformApi = require(path.resolve(destination, 'cordova/Api'));
            return new PlatformApi(PLATFORM, destination, events);
        });
    }
    catch (e) {
        events.emit('error','createPlatform is not callable from the android project API.');
        throw(e);
    }
    return result;
};

/**
 * Updates already installed platform.
 *
 * @param  {String}  destination Destination directory, where platform installed
 * @param  {Object}  [options]  An options object. The most common options are:
 * @param  {String}  [options.customTemplate]  A path to custom template, that
 *   should override the default one from platform.
 * @param  {Boolean}  [options.link]  Flag that indicates that platform's
 *   sources will be linked to installed platform instead of copying.
 * @param {EventEmitter} [events] An EventEmitter instance that will be used for
 *   logging purposes. If no EventEmitter provided, all events will be logged to
 *   console
 *
 * @return {Promise<PlatformApi>} Promise either fulfilled with PlatformApi
 *   instance or rejected with CordovaError.
 */
Api.updatePlatform = function (destination, options, events) {
    events = setupEvents(events);
    var result;
    try {
        result = require('../../lib/create')
        .update(destination, options, events)
        .then(function (destination) {
            var PlatformApi = require(path.resolve(destination, 'cordova/Api'));
            return new PlatformApi('android', destination, events);
        });
    }
    catch (e) {
        events.emit('error','updatePlatform is not callable from the android project API, you will need to do this manually.');
        throw(e);
    }
    return result;
};

/**
 * Gets a CordovaPlatform object, that represents the platform structure.
 *
 * @return  {CordovaPlatform}  A structure that contains the description of
 *   platform's file structure and other properties of platform.
 */
Api.prototype.getPlatformInfo = function () {
    var result = {};
    result.locations = this.locations;
    result.root = this.root;
    result.name = this.platform;
    result.version = require('./version');
    result.projectConfig = this._config;

    return result;
};

/**
 * Updates installed platform with provided www assets and new app
 *   configuration. This method is required for CLI workflow and will be called
 *   each time before build, so the changes, made to app configuration and www
 *   code, will be applied to platform.
 *
 * @param {CordovaProject} cordovaProject A CordovaProject instance, that defines a
 *   project structure and configuration, that should be applied to platform
 *   (contains project's www location and ConfigParser instance for project's
 *   config).
 *
 * @return  {Promise}  Return a promise either fulfilled, or rejected with
 *   CordovaError instance.
 */
Api.prototype.prepare = function (cordovaProject, prepareOptions) {
    return require('./lib/prepare').prepare.call(this, cordovaProject, prepareOptions);
};

/**
 * Installs a new plugin into platform. This method only copies non-www files
 *   (sources, libs, etc.) to platform. It also doesn't resolves the
 *   dependencies of plugin. Both of handling of www files, such as assets and
 *   js-files and resolving dependencies are the responsibility of caller.
 *
 * @param  {PluginInfo}  plugin  A PluginInfo instance that represents plugin
 *   that will be installed.
 * @param  {Object}  installOptions  An options object. Possible options below:
 * @param  {Boolean}  installOptions.link: Flag that specifies that plugin
 *   sources will be symlinked to app's directory instead of copying (if
 *   possible).
 * @param  {Object}  installOptions.variables  An object that represents
 *   variables that will be used to install plugin. See more details on plugin
 *   variables in documentation:
 *   https://cordova.apache.org/docs/en/4.0.0/plugin_ref_spec.md.html
 *
 * @return  {Promise}  Return a promise either fulfilled, or rejected with
 *   CordovaError instance.
 */
Api.prototype.addPlugin = function (plugin, installOptions) {
    var project = AndroidProject.getProjectFile(this.root);
    var self = this;

    installOptions = installOptions || {};
    installOptions.variables = installOptions.variables || {};
    // Add PACKAGE_NAME variable into vars
    if (!installOptions.variables.PACKAGE_NAME) {
        installOptions.variables.PACKAGE_NAME = project.getPackageName();
    }

    if(this.android_studio === true) {
      installOptions.android_studio = true;
    }

    return Q()
       .then(function () {
            //CB-11964: Do a clean when installing the plugin code to get around
            //the Gradle bug introduced by the Android Gradle Plugin Version 2.2
            //TODO: Delete when the next version of Android Gradle plugin comes out

           // Since clean doesn't just clean the build, it also wipes out www, we need
           // to pass additional options.

           // Do some basic argument parsing
            var opts = {};

             // Skip cleaning prepared files when not invoking via cordova CLI.
            opts.noPrepare = true;

            if(!AndroidStudio.isAndroidStudioProject(self.root) && !project.isClean()) {
              return self.clean(opts);
            }
        })
       .then(function () {
            return PluginManager.get(self.platform, self.locations, project)
                .addPlugin(plugin, installOptions);
        })
      .then(function () {
            if (plugin.getFrameworks(this.platform).length === 0) return;

            selfEvents.emit('verbose', 'Updating build files since android plugin contained <framework>');
            require('./lib/builders/builders').getBuilder('gradle').prepBuildFiles();
        }.bind(this))
       // CB-11022 Return truthy value to prevent running prepare after
        .thenResolve(true);
};

/**
 * Removes an installed plugin from platform.
 *
 * Since method accepts PluginInfo instance as input parameter instead of plugin
 *   id, caller shoud take care of managing/storing PluginInfo instances for
 *   future uninstalls.
 *
 * @param  {PluginInfo}  plugin  A PluginInfo instance that represents plugin
 *   that will be installed.
 *
 * @return  {Promise}  Return a promise either fulfilled, or rejected with
 *   CordovaError instance.
 */
Api.prototype.removePlugin = function (plugin, uninstallOptions) {
    var project = AndroidProject.getProjectFile(this.root);

    if(uninstallOptions && uninstallOptions.usePlatformWww === true && this.android_studio === true) {
      uninstallOptions.usePlatformWww = false;
      uninstallOptions.android_studio = true;
    }

    return PluginManager.get(this.platform, this.locations, project)
        .removePlugin(plugin, uninstallOptions)
        .then(function () {
            if (plugin.getFrameworks(this.platform).length === 0) return;

            selfEvents.emit('verbose', 'Updating build files since android plugin contained <framework>');
            require('./lib/builders/builders').getBuilder('gradle').prepBuildFiles();
        }.bind(this))
        // CB-11022 Return truthy value to prevent running prepare after
        .thenResolve(true);
};

/**
 * Builds an application package for current platform.
 *
 * @param  {Object}  buildOptions  A build options. This object's structure is
 *   highly depends on platform's specific. The most common options are:
 * @param  {Boolean}  buildOptions.debug  Indicates that packages should be
 *   built with debug configuration. This is set to true by default unless the
 *   'release' option is not specified.
 * @param  {Boolean}  buildOptions.release  Indicates that packages should be
 *   built with release configuration. If not set to true, debug configuration
 *   will be used.
 * @param   {Boolean}  buildOptions.device  Specifies that built app is intended
 *   to run on device
 * @param   {Boolean}  buildOptions.emulator: Specifies that built app is
 *   intended to run on emulator
 * @param   {String}  buildOptions.target  Specifies the device id that will be
 *   used to run built application.
 * @param   {Boolean}  buildOptions.nobuild  Indicates that this should be a
 *   dry-run call, so no build artifacts will be produced.
 * @param   {String[]}  buildOptions.archs  Specifies chip architectures which
 *   app packages should be built for. List of valid architectures is depends on
 *   platform.
 * @param   {String}  buildOptions.buildConfig  The path to build configuration
 *   file. The format of this file is depends on platform.
 * @param   {String[]} buildOptions.argv Raw array of command-line arguments,
 *   passed to `build` command. The purpose of this property is to pass a
 *   platform-specific arguments, and eventually let platform define own
 *   arguments processing logic.
 *
 * @return {Promise<Object[]>} A promise either fulfilled with an array of build
 *   artifacts (application packages) if package was built successfully,
 *   or rejected with CordovaError. The resultant build artifact objects is not
 *   strictly typed and may conatin arbitrary set of fields as in sample below.
 *
 *     {
 *         architecture: 'x86',
 *         buildType: 'debug',
 *         path: '/path/to/build',
 *         type: 'app'
 *     }
 *
 * The return value in most cases will contain only one item but in some cases
 *   there could be multiple items in output array, e.g. when multiple
 *   arhcitectures is specified.
 */
Api.prototype.build = function (buildOptions) {
    var self = this;
    return require('./lib/check_reqs').run()
    .then(function () {
        return require('./lib/build').run.call(self, buildOptions);
    })
    .then(function (buildResults) {
        // Cast build result to array of build artifacts
        return buildResults.apkPaths.map(function (apkPath) {
            return {
                buildType: buildResults.buildType,
                buildMethod: buildResults.buildMethod,
                path: apkPath,
                type: 'apk'
            };
        });
    });
};

/**
 * Builds an application package for current platform and runs it on
 *   specified/default device. If no 'device'/'emulator'/'target' options are
 *   specified, then tries to run app on default device if connected, otherwise
 *   runs the app on emulator.
 *
 * @param   {Object}  runOptions  An options object. The structure is the same
 *   as for build options.
 *
 * @return {Promise} A promise either fulfilled if package was built and ran
 *   successfully, or rejected with CordovaError.
 */
Api.prototype.run = function(runOptions) {
    var self = this;
    return require('./lib/check_reqs').run()
    .then(function () {
        return require('./lib/run').run.call(self, runOptions);
    });
};

/**
 * Cleans out the build artifacts from platform's directory, and also
 * cleans out the platform www directory if called without options specified.
 *
 * @return  {Promise}  Return a promise either fulfilled, or rejected with
 *   CordovaError.
 */
Api.prototype.clean = function(cleanOptions) {
    var self = this;
    return require('./lib/check_reqs').run()
      .then(function () {
          return require('./lib/build').runClean.call(self, cleanOptions);
      })
      .then(function () {
          return require('./lib/prepare').clean.call(self, cleanOptions);
      });
};

/**
 * Performs a requirements check for current platform. Each platform defines its
 *   own set of requirements, which should be resolved before platform can be
 *   built successfully.
 *
 * @return  {Promise<Requirement[]>}  Promise, resolved with set of Requirement
 *   objects for current platform.
 */
Api.prototype.requirements = function() {
    return require('./lib/check_reqs').check_all();
};

module.exports = Api;


/**
    Licensed to the Apache Software Foundation (ASF) under one
    or more contributor license agreements.  See the NOTICE file
    distributed with this work for additional information
    regarding copyright ownership.  The ASF licenses this file
    to you under the Apache License, Version 2.0 (the
    "License"); you may not use this file except in compliance
    with the License.  You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

    Unless required by applicable law or agreed to in writing,
    software distributed under the License is distributed on an
    "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
    KIND, either express or implied.  See the License for the
    specific language governing permissions and limitations
    under the License.
*/

var PluginInfo = require('cordova-common').PluginInfo;
var events = require('cordova-common').events;
const pify = require('pify');
const initPkgJson = pify(require('init-package-json'));

// returns a promise
function createPackageJson (plugin_path) {
    var pluginInfo = new PluginInfo(plugin_path);

    var defaults = {
        id: pluginInfo.id,
        version: pluginInfo.version,
        description: pluginInfo.description,
        license: pluginInfo.license,
        keywords: pluginInfo.getKeywordsAndPlatforms(),
        repository: pluginInfo.repo,
        engines: pluginInfo.getEngines(),
        platforms: pluginInfo.getPlatformsArray()
    };

    var initFile = require.resolve('./init-defaults');
    return initPkgJson(plugin_path, initFile, defaults)
        .then(_ => {
            events.emit('verbose', 'Package.json successfully created');
        });
}

module.exports = createPackageJson;


/**
    Licensed to the Apache Software Foundation (ASF) under one
    or more contributor license agreements.  See the NOTICE file
    distributed with this work for additional information
    regarding copyright ownership.  The ASF licenses this file
    to you under the Apache License, Version 2.0 (the
    "License"); you may not use this file except in compliance
    with the License.  You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

    Unless required by applicable law or agreed to in writing,
    software distributed under the License is distributed on an
    "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
    KIND, either express or implied.  See the License for the
    specific language governing permissions and limitations
    under the License.
*/

var cordova_util = require('./util');
var ConfigParser = require('cordova-common').ConfigParser;
var PlatformJson = require('cordova-common').PlatformJson;
var PluginInfoProvider = require('cordova-common').PluginInfoProvider;
var PlatformMunger = require('cordova-common').ConfigChanges.PlatformMunger;
var events = require('cordova-common').events;
var platforms = require('../platforms/platforms');
var HooksRunner = require('../hooks/HooksRunner');
var restore = require('./restore-util');
var path = require('path');
var config = require('./config');
var _ = require('underscore');

exports = module.exports = prepare;
module.exports.preparePlatforms = preparePlatforms;
module.exports.restoreMissingPluginsForPlatform = restoreMissingPluginsForPlatform;

function prepare (options) {
    return Promise.resolve().then(function () {
        var projectRoot = cordova_util.cdProjectRoot();
        var config_json = config.read(projectRoot);
        options = options || { verbose: false, platforms: [], options: {} };
        options.save = options.save || false;
        var hooksRunner = new HooksRunner(projectRoot);
        return hooksRunner.fire('before_prepare', options)
            .then(function () {
                return restore.installPlatformsFromConfigXML(options.platforms, { searchpath: options.searchpath, restoring: true });
            })
            .then(function () {
                options = cordova_util.preProcessOptions(options);
                var paths = options.platforms.map(function (p) {
                    var platform_path = path.join(projectRoot, 'platforms', p);
                    return platforms.getPlatformApi(p, platform_path).getPlatformInfo().locations.www;
                });
                options.paths = paths;
            }).then(function () {
                options = cordova_util.preProcessOptions(options);
                return restore.installPluginsFromConfigXML(options);
            }).then(function () {
                options = cordova_util.preProcessOptions(options);
                options.searchpath = options.searchpath || config_json.plugin_search_path;
                // Iterate over each added platform
                return module.exports.preparePlatforms(options.platforms, projectRoot, options);
            }).then(function () {
                options.paths = options.platforms.map(function (platform) {
                    return platforms.getPlatformApi(platform).getPlatformInfo().locations.www;
                });
                return hooksRunner.fire('after_prepare', options);
            });
    });
}

/**
 * Calls `platformApi.prepare` for each platform in project
 *
 * @param   {string[]}  platformList  List of platforms, added to current project
 * @param   {string}    projectRoot   Project root directory
 *
 * @return  {Promise}
 */
function preparePlatforms (platformList, projectRoot, options) {
    return Promise.all(platformList.map(function (platform) {
        // TODO: this need to be replaced by real projectInfo
        // instance for current project.
        var project = {
            root: projectRoot,
            projectConfig: new ConfigParser(cordova_util.projectConfig(projectRoot)),
            locations: {
                plugins: path.join(projectRoot, 'plugins'),
                www: cordova_util.projectWww(projectRoot),
                rootConfigXml: cordova_util.projectConfig(projectRoot)
            }
        };
        // CB-9987 We need to reinstall the plugins for the platform it they were added by cordova@<5.4.0
        return module.exports.restoreMissingPluginsForPlatform(platform, projectRoot, options)
            .then(function () {
                // platformApi prepare takes care of all functionality
                // which previously had been executed by cordova.prepare:
                //   - reset config.xml and then merge changes from project's one,
                //   - update www directory from project's one and merge assets from platform_www,
                //   - reapply config changes, made by plugins,
                //   - update platform's project
                // Please note that plugins' changes, such as installed js files, assets and
                // config changes is not being reinstalled on each prepare.
                var platformApi = platforms.getPlatformApi(platform);
                return platformApi.prepare(project, _.clone(options))
                    .then(function () {
                        // Handle edit-config in config.xml
                        var platformRoot = path.join(projectRoot, 'platforms', platform);
                        var platformJson = PlatformJson.load(platformRoot, platform);
                        var munger = new PlatformMunger(platform, platformRoot, platformJson);
                        // the boolean argument below is "should_increment"
                        munger.add_config_changes(project.projectConfig, true).save_all();
                    });
            });
    }));
}

/**
 * Ensures that plugins, installed with previous versions of CLI (<5.4.0) are
 *   readded to platform correctly. Also triggers regeneration of
 *   cordova_plugins.js file.
 *
 * @param   {String}  platform     Platform name to check for installed plugins
 * @param   {String}  projectRoot  A current cordova project location
 * @param   {Object}  [options]    Options that will be passed to
 *   PlatformApi.pluginAdd/Remove. This object will be extended with plugin
 *   variables, used to install the plugin initially (picked from "old"
 *   plugins/<platform>.json)
 *
 * @return  {Promise}               Promise that'll be fulfilled if all the
 *   plugins reinstalled properly.
 */
function restoreMissingPluginsForPlatform (platform, projectRoot, options) {
    events.emit('verbose', 'Checking for any plugins added to the project that have not been installed in ' + platform + ' platform');
    // Flow:
    // 1. Compare <platform>.json file in <project>/plugins ("old") and platforms/<platform> ("new")
    // 2. If there is any differences - merge "old" one into "new"
    // 3. Reinstall plugins that are missing and was merged on previous step

    var oldPlatformJson = PlatformJson.load(path.join(projectRoot, 'plugins'), platform);
    var platformJson = PlatformJson.load(path.join(projectRoot, 'platforms', platform), platform);
    var missingPlugins = Object.keys(oldPlatformJson.root.installed_plugins)
        .concat(Object.keys(oldPlatformJson.root.dependent_plugins))
        .reduce(function (result, candidate) {
            if (!platformJson.isPluginInstalled(candidate)) {
                result.push({ name: candidate,
                    // Note: isPluginInstalled is actually returns not a boolean,
                    // but object which corresponds to this particular plugin
                    variables: oldPlatformJson.isPluginInstalled(candidate) });
            }
            return result;
        }, []);

    if (missingPlugins.length === 0) {
        events.emit('verbose', 'No differences found between plugins added to project and installed in ' +
            platform + ' platform. Continuing...');
        return Promise.resolve();
    }
    var api = platforms.getPlatformApi(platform);
    var provider = new PluginInfoProvider();
    return missingPlugins.reduce(function (promise, plugin) {
        return promise.then(function () {
            var pluginOptions = options || {};
            pluginOptions.variables = plugin.variables;
            pluginOptions.usePlatformWww = true;
            events.emit('verbose', 'Reinstalling missing plugin ' + plugin.name + ' in ' + platform + ' platform');
            var pluginInfo = provider.get(path.join(projectRoot, 'plugins', plugin.name));
            return api.removePlugin(pluginInfo, pluginOptions)
                .then(function () {
                    return api.addPlugin(pluginInfo, pluginOptions);
                });
        });
    }, Promise.resolve());
}


/**
    Licensed to the Apache Software Foundation (ASF) under one
    or more contributor license agreements.  See the NOTICE file
    distributed with this work for additional information
    regarding copyright ownership.  The ASF licenses this file
    to you under the Apache License, Version 2.0 (the
    "License"); you may not use this file except in compliance
    with the License.  You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

    Unless required by applicable law or agreed to in writing,
    software distributed under the License is distributed on an
    "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
    KIND, either express or implied.  See the License for the
    specific language governing permissions and limitations
    under the License.
*/

var path = require('path');
var events = require('cordova-common').events;
var JsprojManager = require('./lib/JsprojManager');
var PluginManager = require('cordova-common').PluginManager;
var CordovaLogger = require('cordova-common').CordovaLogger;
var PlatformMunger = require('./lib/ConfigChanges.js').PlatformMunger;
var PlatformJson = require('cordova-common').PlatformJson;
var PluginInfo = require('./lib/PluginInfo').PluginInfo;
var PluginInfoProvider = require('cordova-common').PluginInfoProvider;

var PLATFORM = 'windows';

function setupEvents(externalEventEmitter) {
    if (externalEventEmitter) {
        // This will make the platform internal events visible outside
        events.forwardEventsTo(externalEventEmitter);
        return;
    }

    // There is no logger if external emitter is not present,
    // so attach a console logger
    CordovaLogger.get().subscribe(events);
}

/**
 * Class, that acts as abstraction over particular platform. Encapsulates the
 *   platform's properties and methods.
 *
 * Platform that implements own PlatformApi instance _should implement all
 *   prototype methods_ of this class to be fully compatible with cordova-lib.
 *
 * The PlatformApi instance also should define the following field:
 *
 * * platform: String that defines a platform name.
 */
function Api(platform, platformRootDir, eventEmitter) {
    this.platform = PLATFORM;
    this.root = path.resolve(__dirname, '..');

    setupEvents(eventEmitter);

    var self = this;
    this.locations = {
        root: self.root,
        www: path.join(self.root, 'www'),
        platformWww: path.join(self.root, 'platform_www'),
        configXml: path.join(self.root, 'config.xml'),
        defaultConfigXml: path.join(self.root, 'cordova/defaults.xml'),
        // NOTE: Due to platformApi spec we need to return relative paths here
        cordovaJs: 'template/www/cordova.js',
        cordovaJsSrc: 'cordova-js-src'
    };
}

/**
 * Installs platform to specified directory and creates a platform project.
 *
 * @param  {String}  destinationDir  A directory, where platform should be
 *   created/installed.
 * @param  {ConfigParser} [projectConfig] A ConfigParser instance, used to get
 *   some application properties for new platform like application name, package
 *   id, etc. If not defined, this means that platform is used as standalone
 *   project and is not a part of cordova project, so platform will use some
 *   default values.
 * @param  {Object}   [options]  An options object. The most common options are:
 * @param  {String}   [options.customTemplate]  A path to custom template, that
 *   should override the default one from platform.
 * @param  {Boolean}  [options.link=false]  Flag that indicates that platform's
 *   sources will be linked to installed platform instead of copying.
 *
 * @return {Promise<PlatformApi>} Promise either fulfilled with PlatformApi
 *   instance or rejected with CordovaError.
 */
Api.createPlatform = function (destinationDir, projectConfig, options, events) {
    setupEvents(events);
    var result;

    try {
        result = require('../../bin/lib/create')
        .create(destinationDir, projectConfig, options)
        .then(function () {
            var PlatformApi = require(path.resolve(destinationDir, 'cordova/Api'));
            return new PlatformApi(PLATFORM, destinationDir, events);
        });
    }
    catch(e) {
        events.emit('error','createPlatform is not callable from the windows project API.');
        throw(e);
    }

    return result;
};

/**
 * Updates already installed platform.
 *
 * @param  {String}  destinationDir  A directory, where existing platform
 *   installed, that should be updated.
 * @param  {Object}  [options]  An options object. The most common options are:
 * @param  {String}  [options.customTemplate]  A path to custom template, that
 *   should override the default one from platform.
 * @param  {Boolean}  [options.link=false]  Flag that indicates that platform's sources
 *   will be linked to installed platform instead of copying.
 * @param {EventEmitter} [events] The emitter that will be used for logging
 *
 * @return {Promise<PlatformApi>} Promise either fulfilled with PlatformApi
 *   instance or rejected with CordovaError.
 */
Api.updatePlatform = function (destinationDir, options, events) {
    setupEvents(events);
    try {
        return require('../../bin/lib/update')
        .update(destinationDir, options)
        .then(function () {
            var PlatformApi = require(path.resolve(destinationDir, 'cordova/Api'));
            return new PlatformApi(PLATFORM, destinationDir, events);
        });
    }
    catch(e) {
        events.emit('error','updatePlatform is not callable from the windows project API.');
        throw(e);
    }
};

/**
 * Gets a CordovaPlatform object, that represents the platform structure.
 *
 * @return  {CordovaPlatform}  A structure that contains the description of
 *   platform's file structure and other properties of platform.
 */
Api.prototype.getPlatformInfo = function () {

    var result = {};
    result.locations = this.locations;
    result.root = this.root;
    result.name = this.platform;
    result.version = require('./version');
    result.projectConfig = this._config;

    return result;
};

/**
 * Updates installed platform with provided www assets and new app
 *   configuration. This method is required for CLI workflow and will be called
 *   each time before build, so the changes, made to app configuration and www
 *   code, will be applied to platform.
 *
 * @param {CordovaProject} cordovaProject A CordovaProject instance, that defines a
 *   project structure and configuration, that should be applied to platform
 *   (contains project's www location and ConfigParser instance for project's
 *   config).
 *
 * @return  {Promise}  Return a promise either fulfilled, or rejected with
 *   CordovaError instance.
 */
Api.prototype.prepare = function (cordovaProject, prepareOptions) {
    return require('./lib/prepare').prepare.call(this, cordovaProject, prepareOptions);
};

/**
 * Installs a new plugin into platform. This method only copies non-www files
 *   (sources, libs, etc.) to platform. It also doesn't resolves the
 *   dependencies of plugin. Both of handling of www files, such as assets and
 *   js-files and resolving dependencies are the responsibility of caller.
 *
 * @param  {PluginInfo}  plugin  A PluginInfo instance that represents plugin
 *   that will be installed.
 * @param  {Object}  installOptions  An options object. Possible options below:
 * @param  {Boolean}  installOptions.link: Flag that specifies that plugin
 *   sources will be symlinked to app's directory instead of copying (if
 *   possible).
 * @param  {Object}  installOptions.variables  An object that represents
 *   variables that will be used to install plugin. See more details on plugin
 *   variables in documentation:
 *   https://cordova.apache.org/docs/en/4.0.0/plugin_ref_spec.md.html
 *
 * @return  {Promise}  Return a promise either fulfilled, or rejected with
 *   CordovaError instance.
 */
Api.prototype.addPlugin = function (plugin, installOptions) {

    var self = this;

    // We need to use custom PluginInfo to trigger windows-specific processing
    // of changes in .appxmanifest files. See PluginInfo.js for details
    var pluginInfo = new PluginInfo(plugin.dir);
    var jsProject = JsprojManager.getProject(this.root);
    installOptions = installOptions || {};
    installOptions.variables = installOptions.variables || {};
    // Add PACKAGE_NAME variable into vars
    if (!installOptions.variables.PACKAGE_NAME) {
        installOptions.variables.PACKAGE_NAME = jsProject.getPackageName();
    }

    var platformJson = PlatformJson.load(this.root, this.platform);
    var pluginManager = PluginManager.get(this.platform, this.locations, jsProject);
    pluginManager.munger = new PlatformMunger(this.platform, this.locations.root, platformJson, new PluginInfoProvider());
    return pluginManager
        .addPlugin(pluginInfo, installOptions)
        .then(function () {
            // CB-11657 Add BOM to www files here because files added by plugin
            // probably don't have it. Prepare would add BOM but it might not be called
            return require('./lib/prepare').addBOMSignature(self.locations.www);
        })
        // CB-11022 return non-falsy value to indicate
        // that there is no need to run prepare after
        .then(_ => true);
};

/**
 * Removes an installed plugin from platform.
 *
 * Since method accepts PluginInfo instance as input parameter instead of plugin
 *   id, caller shoud take care of managing/storing PluginInfo instances for
 *   future uninstalls.
 *
 * @param  {PluginInfo}  plugin  A PluginInfo instance that represents plugin
 *   that will be installed.
 *
 * @return  {Promise}  Return a promise either fulfilled, or rejected with
 *   CordovaError instance.
 */
Api.prototype.removePlugin = function (plugin, uninstallOptions) {
    var self = this;

    // We need to use custom PluginInfo to trigger windows-specific processing
    // of changes in .appxmanifest files. See PluginInfo.js for details
    var pluginInfo = new PluginInfo(plugin.dir);
    var jsProject = JsprojManager.getProject(this.root);
    var platformJson = PlatformJson.load(this.root, this.platform);
    var pluginManager = PluginManager.get(this.platform, this.locations, jsProject);
    //  CB-11933 We override this field by windows specific one because windows has special logic
    //  for appxmanifest's capabilities removal (see also https://issues.apache.org/jira/browse/CB-11066)
    pluginManager.munger = new PlatformMunger(this.platform, this.locations.root, platformJson, new PluginInfoProvider());
    return pluginManager
        .removePlugin(pluginInfo, uninstallOptions)
        .then(function () {
            // CB-11657 Add BOM to cordova_plugins, since it is was
            // regenerated after plugin uninstallation and does not have BOM
            return require('./lib/prepare').addBOMToFile(path.resolve(self.locations.www, 'cordova_plugins.js'));
        })
        // CB-11022 return non-falsy value to indicate
        // that there is no need to run prepare after
        .then(_ => true);
};

/**
 * Builds an application package for current platform.
 *
 * @param  {Object}  buildOptions  A build options. This object's structure is
 *   highly depends on platform's specific. The most common options are:
 * @param  {Boolean}  buildOptions.debug  Indicates that packages should be
 *   built with debug configuration. This is set to true by default unless the
 *   'release' option is not specified.
 * @param  {Boolean}  buildOptions.release  Indicates that packages should be
 *   built with release configuration. If not set to true, debug configuration
 *   will be used.
 * @param   {Boolean}  buildOptions.device  Specifies that built app is intended
 *   to run on device
 * @param   {Boolean}  buildOptions.emulator: Specifies that built app is
 *   intended to run on emulator
 * @param   {String}  buildOptions.target  Specifies the device id that will be
 *   used to run built application.
 * @param   {Boolean}  buildOptions.nobuild  Indicates that this should be a
 *   dry-run call, so no build artifacts will be produced.
 * @param   {String[]}  buildOptions.archs  Specifies chip architectures which
 *   app packages should be built for. List of valid architectures is depends on
 *   platform.
 * @param   {String}  buildOptions.buildConfig  The path to build configuration
 *   file. The format of this file is depends on platform.
 * @param   {String[]} buildOptions.argv Raw array of command-line arguments,
 *   passed to `build` command. The purpose of this property is to pass a
 *   platform-specific arguments, and eventually let platform define own
 *   arguments processing logic.
 *
 * @return {Promise<Object[]>} A promise either fulfilled with an array of build
 *   artifacts (application packages) if package was built successfully,
 *   or rejected with CordovaError. The resultant build artifact objects is not
 *   strictly typed and may conatin arbitrary set of fields as in sample below.
 *
 *     {
 *         architecture: 'x86',
 *         buildType: 'debug',
 *         path: '/path/to/build',
 *         type: 'app'
 *     }
 *
 * The return value in most cases will contain only one item but in some cases
 *   there could be multiple items in output array, e.g. when multiple
 *   arhcitectures is specified.
 */
Api.prototype.build = function(buildOptions) {
    // TODO: Should we run check_reqs first? Android does this, but Windows appears doesn't.
    return require('./lib/build').run.call(this, buildOptions)
    .then(function (result) {
        // Wrap result into array according to PlatformApi spec
        return [result];
    });
};

/**
 * Builds an application package for current platform and runs it on
 *   specified/default device. If no 'device'/'emulator'/'target' options are
 *   specified, then tries to run app on default device if connected, otherwise
 *   runs the app on emulator.
 *
 * @param   {Object}  runOptions  An options object. The structure is the same
 *   as for build options.
 *
 * @return {Promise} A promise either fulfilled if package was built and ran
 *   successfully, or rejected with CordovaError.
 */
Api.prototype.run = function(runOptions) {
    // TODO: Should we run check_reqs first? Android does this, but Windows appears doesn't.
    return require('./lib/run').run.call(this, runOptions);
};

/**
 * Cleans out the build artifacts from platform's directory.
 *
 * @return  {Promise}  Return a promise either fulfilled, or rejected with
 *   CordovaError.
 */
Api.prototype.clean = function(cleanOpts) {
    var self = this;
    return require('./lib/build').clean.call(this, cleanOpts)
    .then(function () {
        return require('./lib/prepare').clean.call(self, cleanOpts);
    });
};

/**
 * Performs a requirements check for current platform. Each platform defines its
 *   own set of requirements, which should be resolved before platform can be
 *   built successfully.
 *
 * @return  {Promise<Requirement[]>}  Promise, resolved with set of Requirement
 *   objects for current platform.
 */
Api.prototype.requirements = function() {
    return require('./lib/check_reqs').check_all();
};

module.exports = Api;