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

/*
A class for holidng the information currently stored in plugin.xml
It should also be able to answer questions like whether the plugin
is compatible with a given engine version.

TODO (kamrik): refactor this to not use sync functions and return promises.
*/

var path = require('path');
var fs = require('fs-extra');
var xml_helpers = require('../util/xml-helpers');
var CordovaError = require('../CordovaError/CordovaError');

function PluginInfo (dirname) {
    var self = this;

    // METHODS
    // Defined inside the constructor to avoid the "this" binding problems.

    // <preference> tag
    // Example: <preference name="API_KEY" />
    // Used to require a variable to be specified via --variable when installing the plugin.
    // returns { key : default | null}
    self.getPreferences = getPreferences;
    function getPreferences (platform) {
        return _getTags(self._et, 'preference', platform, _parsePreference)
            .reduce(function (preferences, pref) {
                preferences[pref.preference] = pref.default;
                return preferences;
            }, {});
    }

    function _parsePreference (prefTag) {
        var name = prefTag.attrib.name.toUpperCase();
        var def = prefTag.attrib.default || null;
        return { preference: name, default: def };
    }

    // <asset>
    self.getAssets = getAssets;
    function getAssets (platform) {
        var assets = _getTags(self._et, 'asset', platform, _parseAsset);
        return assets;
    }

    function _parseAsset (tag) {
        var src = tag.attrib.src;
        var target = tag.attrib.target;

        if (!src || !target) {
            var msg =
                'Malformed <asset> tag. Both "src" and "target" attributes' +
                'must be specified in\n' +
                self.filepath
                ;
            throw new Error(msg);
        }

        var asset = {
            itemType: 'asset',
            src: src,
            target: target
        };
        return asset;
    }

    // <dependency>
    // Example:
    // <dependency id="com.plugin.id"
    //     url="https://github.com/myuser/someplugin"
    //     commit="428931ada3891801"
    //     subdir="some/path/here" />
    self.getDependencies = getDependencies;
    function getDependencies (platform) {
        var deps = _getTags(
            self._et,
            'dependency',
            platform,
            _parseDependency
        );
        return deps;
    }

    function _parseDependency (tag) {
        var dep =
            { id: tag.attrib.id,
                version: tag.attrib.version || '',
                url: tag.attrib.url || '',
                subdir: tag.attrib.subdir || '',
                commit: tag.attrib.commit
            };

        dep.git_ref = dep.commit;

        if (!dep.id) {
            var msg =
                '<dependency> tag is missing id attribute in ' +
                self.filepath
                ;
            throw new CordovaError(msg);
        }
        return dep;
    }

    // <config-file> tag
    self.getConfigFiles = getConfigFiles;
    function getConfigFiles (platform) {
        var configFiles = _getTags(self._et, 'config-file', platform, _parseConfigFile);
        return configFiles;
    }

    function _parseConfigFile (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;
    }

    self.getEditConfigs = getEditConfigs;
    function getEditConfigs (platform) {
        var editConfigs = _getTags(self._et, 'edit-config', platform, _parseEditConfigs);
        return editConfigs;
    }

    function _parseEditConfigs (tag) {
        var editConfig =
            { file: tag.attrib['file'],
                target: tag.attrib['target'],
                mode: tag.attrib['mode'],
                xmls: tag.getchildren()
            };
        return editConfig;
    }

    // <info> tags, both global and within a <platform>
    // TODO (kamrik): Do we ever use <info> under <platform>? Example wanted.
    self.getInfo = getInfo;
    function getInfo (platform) {
        var infos = _getTags(
            self._et,
            'info',
            platform,
            function (elem) { return elem.text; }
        );
        // Filter out any undefined or empty strings.
        infos = infos.filter(Boolean);
        return infos;
    }

    // <source-file>
    // Examples:
    // <source-file src="src/ios/someLib.a" framework="true" />
    // <source-file src="src/ios/someLib.a" compiler-flags="-fno-objc-arc" />
    self.getSourceFiles = getSourceFiles;
    function getSourceFiles (platform) {
        var sourceFiles = _getTagsInPlatform(self._et, 'source-file', platform, _parseSourceFile);
        return sourceFiles;
    }

    function _parseSourceFile (tag) {
        return {
            itemType: 'source-file',
            src: tag.attrib.src,
            framework: isStrTrue(tag.attrib.framework),
            weak: isStrTrue(tag.attrib.weak),
            compilerFlags: tag.attrib['compiler-flags'],
            targetDir: tag.attrib['target-dir']
        };
    }

    // <header-file>
    // Example:
    // <header-file src="CDVFoo.h" />
    self.getHeaderFiles = getHeaderFiles;
    function getHeaderFiles (platform) {
        var headerFiles = _getTagsInPlatform(self._et, 'header-file', platform, function (tag) {
            return {
                itemType: 'header-file',
                src: tag.attrib.src,
                targetDir: tag.attrib['target-dir'],
                type: tag.attrib['type']
            };
        });
        return headerFiles;
    }

    // <resource-file>
    // Example:
    // <resource-file src="FooPluginStrings.xml" target="res/values/FooPluginStrings.xml" device-target="win" arch="x86" versions="&gt;=8.1" />
    self.getResourceFiles = getResourceFiles;
    function getResourceFiles (platform) {
        var resourceFiles = _getTagsInPlatform(self._et, 'resource-file', platform, function (tag) {
            return {
                itemType: 'resource-file',
                src: tag.attrib.src,
                target: tag.attrib.target,
                versions: tag.attrib.versions,
                deviceTarget: tag.attrib['device-target'],
                arch: tag.attrib.arch,
                reference: tag.attrib.reference
            };
        });
        return resourceFiles;
    }

    // <lib-file>
    // Example:
    // <lib-file src="src/BlackBerry10/native/device/libfoo.so" arch="device" />
    self.getLibFiles = getLibFiles;
    function getLibFiles (platform) {
        var libFiles = _getTagsInPlatform(self._et, 'lib-file', platform, function (tag) {
            return {
                itemType: 'lib-file',
                src: tag.attrib.src,
                arch: tag.attrib.arch,
                Include: tag.attrib.Include,
                versions: tag.attrib.versions,
                deviceTarget: tag.attrib['device-target'] || tag.attrib.target
            };
        });
        return libFiles;
    }

    // <podspec>
    // Example:
    // <podspec>
    //   <config>
    //     <source url="https://github.com/brightcove/BrightcoveSpecs.git" />
    //     <source url="https://github.com/CocoaPods/Specs.git"/>
    //   </config>
    //   <pods use-frameworks="true" inhibit-all-warnings="true">
    //     <pod name="PromiseKit" />
    //     <pod name="Foobar1" spec="~> 2.0.0" />
    //     <pod name="Foobar2" git="git@github.com:hoge/foobar1.git" />
    //     <pod name="Foobar3" git="git@github.com:hoge/foobar2.git" branch="next" />
    //     <pod name="Foobar4" swift-version="4.1" />
    //     <pod name="Foobar5" swift-version="3.0" />
    //   </pods>
    // </podspec>
    self.getPodSpecs = getPodSpecs;
    function getPodSpecs (platform) {
        var podSpecs = _getTagsInPlatform(self._et, 'podspec', platform, function (tag) {
            var declarations = null;
            var sources = null;
            var libraries = null;
            var config = tag.find('config');
            var pods = tag.find('pods');
            if (config != null) {
                sources = config.findall('source').map(function (t) {
                    return {
                        url: t.attrib.url
                    };
                }).reduce(function (acc, val) {
                    return Object.assign({}, acc, { [val.url]: { source: val.url } });
                }, {});
            }
            if (pods != null) {
                declarations = Object.keys(pods.attrib).reduce(function (acc, key) {
                    return pods.attrib[key] === undefined ? acc : Object.assign({}, acc, { [key]: pods.attrib[key] });
                }, {});
                libraries = pods.findall('pod').map(function (t) {
                    return Object.keys(t.attrib).reduce(function (acc, key) {
                        return t.attrib[key] === undefined ? acc : Object.assign({}, acc, { [key]: t.attrib[key] });
                    }, {});
                }).reduce(function (acc, val) {
                    return Object.assign({}, acc, { [val.name]: val });
                }, {});
            }
            return {
                declarations: declarations,
                sources: sources,
                libraries: libraries
            };
        });
        return podSpecs;
    }

    // <hook>
    // Example:
    // <hook type="before_build" src="scripts/beforeBuild.js" />
    self.getHookScripts = getHookScripts;
    function getHookScripts (hook, platforms) {
        var scriptElements = self._et.findall('./hook');

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

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

        return scriptElements.filter(filterScriptByHookType);
    }

    self.getJsModules = getJsModules;
    function getJsModules (platform) {
        var modules = _getTags(self._et, 'js-module', platform, _parseJsModule);
        return modules;
    }

    function _parseJsModule (tag) {
        var ret = {
            itemType: 'js-module',
            name: tag.attrib.name,
            src: tag.attrib.src,
            clobbers: tag.findall('clobbers').map(function (tag) { return { target: tag.attrib.target }; }),
            merges: tag.findall('merges').map(function (tag) { return { target: tag.attrib.target }; }),
            runs: tag.findall('runs').length > 0
        };

        return ret;
    }

    self.getEngines = function () {
        return self._et.findall('engines/engine').map(function (n) {
            return {
                name: n.attrib.name,
                version: n.attrib.version,
                platform: n.attrib.platform,
                scriptSrc: n.attrib.scriptSrc
            };
        });
    };

    self.getPlatforms = function () {
        return self._et.findall('platform').map(function (n) {
            return { name: n.attrib.name };
        });
    };

    self.getPlatformsArray = function () {
        return self._et.findall('platform').map(function (n) {
            return n.attrib.name;
        });
    };

    self.getFrameworks = function (platform, options) {
        return _getTags(self._et, 'framework', platform, function (el) {
            var src = el.attrib.src;
            if (options) {
                var vars = options.cli_variables || {};

                if (Object.keys(vars).length === 0) {
                    // get variable defaults from plugin.xml for removal
                    vars = self.getPreferences(platform);
                }
                var regExp;
                // Iterate over plugin variables.
                // Replace them in framework src if they exist
                Object.keys(vars).forEach(function (name) {
                    if (vars[name]) {
                        regExp = new RegExp('\\$' + name, 'g');
                        src = src.replace(regExp, vars[name]);
                    }
                });
            }
            var ret = {
                itemType: 'framework',
                type: el.attrib.type,
                parent: el.attrib.parent,
                custom: isStrTrue(el.attrib.custom),
                embed: isStrTrue(el.attrib.embed),
                src: src,
                spec: el.attrib.spec,
                weak: isStrTrue(el.attrib.weak),
                versions: el.attrib.versions,
                targetDir: el.attrib['target-dir'],
                deviceTarget: el.attrib['device-target'] || el.attrib.target,
                arch: el.attrib.arch,
                implementation: el.attrib.implementation
            };
            return ret;
        });
    };

    self.getFilesAndFrameworks = getFilesAndFrameworks;
    function getFilesAndFrameworks (platform, options) {
        // Please avoid changing the order of the calls below, files will be
        // installed in this order.
        var items = [].concat(
            self.getSourceFiles(platform),
            self.getHeaderFiles(platform),
            self.getResourceFiles(platform),
            self.getFrameworks(platform, options),
            self.getLibFiles(platform)
        );
        return items;
    }
    /// // End of PluginInfo methods /////

    /// // PluginInfo Constructor logic  /////
    self.filepath = path.join(dirname, 'plugin.xml');
    if (!fs.existsSync(self.filepath)) {
        throw new CordovaError('Cannot find plugin.xml for plugin "' + path.basename(dirname) + '". Please try adding it again.');
    }

    self.dir = dirname;
    var et = self._et = xml_helpers.parseElementtreeSync(self.filepath);
    var pelem = et.getroot();
    self.id = pelem.attrib.id;
    self.version = pelem.attrib.version;

    // Optional fields
    self.name = pelem.findtext('name');
    self.description = pelem.findtext('description');
    self.license = pelem.findtext('license');
    self.repo = pelem.findtext('repo');
    self.issue = pelem.findtext('issue');
    self.keywords = pelem.findtext('keywords');
    self.info = pelem.findtext('info');
    if (self.keywords) {
        self.keywords = self.keywords.split(',').map(function (s) { return s.trim(); });
    }
    self.getKeywordsAndPlatforms = function () {
        var ret = self.keywords || [];
        return ret.concat('ecosystem:cordova').concat(addCordova(self.getPlatformsArray()));
    };
} // End of PluginInfo constructor.

