/**
    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 fs = require('fs');
var util = require('util');
var et = require('elementtree');
var path = require('path');
var xml= require('cordova-common').xmlHelpers;

var UAP_RESTRICTED_CAPS = ['enterpriseAuthentication', 'sharedUserCertificates',
                           'documentsLibrary', 'musicLibrary', 'picturesLibrary',
                           'videosLibrary', 'removableStorage', 'internetClientClientServer',
                           'privateNetworkClientServer'];

// UAP namespace capabilities come from the XSD type ST_Capability_Uap from AppxManifestTypes.xsd
var CAPS_NEEDING_UAPNS  = ['documentsLibrary', 'picturesLibrary', 'videosLibrary',
                           'musicLibrary', 'enterpriseAuthentication', 'sharedUserCertificates',
                           'removableStorage', 'appointments', 'contacts', 'userAccountInformation',
                           'phoneCall', 'blockedChatMessages', 'objects3D'];

var KNOWN_ORIENTATIONS = {
    'default':   ['portrait', 'landscape', 'landscapeFlipped'],
    'portrait':  ['portrait'],
    'landscape': ['landscape', 'landscapeFlipped']
};

/**
 * Store to cache appxmanifest files based on file location
 * @type  {Object}
 */
var manifestCache = {};

/**
 * @constructor
 * @constructs AppxManifest
 *
 * Wraps an AppxManifest file. Shouldn't be instantiated directly.
 *   AppxManifest.get should be used instead to select proper manifest type
 *   (AppxManifest for Win 8/8.1/Phone 8.1, Win10AppxManifest for Win 10)
 *
 * @param  {string}  path    Path to appxmanifest to wrap
 * @param  {string}  prefix  A namespace prefix used to prepend some elements.
 *   Depends on manifest type.
 */
function AppxManifest(path, prefix) {
    this.path = path;
    // Append ':' to prefix if needed
    prefix = prefix || '';
    this.prefix = (prefix.indexOf(':') === prefix.length - 1) ? prefix : prefix + ':';
    this.doc = xml.parseElementtreeSync(path);
    if (this.doc.getroot().tag !== 'Package') {
        // Some basic validation
        throw new Error(path + ' has incorrect root node name (expected "Package")');
    }

    // Indicates that this manifest is for phone application (either WinPhone 8.1 or Universal Windows 10)
    this.hasPhoneIdentity = this.prefix === 'uap:' || this.prefix === 'm3:';
}

//  Static read-only property to get capabilities which need to be prefixed with uap
Object.defineProperty(AppxManifest, 'CapsNeedUapPrefix', {
    writable: false,
    configurable: false,
    value: CAPS_NEEDING_UAPNS
});

/**
 * @static
 * @constructs AppxManifest|Win10AppxManifest
 *
 * Instantiates a new AppxManifest/Win10AppxManifest class. Chooses which
 *   constructor to use based on xmlns attributes of Package node
 *
 * @param   {String}  fileName  File to create manifest for
 * @param   {Boolean} [ignoreCache=false]  Specifies, whether manifest cache will be
 *   used to return resultant object
 *
 * @return  {AppxManifest|Win10AppxManifest}  Manifest instance
 */
AppxManifest.get = function (fileName, ignoreCache) {

    if (!ignoreCache && manifestCache[fileName]) {
        return manifestCache[fileName];
    }

    var root = xml.parseElementtreeSync(fileName).getroot();
    var prefixes = Object.keys(root.attrib)
    .reduce(function (result, attrib) {
        if (attrib.indexOf('xmlns') === 0 && attrib !== 'xmlns:mp') {
            result.push(attrib.replace('xmlns', '').replace(':', ''));
        }

        return result;
    }, []).sort();

    var prefix = prefixes[prefixes.length - 1];
    var Manifest = prefix === 'uap' ? Win10AppxManifest : AppxManifest;
    var result = new Manifest(fileName, prefix);

    if (!ignoreCache) {
        manifestCache[fileName] = result;
    }

    return result;
};

/**
 * Removes manifests from cache to prevent using stale entries
 *
 * @param {String|String[]} [cacheKeys] The keys to delete from cache. If not
 *   specified, the whole cache will be purged
 */
AppxManifest.purgeCache = function (cacheKeys) {
    if (!cacheKeys) {
        // if no arguments passed, remove all entries
        manifestCache = {};
        return;
    }

    var keys = Array.isArray(cacheKeys) ? cacheKeys : [cacheKeys];
    keys.forEach(function (key) {
        delete manifestCache[key];
    });
};

AppxManifest.prototype.getPhoneIdentity = function () {
    var phoneIdentity = this.doc.getroot().find('./mp:PhoneIdentity');
    if (!phoneIdentity)
        throw new Error('Failed to find PhoneIdentity element in appxmanifest at ' + this.path);

    return {
        getPhoneProductId: function () {
            return phoneIdentity.attrib.PhoneProductId;
        },
        setPhoneProductId: function (id) {
            if (!id) throw new Error('Argument for "setPhoneProductId" must be defined in appxmanifest at ' + this.path);
            phoneIdentity.attrib.PhoneProductId = id;
            return this;
        }
    };
};

AppxManifest.prototype.getIdentity = function () {
    var identity = this.doc.getroot().find('./Identity');
    if (!identity)
        throw new Error('Failed to find "Identity" node. The appxmanifest at ' + this.path + ' is invalid');

    return {
        getName: function () {
            return identity.attrib.Name;
        },
        setName: function (name) {
            if (!name) throw new TypeError('Identity.Name attribute must be non-empty in appxmanifest at ' + this.path);
            identity.attrib.Name = name;
            return this;
        },
        getPublisher: function () {
            return identity.attrib.Publisher;
        },
        setPublisher: function (publisherId) {
            if (!publisherId) throw new TypeError('Identity.Publisher attribute must be non-empty in appxmanifest at ' + this.path);
            identity.attrib.Publisher = publisherId;
            return this;
        },
        getVersion: function () {
            return identity.attrib.Version;
        },
        setVersion: function (version) {
            if (!version) throw new TypeError('Identity.Version attribute must be non-empty in appxmanifest at ' + this.path );

            // Adjust version number as per CB-5337 Windows8 build fails due to invalid app version
            if(version && version.match(/\.\d/g)) {
                var numVersionComponents = version.match(/\.\d/g).length + 1;
                while (numVersionComponents++ < 4) {
                    version += '.0';
                }
            }

            identity.attrib.Version = version;
            return this;
        }
    };
};

AppxManifest.prototype.getProperties = function () {
    var properties = this.doc.getroot().find('./Properties');

    if (!properties)
        throw new Error('Failed to find "Properties" node. The appxmanifest at ' + this.path + ' is invalid');

    return {
        getDisplayName: function () {
            var displayName = properties.find('./DisplayName');
            return displayName && displayName.text;
        },
        setDisplayName: function (name) {
            if (!name) throw new TypeError('Properties.DisplayName elements must be non-empty in appxmanifest at ' + this.path);
            var displayName = properties.find('./DisplayName');

            if (!displayName) {
                displayName = new et.Element('DisplayName');
                properties.append(displayName);
            }

            displayName.text = name;

            return this;
        },
        getPublisherDisplayName: function () {
            var publisher = properties.find('./PublisherDisplayName');
            return publisher && publisher.text;
        },
        setPublisherDisplayName: function (name) {
            if (!name) throw new TypeError('Properties.PublisherDisplayName elements must be non-empty in appxmanifest at ' + this.path);
            var publisher = properties.find('./PublisherDisplayName');

            if (!publisher) {
                publisher = new et.Element('PublisherDisplayName');
                properties.append(publisher);
            }

            publisher.text = name;

            return this;
        },
        getDescription: function () {
            var description = properties.find('./Description');
            return description && description.text;
        },
        setDescription: function (text) {

            var description = properties.find('./Description');

            if (!text || text.length === 0) {
                if (description) properties.remove(description);
                return this;
            }

            if (!description) {
                description = new et.Element('Description');
                properties.append(description);
            }

            description.text = processDescription(text);

            return this;
        },
    };
};

AppxManifest.prototype.getApplication = function () {
    var application = this.doc.getroot().find('./Applications/Application');
    if (!application)
        throw new Error('Failed to find "Application" element. The appxmanifest at ' + this.path + ' is invalid');

    var self = this;

    return {
        _node: application,
        getVisualElements: function () {
            return self.getVisualElements();
        },
        getId: function () {
            return application.attrib.Id;
        },
        setId: function (id) {
            if (!id) throw new TypeError('Application.Id attribute must be defined in appxmanifest at ' + this.path);
            // 64 symbols restriction goes from manifest schema definition
            // http://msdn.microsoft.com/en-us/library/windows/apps/br211415.aspx
            var appId = id.length <= 64 ? id : id.substr(0, 64);
            application.attrib.Id = appId;
            return this;
        },
        getStartPage: function () {
            return application.attrib.StartPage;
        },
        setStartPage: function (page) {
            if (!page) page = 'www/index.html'; // Default valur is always index.html
            application.attrib.StartPage = page;
            return this;
        },
        getAccessRules: function () {
            return application
                .findall('./ApplicationContentUriRules/Rule')
                .map(function (rule) {
                    return rule.attrib.Match;
                });
        },
        setAccessRules: function (rules) {
            var appUriRules = application.find('ApplicationContentUriRules');
            if (appUriRules) {
                application.remove(appUriRules);
            }

            // No rules defined
            if (!rules || rules.length === 0) {
                return;
            }

            appUriRules = new et.Element('ApplicationContentUriRules');
            application.append(appUriRules);

            rules.forEach(function(rule) {
                appUriRules.append(new et.Element('Rule', {Match: rule, Type: 'include'}));
            });

            return this;
        }
    };
};

