var Q = require('q');
var mout = require('mout');
var path = require('path');
var mkdirp = require('mkdirp');
var rimraf = require('../util/rimraf');
var fs = require('../util/fs');
var endpointParser = require('bower-endpoint-parser');
var PackageRepository = require('./PackageRepository');
var semver = require('../util/semver');
var copy = require('../util/copy');
var createError = require('../util/createError');
var scripts = require('./scripts');
var relativeToBaseDir = require('../util/relativeToBaseDir');

function Manager(config, logger) {
    this._config = config;
    this._logger = logger;
    this._repository = new PackageRepository(this._config, this._logger);

    this.configure({});
}

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

Manager.prototype.configure = function(setup) {
    var targetsHash = {};
    this._conflicted = {};

    // Targets - ignore those specified in ignoredDependencies
    this._targets = mout.array.reject(
        setup.targets || [],
        function(target) {
            return mout.array.contains(
                this._config.ignoredDependencies,
                target.name
            );
        },
        this
    );

    this._targets.forEach(function(decEndpoint) {
        decEndpoint.initialName = decEndpoint.name;
        decEndpoint.dependants = mout.object.values(decEndpoint.dependants);
        targetsHash[decEndpoint.name] = true;

        // If the endpoint is marked as newly, make it unresolvable
        decEndpoint.unresolvable = !!decEndpoint.newly;
    });

    // Resolved & installed
    this._resolved = {};
    this._installed = {};
    mout.object.forOwn(
        setup.resolved,
        function(decEndpoint, name) {
            decEndpoint.dependants = mout.object.values(decEndpoint.dependants);
            this._resolved[name] = [decEndpoint];
            this._installed[name] = decEndpoint.pkgMeta;
        },
        this
    );

    // Installed
    mout.object.mixIn(this._installed, setup.installed);

    // Incompatibles
    this._incompatibles = {};
    setup.incompatibles = this._uniquify(setup.incompatibles || []);
    setup.incompatibles.forEach(function(decEndpoint) {
        var name = decEndpoint.name;

        this._incompatibles[name] = this._incompatibles[name] || [];
        this._incompatibles[name].push(decEndpoint);
        decEndpoint.dependants = mout.object.values(decEndpoint.dependants);

        // Mark as conflicted so that the resolution is not removed
        this._conflicted[name] = true;

        // If not a target/resolved, add as target
        if (!targetsHash[name] && !this._resolved[name]) {
            this._targets.push(decEndpoint);
        }
    }, this);

    // Resolutions
    this._resolutions = setup.resolutions || {};

    // Uniquify targets
    this._targets = this._uniquify(this._targets);

    // Force-latest
    this._forceLatest = !!setup.forceLatest;

    return this;
};

/**
 * It starts recursive fetching of targets. The flow is as follows:
 *
 * 1. this._fetch() that calls:
 * 2. this._onFetchError() or this._onFetchSuccess() that calls:
 * 3. this._parseDependencies() that calls for all dependencies:
 * 4. this._fetch() again.
 *
 * Fetching and parsing of dependencies continues until all dependencies are fetched.
 *
 */
Manager.prototype.resolve = function() {
    // If already resolving, error out
    if (this._working) {
        return Q.reject(createError('Already working', 'EWORKING'));
    }

    // Reset stuff
    this._fetching = {};
    this._nrFetching = 0;
    this._failed = {};
    this._hasFailed = false;
    this._deferred = Q.defer();

    // If there's nothing to resolve, simply dissect
    if (!this._targets.length) {
        process.nextTick(this._dissect.bind(this));
        // Otherwise, fetch each target from the repository
        // and let the process roll out
    } else {
        this._targets.forEach(this._fetch.bind(this));
    }

    // Unset working flag when done
    return this._deferred.promise.fin(
        function() {
            this._working = false;
        }.bind(this)
    );
};

Manager.prototype.preinstall = function(json) {
    var that = this;
    var componentsDir = relativeToBaseDir(this._config.cwd)(
        this._config.directory
    );

    // If nothing to install, skip the code bellow
    if (mout.lang.isEmpty(that._dissected)) {
        return Q.resolve({});
    }

    return Q.nfcall(mkdirp, componentsDir).then(function() {
        return scripts.preinstall(
            that._config,
            that._logger,
            that._dissected,
            that._installed,
            json
        );
    });
};

