var Q = require('q');
var path = require('path');
var fs = require('../../util/fs');
var object = require('mout/object');

var semver = require('../../util/semver');
var createError = require('../../util/createError');
var readJson = require('../../util/readJson');
var removeIgnores = require('../../util/removeIgnores');

function pluginResolverFactory(resolverFactory, bower) {
    bower = bower || {};

    if (typeof resolverFactory !== 'function') {
        throw createError('Resolver has "' + typeof resolverFactory + '" type instead of "function" type.', 'ERESOLERAPI');
    }

    var resolver = resolverFactory(bower);

    function maxSatisfyingVersion(versions, target) {
        var versionsArr, index;

        versionsArr = versions.map(function (obj) { return obj.version; });

        // Find a satisfying version, enabling strict match so that pre-releases
        // have lower priority over normal ones when target is *
        index = semver.maxSatisfyingIndex(versionsArr, target, true);

        if (index !== -1) {
            return versions[index];
        }
    }

    function PluginResolver(decEndpoint) {
        this._decEndpoint = decEndpoint;
    }

    // @private
    PluginResolver.prototype.getEndpoint = function () {
        return object.merge(this._decEndpoint, {
            name: this.getName(),
            source: this.getSource(),
            target: this.getTarget()
        });
    };

    PluginResolver.prototype.getSource = function () {
        return this._decEndpoint.source;
    };

    PluginResolver.prototype.getTarget = function () {
        return this._decEndpoint.target || '*';
    };

    PluginResolver.prototype.getName = function () {
        if (!this._decEndpoint.name && typeof resolver.getName === 'function') {
            return resolver.getName.call(resolver, this.getSource());
        } else if (!this._decEndpoint.name) {
            return path.basename(this.getSource());
        } else {
            return this._decEndpoint.name;
        }
    };

    PluginResolver.prototype.getPkgMeta = function () {
        return this._pkgMeta;
    };

    // -----------------

    // Plugin Resolver is always considered potentially cacheable
    // The "resolve" method decides whether to use cached or fetch new version.
    PluginResolver.prototype.isCacheable = function () {
        return true;
    };

    // Not only it's always potentially cacheable, but also always potenially new.
    // The "resolve" handles logic of re-downloading target if needed.
    PluginResolver.prototype.hasNew = function (pkgMeta) {
        if (this.hasNewPromise) {
            return this.hasNewPromise;
        }

        this._pkgMeta = pkgMeta;

        return this.hasNewPromise = this.resolve().then(function (result) {
            return result !== undefined;
        });
    };

    PluginResolver.prototype.resolve = function () {
        if (this.resolvePromise) {
            return this.resolvePromise;
        }

        var that = this;

        return this.resolvePromise = Q.fcall(function () {
            var target = that.getTarget();

            // It means that we can accept ranges as targets
            if (that.constructor.isTargetable()) {
                that._release = target;

                if (semver.validRange(target)) {
                    return Q.fcall(resolver.releases.bind(resolver), that.getSource())
                    .then(function (result) {
                        if (!result) {
                            throw createError('Resolver did not provide releases of package.');
                        }

                        var releases = that._releases = result;

                        var versions = releases.filter(function (target) {
                            return semver.clean(target.version);
                        });

                        var maxRelease = maxSatisfyingVersion(versions, target);

                        if (maxRelease) {
                            that._version = maxRelease.version;
                            that._release = that._decEndpoint.target = maxRelease.target;
                        } else {
                            throw createError('No version found that was able to satisfy ' + target, 'ENORESTARGET', {
                                details: !versions.length ?
                                    'No versions found in ' + that.getSource() :
                                    'Available versions: ' + versions.map(function (version) { return version.version; }).join(', ')
                            });
                        }
                    });
                }
            } else {
                if (semver.validRange(target) && target !== '*') {
                    return Q.reject(createError('Resolver does not accept version ranges (' + target + ')'));
                }
            }
        })
        .then(function () {

            // We pass old _resolution (if hasNew has been called before contents).
            // So resolver can decide wheter use cached version of contents new one.
            if (typeof resolver.fetch !== 'function') {
                throw createError('Resolver does not implement the "fetch" method.');
            }

            var cached = {};

            if (that._releases) {
                cached.releases = that._releases;
            }

            if (that._pkgMeta) {
                cached.endpoint = {
                    name: that._pkgMeta.name,
                    source: that._pkgMeta._source,
                    target: that._pkgMeta._target
                };

                cached.release = that._pkgMeta._release;

                cached.version = that._pkgMeta.version;

                cached.resolution = that._pkgMeta._resolution || {};
            }

            return Q.fcall(resolver.fetch.bind(resolver), that.getEndpoint(), cached);
        })
        .then(function (result) {
            // Empty result means to re-use existing resolution
            if (!result) {
                return;
            } else {
                if (!result.tempPath) {
                    throw createError('Resolver did not provide path to extracted contents of package.');
                }

                that._tempDir = result.tempPath;

                return that._readJson(that._tempDir).then(function (meta) {
                    return that._applyPkgMeta(meta, result)
                    .then(that._savePkgMeta.bind(that, meta, result))
                    .then(function () {
                        return that._tempDir;
                    });
                });
            }
        });
    };

    PluginResolver.prototype._readJson = function (dir) {
        var that = this;

        return readJson(dir, {
            assume: { name: that.getName() },
            logger: bower.logger
        })
        .spread(function (json, deprecated) {
            if (deprecated) {
                bower.logger.warn('deprecated', 'Package ' + that.getName() + ' is using the deprecated ' + deprecated);
            }

            return json;
        });
    };

    PluginResolver.prototype._applyPkgMeta = function (meta, result) {
        // Check if name defined in the json is different
        // If so and if the name was "guessed", assume the json name
        if (meta.name !== this._name) {
            this._name = meta.name;
        }

        // Handle ignore property, deleting all files from the temporary directory
        // If no ignores were specified, simply resolve
        if (result.removeIgnores === false || !meta.ignore || !meta.ignore.length) {
            return Q.resolve(meta);
        }

        // Otherwise remove them from the temp dir
        return removeIgnores(this._tempDir, meta).then(function () {
            return meta;
        });
    };

    PluginResolver.prototype._savePkgMeta = function (meta, result) {
        var that = this;

        meta._source = that.getSource();
        meta._target = that.getTarget();

        if (result.resolution) {
            meta._resolution = result.resolution;
        }

        if (that._release) {
            meta._release = that._release;
        }

        if (that._version) {
            meta.version = that._version;
        } else {
            delete meta.version;
        }

        // Stringify contents
        var contents = JSON.stringify(meta, null, 2);

        return Q.nfcall(fs.writeFile, path.join(this._tempDir, '.bower.json'), contents)
        .then(function () {
            return that._pkgMeta = meta;
        });
    };

    // It is used only by "bower info". It returns all semver versions.
    PluginResolver.versions = function (source) {
        return Q.fcall(resolver.releases.bind(resolver), source).then(function (result) {
            if (!result) {
                throw createError('Resolver did not provide releases of package.');
            }

            var releases = this._releases = result;

            var versions = releases.map(function (version) {
                return semver.clean(version.version);
            });

            versions = versions.filter(function (version) {
                return version;
            });

            versions.sort(function (a, b) {
                return semver.rcompare(a, b);
            });

            return versions;
        });
    };

    PluginResolver.isTargetable = function () {
        // If resolver doesn't define versions function, it's not targetable..
        return typeof resolver.releases === 'function';
    };

    PluginResolver.clearRuntimeCache = function () {
        resolver = resolverFactory(bower);
    };

    PluginResolver.match = function (source) {
        if (typeof resolver.match !== 'function') {
            throw createError('Resolver is missing "match" method.', 'ERESOLVERAPI');
        }

        var match = resolver.match.bind(resolver);

        return Q.fcall(match, source).then(function (result) {
            if (typeof result !== 'boolean') {
                throw createError('Resolver\'s "match" method should return a boolean', 'ERESOLVERAPI');
            }

            return result;
        });
    };

    PluginResolver.locate = function (source) {
        if (typeof resolver.locate !== 'function') {
            return source;
        }

        return Q.fcall(resolver.locate.bind(resolver), source).then(function (result) {
            if (typeof result !== 'string') {
                throw createError('Resolver\'s "locate" method should return a string', 'ERESOLVERAPI');
            }

            return result;
        });
    };

    return PluginResolver;
}


module.exports = pluginResolverFactory;