AppxManifest.prototype.getVisualElements = function () {
    var self = this;
    var visualElements = this.doc.getroot().find('./Applications/Application/' +
        this.prefix  + 'VisualElements');

    if (!visualElements)
        throw new Error('Failed to find "VisualElements" node. The appxmanifest at ' + this.path + ' is invalid');

    return {
        _node: visualElements,
        getDisplayName: function () {
            return visualElements.attrib.DisplayName;
        },
        setDisplayName: function (name) {
            if (!name) throw new TypeError('VisualElements.DisplayName attribute must be defined in appxmanifest at ' + this.path);
            visualElements.attrib.DisplayName = name;
            return this;
        },
        getOrientation: function () {
            return visualElements.findall(self.prefix + 'Rotation')
                .map(function (element) {
                    return element.attrib.Preference;
                });
        },
        setOrientation: function (orientation) {
            if (!orientation || orientation === ''){
                orientation = 'default';
            }

            var rotationPreferenceRootName = self.prefix + 'InitialRotationPreference';
            var rotationPreferenceRoot = visualElements.find('./' + rotationPreferenceRootName);

            if (!orientation && rotationPreferenceRoot) {
                // Remove InitialRotationPreference root element to revert to defaults
                visualElements.remove(rotationPreferenceRoot);
                return this;
            }

            if(!rotationPreferenceRoot) {
                rotationPreferenceRoot = new et.Element(rotationPreferenceRootName);
                visualElements.append(rotationPreferenceRoot);
            }

            rotationPreferenceRoot.clear();

            var orientations = KNOWN_ORIENTATIONS[orientation] || orientation.split(',');
            orientations.forEach(function(orientation) {
                var el = new et.Element(self.prefix + 'Rotation', {Preference: orientation} );
                rotationPreferenceRoot.append(el);
            });

            return this;
        },
        getBackgroundColor: function () {
            return visualElements.attrib.BackgroundColor;
        },
        setBackgroundColor: function (color) {
            if (!color)
                throw new TypeError('VisualElements.BackgroundColor attribute must be defined in appxmanifest at ' + this.path);

            visualElements.attrib.BackgroundColor = refineColor(color);
            return this;
        },
        trySetBackgroundColor: function (color) {
            try {
                return this.setBackgroundColor(color);
            } catch (e) { return this; }
        },
        getForegroundText: function () {
            return visualElements.attrib.ForegroundText;
        },
        setForegroundText: function (color) {
            // If color is not set, fall back to 'light' by default
            visualElements.attrib.ForegroundText = color || 'light';

            return this;
        },
        getSplashBackgroundColor: function () {
            var splashNode = visualElements.find('./' + self.prefix + 'SplashScreen');
            return splashNode && splashNode.attrib.BackgroundColor;
        },
        setSplashBackgroundColor: function (color) {
            var splashNode = visualElements.find('./' + self.prefix + 'SplashScreen');
            if (splashNode) {
                if (color) {
                    splashNode.attrib.BackgroundColor = refineColor(color);
                } else {
                    delete splashNode.attrib.BackgroundColor;
                }
            }
            return this;
        },
        getSplashScreenExtension: function (extension) {
            var splashNode = visualElements.find('./' + self.prefix + 'SplashScreen');
            return splashNode && splashNode.attrib.Image && path.extname(splashNode.attrib.Image);
        },
        setSplashScreenExtension: function (extension) {
            var splashNode = visualElements.find('./' + self.prefix + 'SplashScreen');
            if (splashNode) {
                var oldPath = splashNode.attrib.Image;
                splashNode.attrib.Image = path.dirname(oldPath) + '\\' + path.basename(oldPath, path.extname(oldPath)) + extension;
            }
            return this;
        },
        getToastCapable: function () {
            return visualElements.attrib.ToastCapable;
        },
        setToastCapable: function (isToastCapable) {
            if (isToastCapable === true || isToastCapable.toString().toLowerCase() === 'true') {
                visualElements.attrib.ToastCapable = 'true';
            } else {
                delete visualElements.attrib.ToastCapable;
            }

            return this;
        },
        getDescription: function () {
            return visualElements.attrib.Description;
        },
        setDescription: function (description) {
            if (!description || description.length === 0)
                throw new TypeError('VisualElements.Description attribute must be defined and non-empty in appxmanifest at ' + this.path);

            visualElements.attrib.Description = processDescription(description);
            return this;
        },
    };
};

AppxManifest.prototype.getCapabilities = function () {
    var capabilities = this.doc.find('./Capabilities');
    if (!capabilities) return [];

    return capabilities.getchildren()
    .map(function (element) {
        return { type: element.tag, name: element.attrib.Name };
    });
};

function isCSSColorName(color) {
    return color.indexOf('0x') === -1 && color.indexOf('#') === -1;
}

function refineColor(color) {
    if (isCSSColorName(color)) {
        return color;
    }

    // return three-byte hexadecimal number preceded by "#" (required for Windows)
    color = color.replace('0x', '').replace('#', '');
    if (color.length == 3) {
        color = color[0] + color[0] + color[1] + color[1] + color[2] + color[2];
    }
    // alpha is not supported, so we remove it
    if (color.length == 8) { // AArrggbb
        color = color.slice(2);
    }
    return '#' + color;
}

function processDescription(text) {
    var result = text;

    // Description value limitations: https://msdn.microsoft.com/en-us/library/windows/apps/br211429.aspx
    // value should be no longer than 2048 characters
    if (text.length > 2048) {
        result = text.substr(0, 2048);
    }

    // value should not contain newlines and tabs
    return result.replace(/(\n|\r)/g, ' ').replace(/\t/g, '    ');
}

// Shortcut for getIdentity.setName
AppxManifest.prototype.setPackageName = function (name) {
    this.getIdentity().setName(name);
    return this;
};

// Shortcut for multiple inner methods calls
AppxManifest.prototype.setAppName = function (name) {
    this.getProperties().setDisplayName(name);
    this.getVisualElements().setDisplayName(name);

    return this;
};

/**
 * Writes manifest to disk syncronously. If filename is specified, then manifest
 *   will be written to that file
 *
 * @param   {String}  [destPath]  File to write manifest to. If omitted,
 *   manifest will be written to file it has been read from.
 */
AppxManifest.prototype.write = function(destPath) {
    // sort Capability elements as per CB-5350 Windows8 build fails due to invalid 'Capabilities' definition
    sortCapabilities(this.doc);
    fs.writeFileSync(destPath || this.path, this.doc.write({indent: 4}), 'utf-8');
};

/**
 * Sorts 'capabilities' elements in manifest in ascending order
 * @param   {Elementtree.Document}  manifest  An XML document that represents
 *   appxmanifest
 */
function sortCapabilities(manifest) {

    // removes namespace prefix (m3:Capability -> Capability)
    // this is required since elementtree returns qualified name with namespace
    function extractLocalName(tag) {
        return tag.split(':').pop(); // takes last part of string after ':'
    }

    var capabilitiesRoot = manifest.find('.//Capabilities'),
        capabilities = capabilitiesRoot.getchildren() || [];
    // to sort elements we remove them and then add again in the appropriate order
    capabilities.forEach(function(elem) { // no .clear() method
        capabilitiesRoot.remove(elem);
        // CB-7601 we need local name w/o namespace prefix to sort capabilities correctly
        elem.localName = extractLocalName(elem.tag);
    });
    capabilities.sort(function(a, b) {
        return (a.localName > b.localName) ? 1: -1;
    });
    capabilities.forEach(function(elem) {
        capabilitiesRoot.append(elem);
    });
}

function Win10AppxManifest(path) {
    AppxManifest.call(this, path, /*prefix=*/'uap');
}

util.inherits(Win10AppxManifest, AppxManifest);

Win10AppxManifest.prototype.getApplication = function () {
    // Call overridden method
    var result = AppxManifest.prototype.getApplication.call(this);
    var application = result._node;

    result.getAccessRules = function () {
        return application
            .findall('./uap:ApplicationContentUriRules/uap:Rule')
            .map(function (rule) {
                return rule.attrib.Match;
            });
    };

    result.setAccessRules = function (rules) {
        var appUriRules = application.find('./uap:ApplicationContentUriRules');
        if (appUriRules) {
            application.remove(appUriRules);
        }

        // No rules defined
        if (!rules || rules.length === 0) {
            return;
        }

        appUriRules = new et.Element('uap:ApplicationContentUriRules');
        application.append(appUriRules);

        rules.forEach(function(rule) {
            appUriRules.append(new et.Element('uap:Rule', { Match: rule, Type: 'include', WindowsRuntimeAccess: 'all' }));
        });

        return this;
    };

    return result;
};

Win10AppxManifest.prototype.getVisualElements = function () {
    // Call base method and extend its results
    var result = AppxManifest.prototype.getVisualElements.call(this);
    var defaultTitle = result._node.find('./uap:DefaultTile');

    result.getDefaultTitle = function () {
        return {
            getShortName: function () {
                return defaultTitle.attrib.ShortName;
            },
            setShortName: function (name) {
                if (!name) throw new TypeError('Argument for "setDisplayName" must be defined in appxmanifest at ' + this.path);
                defaultTitle.attrib.ShortName = name;
                return this;
            }
        };
    };

    // ToastCapable attribute was removed in Windows 10.
    // See https://msdn.microsoft.com/ru-ru/library/windows/apps/dn423310.aspx
    result.getToastCapable = function () {};
    result.setToastCapable = function () { return this; };

    // ForegroundText was removed in Windows 10 as well
    result.getForegroundText = function () {};
    result.setForegroundText = function () { return this; };

    return result;
};