Manager.prototype.postinstall = function(json) {
    var that = this;
    var componentsDir = relativeToBaseDir(this._config.cwd)(
        this._config.directory
    );

    // If nothing to install, skip the code bellow
    if (mout.lang.isEmpty(that._dissected)) {
        return Q.resolve({});
    }

    return Q.nfcall(mkdirp, componentsDir).then(function() {
        return scripts.postinstall(
            that._config,
            that._logger,
            that._dissected,
            that._installed,
            json
        );
    });
};

Manager.prototype.install = function(json) {
    var componentsDir;
    var componentsDirContents;
    var that = this;

    // If already resolving, error out
    if (this._working) {
        return Q.reject(createError('Already working', 'EWORKING'));
    }

    // If nothing to install, skip the code bellow
    if (mout.lang.isEmpty(that._dissected)) {
        return Q.resolve({});
    }

    componentsDir = relativeToBaseDir(this._config.cwd)(this._config.directory);
    return Q.nfcall(mkdirp, componentsDir)
        .then(function() {
            var promises = [];
            componentsDirContents = fs.readdirSync(componentsDir);

            mout.object.forOwn(that._dissected, function(decEndpoint, name) {
                var promise;
                var dst;
                var release = decEndpoint.pkgMeta._release;
                var exists = mout.array.contains(componentsDirContents, name);

                that._logger.action(
                    'install',
                    name + (release ? '#' + release : ''),
                    that.toData(decEndpoint)
                );

                dst = path.join(componentsDir, name);

                if (exists && !that._installed[name] && !that._config.force) {
                    // There is a folder in the components directory that has
                    // this name, but it was not installed by bower.
                    that._logger.warn(
                        'skipped',
                        name +
                            ' was not installed because there is already a non-bower directory with that name in the components directory (' +
                            path.relative(that._config.cwd, dst) +
                            '). You can force installation with --force.'
                    );
                    return;
                }

                // Remove existent and copy canonical dir
                promise = Q.nfcall(rimraf, dst)
                    .then(
                        copy.copyDir.bind(copy, decEndpoint.canonicalDir, dst)
                    )
                    .then(function() {
                        var metaFile = path.join(dst, '.bower.json');

                        decEndpoint.canonicalDir = dst;

                        // Store additional metadata in bower.json
                        return Q.nfcall(fs.readFile, metaFile).then(function(
                            contents
                        ) {
                            var json = JSON.parse(contents.toString());

                            json._target = decEndpoint.target;
                            json._originalSource = decEndpoint.source;
                            if (decEndpoint.newly) {
                                json._direct = true;
                            }

                            json = JSON.stringify(json, null, '  ');
                            return Q.nfcall(fs.writeFile, metaFile, json);
                        });
                    });

                promises.push(promise);
            });

            return Q.all(promises);
        })
        .then(function() {
            // Sync up dissected dependencies and dependants
            // See: https://github.com/bower/bower/issues/879
            mout.object.forOwn(
                that._dissected,
                function(pkg) {
                    // Sync dependencies
                    mout.object.forOwn(
                        pkg.dependencies,
                        function(dependency, name) {
                            var dissected =
                                this._dissected[name] ||
                                (this._resolved[name]
                                    ? this._resolved[name][0]
                                    : dependency);
                            pkg.dependencies[name] = dissected;
                        },
                        this
                    );

                    // Sync dependants
                    pkg.dependants = pkg.dependants.map(function(dependant) {
                        var name = dependant.name;
                        var dissected =
                            this._dissected[name] ||
                            (this._resolved[name]
                                ? this._resolved[name][0]
                                : dependant);

                        return dissected;
                    }, this);
                },
                that
            );

            // Resolve with meaningful data
            return mout.object.map(
                that._dissected,
                function(decEndpoint) {
                    return this.toData(decEndpoint);
                },
                that
            );
        })
        .fin(
            function() {
                this._working = false;
            }.bind(this)
        );
};

