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

const fs = require('fs-extra');
const url = require('url');
const semver = require('semver');
const PluginInfoProvider = require('cordova-common').PluginInfoProvider;
const CordovaError = require('cordova-common').CordovaError;
const events = require('cordova-common').events;
const metadata = require('./util/metadata');
const path = require('path');
const pluginSpec = require('../cordova/plugin/plugin_spec_parser');
const fetch = require('cordova-fetch');
const cordovaUtil = require('../cordova/util');

let projectRoot;

// Cache of PluginInfo objects for plugins in search path.
let 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);
    }

    const 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) {
        const result = uri.hash.match(/^#([^:]*)(?::\/?(.*?)\/?)?$/);
        if (result) {
            if (result[1]) { options.git_ref = result[1]; }
            if (result[2]) { options.subdir = result[2]; }
        }
    }
    return Promise.resolve().then(function () {
        let 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).
            const pinfo = findLocalPlugin(plugin_src, options.searchpath, pluginInfoProvider);
            if (pinfo) {
                events.emit('verbose', 'Found ' + plugin_src + ' at ' + pinfo.dir);
                return {
                    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.
            const parsedSpec = pluginSpec.parse(plugin_src);
            let P;
            let 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) {
                    const 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
                    };
                });
        }).then(function (result) {
            options.plugin_src_dir = result.pinfo.dir;
            let 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);
        const 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;

    const 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 (localPlugins.searchpath.join(path.delimiter) !== searchpath.join(path.delimiter)) {
            const 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) {
        const ps = pluginInfoProvider.getAllWithinSearchPath(dir);
        ps.forEach(function (p) {
            const 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);
    const parsedSpec = pluginSpec.parse(plugin_src);
    const versionspec = parsedSpec.version || '*';

    let latest = null;
    const 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) {
    const plugin_dir = pinfo.dir;
    const 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) {
        const isRelativePath = plugin_dir.charAt(1) !== ':' && plugin_dir.charAt(0) !== path.sep;
        const 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;
}
