/**
    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 et = require('elementtree');
var xml = require('../util/xml-helpers');
var CordovaError = require('../CordovaError/CordovaError');
var fs = require('fs-extra');
var events = require('../events');

/** Wraps a config.xml file */
function ConfigParser (path) {
    this.path = path;
    try {
        this.doc = xml.parseElementtreeSync(path);
        this.cdvNamespacePrefix = getCordovaNamespacePrefix(this.doc);
        et.register_namespace(this.cdvNamespacePrefix, 'http://cordova.apache.org/ns/1.0');
    } catch (e) {
        events.emit('error', 'Parsing ' + path + ' failed');
        throw e;
    }
    var r = this.doc.getroot();
    if (r.tag !== 'widget') {
        throw new CordovaError(path + ' has incorrect root node name (expected "widget", was "' + r.tag + '")');
    }
}

function getNodeTextSafe (el) {
    return el && el.text && el.text.trim();
}

function findOrCreate (doc, name) {
    var ret = doc.find(name);
    if (!ret) {
        ret = new et.Element(name);
        doc.getroot().append(ret);
    }
    return ret;
}

function getCordovaNamespacePrefix (doc) {
    var rootAtribs = Object.getOwnPropertyNames(doc.getroot().attrib);
    var prefix = 'cdv';
    for (var j = 0; j < rootAtribs.length; j++) {
        if (rootAtribs[j].startsWith('xmlns:') &&
            doc.getroot().attrib[rootAtribs[j]] === 'http://cordova.apache.org/ns/1.0') {
            var strings = rootAtribs[j].split(':');
            prefix = strings[1];
            break;
        }
    }
    return prefix;
}

/**
 * Finds the value of an element's attribute
 * @param  {String} attributeName Name of the attribute to search for
 * @param  {Array}  elems         An array of ElementTree nodes
 * @return {String}
 */
function findElementAttributeValue (attributeName, elems) {
    elems = Array.isArray(elems) ? elems : [elems];

    var value = elems.filter(function (elem) {
        return elem.attrib.name.toLowerCase() === attributeName.toLowerCase();
    }).map(function (filteredElems) {
        return filteredElems.attrib.value;
    }).pop();

    return value || '';
}

function removeChildren (el, selector) {
    const matches = el.findall(selector);
    matches.forEach(child => el.remove(child));
}