Manager.prototype.toData = function(decEndpoint, extraKeys, upperDeps) {
    var names;
    var extra;

    var data = {};

    upperDeps = upperDeps || [];
    data.endpoint = mout.object.pick(decEndpoint, ['name', 'source', 'target']);

    if (decEndpoint.canonicalDir) {
        data.canonicalDir = decEndpoint.canonicalDir;
        data.pkgMeta = decEndpoint.pkgMeta;
    }

    if (extraKeys) {
        extra = mout.object.pick(decEndpoint, extraKeys);
        extra = mout.object.filter(extra, function(value) {
            return !!value;
        });
        mout.object.mixIn(data, extra);
    }

    if (decEndpoint.dependencies) {
        data.dependencies = {};

        // Call recursively for each dependency but ordered
        // by dependency names
        names = Object.keys(decEndpoint.dependencies).sort();
        names.forEach(function(name) {
            // Prevent from infinite recursion when installing cyclic
            // dependencies
            if (!mout.array.contains(upperDeps, name)) {
                data.dependencies[name] = this.toData(
                    decEndpoint.dependencies[name],
                    extraKeys,
                    upperDeps.concat(decEndpoint.name)
                );
            }
        }, this);
    }

    data.nrDependants = mout.object.size(decEndpoint.dependants);

    return data;
};

Manager.prototype.getPackageRepository = function() {
    return this._repository;
};

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

Manager.prototype._fetch = function(decEndpoint) {
    var name = decEndpoint.name;

    // Check if the whole process started to fail fast
    if (this._hasFailed) {
        return;
    }

    // Mark as being fetched
    this._fetching[name] = this._fetching[name] || [];
    this._fetching[name].push(decEndpoint);
    this._nrFetching++;

    // Fetch it from the repository
    // Note that the promise is stored in the decomposed endpoint
    // because it might be reused if a similar endpoint needs to be resolved
    return (decEndpoint.promise = this._repository
        .fetch(decEndpoint)
        // When done, call onFetchSuccess
        .spread(this._onFetchSuccess.bind(this, decEndpoint))
        // If it fails, call onFetchFailure
        .fail(this._onFetchError.bind(this, decEndpoint)));
};

Manager.prototype._onFetchSuccess = function(
    decEndpoint,
    canonicalDir,
    pkgMeta,
    isTargetable
) {
    var name;
    var resolved;
    var index;
    var incompatibles;
    var initialName =
        decEndpoint.initialName != null
            ? decEndpoint.initialName
            : decEndpoint.name;
    var fetching = this._fetching[initialName];

    // Remove from being fetched list
    mout.array.remove(fetching, decEndpoint);
    this._nrFetching--;

    // Store some needed stuff
    decEndpoint.name = name = decEndpoint.name || pkgMeta.name;
    decEndpoint.canonicalDir = canonicalDir;
    decEndpoint.pkgMeta = pkgMeta;
    delete decEndpoint.promise;

    // Add to the resolved list
    // If there's an exact equal endpoint, replace instead of adding
    // This can happen because the name might not be known from the start
    resolved = this._resolved[name] = this._resolved[name] || [];
    index = mout.array.findIndex(resolved, function(resolved) {
        return resolved.target === decEndpoint.target;
    });
    if (index !== -1) {
        // Merge dependants
        decEndpoint.dependants.push.apply(
            decEndpoint.dependants,
            resolved[index.dependants]
        );
        decEndpoint.dependants = this._uniquify(decEndpoint.dependants);
        resolved.splice(index, 1);
    }
    resolved.push(decEndpoint);

    // Parse dependencies
    this._parseDependencies(decEndpoint, pkgMeta);

    // Check if there are incompatibilities for this package name
    // If there are, we need to fetch them
    incompatibles = this._incompatibles[name];
    if (incompatibles) {
        // Filter already resolved
        incompatibles = incompatibles.filter(function(incompatible) {
            return !resolved.some(function(decEndpoint) {
                return incompatible.target === decEndpoint.target;
            });
        }, this);
        // Filter being resolved
        incompatibles = incompatibles.filter(function(incompatible) {
            return !fetching.some(function(decEndpoint) {
                return incompatible.target === decEndpoint.target;
            });
        }, this);

        incompatibles.forEach(this._fetch.bind(this));
        delete this._incompatibles[name];
    }

    // If the package is not targetable, flag it
    // It will be needed later so that untargetable endpoints
    // will not get * converted to ^version
    if (!isTargetable) {
        decEndpoint.untargetable = true;
    }

    // If there are no more packages being fetched,
    // finish the resolve process by dissecting all resolved packages
    if (this._nrFetching <= 0) {
        process.nextTick(this._dissect.bind(this));
    }
};