// Shortcut for multiple inner methods calls
Win10AppxManifest.prototype.setAppName = function (name) {
    // Call base method
    AppxManifest.prototype.setAppName.call(this, name);
    this.getVisualElements().getDefaultTitle().setShortName(name);

    return this;
};

/**
 * Checks for capabilities which are Restricted in Windows 10 UAP.
 * @return {string[]|false} An array of restricted capability names, or false.
 */
Win10AppxManifest.prototype.getRestrictedCapabilities = function () {
    var restrictedCapabilities = this.getCapabilities()
    .filter(function (capability) {
        return UAP_RESTRICTED_CAPS.indexOf(capability.name) >= 0;
    });

    return restrictedCapabilities.length === 0 ? false : restrictedCapabilities;
};

/**
 * Sets up a Dependencies section for appxmanifest. If no arguments provided,
 *   deletes Dependencies section.
 *
 * @param  {Object[]}  dependencies  Array of arbitrary object, which fields
 *   will be used to set each dependency attributes.
 *
 * @returns {Win10AppxManifest}  self instance
 */
Win10AppxManifest.prototype.setDependencies = function (dependencies) {
    var dependenciesElement = this.doc.find('./Dependencies');

    if ((!dependencies || dependencies.length === 0) && dependenciesElement) {
        this.doc.remove(dependenciesElement);
        return this;
    }

    if (!dependenciesElement) {
        dependenciesElement = new et.Element('Dependencies');
        this.doc.append(dependenciesElement);
    }

    if (dependenciesElement.len() > 0) {
        dependenciesElement.clear();
    }

    dependencies.forEach(function (uapVersionInfo) {
        dependenciesElement.append(new et.Element('TargetDeviceFamily', uapVersionInfo));
    });
};

/**
 * Writes manifest to disk syncronously. If filename is specified, then manifest
 *   will be written to that file
 *
 * @param   {String}  [destPath]  File to write manifest to. If omitted,
 *   manifest will be written to file it has been read from.
 */
Win10AppxManifest.prototype.write = function(destPath) {
    fs.writeFileSync(destPath || this.path, this.writeToString(), 'utf-8');
};

Win10AppxManifest.prototype.writeToString = function() {
    ensureUapPrefixedCapabilities(this.doc.find('.//Capabilities'));
    ensureUniqueCapabilities(this.doc.find('.//Capabilities'));
    // sort Capability elements as per CB-5350 Windows8 build fails due to invalid 'Capabilities' definition
    sortCapabilities(this.doc);
    return this.doc.write({indent: 4});
};

/**
 * Checks for capabilities which require the uap: prefix in Windows 10.
 * @param capabilities {ElementTree.Element} The appx manifest element for <capabilities>
 */
function ensureUapPrefixedCapabilities(capabilities) {
    capabilities.getchildren()
    .forEach(function(el) {
        if (CAPS_NEEDING_UAPNS.indexOf(el.attrib.Name) > -1 && el.tag.indexOf('uap:') !== 0) {
            el.tag = 'uap:' + el.tag;
        }
    });
}

/**
 * Cleans up duplicate capability declarations that were generated during the prepare process
 * @param capabilities {ElementTree.Element} The appx manifest element for <capabilities>
 */
function ensureUniqueCapabilities(capabilities) {
    var uniqueCapabilities = [];
    capabilities.getchildren()
    .forEach(function(el) {
        var name = el.attrib.Name;
        if (uniqueCapabilities.indexOf(name) !== -1) {
            capabilities.remove(el);
        } else {
            uniqueCapabilities.push(name);
        }
    });
}

module.exports = AppxManifest;


/**
    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 PluginInfoProvider = require('cordova-common').PluginInfoProvider;
var fs = require('fs-extra');
var events = require('cordova-common').events;
var CordovaError = require('cordova-common').CordovaError;
var fetch = require('cordova-fetch');
var superspawn = require('cordova-common').superspawn;

module.exports.saveToConfigXmlOn = saveToConfigXmlOn;
module.exports.getInstalledPlugins = getInstalledPlugins;
module.exports.mergeVariables = mergeVariables;
module.exports.info = info;

function getInstalledPlugins (projectRoot) {
    var pluginsDir = path.join(projectRoot, 'plugins');
    // TODO: This should list based off of platform.json, not directories within plugins/
    var pluginInfoProvider = new PluginInfoProvider();
    return pluginInfoProvider.getAllWithinSearchPath(pluginsDir);
}

function saveToConfigXmlOn (config_json, options) {
    options = options || {};
    var autosave = config_json.auto_save_plugins || false;
    return autosave || options.save;
}

/*
 * Merges cli and config.xml variables.
 *
 * @param   {object}    pluginInfo
 * @param   {object}    config.xml
 * @param   {object}    options
 *
 * @return  {object}    object containing the new merged variables
 */

function mergeVariables (pluginInfo, cfg, opts) {
    // Validate top-level required variables
    var pluginVariables = pluginInfo.getPreferences();
    opts.cli_variables = opts.cli_variables || {};
    var pluginEntry = cfg.getPlugin(pluginInfo.id);
    // Get variables from config.xml
    var configVariables = pluginEntry ? pluginEntry.variables : {};
    // Add config variable if it's missing in cli_variables
    Object.keys(configVariables).forEach(function (variable) {
        opts.cli_variables[variable] = opts.cli_variables[variable] || configVariables[variable];
    });
    var missingVariables = Object.keys(pluginVariables)
        .filter(function (variableName) {
            // discard variables with default value
            return !(pluginVariables[variableName] || opts.cli_variables[variableName]);
        });

    if (missingVariables.length) {
        events.emit('verbose', 'Removing ' + pluginInfo.dir + ' because mandatory plugin variables were missing.');
        fs.removeSync(pluginInfo.dir);
        var msg = 'Variable(s) missing (use: --variable ' + missingVariables.join('=value --variable ') + '=value).';
        throw new CordovaError(msg);
    }
    return opts.cli_variables;
}

function info (plugin) {
    var viewArgs = ['view'];
    plugin = plugin.shift();
    viewArgs.push(plugin);
    viewArgs.push('--json');
    // check if npm is installed
    return fetch.isNpmInstalled()
        .then(function () {
            return superspawn.spawn('npm', viewArgs)
                .then(function (info) {
                    var pluginInfo = JSON.parse(info);
                    return pluginInfo;
                });
        });
}


/*
 *
 * Copyright 2013 Anis Kadri
 *
 * Licensed 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.
 *
*/

/* jshint unused: vars */

var fs = require('fs');
var path = require('path');
var shell = require('shelljs');
var events = require('cordova-common').events;
var CordovaError = require('cordova-common').CordovaError;