// Helper function used to prefix every element of an array with cordova-
// Useful when we want to modify platforms to be cordova-platform
function addCordova (someArray) {
    var newArray = someArray.map(function (element) {
        return 'cordova-' + element;
    });
    return newArray;
}

// Helper function used by most of the getSomething methods of PluginInfo.
// Get all elements of a given name. Both in root and in platform sections
// for the given platform. If transform is given and is a function, it is
// applied to each element.
function _getTags (pelem, tag, platform, transform) {
    var platformTag = pelem.find('./platform[@name="' + platform + '"]');
    var tagsInRoot = pelem.findall(tag);
    tagsInRoot = tagsInRoot || [];
    var tagsInPlatform = platformTag ? platformTag.findall(tag) : [];
    var tags = tagsInRoot.concat(tagsInPlatform);
    if (typeof transform === 'function') {
        tags = tags.map(transform);
    }
    return tags;
}

// Same as _getTags() but only looks inside a platform section.
function _getTagsInPlatform (pelem, tag, platform, transform) {
    var platformTag = pelem.find('./platform[@name="' + platform + '"]');
    var tags = platformTag ? platformTag.findall(tag) : [];
    if (typeof transform === 'function') {
        tags = tags.map(transform);
    }
    return tags;
}

// Check if x is a string 'true'.
function isStrTrue (x) {
    return String(x).toLowerCase() === 'true';
}

module.exports = PluginInfo;
// Backwards compat:
PluginInfo.PluginInfo = PluginInfo;
PluginInfo.loadPluginsDir = function (dir) {
    var PluginInfoProvider = require('./PluginInfoProvider');
    return new PluginInfoProvider().getAllWithinSearchPath(dir);
};