Manager.prototype._onFetchError = function(decEndpoint, err) {
    var name = decEndpoint.name;

    err.data = err.data || {};
    err.data.endpoint = mout.object.pick(decEndpoint, [
        'name',
        'source',
        'target'
    ]);

    // Remove from being fetched list
    mout.array.remove(this._fetching[name], decEndpoint);
    this._nrFetching--;

    // Add to the failed list
    this._failed[name] = this._failed[name] || [];
    this._failed[name].push(err);
    delete decEndpoint.promise;

    // Make the whole process to fail fast
    this._failFast();

    // If there are no more packages being fetched,
    // finish the resolve process (with an error)
    if (this._nrFetching <= 0) {
        process.nextTick(this._dissect.bind(this));
    }
};

Manager.prototype._failFast = function() {
    if (this._hasFailed) {
        return;
    }

    this._hasFailed = true;

    // If after some amount of time all pending tasks haven't finished,
    // we force the process to end
    this._failFastTimeout = setTimeout(
        function() {
            this._nrFetching = Infinity;
            this._dissect();
        }.bind(this),
        20000
    );
};

/**
 * Parses decEndpoint dependencies and fetches missing ones.
 *
 * It fetches all non-yet-fetching dependencies with _fetch.
 *
 *
 * @param {string} pkgMeta Metadata of package to resolve dependencies.
 */
Manager.prototype._parseDependencies = function(decEndpoint, pkgMeta) {
    var pending = [];

    decEndpoint.dependencies = decEndpoint.dependencies || {};

    // Parse package dependencies
    mout.object.forOwn(
        pkgMeta.dependencies,
        function(value, key) {
            var resolved;
            var fetching;
            var compatible;
            var childDecEndpoint = endpointParser.json2decomposed(key, value);

            // Check if this dependency should be skipped.
            if (
                mout.array.contains(
                    this._config.ignoredDependencies,
                    childDecEndpoint.name
                )
            ) {
                this._logger.action(
                    'skipped',
                    childDecEndpoint.name,
                    this.toData(decEndpoint)
                );
                return;
            }

            // Check if a compatible one is already resolved
            // If there's one, we don't need to resolve it twice
            resolved = this._resolved[key];
            if (resolved) {
                // Find if there's one with the exact same target
                compatible = mout.array.find(
                    resolved,
                    function(resolved) {
                        return childDecEndpoint.target === resolved.target;
                    },
                    this
                );

                // If we found one, merge stuff instead of adding as resolved
                if (compatible) {
                    decEndpoint.dependencies[key] = compatible;
                    compatible.dependants.push(decEndpoint);
                    compatible.dependants = this._uniquify(
                        compatible.dependants
                    );

                    return;
                }

                // Find one that is compatible
                compatible = mout.array.find(
                    resolved,
                    function(resolved) {
                        return this._areCompatible(childDecEndpoint, resolved);
                    },
                    this
                );

                // If we found one, add as resolved
                // and copy resolved properties from the compatible one
                if (compatible) {
                    decEndpoint.dependencies[key] = compatible;
                    childDecEndpoint.canonicalDir = compatible.canonicalDir;
                    childDecEndpoint.pkgMeta = compatible.pkgMeta;
                    childDecEndpoint.dependencies = compatible.dependencies;
                    childDecEndpoint.dependants = [decEndpoint];
                    this._resolved[key].push(childDecEndpoint);

                    return;
                }
            }

            // Check if a compatible one is being fetched
            // If there's one, we wait and reuse it to avoid resolving it twice
            fetching = this._fetching[key];
            if (fetching) {
                compatible = mout.array.find(
                    fetching,
                    function(fetching) {
                        return this._areCompatible(childDecEndpoint, fetching);
                    },
                    this
                );

                if (compatible) {
                    pending.push(compatible.promise);
                    return;
                }
            }

            // Mark endpoint as unresolvable if the parent is also unresolvable
            childDecEndpoint.unresolvable = !!decEndpoint.unresolvable;

            // Otherwise, just fetch it from the repository
            decEndpoint.dependencies[key] = childDecEndpoint;
            childDecEndpoint.dependants = [decEndpoint];
            this._fetch(childDecEndpoint);
        },
        this
    );

    if (pending.length > 0) {
        Q.all(pending).then(
            function() {
                this._parseDependencies(decEndpoint, pkgMeta);
            }.bind(this)
        );
    }
};