var handlers = {
    'source-file':{
        install:function(obj, plugin, project, options) {
            if (!obj.src) throw new CordovaError(generateAttributeError('src', 'source-file', plugin.id));
            if (!obj.targetDir) throw new CordovaError(generateAttributeError('target-dir', 'source-file', plugin.id));

            var dest = path.join(obj.targetDir, path.basename(obj.src));

            if(options && options.android_studio === true) {
              dest = path.join('app/src/main/java', obj.targetDir.substring(4), path.basename(obj.src));
            }

            if (options && options.force) {
                copyFile(plugin.dir, obj.src, project.projectDir, dest, !!(options && options.link));
            } else {
                copyNewFile(plugin.dir, obj.src, project.projectDir, dest, !!(options && options.link));
            }
        },
        uninstall:function(obj, plugin, project, options) {
            var dest = path.join(obj.targetDir, path.basename(obj.src));

            if(options && options.android_studio === true) {
              dest = path.join('app/src/main/java', obj.targetDir.substring(4), path.basename(obj.src));
            }

            deleteJava(project.projectDir, dest);
        }
    },
    'lib-file':{
        install:function(obj, plugin, project, options) {
            var dest = path.join('libs', path.basename(obj.src));
            if(options && options.android_studio === true) {
              dest = path.join('app/libs', path.basename(obj.src));
            }
            copyFile(plugin.dir, obj.src, project.projectDir, dest, !!(options && options.link));
        },
        uninstall:function(obj, plugin, project, options) {
            var dest = path.join('libs', path.basename(obj.src));
            if(options && options.android_studio === true) {
              dest = path.join('app/libs', path.basename(obj.src));
            }
            removeFile(project.projectDir, dest);
        }
    },
    'resource-file':{
        install:function(obj, plugin, project, options) {
            copyFile(plugin.dir, obj.src, project.projectDir, path.normalize(obj.target), !!(options && options.link));
        },
        uninstall:function(obj, plugin, project, options) {
            removeFile(project.projectDir, path.normalize(obj.target));
        }
    },
    'framework': {
        install:function(obj, plugin, project, options) {
            var src = obj.src;
            if (!src) throw new CordovaError(generateAttributeError('src', 'framework', plugin.id));

            events.emit('verbose', 'Installing Android library: ' + src);
            var parentDir = obj.parent ? path.resolve(project.projectDir, obj.parent) : project.projectDir;
            var subDir;

            if (obj.custom) {
                var subRelativeDir = project.getCustomSubprojectRelativeDir(plugin.id, src);
                copyNewFile(plugin.dir, src, project.projectDir, subRelativeDir, !!(options && options.link));
                subDir = path.resolve(project.projectDir, subRelativeDir);
            } else {
                obj.type = 'sys';
                subDir = src;
            }

            if (obj.type == 'gradleReference') {
                project.addGradleReference(parentDir, subDir);
            } else if (obj.type == 'sys') {
                project.addSystemLibrary(parentDir, subDir);
            } else {
                project.addSubProject(parentDir, subDir);
            }
        },
        uninstall:function(obj, plugin, project, options) {
            var src = obj.src;
            if (!src) throw new CordovaError(generateAttributeError('src', 'framework', plugin.id));

            events.emit('verbose', 'Uninstalling Android library: ' + src);
            var parentDir = obj.parent ? path.resolve(project.projectDir, obj.parent) : project.projectDir;
            var subDir;

            if (obj.custom) {
                var subRelativeDir = project.getCustomSubprojectRelativeDir(plugin.id, src);
                removeFile(project.projectDir, subRelativeDir);
                subDir = path.resolve(project.projectDir, subRelativeDir);
                // If it's the last framework in the plugin, remove the parent directory.
                var parDir = path.dirname(subDir);
                if (fs.existsSync(parDir) && fs.readdirSync(parDir).length === 0) {
                    fs.rmdirSync(parDir);
                }
            } else {
                obj.type = 'sys';
                subDir = src;
            }

            if (obj.type == 'gradleReference') {
                project.removeGradleReference(parentDir, subDir);
            } else if (obj.type == 'sys') {
                project.removeSystemLibrary(parentDir, subDir);
            } else {
                project.removeSubProject(parentDir, subDir);
            }
        }
    },
    asset:{
        install:function(obj, plugin, project, options) {
            if (!obj.src) {
                throw new CordovaError(generateAttributeError('src', 'asset', plugin.id));
            }
            if (!obj.target) {
                throw new CordovaError(generateAttributeError('target', 'asset', plugin.id));
            }

            copyFile(plugin.dir, obj.src, project.www, obj.target);
            if (options && options.usePlatformWww) {
                // CB-11022 copy file to both directories if usePlatformWww is specified
                copyFile(plugin.dir, obj.src, project.platformWww, obj.target);
            }
        },
        uninstall:function(obj, plugin, project, options) {
            var target = obj.target || obj.src;

            if (!target) throw new CordovaError(generateAttributeError('target', 'asset', plugin.id));

            removeFileF(path.resolve(project.www, target));
            removeFileF(path.resolve(project.www, 'plugins', plugin.id));
            if (options && options.usePlatformWww) {
                // CB-11022 remove file from both directories if usePlatformWww is specified
                removeFileF(path.resolve(project.platformWww, target));
                removeFileF(path.resolve(project.platformWww, 'plugins', plugin.id));
            }
        }
    },
    'js-module': {
        install: function (obj, plugin, project, options) {
            // Copy the plugin's files into the www directory.
            var moduleSource = path.resolve(plugin.dir, obj.src);
            var moduleName = plugin.id + '.' + (obj.name || path.basename(obj.src, path.extname (obj.src)));

            // Read in the file, prepend the cordova.define, and write it back out.
            var scriptContent = fs.readFileSync(moduleSource, 'utf-8').replace(/^\ufeff/, ''); // Window BOM
            if (moduleSource.match(/.*\.json$/)) {
                scriptContent = 'module.exports = ' + scriptContent;
            }
            scriptContent = 'cordova.define("' + moduleName + '", function(require, exports, module) {\n' + scriptContent + '\n});\n';

            var wwwDest = path.resolve(project.www, 'plugins', plugin.id, obj.src);
            shell.mkdir('-p', path.dirname(wwwDest));
            fs.writeFileSync(wwwDest, scriptContent, 'utf-8');

            if (options && options.usePlatformWww) {
                // CB-11022 copy file to both directories if usePlatformWww is specified
                var platformWwwDest = path.resolve(project.platformWww, 'plugins', plugin.id, obj.src);
                shell.mkdir('-p', path.dirname(platformWwwDest));
                fs.writeFileSync(platformWwwDest, scriptContent, 'utf-8');
            }
        },
        uninstall: function (obj, plugin, project, options) {
            var pluginRelativePath = path.join('plugins', plugin.id, obj.src);
            removeFileAndParents(project.www, pluginRelativePath);
            if (options && options.usePlatformWww) {
                // CB-11022 remove file from both directories if usePlatformWww is specified
                removeFileAndParents(project.platformWww, pluginRelativePath);
            }
        }
    }
};

module.exports.getInstaller = function (type) {
    if (handlers[type] && handlers[type].install) {
        return handlers[type].install;
    }

    events.emit('verbose', '<' + type + '> is not supported for android plugins');
};

module.exports.getUninstaller = function(type) {
    if (handlers[type] && handlers[type].uninstall) {
        return handlers[type].uninstall;
    }

    events.emit('verbose', '<' + type + '> is not supported for android plugins');
};

function copyFile (plugin_dir, src, project_dir, dest, link) {
    src = path.resolve(plugin_dir, src);
    if (!fs.existsSync(src)) throw new CordovaError('"' + src + '" not found!');

    // check that src path is inside plugin directory
    var real_path = fs.realpathSync(src);
    var real_plugin_path = fs.realpathSync(plugin_dir);
    if (real_path.indexOf(real_plugin_path) !== 0)
        throw new CordovaError('File "' + src + '" is located outside the plugin directory "' + plugin_dir + '"');

    dest = path.resolve(project_dir, dest);

    // check that dest path is located in project directory
    if (dest.indexOf(project_dir) !== 0)
        throw new CordovaError('Destination "' + dest + '" for source file "' + src + '" is located outside the project');

    shell.mkdir('-p', path.dirname(dest));
    if (link) {
        symlinkFileOrDirTree(src, dest);
    } else if (fs.statSync(src).isDirectory()) {
        // XXX shelljs decides to create a directory when -R|-r is used which sucks. http://goo.gl/nbsjq
        shell.cp('-Rf', src+'/*', dest);
    } else {
        shell.cp('-f', src, dest);
    }
}

// Same as copy file but throws error if target exists
function copyNewFile (plugin_dir, src, project_dir, dest, link) {
    var target_path = path.resolve(project_dir, dest);
    if (fs.existsSync(target_path))
        throw new CordovaError('"' + target_path + '" already exists!');

    copyFile(plugin_dir, src, project_dir, dest, !!link);
}

function symlinkFileOrDirTree(src, dest) {
    if (fs.existsSync(dest)) {
        shell.rm('-Rf', dest);
    }

    if (fs.statSync(src).isDirectory()) {
        shell.mkdir('-p', dest);
        fs.readdirSync(src).forEach(function(entry) {
            symlinkFileOrDirTree(path.join(src, entry), path.join(dest, entry));
        });
    }
    else {
        fs.symlinkSync(path.relative(fs.realpathSync(path.dirname(dest)), src), dest);
    }
}

// checks if file exists and then deletes. Error if doesn't exist
function removeFile (project_dir, src) {
    var file = path.resolve(project_dir, src);
    shell.rm('-Rf', file);
}

// deletes file/directory without checking
function removeFileF (file) {
    shell.rm('-Rf', file);
}

// Sometimes we want to remove some java, and prune any unnecessary empty directories
function deleteJava (project_dir, destFile) {
    removeFileAndParents(project_dir, destFile, 'src');
}

function removeFileAndParents (baseDir, destFile, stopper) {
    stopper = stopper || '.';
    var file = path.resolve(baseDir, destFile);
    if (!fs.existsSync(file)) return;

    removeFileF(file);

    // check if directory is empty
    var curDir = path.dirname(file);

    while(curDir !== path.resolve(baseDir, stopper)) {
        if(fs.existsSync(curDir) && fs.readdirSync(curDir).length === 0) {
            fs.rmdirSync(curDir);
            curDir = path.resolve(curDir, '..');
        } else {
            // directory not empty...do nothing
            break;
        }
    }
}

function generateAttributeError(attribute, element, id) {
    return 'Required attribute "' + attribute + '" not specified in <' + element + '> element from plugin: ' + id;
}


/**
    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.
*/

// npm packages follow the pattern of (@scope/)?package(@spec)? where scope and tag are optional
var NPM_SPEC_REGEX = /^(@[^/]+\/)?([^@/]+)(?:@(.+))?$/;

module.exports.parse = parse;

/**
 * Represents a parsed specification for a plugin
 * @class
 * @param {String} raw      The raw specification (i.e. provided by the user)
 * @param {String} scope    The scope of the package if this is an npm package
 * @param {String} id       The id of the package if this is an npm package
 * @param {String} version  The version specified for the package if this is an npm package
 */
function PluginSpec (raw, scope, id, version) {
    /** @member {String|null} The npm scope of the plugin spec or null if it does not have one */
    this.scope = scope || null;

    /** @member {String|null} The id of the plugin or the raw plugin spec if it is not an npm package */
    this.id = id || raw;

    /** @member {String|null} The specified version of the plugin or null if no version was specified */
    this.version = version || null;

    /** @member {String|null} The npm package of the plugin (with scope) or null if this is not a spec for an npm package */
    this.package = (scope ? scope + id : id) || null;
}

