var Q = require('q');
var fs = require('../util/fs');
var path = require('path');
var mout = require('mout');
var resolvers = require('./resolvers');
var createError = require('../util/createError');
var resolve = require('../util/resolve');

var pluginResolverFactory = require('./resolvers/pluginResolverFactory');

function createInstance(decEndpoint, options, registryClient) {
    decEndpoint = mout.object.pick(decEndpoint, ['name', 'target', 'source']);

    options.version = require('../version');

    return getConstructor(decEndpoint, options, registryClient).spread(function(
        ConcreteResolver,
        decEndpoint
    ) {
        return new ConcreteResolver(
            decEndpoint,
            options.config,
            options.logger
        );
    });
}

function getConstructor(decEndpoint, options, registryClient) {
    var source = decEndpoint.source;
    var config = options.config;

    // Below we try a series of async tests to guess the type of resolver to use
    // If a step was unable to guess the resolver, it returns undefined
    // If a step can guess the resolver, it returns with constructor of resolver

    var promise = Q.resolve();

    var addResolver = function(resolverFactory) {
        promise = promise.then(function(result) {
            if (result === undefined) {
                return resolverFactory(decEndpoint, options);
            } else {
                return result;
            }
        });
    };

    // Plugin resolvers.
    //
    // It requires each resolver defined in config.resolvers and calls
    // its "match" to check if given resolves supports given decEndpoint
    addResolver(function() {
        var selectedResolver;
        var resolverNames;

        if (Array.isArray(config.resolvers)) {
            resolverNames = config.resolvers;
        } else if (!!config.resolvers) {
            resolverNames = config.resolvers.split(',');
        } else {
            resolverNames = [];
        }

        var resolverPromises = resolverNames.map(function(resolverName) {
            var resolver = resolvers[resolverName];

            if (resolver === undefined) {
                var resolverPath = resolve(resolverName, { cwd: config.cwd });

                if (resolverPath === undefined) {
                    throw createError(
                        'Bower resolver not found: ' + resolverName,
                        'ENORESOLVER'
                    );
                }

                resolver = pluginResolverFactory(
                    require(resolverPath),
                    options
                );
            }

            return function() {
                if (selectedResolver === undefined) {
                    var match = resolver.match.bind(resolver);

                    return Q.fcall(match, source).then(function(result) {
                        if (result) {
                            return (selectedResolver = resolver);
                        }
                    });
                } else {
                    return selectedResolver;
                }
            };
        });

        return resolverPromises
            .reduce(Q.when, new Q(undefined))
            .then(function(resolver) {
                if (resolver) {
                    return Q.fcall(
                        resolver.locate.bind(resolver),
                        decEndpoint.source
                    ).then(function(result) {
                        if (result && result !== decEndpoint.source) {
                            decEndpoint.source = result;
                            decEndpoint.registry = true;
                            return getConstructor(
                                decEndpoint,
                                options,
                                registryClient
                            );
                        } else {
                            return [resolver, decEndpoint];
                        }
                    });
                }
            });
    });

    // Git case: git git+ssh, git+http, git+https
    //           .git at the end (probably ssh shorthand)
    //           git@ at the start
    addResolver(function() {
        if (
            /^git(\+(ssh|https?))?:\/\//i.test(source) ||
            /\.git\/?$/i.test(source) ||
            /^git@/i.test(source)
        ) {
            decEndpoint.source = source.replace(/^git\+/, '');

            // If it's a GitHub repository, return the specialized resolver
            if (resolvers.GitHub.getOrgRepoPair(source)) {
                return [resolvers.GitHub, decEndpoint];
            }

            return [resolvers.GitRemote, decEndpoint];
        }
    });

    // SVN case: svn, svn+ssh, svn+http, svn+https, svn+file
    addResolver(function() {
        if (/^svn(\+(ssh|https?|file))?:\/\//i.test(source)) {
            return [resolvers.Svn, decEndpoint];
        }
    });

    // URL case
    addResolver(function() {
        if (/^https?:\/\//i.exec(source)) {
            return [resolvers.Url, decEndpoint];
        }
    });

    // If source is ./ or ../ or an absolute path

    addResolver(function() {
        var absolutePath = path.resolve(config.cwd, source);

        if (
            /^\.\.?[\/\\]/.test(source) ||
            /^~\//.test(source) ||
            path.normalize(source).replace(/[\/\\]+$/, '') === absolutePath
        ) {
            return (
                Q.nfcall(fs.stat, path.join(absolutePath, '.git'))
                    .then(function(stats) {
                        decEndpoint.source = absolutePath;

                        if (stats.isDirectory()) {
                            return Q.resolve([resolvers.GitFs, decEndpoint]);
                        }

                        throw new Error('Not a Git repository');
                    })
                    // If not, check if source is a valid Subversion repository
                    .fail(function() {
                        return Q.nfcall(
                            fs.stat,
                            path.join(absolutePath, '.svn')
                        ).then(function(stats) {
                            decEndpoint.source = absolutePath;

                            if (stats.isDirectory()) {
                                return Q.resolve([resolvers.Svn, decEndpoint]);
                            }

                            throw new Error('Not a Subversion repository');
                        });
                    })
                    // If not, check if source is a valid file/folder
                    .fail(function() {
                        return Q.nfcall(fs.stat, absolutePath).then(function() {
                            decEndpoint.source = absolutePath;

                            return Q.resolve([resolvers.Fs, decEndpoint]);
                        });
                    })
            );
        }
    });

    // Check if is a shorthand and expand it
    addResolver(function() {
        // Check if the shorthandResolver is falsy
        if (!config.shorthandResolver) {
            return;
        }

        // Skip ssh and/or URL with auth
        if (/[:@]/.test(source)) {
            return;
        }

        // Ensure exactly only one "/"
        var parts = source.split('/');
        if (parts.length === 2) {
            decEndpoint.source = mout.string.interpolate(
                config.shorthandResolver,
                {
                    shorthand: source,
                    owner: parts[0],
                    package: parts[1]
                }
            );

            return getConstructor(decEndpoint, options, registryClient);
        }
    });

    // As last resort, we try the registry
    addResolver(function() {
        if (!registryClient) {
            return;
        }

        return Q.nfcall(
            registryClient.lookup.bind(registryClient),
            source
        ).then(function(entry) {
            if (!entry) {
                throw createError(
                    'Package ' + source + ' not found',
                    'ENOTFOUND'
                );
            }

            decEndpoint.registry = true;

            if (!decEndpoint.name) {
                decEndpoint.name = decEndpoint.source;
            }

            decEndpoint.source = entry.url;

            return getConstructor(decEndpoint, options);
        });
    });

    addResolver(function() {
        throw createError(
            'Could not find appropriate resolver for ' + source,
            'ENORESOLVER'
        );
    });

    return promise;
}

function clearRuntimeCache() {
    mout.object.values(resolvers).forEach(function(ConcreteResolver) {
        ConcreteResolver.clearRuntimeCache();
    });
}

module.exports = createInstance;
module.exports.getConstructor = getConstructor;
module.exports.clearRuntimeCache = clearRuntimeCache;