/**
 * After batch of packages has been fetched, the results are stored in _resolved hash.
 *
 * {
 *   packageName: [array, of, decEndpoints]
 * }
 *
 * This method takes all possible decEndpoints and selects only one of them per packageName.
 *
 * It stores selected packages in the this._dissected array.
 *
 * The _dissected array is used later on by install method to move packages to config.directory.
 */
Manager.prototype._dissect = function() {
    var err;
    var componentsDir;
    var promise = Q.resolve();
    var suitables = {};
    var that = this;

    // If something failed, reject the whole resolve promise
    // with the first error
    if (this._hasFailed) {
        clearTimeout(this._failFastTimeout); // Cancel fail fast timeout

        err = mout.object.values(this._failed)[0][0];
        this._deferred.reject(err);
        return;
    }

    // Find a suitable version for each package name
    mout.object.forOwn(
        this._resolved,
        function(decEndpoints, name) {
            var semvers;
            var nonSemvers;

            // Filter out non-semver ones
            semvers = decEndpoints.filter(function(decEndpoint) {
                return !!decEndpoint.pkgMeta.version;
            });

            // Sort semver ones DESC
            semvers.sort(function(first, second) {
                var result = semver.rcompare(
                    first.pkgMeta.version,
                    second.pkgMeta.version
                );

                // If they are equal and one of them is a wildcard target,
                // give lower priority
                if (!result) {
                    if (first.target === '*') {
                        return 1;
                    }
                    if (second.target === '*') {
                        return -1;
                    }
                }

                // If they are equal and one of them is an exact target,
                // give higher priority
                if (!result) {
                    if (first.target === first.pkgMeta.version) {
                        return -1;
                    }
                    if (second.target === second.pkgMeta.version) {
                        return 1;
                    }
                }

                return result;
            });

            // Convert wildcard targets to semver range targets if they are newly
            // Note that this can only be made if they can be targetable
            // If they are not, the resolver is incapable of handling targets
            semvers.forEach(function(decEndpoint) {
                if (
                    decEndpoint.newly &&
                    decEndpoint.target === '*' &&
                    !decEndpoint.untargetable
                ) {
                    decEndpoint.target = '^' + decEndpoint.pkgMeta.version;
                    decEndpoint.originalTarget = '*';
                }
            });

            // Filter non-semver ones
            nonSemvers = decEndpoints.filter(function(decEndpoint) {
                return !decEndpoint.pkgMeta.version;
            });

            promise = promise.then(function() {
                return that
                    ._electSuitable(name, semvers, nonSemvers)
                    .then(function(suitable) {
                        suitables[name] = suitable;
                    });
            });
        },
        this
    );

    // After a suitable version has been elected for every package
    promise
        .then(
            function() {
                // Look for extraneous resolutions
                mout.object.forOwn(
                    this._resolutions,
                    function(resolution, name) {
                        if (this._conflicted[name]) {
                            return;
                        }

                        // Packages that didn't have any conflicts in resolution yet have
                        // "resolution" entry in bower.json are deemed unnecessary
                        //
                        // Probably in future we want to use them for force given resolution, but for now...
                        // See: https://github.com/bower/bower/issues/1362
                        this._logger.warn(
                            'extra-resolution',
                            'Unnecessary resolution: ' +
                                name +
                                '#' +
                                resolution,
                            {
                                name: name,
                                resolution: resolution,
                                action: 'delete'
                            }
                        );
                    },
                    this
                );

                // Filter only packages that need to be installed
                componentsDir = relativeToBaseDir(this._config.cwd)(
                    this._config.directory
                );
                this._dissected = mout.object.filter(
                    suitables,
                    function(decEndpoint, name) {
                        var installedMeta = this._installed[name];
                        var dst;

                        // Skip linked dependencies
                        if (decEndpoint.linked) {
                            return false;
                        }

                        // Skip if source is the same as dest
                        // FIXME: Shoudn't we force installation if force flag is used here?
                        dst = path.join(componentsDir, name);
                        if (dst === decEndpoint.canonicalDir) {
                            return false;
                        }

                        // We skip installing decEndpoint, if:
                        //   1. We have installed package with the same name
                        //   2. Packages have matching meta fields (needs explanation)
                        //   3. We didn't force the installation
                        if (
                            installedMeta &&
                            installedMeta._target === decEndpoint.target &&
                            installedMeta._originalSource ===
                                decEndpoint.source &&
                            installedMeta._release ===
                                decEndpoint.pkgMeta._release
                        ) {
                            return this._config.force;
                        }

                        return true;
                    },
                    this
                );
            }.bind(this)
        )
        .then(this._deferred.resolve, this._deferred.reject);
};