/**
 * Tries to parse the given string as an npm-style package specification of
 * the form (@scope/)?package(@version)? and return the various parts.
 *
 * @param {String} raw  The string to be parsed
 * @return {PluginSpec}  The parsed plugin spec
 */
function parse (raw) {
    var split = NPM_SPEC_REGEX.exec(raw);
    if (split) {
        return new PluginSpec(raw, split[1], split[2], split[3]);
    }

    return new PluginSpec(raw);
}


/*
 *
 * Copyright 2013 Jesse MacFadyen
 *
 * Licensed 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.
 *
 */

/* jshint sub:true */

var fs   = require('fs');
var path = require('path');
var shell = require('shelljs');
var events = require('cordova-common').events;
var CordovaError = require('cordova-common').CordovaError;

// returns relative file path for a file in the plugin's folder that can be referenced
// from a project file.
function getPluginFilePath(plugin, pluginFile, targetDir) {
    var src = path.resolve(plugin.dir, pluginFile);
    return '$(ProjectDir)' + path.relative(targetDir, src);
}

var handlers = {
    'source-file': {
        install:function(obj, plugin, project, options) {
            var dest = path.join('plugins', plugin.id, obj.targetDir || '', path.basename(obj.src));
            if (options && options.force) {
                copyFile(plugin.dir, obj.src, project.root, dest);
            } else {
                copyNewFile(plugin.dir, obj.src, project.root, dest);
            }
            // add reference to this file to jsproj.
            project.addSourceFile(dest);
        },
        uninstall:function(obj, plugin, project, options) {
            var dest = path.join('plugins', plugin.id, obj.targetDir || '', path.basename(obj.src));
            removeFile(project.root, dest);
            // remove reference to this file from csproj.
            project.removeSourceFile(dest);
        }
    },
    'resource-file':{
        install:function(obj, plugin, project, options) {
            if (obj.reference) {
                // do not copy, but reference the file in the plugin folder. This allows to
                // have multiple source files map to the same target and select the appropriate
                // one based on the current build settings, e.g. architecture.
                // also, we don't check for existence. This allows to insert build variables
                // into the source file name, e.g.
                // <resource-file src="$(Platform)/My.dll" target="My.dll" />
                var relativeSrcPath = getPluginFilePath(plugin, obj.src, project.projectFolder);
                project.addResourceFileToProject(relativeSrcPath, obj.target, getTargetConditions(obj));
            } else {
                // if target already exists, emit warning to consider using a reference instead of copying
                if (fs.existsSync(path.resolve(project.root, obj.target))) {
                    events.emit('warn', '<resource-file> with target ' + obj.target + ' already exists and will be overwritten ' +
                    'by a <resource-file> with the same target. Consider using the attribute reference="true" in the ' +
                    '<resource-file> tag to avoid overwriting files with the same target. Using reference will not copy files ' +
                    'to the destination, instead will create a reference to the source path.');
                }
                // as per specification resource-file target is specified relative to platform root
                copyFile(plugin.dir, obj.src, project.root, obj.target);
                project.addResourceFileToProject(obj.target, obj.target, getTargetConditions(obj));
            }
        },
        uninstall:function(obj, plugin, project, options) {
            if (obj.reference) {
                var relativeSrcPath = getPluginFilePath(plugin, obj.src, project.projectFolder);
                project.removeResourceFileFromProject(relativeSrcPath, getTargetConditions(obj));
            } else {
                removeFile(project.root, obj.target);
                project.removeResourceFileFromProject(obj.target, getTargetConditions(obj));
            }
        }
    },
    'lib-file': {
        install:function(obj, plugin, project, options) {
            var inc  = obj.Include || obj.src;
            project.addSDKRef(inc, getTargetConditions(obj));
        },
        uninstall:function(obj, plugin, project, options) {
            events.emit('verbose', 'windows lib-file uninstall :: ' + plugin.id);
            var inc = obj.Include || obj.src;
            project.removeSDKRef(inc, getTargetConditions(obj));
        }
    },
    'framework': {
        install:function(obj, plugin, project, options) {
            events.emit('verbose', 'windows framework install :: ' + plugin.id);

            var src = obj.src;
            var dest = src;
            var type = obj.type;
            var targetDir = obj.targetDir || '';
            var implementPath = obj.implementation;

            if(type === 'projectReference') {
                dest = path.join(path.relative(project.projectFolder, plugin.dir), targetDir, src);
                project.addProjectReference(dest, getTargetConditions(obj));
            } else {
                // path.join ignores empty paths passed so we don't check whether targetDir is not empty
                dest = path.join('plugins', plugin.id, targetDir, path.basename(src));
                copyFile(plugin.dir, src, project.root, dest);
                if (implementPath) {
                    copyFile(plugin.dir, implementPath, project.root, path.join(path.dirname(dest), path.basename(implementPath)));
                }
                project.addReference(dest, getTargetConditions(obj), implementPath);
            }

        },
        uninstall:function(obj, plugin, project, options) {
            events.emit('verbose', 'windows framework uninstall :: ' + plugin.id  );

            var src = obj.src;
            var type = obj.type;

            if(type === 'projectReference') {
                project.removeProjectReference(path.join(path.relative(project.projectFolder, plugin.dir), src), getTargetConditions(obj));
            }
            else {
                var targetPath = path.join('plugins', plugin.id);
                removeFile(project.root, targetPath);
                project.removeReference(src, getTargetConditions(obj));
            }
        }
    },
    asset:{
        install:function(obj, plugin, project, options) {
            if (!obj.src) {
                throw new CordovaError(generateAttributeError('src', 'asset', plugin.id));
            }
            if (!obj.target) {
                throw new CordovaError(generateAttributeError('target', 'asset', plugin.id));
            }

            copyFile(plugin.dir, obj.src, project.www, obj.target);
            if (options && options.usePlatformWww) copyFile(plugin.dir, obj.src, project.platformWww, obj.target);
        },
        uninstall:function(obj, plugin, project, options) {
            var target = obj.target || obj.src;

            if (!target) throw new CordovaError(generateAttributeError('target', 'asset', plugin.id));

            removeFile(project.www, target);
            removeFile(project.www, path.join('plugins', plugin.id));
            if (options && options.usePlatformWww) {
                removeFile(project.platformWww, target);
                removeFile(project.platformWww, path.join('plugins', plugin.id));
            }
        }
    },
    'js-module': {
        install: function (obj, plugin, project, options) {
            // Copy the plugin's files into the www directory.
            var moduleSource = path.resolve(plugin.dir, obj.src);
            var moduleName = plugin.id + '.' + (obj.name || path.basename(obj.src, path.extname (obj.src)));

            // Read in the file, prepend the cordova.define, and write it back out.
            var scriptContent = fs.readFileSync(moduleSource, 'utf-8').replace(/^\ufeff/, ''); // Window BOM
            if (moduleSource.match(/.*\.json$/)) {
                scriptContent = 'module.exports = ' + scriptContent;
            }
            scriptContent = 'cordova.define("' + moduleName + '", function(require, exports, module) {\n' + scriptContent + '\n});\n';

            var moduleDestination = path.resolve(project.www, 'plugins', plugin.id, obj.src);
            shell.mkdir('-p', path.dirname(moduleDestination));
            fs.writeFileSync(moduleDestination, scriptContent, 'utf-8');
            if (options && options.usePlatformWww) {
                var platformWwwDestination = path.resolve(project.platformWww, 'plugins', plugin.id, obj.src);
                shell.mkdir('-p', path.dirname(platformWwwDestination));
                fs.writeFileSync(platformWwwDestination, scriptContent, 'utf-8');
            }
        },
        uninstall: function (obj, plugin, project, options) {
            var pluginRelativePath = path.join('plugins', plugin.id, obj.src);
            removeFileAndParents(project.www, pluginRelativePath);
            if (options && options.usePlatformWww) removeFileAndParents(project.platformWww, pluginRelativePath);
        }
    }
};

// Helpers from common

module.exports.getInstaller = function (type) {
    if (handlers[type] && handlers[type].install) {
        return handlers[type].install;
    }

    events.emit('verbose', '<' + type + '> is not supported for Windows plugins');
};

module.exports.getUninstaller = function(type) {
    if (handlers[type] && handlers[type].uninstall) {
        return handlers[type].uninstall;
    }

    events.emit('verbose', '<' + type + '> is not supported for Windows plugins');
};

function getTargetConditions(obj) {
    return { versions: obj.versions, deviceTarget: obj.deviceTarget, arch: obj.arch };
}

function copyFile (plugin_dir, src, project_dir, dest, link) {
    src = path.resolve(plugin_dir, src);
    if (!fs.existsSync(src)) throw new CordovaError('"' + src + '" not found!');

    // check that src path is inside plugin directory
    var real_path = fs.realpathSync(src);
    var real_plugin_path = fs.realpathSync(plugin_dir);
    if (real_path.indexOf(real_plugin_path) !== 0)
        throw new CordovaError('File "' + src + '" is located outside the plugin directory "' + plugin_dir + '"');

    dest = path.resolve(project_dir, dest);

    // check that dest path is located in project directory
    if (dest.indexOf(path.resolve(project_dir)) !== 0)
        throw new CordovaError('Destination "' + dest + '" for source file "' + src + '" is located outside the project');

    shell.mkdir('-p', path.dirname(dest));

    if (link) {
        fs.symlinkSync(path.relative(path.dirname(dest), src), dest);
    } else if (fs.statSync(src).isDirectory()) {
        // XXX shelljs decides to create a directory when -R|-r is used which sucks. http://goo.gl/nbsjq
        shell.cp('-Rf', src+'/*', dest);
    } else {
        shell.cp('-f', src, dest);
    }
}

