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 () {
        // 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;