Manager.prototype._electSuitable = function(name, semvers, nonSemvers) {
    var suitable;
    var resolution;
    var unresolvable;
    var dataPicks;
    var save;
    var picks = [];
    var versionRegex = /(?:[\d\w]\.){2}[\d\w](?:.)*/;
    var picksReleases;

    // If there are both semver and non-semver, there's no way
    // to figure out the suitable one
    if (semvers.length && nonSemvers.length) {
        picks.push.apply(picks, semvers);
        picks.push.apply(picks, nonSemvers);
        // If there are only non-semver ones, the suitable is elected
        // only if there's one
    } else if (nonSemvers.length) {
        if (nonSemvers.length === 1) {
            return Q.resolve(nonSemvers[0]);
        }

        picks.push.apply(picks, nonSemvers);
        // If there are only semver ones, figure out which one is
        // compatible with every requirement
    } else {
        suitable = mout.array.find(semvers, function(subject) {
            return semvers.every(function(decEndpoint) {
                return (
                    subject === decEndpoint ||
                    semver.satisfies(
                        subject.pkgMeta.version,
                        decEndpoint.target
                    )
                );
            });
        });

        if (suitable) {
            return Q.resolve(suitable);
        }

        picks.push.apply(picks, semvers);
    }

    // At this point, there's a conflict
    this._conflicted[name] = true;

    // Prepare data to be sent bellow
    // 1 - Sort picks by version/release
    picks.sort(function(pick1, pick2) {
        var version1 = pick1.pkgMeta.version;
        var version2 = pick2.pkgMeta.version;
        var comp;

        // If both have versions, compare their versions using semver
        if (version1 && version2) {
            comp = semver.compare(version1, version2);
            if (comp) {
                return comp;
            }
        } else {
            // If one of them has a version, it's considered higher
            if (version1) {
                return 1;
            }
            if (version2) {
                return -1;
            }
        }

        // Give priority to the one with most dependants
        if (pick1.dependants.length > pick2.dependants.length) {
            return -1;
        }
        if (pick1.dependants.length < pick2.dependants.length) {
            return 1;
        }

        return 0;
    });

    // 2 - Transform data
    dataPicks = picks.map(function(pick) {
        var dataPick = this.toData(pick);
        dataPick.dependants = pick.dependants.map(this.toData, this);
        dataPick.dependants.sort(function(dependant1, dependant2) {
            return dependant1.endpoint.name.localeCompare(
                dependant2.endpoint.name
            );
        });
        return dataPick;
    }, this);

    // Check if there's a resolution that resolves the conflict
    // Note that if one of them is marked as unresolvable,
    // the resolution has no effect
    resolution = this._resolutions[name];
    unresolvable = mout.object.find(picks, function(pick) {
        return pick.unresolvable;
    });

    if (resolution && !unresolvable) {
        suitable = -1;

        // Range resolution
        if (semver.validRange(resolution)) {
            suitable = mout.array.findIndex(picks, function(pick) {
                return (
                    pick.pkgMeta.version &&
                    semver.satisfies(pick.pkgMeta.version, resolution)
                );
            });
        }

        // Exact match resolution (e.g. branches/tags)
        if (suitable === -1) {
            suitable = mout.array.findIndex(picks, function(pick) {
                return (
                    pick.target === resolution ||
                    pick.pkgMeta._release === resolution
                );
            });
        }

        if (suitable === -1) {
            this._logger.warn(
                'resolution',
                'Unsuitable resolution declared for ' +
                    name +
                    ': ' +
                    resolution,
                {
                    name: name,
                    picks: dataPicks,
                    resolution: resolution
                }
            );
        } else {
            this._logger.conflict(
                'solved',
                'Unable to find suitable version for ' + name,
                {
                    name: name,
                    picks: dataPicks,
                    resolution: resolution,
                    suitable: dataPicks[suitable]
                }
            );
            return Q.resolve(picks[suitable]);
        }
    }

    // If force latest is enabled, resolve to the highest semver version
    // or whatever non-semver if none available
    if (this._forceLatest) {
        suitable = picks.length - 1;

        this._logger.conflict(
            'solved',
            'Unable to find suitable version for ' + name,
            {
                name: name,
                picks: dataPicks,
                suitable: dataPicks[suitable],
                forced: true
            }
        );

        // Save resolution
        if (this._config.argv.cooked.includes('--save')) {
            this._storeResolution(picks[suitable]);
        }

        return Q.resolve(picks[suitable]);
    }

    // If interactive is disabled, error out
    if (!this._config.interactive) {
        throw createError(
            'Unable to find suitable version for ' + name,
            'ECONFLICT',
            {
                name: name,
                picks: dataPicks
            }
        );
    }

    // At this point the user needs to make a decision
    this._logger.conflict(
        'incompatible',
        'Unable to find suitable version for ' + name,
        {
            name: name,
            picks: dataPicks
        }
    );

    picksReleases = picks.map(function(pick) {
        return pick.pkgMeta._release;
    });
    return Q.nfcall(this._logger.prompt.bind(this._logger), {
        type: 'input',
        message: 'Answer',
        validate: function(choice) {
            var invalidChoice = 'Invalid choice';

            if (choice.match(versionRegex)) {
                return picksReleases.indexOf(choice) != -1
                    ? true
                    : invalidChoice;
            }

            choice = Number(mout.string.trim(choice.trim(), '!'));

            if (!choice || choice < 1 || choice > picks.length) {
                return invalidChoice;
            }

            return true;
        }
    }).then(
        function(choice) {
            var pick;

            // Sanitize choice
            choice = choice.trim();
            save = /^!/.test(choice) || /!$/.test(choice); // Save if prefixed or suffixed with !

            if (choice.match(versionRegex)) {
                pick = picks[picksReleases.indexOf(choice)];
            } else {
                choice = Number(mout.string.trim(choice, '!'));
                pick = picks[choice - 1];
            }

            // Save resolution
            if (save) {
                this._storeResolution(pick);
            }

            return pick;
        }.bind(this)
    );
};