// Same as copy file but throws error if target exists
function copyNewFile (plugin_dir, src, project_dir, dest, link) {
    var target_path = path.resolve(project_dir, dest);
    if (fs.existsSync(target_path))
        throw new CordovaError('"' + target_path + '" already exists!');

    copyFile(plugin_dir, src, project_dir, dest, !!link);
}

// checks if file exists and then deletes. Error if doesn't exist
function removeFile (project_dir, src) {
    var file = path.resolve(project_dir, src);
    shell.rm('-Rf', file);
}

function removeFileAndParents (baseDir, destFile, stopper) {
    stopper = stopper || '.';
    var file = path.resolve(baseDir, destFile);
    if (!fs.existsSync(file)) return;

    shell.rm('-rf', file);

    // check if directory is empty
    var curDir = path.dirname(file);

    while(curDir !== path.resolve(baseDir, stopper)) {
        if(fs.existsSync(curDir) && fs.readdirSync(curDir).length === 0) {
            fs.rmdirSync(curDir);
            curDir = path.resolve(curDir, '..');
        } else {
            // directory not empty...do nothing
            break;
        }
    }
}

function generateAttributeError(attribute, element, id) {
    return 'Required attribute "' + attribute + '" not specified in <' + element + '> element from plugin: ' + id;
}


/**
    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.
*/

// Given a function and an array of values, creates a chain of promises that
// will sequentially execute func(args[i]).
// Returns a promise.
//
function Q_chainmap (args, func) {
    return Promise.resolve().then(function (inValue) {
        return args.reduce(function (soFar, arg) {
            return soFar.then(function (val) {
                return func(arg, val);
            });
        }, Promise.resolve(inValue));
    });
}

// Behaves similar to Q_chainmap but gracefully handles failures.
// When a promise in the chain is rejected, it will call the failureCallback and then continue the processing, instead of stopping
function Q_chainmap_graceful (args, func, failureCallback) {
    return Promise.resolve().then(function (inValue) {
        return args.reduce(function (soFar, arg) {
            return soFar.then(function (val) {
                return func(arg, val);
            }).catch(function (err) {
                if (failureCallback) {
                    failureCallback(err);
                }
            });
        }, Promise.resolve(inValue));
    });
}

exports.Q_chainmap = Q_chainmap;
exports.Q_chainmap_graceful = Q_chainmap_graceful;


/**
    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 HooksRunner = require('../../hooks/HooksRunner');
var CordovaError = require('cordova-common').CordovaError;
var platforms = require('../../platforms/platforms');
var addHelper = require('./addHelper');

module.exports = platform;

module.exports.add = function add (hooksRunner, projectRoot, targets, opts) {
    return addHelper('add', hooksRunner, projectRoot, targets, opts);
};
module.exports.update = function update (hooksRunner, projectRoot, targets, opts) {
    return addHelper('update', hooksRunner, projectRoot, targets, opts);
};
module.exports.remove = require('./remove');
module.exports.check = require('./check');
module.exports.list = require('./list');
module.exports.getPlatformDetailsFromDir = require('./getPlatformDetailsFromDir');

// Expose the platform parsers on top of this command
for (var p in platforms) {
    module.exports[p] = platforms[p];
}

/**
 * Handles all cordova platform commands.
 * @param {string} command - Command to execute (add, rm, ls, update, save)
 * @param {Object[]} targets - Array containing platforms to execute commands on
 * @param {Object} opts
 * @returns {Promise}
 */
function platform (command, targets, opts) {
    // CB-10519 wrap function code into promise so throwing error
    // would result in promise rejection instead of uncaught exception
    return Promise.resolve().then(function () {
        var msg;
        var projectRoot = cordova_util.cdProjectRoot();
        var hooksRunner = new HooksRunner(projectRoot);

        if (arguments.length === 0) command = 'ls';

        if (targets && !(targets instanceof Array)) targets = [targets];

        // TODO: wouldn't update need a platform, too? what about save?
        if ((command === 'add' || command === 'rm' || command === 'remove') && (!targets || (targets instanceof Array && targets.length === 0))) {
            msg = 'You need to qualify `' + command + '` with one or more platforms!';
            return Promise.reject(new CordovaError(msg));
        }

        opts = opts || {};
        opts.platforms = targets;
        switch (command) {
        case 'add':
            return module.exports.add(hooksRunner, projectRoot, targets, opts);
        case 'rm':
        case 'remove':
            return module.exports.remove(hooksRunner, projectRoot, targets, opts);
        case 'update':
        case 'up':
            return module.exports.update(hooksRunner, projectRoot, targets, opts);
        case 'check':
            return module.exports.check(hooksRunner, projectRoot);
        default:
            return module.exports.list(hooksRunner, projectRoot, opts);
        }
    });
}


/*
 *
 * 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.
 *
*/

/*jslint sloppy:true, plusplus:true*/
/*global require, module, console */

var cordova = require('cordova');
var execProxy = require('cordova/exec/proxy');

/**
 * 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
 */
module.exports = function (success, fail, service, action, args) {

    var proxy = execProxy.get(service, action);

    args = args || [];

    if (proxy) {

        var callbackId = service + cordova.callbackId++;

        if (typeof success === "function" || typeof fail === "function") {
            cordova.callbacks[callbackId] = {success: success, fail: fail};
        }
        try {

            // callbackOptions param represents additional optional parameters command could pass back, like keepCallback or
            // custom callbackId, for example {callbackId: id, keepCallback: true, status: cordova.callbackStatus.JSON_EXCEPTION }
            var onSuccess = function (result, callbackOptions) {
                callbackOptions = callbackOptions || {};
                var callbackStatus;
                // covering both undefined and null.
                // strict null comparison was causing callbackStatus to be undefined
                // and then no callback was called because of the check in cordova.callbackFromNative
                // see CB-8996 Mobilespec app hang on windows
                if (callbackOptions.status !== undefined && callbackOptions.status !== null) {
                    callbackStatus = callbackOptions.status;
                }
                else {
                    callbackStatus = cordova.callbackStatus.OK;
                }
                cordova.callbackSuccess(callbackOptions.callbackId || callbackId,
                    {
                        status: callbackStatus,
                        message: result,
                        keepCallback: callbackOptions.keepCallback || false
                    });
            };
            var onError = function (err, callbackOptions) {
                callbackOptions = callbackOptions || {};
                var callbackStatus;
                // covering both undefined and null.
                // strict null comparison was causing callbackStatus to be undefined
                // and then no callback was called because of the check in cordova.callbackFromNative
                // note: status can be 0
                if (callbackOptions.status !== undefined && callbackOptions.status !== null) {
                    callbackStatus = callbackOptions.status;
                }
                else {
                    callbackStatus = cordova.callbackStatus.OK;
                }
                cordova.callbackError(callbackOptions.callbackId || callbackId,
                {
                    status: callbackStatus,
                    message: err,
                    keepCallback: callbackOptions.keepCallback || false
                });
            };
            proxy(onSuccess, onError, args);

        } catch (e) {
            console.log("Exception calling native with command :: " + service + " :: " + action  + " ::exception=" + e);
        }
    } else {

        console.log("Error: exec proxy not found for :: " + service + " :: " + action);

        if(typeof fail === "function" ) {
            fail("Missing Command Error");
        }
    }
};


/**
    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 semver = require('semver');

/** Returns all the platforms that are currently saved into config.xml
 *  @return {Promise<{name: string, version: string, src: string}[]>}
 *      e.g: [ {name: 'android', version: '3.5.0'}, {name: 'wp8', src: 'C:/path/to/platform'}, {name: 'ios', src: 'git://...'} ]
 */
function getPlatforms (projectRoot) {
    var xml = cordova_util.projectConfig(projectRoot);
    var cfg = new ConfigParser(xml);

    // If an engine's 'version' property is really its source, map that to the appropriate field.
    var engines = cfg.getEngines().map(function (engine) {
        var result = {
            name: engine.name
        };

        if (semver.validRange(engine.spec, true)) {
            result.version = engine.spec;
        } else {
            result.src = engine.spec;
        }

        return result;
    });

    return Promise.resolve(engines);
}

/** Returns all the plugins that are currently saved into config.xml
 *  @return {Promise<{id: string, version: string, variables: {name: string, value: string}[]}[]>}
 *      e.g: [ {id: 'org.apache.cordova.device', variables: [{name: 'APP_ID', value: 'my-app-id'}, {name: 'APP_NAME', value: 'my-app-name'}]} ]
 */
function getPlugins (projectRoot) {
    var xml = cordova_util.projectConfig(projectRoot);
    var cfg = new ConfigParser(xml);

    // Map variables object to an array
    var plugins = cfg.getPlugins().map(function (plugin) {
        var result = {
            name: plugin.name
        };

        if (semver.validRange(plugin.spec, true)) {
            result.version = plugin.spec;
        } else {
            result.src = plugin.spec;
        }

        var variablesObject = plugin.variables;
        var variablesArray = [];
        if (variablesObject) {
            for (var variable in variablesObject) {
                variablesArray.push({
                    name: variable,
                    value: variablesObject[variable]
                });
            }
        }
        result.variables = variablesArray;
        return result;
    });

    return Promise.resolve(plugins);
}

module.exports = {
    getPlatforms: getPlatforms,
    getPlugins: getPlugins
};