ConfigParser.prototype = {
    getAttribute: function (attr) {
        return this.doc.getroot().attrib[attr];
    },

    packageName: function () {
        return this.getAttribute('id');
    },
    setPackageName: function (id) {
        this.doc.getroot().attrib['id'] = id;
    },
    android_packageName: function () {
        return this.getAttribute('android-packageName');
    },
    android_activityName: function () {
        return this.getAttribute('android-activityName');
    },
    ios_CFBundleIdentifier: function () {
        return this.getAttribute('ios-CFBundleIdentifier');
    },
    name: function () {
        return getNodeTextSafe(this.doc.find('name'));
    },
    setName: function (name) {
        var el = findOrCreate(this.doc, 'name');
        el.text = name;
    },
    shortName: function () {
        return this.doc.find('name').attrib['short'] || this.name();
    },
    setShortName: function (shortname) {
        var el = findOrCreate(this.doc, 'name');
        if (!el.text) {
            el.text = shortname;
        }
        el.attrib['short'] = shortname;
    },
    description: function () {
        return getNodeTextSafe(this.doc.find('description'));
    },
    setDescription: function (text) {
        var el = findOrCreate(this.doc, 'description');
        el.text = text;
    },
    version: function () {
        return this.getAttribute('version');
    },
    windows_packageVersion: function () {
        return this.getAttribute('windows-packageVersion');
    },
    android_versionCode: function () {
        return this.getAttribute('android-versionCode');
    },
    ios_CFBundleVersion: function () {
        return this.getAttribute('ios-CFBundleVersion');
    },
    setVersion: function (value) {
        this.doc.getroot().attrib['version'] = value;
    },
    author: function () {
        return getNodeTextSafe(this.doc.find('author'));
    },
    getGlobalPreference: function (name) {
        return findElementAttributeValue(name, this.doc.findall('preference'));
    },
    setGlobalPreference: function (name, value) {
        var pref = this.doc.find('preference[@name="' + name + '"]');
        if (!pref) {
            pref = new et.Element('preference');
            pref.attrib.name = name;
            this.doc.getroot().append(pref);
        }
        pref.attrib.value = value;
    },
    getPlatformPreference: function (name, platform) {
        return findElementAttributeValue(name, this.doc.findall('./platform[@name="' + platform + '"]/preference'));
    },
    setPlatformPreference: function (name, platform, value) {
        const platformEl = this.doc.find('./platform[@name="' + platform + '"]');
        if (!platformEl) {
            throw new CordovaError('platform does not exist (received platform: ' + platform + ')');
        }
        const elems = this.doc.findall('./platform[@name="' + platform + '"]/preference');
        let pref = elems.filter(function (elem) {
            return elem.attrib.name.toLowerCase() === name.toLowerCase();
        }).pop();

        if (!pref) {
            pref = new et.Element('preference');
            pref.attrib.name = name;
            platformEl.append(pref);
        }
        pref.attrib.value = value;
    },
    getPreference: function (name, platform) {
        var platformPreference = '';

        if (platform) {
            platformPreference = this.getPlatformPreference(name, platform);
        }

        return platformPreference || this.getGlobalPreference(name);
    },
    setPreference: function (name, platform, value) {
        if (!value) {
            value = platform;
            platform = undefined;
        }

        if (platform) {
            this.setPlatformPreference(name, platform, value);
        } else {
            this.setGlobalPreference(name, value);
        }
    },
    /**
     * Returns all resources for the platform specified.
     * @param  {String} platform     The platform.
     * @param {string}  resourceName Type of static resources to return.
     *                               "icon" and "splash" currently supported.
     * @return {Array}               Resources for the platform specified.
     */
    getStaticResources: function (platform, resourceName) {
        var ret = [];
        var staticResources = [];
        if (platform) { // platform specific icons
            this.doc.findall('./platform[@name="' + platform + '"]/' + resourceName).forEach(function (elt) {
                elt.platform = platform; // mark as platform specific resource
                staticResources.push(elt);
            });
        }
        // root level resources
        staticResources = staticResources.concat(this.doc.findall(resourceName));
        // parse resource elements
        var that = this;
        staticResources.forEach(function (elt) {
            var res = {};
            res.src = elt.attrib.src;
            res.target = elt.attrib.target || undefined;
            res.density = elt.attrib['density'] || elt.attrib[that.cdvNamespacePrefix + ':density'] || elt.attrib['gap:density'];
            res.platform = elt.platform || null; // null means icon represents default icon (shared between platforms)
            res.width = +elt.attrib.width || undefined;
            res.height = +elt.attrib.height || undefined;
            res.background = elt.attrib.background || undefined;
            res.foreground = elt.attrib.foreground || undefined;

            // default icon
            if (!res.width && !res.height && !res.density) {
                ret.defaultResource = res;
            }
            ret.push(res);
        });

        /**
         * Returns resource with specified width and/or height.
         * @param  {number} width Width of resource.
         * @param  {number} height Height of resource.
         * @return {Resource} Resource object or null if not found.
         */
        ret.getBySize = function (width, height) {
            return ret.filter(function (res) {
                if (!res.width && !res.height) {
                    return false;
                }
                return ((!res.width || (width === res.width)) &&
                    (!res.height || (height === res.height)));
            })[0] || null;
        };

        /**
         * Returns resource with specified density.
         * @param  {string} density Density of resource.
         * @return {Resource}       Resource object or null if not found.
         */
        ret.getByDensity = function (density) {
            return ret.filter(function (res) {
                return res.density === density;
            })[0] || null;
        };

        /** Returns default icons */
        ret.getDefault = function () {
            return ret.defaultResource;
        };

        return ret;
    },

    /**
     * Returns all icons for specific platform.
     * @param  {string} platform Platform name
     * @return {Resource[]}      Array of icon objects.
     */
    getIcons: function (platform) {
        return this.getStaticResources(platform, 'icon');
    },

    /**
     * Returns all splash images for specific platform.
     * @param  {string} platform Platform name
     * @return {Resource[]}      Array of Splash objects.
     */
    getSplashScreens: function (platform) {
        return this.getStaticResources(platform, 'splash');
    },

    /**
     * Returns all resource-files for a specific platform.
     * @param  {string} platform Platform name
     * @param  {boolean} includeGlobal Whether to return resource-files at the
     *                                 root level.
     * @return {Resource[]}      Array of resource file objects.
     */
    getFileResources: function (platform, includeGlobal) {
        var fileResources = [];

        if (platform) { // platform specific resources
            fileResources = this.doc.findall('./platform[@name="' + platform + '"]/resource-file').map(function (tag) {
                return {
                    platform: platform,
                    src: tag.attrib.src,
                    target: tag.attrib.target,
                    versions: tag.attrib.versions,
                    deviceTarget: tag.attrib['device-target'],
                    arch: tag.attrib.arch
                };
            });
        }

        if (includeGlobal) {
            this.doc.findall('resource-file').forEach(function (tag) {
                fileResources.push({
                    platform: platform || null,
                    src: tag.attrib.src,
                    target: tag.attrib.target,
                    versions: tag.attrib.versions,
                    deviceTarget: tag.attrib['device-target'],
                    arch: tag.attrib.arch
                });
            });
        }

        return fileResources;
    },

    /**
     * Returns all hook scripts for the hook type specified.
     * @param  {String} hook     The hook type.
     * @param {Array}  platforms Platforms to look for scripts into (root scripts will be included as well).
     * @return {Array}               Script elements.
     */
    getHookScripts: function (hook, platforms) {
        var self = this;
        var scriptElements = self.doc.findall('./hook');

        if (platforms) {
            platforms.forEach(function (platform) {
                scriptElements = scriptElements.concat(self.doc.findall('./platform[@name="' + platform + '"]/hook'));
            });
        }

        function filterScriptByHookType (el) {
            return el.attrib.src && el.attrib.type && el.attrib.type.toLowerCase() === hook;
        }

        return scriptElements.filter(filterScriptByHookType);
    },
    /**
    * Returns a list of plugin (IDs).
    *
    * This function also returns any plugin's that
    * were defined using the legacy <feature> tags.
    * @return {string[]} Array of plugin IDs
    */
    getPluginIdList: function () {
        var plugins = this.doc.findall('plugin');
        var result = plugins.map(function (plugin) {
            return plugin.attrib.name;
        });
        var features = this.doc.findall('feature');
        features.forEach(function (element) {
            var idTag = element.find('./param[@name="id"]');
            if (idTag) {
                result.push(idTag.attrib.value);
            }
        });
        return result;
    },
    getPlugins: function () {
        return this.getPluginIdList().map(function (pluginId) {
            return this.getPlugin(pluginId);
        }, this);
    },
    /**
     * Adds a plugin element. Does not check for duplicates.
     * @name addPlugin
     * @function
     * @param {object} attributes name and spec are supported
     * @param {Array|object} variables name, value or arbitary object
     */
    addPlugin: function (attributes, variables) {
        if (!attributes && !attributes.name) return;
        var el = new et.Element('plugin');
        el.attrib.name = attributes.name;
        if (attributes.spec) {
            el.attrib.spec = attributes.spec;
        }

        // support arbitrary object as variables source
        if (variables && typeof variables === 'object' && !Array.isArray(variables)) {
            variables = Object.keys(variables)
                .map(function (variableName) {
                    return { name: variableName, value: variables[variableName] };
                });
        }

        if (variables) {
            variables.forEach(function (variable) {
                el.append(new et.Element('variable', { name: variable.name, value: variable.value }));
            });
        }
        this.doc.getroot().append(el);
    },
    /**
     * Retrives the plugin with the given id or null if not found.
     *
     * This function also returns any plugin's that
     * were defined using the legacy <feature> tags.
     * @name getPlugin
     * @function
     * @param {String} id
     * @returns {object} plugin including any variables
     */
    getPlugin: function (id) {
        if (!id) {
            return undefined;
        }
        var pluginElement = this.doc.find('./plugin/[@name="' + id + '"]');
        if (pluginElement === null) {
            var legacyFeature = this.doc.find('./feature/param[@name="id"][@value="' + id + '"]/..');
            if (legacyFeature) {
                events.emit('log', 'Found deprecated feature entry for ' + id + ' in config.xml.');
                return featureToPlugin(legacyFeature);
            }
            return undefined;
        }
        var plugin = {};

        plugin.name = pluginElement.attrib.name;
        plugin.spec = pluginElement.attrib.spec || pluginElement.attrib.src || pluginElement.attrib.version;
        plugin.variables = {};
        var variableElements = pluginElement.findall('variable');
        variableElements.forEach(function (varElement) {
            var name = varElement.attrib.name;
            var value = varElement.attrib.value;
            if (name) {
                plugin.variables[name] = value;
            }
        });
        return plugin;
    },
    /**
     * Remove the plugin entry with give name (id).
     *
     * This function also operates on any plugin's that
     * were defined using the legacy <feature> tags.
     * @name removePlugin
     * @function
     * @param id name of the plugin
     */
    removePlugin: function (id) {
        if (!id) return;
        const root = this.doc.getroot();
        removeChildren(root, `./plugin/[@name="${id}"]`);
        removeChildren(root, `./feature/param[@name="id"][@value="${id}"]/..`);
    },

    // Add any element to the root
    addElement: function (name, attributes) {
        var el = et.Element(name);
        for (var a in attributes) {
            el.attrib[a] = attributes[a];
        }
        this.doc.getroot().append(el);
    },

    /**
     * Adds an engine. Does not check for duplicates.
     * @param  {String} name the engine name
     * @param  {String} spec engine source location or version (optional)
     */
    addEngine: function (name, spec) {
        if (!name) return;
        var el = et.Element('engine');
        el.attrib.name = name;
        if (spec) {
            el.attrib.spec = spec;
        }
        this.doc.getroot().append(el);
    },
    /**
     * Removes all the engines with given name
     * @param  {String} name the engine name.
     */
    removeEngine: function (name) {
        removeChildren(this.doc.getroot(), `./engine/[@name="${name}"]`);
    },
    getEngines: function () {
        var engines = this.doc.findall('./engine');
        return engines.map(function (engine) {
            var spec = engine.attrib.spec || engine.attrib.version;
            return {
                name: engine.attrib.name,
                spec: spec || null
            };
        });
    },
    /* Get all the access tags */
    getAccesses: function () {
        var accesses = this.doc.findall('./access');
        return accesses.map(function (access) {
            var minimum_tls_version = access.attrib['minimum-tls-version']; /* String */
            var requires_forward_secrecy = access.attrib['requires-forward-secrecy']; /* Boolean */
            var requires_certificate_transparency = access.attrib['requires-certificate-transparency']; /* Boolean */
            var allows_arbitrary_loads_in_web_content = access.attrib['allows-arbitrary-loads-in-web-content']; /* Boolean */
            var allows_arbitrary_loads_in_media = access.attrib['allows-arbitrary-loads-in-media']; /* Boolean (DEPRECATED) */
            var allows_arbitrary_loads_for_media = access.attrib['allows-arbitrary-loads-for-media']; /* Boolean */
            var allows_local_networking = access.attrib['allows-local-networking']; /* Boolean */

            return {
                origin: access.attrib.origin,
                minimum_tls_version: minimum_tls_version,
                requires_forward_secrecy: requires_forward_secrecy,
                requires_certificate_transparency: requires_certificate_transparency,
                allows_arbitrary_loads_in_web_content: allows_arbitrary_loads_in_web_content,
                allows_arbitrary_loads_in_media: allows_arbitrary_loads_in_media,
                allows_arbitrary_loads_for_media: allows_arbitrary_loads_for_media,
                allows_local_networking: allows_local_networking
            };
        });
    },
    /* Get all the allow-navigation tags */
    getAllowNavigations: function () {
        var allow_navigations = this.doc.findall('./allow-navigation');
        return allow_navigations.map(function (allow_navigation) {
            var minimum_tls_version = allow_navigation.attrib['minimum-tls-version']; /* String */
            var requires_forward_secrecy = allow_navigation.attrib['requires-forward-secrecy']; /* Boolean */
            var requires_certificate_transparency = allow_navigation.attrib['requires-certificate-transparency']; /* Boolean */

            return {
                href: allow_navigation.attrib.href,
                minimum_tls_version: minimum_tls_version,
                requires_forward_secrecy: requires_forward_secrecy,
                requires_certificate_transparency: requires_certificate_transparency
            };
        });
    },
    /* Get all the allow-intent tags */
    getAllowIntents: function () {
        var allow_intents = this.doc.findall('./allow-intent');
        return allow_intents.map(function (allow_intent) {
            return {
                href: allow_intent.attrib.href
            };
        });
    },
    /* Get all edit-config tags */
    getEditConfigs: function (platform) {
        var platform_edit_configs = this.doc.findall('./platform[@name="' + platform + '"]/edit-config');
        var edit_configs = this.doc.findall('edit-config').concat(platform_edit_configs);

        return edit_configs.map(function (tag) {
            var editConfig =
                {
                    file: tag.attrib['file'],
                    target: tag.attrib['target'],
                    mode: tag.attrib['mode'],
                    id: 'config.xml',
                    xmls: tag.getchildren()
                };
            return editConfig;
        });
    },

    /* Get all config-file tags */
    getConfigFiles: function (platform) {
        var platform_config_files = this.doc.findall('./platform[@name="' + platform + '"]/config-file');
        var config_files = this.doc.findall('config-file').concat(platform_config_files);

        return config_files.map(function (tag) {
            var configFile =
                {
                    target: tag.attrib['target'],
                    parent: tag.attrib['parent'],
                    after: tag.attrib['after'],
                    xmls: tag.getchildren(),
                    // To support demuxing via versions
                    versions: tag.attrib['versions'],
                    deviceTarget: tag.attrib['device-target']
                };
            return configFile;
        });
    },

    write: function () {
        fs.writeFileSync(this.path, this.doc.write({ indent: 4 }), 'utf-8');
    }
};

function featureToPlugin (featureElement) {
    var plugin = {};
    plugin.variables = [];
    var pluginVersion,
        pluginSrc;

    var nodes = featureElement.findall('param');
    nodes.forEach(function (element) {
        var n = element.attrib.name;
        var v = element.attrib.value;
        if (n === 'id') {
            plugin.name = v;
        } else if (n === 'version') {
            pluginVersion = v;
        } else if (n === 'url' || n === 'installPath') {
            pluginSrc = v;
        } else {
            plugin.variables[n] = v;
        }
    });

    var spec = pluginSrc || pluginVersion;
    if (spec) {
        plugin.spec = spec;
    }

    return plugin;
}
module.exports = ConfigParser;