Manager.prototype._storeResolution = function(pick) {
    var resolution;
    var name = pick.name;

    if (pick.target === '*') {
        resolution = pick.pkgMeta._release || '*';
    } else {
        resolution = pick.target;
    }

    this._logger.info(
        'resolution',
        'Saved ' + name + '#' + resolution + ' as resolution',
        {
            name: name,
            resolution: resolution,
            action: this._resolutions[name] ? 'edit' : 'add'
        }
    );
    this._resolutions[name] = resolution;
};

/**
 * Checks if some endpoint is compatible with already resolved target.
 *
 * It is used in two situations:
 *   * checks if resolved component matches dependency constraint
 *   * checks if not resolved component matches already fetched component
 *
 * If candidate matches already resolved component, it won't be downloaded.
 *
 * @param {Endpoint} candidate endpoint
 * @param {Endpoint} resolved endpoint
 *
 * @return {Boolean}
 */
Manager.prototype._areCompatible = function(candidate, resolved) {
    var resolvedVersion;
    var highestCandidate;
    var highestResolved;
    var candidateIsRange = semver.validRange(candidate.target);
    var resolvedIsRange = semver.validRange(resolved.target);
    var candidateIsVersion = semver.valid(candidate.target);
    var resolvedIsVersion = semver.valid(resolved.target);

    // Check if targets are equal
    if (candidate.target === resolved.target) {
        return true;
    }

    resolvedVersion = resolved.pkgMeta && resolved.pkgMeta.version;
    // If there is no pkgMeta, resolvedVersion is downloading now
    // Check based on target requirements
    if (!resolvedVersion) {
        // If one of the targets is range and other is version,
        // check version against the range
        if (candidateIsVersion && resolvedIsRange) {
            return semver.satisfies(candidate.target, resolved.target);
        }

        if (resolvedIsVersion && candidateIsRange) {
            return semver.satisfies(resolved.target, candidate.target);
        }

        if (resolvedIsVersion && candidateIsVersion) {
            return semver.eq(resolved.target, candidate.target);
        }

        // If both targets are range, check that both have same
        // higher cap
        if (resolvedIsRange && candidateIsRange) {
            highestCandidate = this._getCap(
                semver.toComparators(candidate.target),
                'highest'
            );
            highestResolved = this._getCap(
                semver.toComparators(resolved.target),
                'highest'
            );

            // This never happens, but you can't be sure without tests
            if (!highestResolved.version || !highestCandidate.version) {
                return false;
            }

            return (
                semver.eq(highestCandidate.version, highestResolved.version) &&
                highestCandidate.comparator === highestResolved.comparator
            );
        }
        return false;
    }

    // If target is a version, compare against the resolved version
    if (candidateIsVersion) {
        return semver.eq(candidate.target, resolvedVersion);
    }

    // If target is a range, check if resolved version satisfies it
    if (candidateIsRange) {
        return semver.satisfies(resolvedVersion, candidate.target);
    }

    return false;
};