/**
    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 util = require('../cordova/util');
var platforms = require('./platformsConfig.json');
var events = require('cordova-common').events;

// Avoid loading the same platform projects more than once (identified by path)
var cachedApis = {};

// getPlatformApi() should be the only method of instantiating the
// PlatformProject classes for now.
function getPlatformApi (platform, platformRootDir) {
    // if platformRootDir is not specified, try to detect it first
    if (!platformRootDir) {
        var projectRootDir = util.isCordova();
        platformRootDir = projectRootDir && path.join(projectRootDir, 'platforms', platform);
    }
    if (!platformRootDir) {
        // If platformRootDir is still undefined, then we're probably is not inside of cordova project
        throw new Error('Current location is not a Cordova project');
    }
    // CB-11174 Resolve symlinks first before working with root directory
    platformRootDir = util.convertToRealPathSafe(platformRootDir);

    // Make sure the platforms/platform folder exists
    if (!fs.existsSync(platformRootDir)) {
        throw new Error('The platform "' + platform + '" does not appear to have been added to this project.');
    }

    var platformApi;
    var cached = cachedApis[platformRootDir];
    var libDir = path.join(platformRootDir, 'cordova', 'Api.js');
    if (cached && cached.platform === platform) {
        platformApi = cached;
    } else {
        var PlatformApi = util.getPlatformApiFunction(libDir, platform);
        platformApi = new PlatformApi(platform, platformRootDir, events);
        cachedApis[platformRootDir] = platformApi;
    }
    return platformApi;
}

module.exports = platforms;

// We don't want these methods to be enumerable on the platforms object, because we expect enumerable properties of the
// platforms object to be platforms.
Object.defineProperties(module.exports, {
    'getPlatformApi': { value: getPlatformApi, configurable: true, writable: true }
});


/**
    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 DepGraph = require('dep-graph');
var path = require('path');
var fs = require('fs-extra');
var underscore = require('underscore');
var events = require('cordova-common').events;
var pkg;

module.exports = pkg = {

    generateDependencyInfo: function (platformJson, plugins_dir, pluginInfoProvider) {
        var json = platformJson.root;

        // TODO: store whole dependency tree in plugins/[platform].json
        // in case plugins are forcefully removed...
        var tlps = [];
        var graph = new DepGraph();
        Object.keys(json.installed_plugins).forEach(function (plugin_id) {
            tlps.push(plugin_id);

            var plugin_dir = path.join(plugins_dir, plugin_id);
            var pluginInfo = pluginInfoProvider.get(plugin_dir);
            var deps = pluginInfo.getDependencies(platformJson.platform);
            deps.forEach(function (dep) {
                graph.add(plugin_id, dep.id);
            });
        });
        Object.keys(json.dependent_plugins).forEach(function (plugin_id) {
            var plugin_dir = path.join(plugins_dir, plugin_id);
            // dependency plugin does not exist (CB-7846)
            if (!fs.existsSync(plugin_dir)) {
                events.emit('verbose', 'Plugin "' + plugin_id + '" does not exist (' + plugin_dir + ')');
                return;
            }

            var pluginInfo = pluginInfoProvider.get(plugin_dir);
            var deps = pluginInfo.getDependencies(platformJson.platform);
            deps.forEach(function (dep) {
                graph.add(plugin_id, dep.id);
            });
        });

        return {
            graph: graph,
            top_level_plugins: tlps
        };
    },

    // Returns a list of top-level plugins which are (transitively) dependent on the given plugin.
    dependents: function (plugin_id, plugins_dir, platformJson, pluginInfoProvider) {
        var depsInfo;
        if (typeof plugins_dir === 'object') { depsInfo = plugins_dir; } else { depsInfo = pkg.generateDependencyInfo(platformJson, plugins_dir, pluginInfoProvider); }

        var graph = depsInfo.graph;
        var tlps = depsInfo.top_level_plugins;
        var dependents = tlps.filter(function (tlp) {
            return tlp !== plugin_id && graph.getChain(tlp).indexOf(plugin_id) >= 0;
        });

        return dependents;
    },

    // Returns a list of plugins which the given plugin depends on, for which it is the only dependent.
    // In other words, if the given plugin were deleted, these dangling dependencies should be deleted too.
    danglers: function (plugin_id, plugins_dir, platformJson, pluginInfoProvider) {
        var depsInfo;
        if (typeof plugins_dir === 'object') { depsInfo = plugins_dir; } else { depsInfo = pkg.generateDependencyInfo(platformJson, plugins_dir, pluginInfoProvider); }

        var graph = depsInfo.graph;
        var dependencies = graph.getChain(plugin_id);

        var tlps = depsInfo.top_level_plugins;
        var diff_arr = [];
        tlps.forEach(function (tlp) {
            if (tlp !== plugin_id) {
                diff_arr.push(graph.getChain(tlp));
            }
        });

        // if this plugin has dependencies, do a set difference to determine which dependencies are not required by other existing plugins
        diff_arr.unshift(dependencies);
        var danglers = underscore.difference.apply(null, diff_arr);

        // Ensure no top-level plugins are tagged as danglers.
        danglers = danglers && danglers.filter(function (x) { return tlps.indexOf(x) < 0; });
        return danglers;
    }
};


/*
       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 semver = require('semver');
var CommonPluginInfo = require('cordova-common').PluginInfo;

var MANIFESTS = {
    'windows': {
        '8.1.0': 'package.windows.appxmanifest',
        '10.0.0': 'package.windows10.appxmanifest'
    },
    'phone': {
        '8.1.0': 'package.phone.appxmanifest',
        '10.0.0': 'package.windows10.appxmanifest'
    },
    'all': {
        '8.1.0': ['package.windows.appxmanifest', 'package.phone.appxmanifest'],
        '10.0.0': 'package.windows10.appxmanifest'
    }
};

var SUBSTS = ['package.phone.appxmanifest', 'package.windows.appxmanifest', 'package.windows10.appxmanifest'];
var TARGETS = ['windows', 'phone', 'all'];

function processChanges(changes) {
    var hasManifestChanges  = changes.some(function(change) {
        return change.target === 'package.appxmanifest';
    });

    if (!hasManifestChanges) {
        return changes;
    }

    // Demux 'package.appxmanifest' into relevant platform-specific appx manifests.
    // Only spend the cycles if there are version-specific plugin settings
    var oldChanges = changes;
    changes = [];

    oldChanges.forEach(function(change) {
        // Only support semver/device-target demux for package.appxmanifest
        // Pass through in case something downstream wants to use it
        if (change.target !== 'package.appxmanifest') {
            changes.push(change);
            return;
        }

        var manifestsForChange = getManifestsForChange(change);
        changes = changes.concat(demuxChangeWithSubsts(change, manifestsForChange));
    });

    return changes;
}

function demuxChangeWithSubsts(change, manifestFiles) {
    return manifestFiles.map(function(file) {
         return createReplacement(file, change);
    });
}

function getManifestsForChange(change) {
    var hasTarget = (typeof change.deviceTarget !== 'undefined');
    var hasVersion = (typeof change.versions !== 'undefined');

    var targetDeviceSet = hasTarget ? change.deviceTarget : 'all';

    if (TARGETS.indexOf(targetDeviceSet) === -1) {
        // target-device couldn't be resolved, fix it up here to a valid value
        targetDeviceSet = 'all';
    }

    // No semver/device-target for this config-file, pass it through
    if (!(hasTarget || hasVersion)) {
        return SUBSTS;
    }

    var knownWindowsVersionsForTargetDeviceSet = Object.keys(MANIFESTS[targetDeviceSet]);
    return knownWindowsVersionsForTargetDeviceSet.reduce(function(manifestFiles, winver) {
        if (hasVersion && !semver.satisfies(winver, change.versions)) {
            return manifestFiles;
        }
        return manifestFiles.concat(MANIFESTS[targetDeviceSet][winver]);
    }, []);
}

// This is a local function that creates the new replacement representing the
// mutation.  Used to save code further down.
function createReplacement(manifestFile, originalChange) {
    var replacement = {
        target:         manifestFile,
        parent:         originalChange.parent,
        after:          originalChange.after,
        xmls:           originalChange.xmls,
        versions:       originalChange.versions,
        deviceTarget:   originalChange.deviceTarget
    };
    return replacement;
}

/*
A class for holidng the information currently stored in plugin.xml
It's inherited from cordova-common's PluginInfo class
In addition it overrides getConfigFiles, getEditConfigs, getFrameworks methods to use windows-specific logic
 */
function PluginInfo(dirname) {
    //  We're not using `util.inherit' because original PluginInfo defines
    //  its' methods inside of constructor
    CommonPluginInfo.apply(this, arguments);
    var parentGetConfigFiles = this.getConfigFiles;
    var parentGetEditConfigs = this.getEditConfigs;

    this.getEditConfigs = function(platform) {
        var editConfigs = parentGetEditConfigs(platform);
        return processChanges(editConfigs);
    };

    this.getConfigFiles = function(platform) {
        var configFiles = parentGetConfigFiles(platform);
        return processChanges(configFiles);
    };
}

exports.PluginInfo = PluginInfo;


/**
    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 fs = require('fs-extra');
var url = require('url');
var underscore = require('underscore');
var semver = require('semver');
var PluginInfoProvider = require('cordova-common').PluginInfoProvider;
var CordovaError = require('cordova-common').CordovaError;
var events = require('cordova-common').events;
var metadata = require('./util/metadata');
var path = require('path');
var pluginSpec = require('../cordova/plugin/plugin_spec_parser');
var fetch = require('cordova-fetch');
var cordovaUtil = require('../cordova/util');

var projectRoot;

// Cache of PluginInfo objects for plugins in search path.
var localPlugins = null;

// possible options: link, subdir, git_ref, client, expected_id
// Returns a promise.
module.exports = fetchPlugin;
function fetchPlugin (plugin_src, plugins_dir, options) {
    // Ensure the containing directory exists.
    fs.ensureDirSync(plugins_dir);
    options = options || {};
    options.subdir = options.subdir || '.';
    options.searchpath = options.searchpath || [];
    if (typeof options.searchpath === 'string') {
        options.searchpath = options.searchpath.split(path.delimiter);
    }

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

    // clone from git repository
    // @todo Use 'url.URL' constructor instead since 'url.parse' was deprecated since v11.0.0
    var uri = url.parse(plugin_src); // eslint-disable-line

    // If the hash exists, it has the form from npm: http://foo.com/bar#git-ref[:subdir]
    // git-ref can be a commit SHA, a tag, or a branch
    // NB: No leading or trailing slash on the subdir.
    if (uri.hash) {
        var result = uri.hash.match(/^#([^:]*)(?::\/?(.*?)\/?)?$/);
        if (result) {
            if (result[1]) { options.git_ref = result[1]; }
            if (result[2]) { options.subdir = result[2]; }

            // throw error for subdirectories
            if (options.subdir && options.subdir !== '.') {
                return Promise.reject(new CordovaError('Cordova does not support subdirectories'));
            }
        }
    }
    return Promise.resolve().then(function () {
        var plugin_dir = cordovaUtil.fixRelativePath(path.join(plugin_src, options.subdir));
        return Promise.resolve().then(function () {
            // check if it is a local path
            if (fs.existsSync(plugin_dir)) {
                if (!fs.existsSync(path.join(plugin_dir, 'package.json'))) {
                    return Promise.reject(new CordovaError('Invalid Plugin! ' + plugin_dir + ' needs a valid package.json'));
                }

                projectRoot = path.join(plugins_dir, '..');
                // Plugman projects need to go up two directories to reach project root.
                // Plugman projects have an options.projectRoot variable
                if (options.projectRoot) {
                    projectRoot = options.projectRoot;
                }
                return fetch(path.resolve(plugin_dir), projectRoot, options)
                    .then(function (directory) {
                        return {
                            pinfo: pluginInfoProvider.get(directory),
                            fetchJsonSource: {
                                type: 'local',
                                path: directory
                            }
                        };
                    }).catch(function (error) {
                        // something went wrong with cordova-fetch
                        return Promise.reject(new CordovaError(error.message));
                    });
            }
            // If there is no such local path or it's a git URL, it's a plugin id or id@versionspec.
            // First look for it in the local search path (if provided).
            var pinfo = findLocalPlugin(plugin_src, options.searchpath, pluginInfoProvider);
            if (pinfo) {
                events.emit('verbose', 'Found ' + plugin_src + ' at ' + pinfo.dir);
                return {
                    pinfo: pinfo,
                    fetchJsonSource: {
                        type: 'local',
                        path: pinfo.dir
                    }
                };
            } else if (options.noregistry) {
                return Promise.reject(new CordovaError(
                    'Plugin ' + plugin_src + ' not found locally. ' +
                    'Note, plugin registry was disabled by --noregistry flag.'
                ));
            }
            // If not found in local search path, fetch from the registry.
            var parsedSpec = pluginSpec.parse(plugin_src);
            var P;
            var skipCopyingPlugin;
            plugin_dir = path.join(plugins_dir, parsedSpec.id);
            // if the plugin has already been fetched, use it.
            if (fs.existsSync(plugin_dir)) {
                P = Promise.resolve(plugin_dir);
                skipCopyingPlugin = true;
            } else {
                // use cordova-fetch
                projectRoot = path.join(plugins_dir, '..');
                // Plugman projects need to go up two directories to reach project root.
                // Plugman projects have an options.projectRoot variable
                if (options.projectRoot) {
                    projectRoot = options.projectRoot;
                }

                P = fetch(plugin_src, projectRoot, options);
                skipCopyingPlugin = false;
            }
            return P
                .catch(function (error) {
                    var message = 'Failed to fetch plugin ' + plugin_src + ' via registry.' +
                        '\nProbably this is either a connection problem, or plugin spec is incorrect.' +
                        '\nCheck your connection and plugin name/version/URL.' +
                        '\n' + error;
                    return Promise.reject(new CordovaError(message));
                })
                .then(function (dir) {
                    return {
                        pinfo: pluginInfoProvider.get(dir),
                        fetchJsonSource: {
                            type: 'registry',
                            id: plugin_src
                        },
                        skipCopyingPlugin: skipCopyingPlugin
                    };
                });
        }).then(function (result) {
            options.plugin_src_dir = result.pinfo.dir;
            var P;
            if (result.skipCopyingPlugin) {
                P = Promise.resolve(options.plugin_src_dir);
            } else {
                P = Promise.resolve(copyPlugin(result.pinfo, plugins_dir, options.link && result.fetchJsonSource.type === 'local'));
            }
            return P.then(function (dir) {
                result.dest = dir;
                return result;
            });
        });
    }).then(function (result) {
        checkID(options.expected_id, result.pinfo);
        var data = { source: result.fetchJsonSource };
        data.is_top_level = options.is_top_level;
        data.variables = options.variables || {};
        metadata.save_fetch_metadata(plugins_dir, result.pinfo.id, data);
        return result.dest;
    });
}

// Helper function for checking expected plugin IDs against reality.
function checkID (expectedIdAndVersion, pinfo) {
    if (!expectedIdAndVersion) return;

    var parsedSpec = pluginSpec.parse(expectedIdAndVersion);

    if (parsedSpec.id !== pinfo.id) {
        throw new Error('Expected plugin to have ID "' + parsedSpec.id + '" but got "' + pinfo.id + '".');
    }

    if (parsedSpec.version && !semver.satisfies(pinfo.version, parsedSpec.version)) {
        throw new Error('Expected plugin ' + pinfo.id + ' to satisfy version "' + parsedSpec.version + '" but got "' + pinfo.version + '".');
    }
}

// Note, there is no cache invalidation logic for local plugins.
// As of this writing loadLocalPlugins() is never called with different
// search paths and such case would not be handled properly.
function loadLocalPlugins (searchpath, pluginInfoProvider) {
    if (localPlugins) {
        // localPlugins already populated, nothing to do.
        // just in case, make sure it was loaded with the same search path
        if (!underscore.isEqual(localPlugins.searchpath, searchpath)) {
            var msg =
                'loadLocalPlugins called twice with different search paths.' +
                'Support for this is not implemented.  Using previously cached path.';
            events.emit('warn', msg);
        }
        return;
    }

    // Populate localPlugins object.
    localPlugins = {};
    localPlugins.searchpath = searchpath;
    localPlugins.plugins = {};

    searchpath.forEach(function (dir) {
        var ps = pluginInfoProvider.getAllWithinSearchPath(dir);
        ps.forEach(function (p) {
            var versions = localPlugins.plugins[p.id] || [];
            versions.push(p);
            localPlugins.plugins[p.id] = versions;
        });
    });
}

// If a plugin is fund in local search path, return a PluginInfo for it.
// Ignore plugins that don't satisfy the required version spec.
// If several versions are present in search path, return the latest.
// Examples of accepted plugin_src strings:
//      org.apache.cordova.file
//      org.apache.cordova.file@>=1.2.0
function findLocalPlugin (plugin_src, searchpath, pluginInfoProvider) {
    loadLocalPlugins(searchpath, pluginInfoProvider);
    var parsedSpec = pluginSpec.parse(plugin_src);
    var versionspec = parsedSpec.version || '*';

    var latest = null;
    var versions = localPlugins.plugins[parsedSpec.id];

    if (!versions) return null;

    versions.forEach(function (pinfo) {
        // Ignore versions that don't satisfy the the requested version range.
        // Ignore -dev suffix because latest semver versions doesn't handle it properly (CB-9421)
        if (!semver.satisfies(pinfo.version.replace(/-dev$/, ''), versionspec)) {
            return;
        }
        if (!latest) {
            latest = pinfo;
            return;
        }
        if (semver.gt(pinfo.version, latest.version)) {
            latest = pinfo;
        }

    });
    return latest;
}

// Copy or link a plugin from plugin_dir to plugins_dir/plugin_id.
// if alternative ID of plugin exists in plugins_dir/plugin_id, skip copying
function copyPlugin (pinfo, plugins_dir, link) {

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

    fs.removeSync(dest);

    if (!link && dest.indexOf(path.resolve(plugin_dir) + path.sep) === 0) {
        events.emit('verbose', 'Copy plugin destination is child of src. Forcing --link mode.');
        link = true;
    }

    if (link) {
        var isRelativePath = plugin_dir.charAt(1) !== ':' && plugin_dir.charAt(0) !== path.sep;
        var fixedPath = isRelativePath ? path.join(path.relative(plugins_dir, process.env.PWD || process.cwd()), plugin_dir) : plugin_dir;
        events.emit('verbose', 'Linking "' + dest + '" => "' + fixedPath + '"');
        fs.symlinkSync(fixedPath, dest, 'junction');
    } else {
        events.emit('verbose', 'Copying plugin "' + plugin_dir + '" => "' + dest + '"');
        fs.copySync(plugin_dir, dest, { dereference: true });
    }
    return dest;
}