/**
 * Gets highest/lowest version from set of comparators.
 *
 * The only thing that matters for this function is version number.
 * Returned comparator is splitted to comparator and version parts.
 *
 * It is used to receive lowest / highest bound of toComparators result:
 * semver.toComparators('~0.1.1') // => [ [ '>=0.1.1-0', '<0.2.0-0' ] ]
 *
 * Examples:
 *
 * _getCap([['>=2.1.1-0', '<2.2.0-0'], '<3.2.0'], 'highest')
 * // => { comparator: '<', version: '3.2.0' }
 *
 * _getCap([['>=2.1.1-0', '<2.2.0-0'], '<3.2.0'], 'lowest')
 * // => { comparator: '>=', version: '2.1.1-0' }
 *
 * @param {Array.<Array|string>} comparators
 * @param {string} side, 'highest' (default) or 'lowest'
 *
 * @return {{ comparator: string, version: string }}
 */
Manager.prototype._getCap = function(comparators, side) {
    var matches;
    var candidate;
    var cap = {};
    var compare = side === 'lowest' ? semver.lt : semver.gt;

    comparators.forEach(function(comparator) {
        // Get version of this comparator
        // If it's an array, call recursively
        if (Array.isArray(comparator)) {
            candidate = this._getCap(comparator, side);

            // Compare with the current highest version
            if (!cap.version || compare(candidate.version, cap.version)) {
                cap = candidate;
            }
            // Otherwise extract the version from the comparator
            // using a simple regexp
        } else {
            matches = comparator.match(/(.*?)(\d+\.\d+\.\d+.*)$/);
            if (!matches) {
                return;
            }

            // Compare with the current highest version
            if (!cap.version || compare(matches[2], cap.version)) {
                cap.version = matches[2];
                cap.comparator = matches[1];
            }
        }
    }, this);

    return cap;
};

/**
 * Filters out unique endpoints, comparing by name and then source.
 *
 * It leaves last matching endpoint.
 *
 * Examples:
 *
 *  manager._uniquify([
 *      { name: 'foo', source: 'google.com' },
 *      { name: 'foo', source: 'facebook.com' }
 *  ]);
 *  // => { name: 'foo', source: 'facebook.com' }
 *
 * @param {Array.<Endpoint>} decEndpoints
 * @return {Array.<Endpoint>} Filtered elements of decEndpoints
 *
 */
Manager.prototype._uniquify = function(decEndpoints) {
    var length = decEndpoints.length;

    return decEndpoints.filter(function(decEndpoint, index) {
        var x;
        var current;

        for (x = index + 1; x < length; ++x) {
            current = decEndpoints[x];

            if (current === decEndpoint) {
                return false;
            }

            // Compare name if both set
            // Fallback to compare sources
            if (!current.name && !decEndpoint.name) {
                if (current.source !== decEndpoint.source) {
                    continue;
                }
            } else if (current.name !== decEndpoint.name) {
                continue;
            }

            // Compare targets if name/sources are equal
            if (current.target === decEndpoint.target) {
                return false;
            }
        }

        return true;
    });
};

module.exports = Manager;
