var util = require('util');
var Q = require('q');
var which = require('which');
var LRU = require('lru-cache');
var mout = require('mout');
var Resolver = require('./Resolver');
var semver = require('../../util/semver');
var createError = require('../../util/createError');
var cmd = require('../../util/cmd');

var hasSvn;

// Check if svn is installed
try {
    which.sync('svn');
    hasSvn = true;
} catch (ex) {
    hasSvn = false;
}

function SvnResolver(decEndpoint, config, logger) {
    Resolver.call(this, decEndpoint, config, logger);

    if (!hasSvn) {
        throw createError('svn is not installed or not in the PATH', 'ENOSVN');
    }
}

util.inherits(SvnResolver, Resolver);
mout.object.mixIn(SvnResolver, Resolver);

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

SvnResolver.getSource = function (source) {
    var uri = this._source || source;

    return uri
        .replace(/^svn\+(https?|file):\/\//i, '$1://')  // Change svn+http or svn+https or svn+file to http(s), file respectively
        .replace('svn://', 'http://')  // Change svn to http
        .replace(/\/+$/, '');  // Remove trailing slashes
};

SvnResolver.prototype._hasNew = function (pkgMeta) {
    var oldResolution = pkgMeta._resolution || {};

    return this._findResolution()
    .then(function (resolution) {
        // Check if resolution types are different
        if (oldResolution.type !== resolution.type) {
            return true;
        }

        // If resolved to a version, there is new content if the tags are not equal
        if (resolution.type === 'version' && semver.neq(resolution.tag, oldResolution.tag)) {
            return true;
        }

        // As last check, we compare both commit hashes
        return resolution.commit !== oldResolution.commit;
    });
};

SvnResolver.prototype._resolve = function () {
    var that = this;

    return this._findResolution()
    .then(function () {
        return that._export();
    });
};

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

SvnResolver.prototype._export = function () {
    var promise;
    var timer;
    var reporter;
    var that = this;
    var resolution = this._resolution;

    this.source = SvnResolver.getSource(this._source);

    this._logger.action('export', resolution.tag || resolution.branch || resolution.commit, {
        resolution: resolution,
        to: this._tempDir
    });

    if (resolution.type === 'commit') {
        promise = cmd('svn', ['export', '--force', '--non-interactive', this._source + '/trunk', '-r' + resolution.commit, this._tempDir]);
    } else if (resolution.type === 'branch' && resolution.branch === 'trunk') {
        promise = cmd('svn', ['export', '--force', '--non-interactive', this._source + '/trunk', this._tempDir]);
    } else if (resolution.type === 'branch') {
        promise = cmd('svn', ['export', '--force', '--non-interactive', this._source + '/branches/' + resolution.branch, this._tempDir]);
    } else {
        promise = cmd('svn', ['export', '--force', '--non-interactive', this._source + '/tags/' + resolution.tag, this._tempDir]);
    }

    // Throttle the progress reporter to 1 time each sec
    reporter = mout.fn.throttle(function (data) {
        var lines;

        lines = data.split(/[\r\n]+/);
        lines.forEach(function (line) {
            if (/\d{1,3}\%/.test(line)) {
                // TODO: There are some strange chars that appear once in a while (\u001b[K)
                //       Trim also those?
                that._logger.info('progress', line.trim());
            }
        });
    }, 1000);

    // Start reporting progress after a few seconds
    timer = setTimeout(function () {
        promise.progress(reporter);
    }, 8000);

    return promise
    // Add additional proxy information to the error if necessary
    .fail(function (err) {
        throw err;
    })
    // Clear timer at the end
    .fin(function () {
        clearTimeout(timer);
        reporter.cancel();
    });
};

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

SvnResolver.prototype._findResolution = function (target) {
    var err;
    var self = this.constructor;
    var that = this;

    target = target || this._target || '*';

    this._source = SvnResolver.getSource(this._source);

    // Target is a revision, so it's a stale target (not a moving target)
    // There's nothing to do in this case
    if ((/^r\d+/).test(target)) {
        target = target.split('r');

        this._resolution = { type: 'commit', commit: target[1] };
        return Q.resolve(this._resolution);
    }

    // Target is a range/version
    if (semver.validRange(target)) {
        return self.versions(this._source, true)
        .then(function (versions) {
            var versionsArr,
                version,
                index;

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

            // If there are no tags and target is *,
            // fallback to the latest commit on trunk
            if (!versions.length && target === '*') {
                return that._findResolution('trunk');
            }

            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) {
                version = versions[index];
                return that._resolution = { type: 'version', tag: version.tag, commit: version.commit };
            }

            // Check if there's an exact branch/tag with this name as last resort
            return Q.all([
                self.branches(that._source),
                self.tags(that._source)
            ])
            .spread(function (branches, tags) {
                // Use hasOwn because a branch/tag could have a name like "hasOwnProperty"
                if (mout.object.hasOwn(tags, target)) {
                    return that._resolution = { type: 'tag', tag: target, commit: tags[target] };
                }
                if (mout.object.hasOwn(branches, target)) {
                    return that._resolution = { type: 'branch', branch: target, commit: branches[target] };
                }

                throw createError('No tag found that was able to satisfy ' + target, 'ENORESTARGET', {
                    details: !versions.length ?
                        'No versions found in ' + that._source :
                        'Available versions in ' + that._source + ': ' + versions.map(function (version) { return version.version; }).join(', ')
                });
            });
        });
    }

    // Otherwise, target is either a tag or a branch
    return Q.all([
        self.branches(that._source),
        self.tags(that._source)
    ])
    .spread(function (branches, tags) {
        // Use hasOwn because a branch/tag could have a name like "hasOwnProperty"
        if (mout.object.hasOwn(tags, target)) {
            return that._resolution = { type: 'tag', tag: target, commit: tags[target] };
        }
        if (mout.object.hasOwn(branches, target)) {
            return that._resolution = { type: 'branch', branch: target, commit: branches[target] };
        }

        branches = Object.keys(branches);
        tags = Object.keys(tags);

        err = createError('target ' + target + ' does not exist', 'ENORESTARGET');
        err.details = !tags.length ?
                'No tags found in ' + that._source :
                'Available tags: ' + tags.join(', ');
        err.details += '\n';
        err.details += !branches.length ?
                'No branches found in ' + that._source :
                'Available branches: ' + branches.join(', ');

        throw err;
    });
};

SvnResolver.prototype._savePkgMeta = function (meta) {
    var version;

    if (this._resolution.type === 'version') {
        version = semver.clean(this._resolution.tag);

        // Warn if the package meta version is different than the resolved one
        if (typeof meta.version === 'string' && semver.neq(meta.version, version)) {
            this._logger.warn('mismatch', 'Version declared in the json (' + meta.version + ') is different than the resolved one (' + version + ')', {
                resolution: this._resolution,
                pkgMeta: meta
            });
        }

        // Ensure package meta version is the same as the resolution
        meta.version = version;
    } else {
        // If resolved to a target that is not a version,
        // remove the version from the meta
        delete meta.version;
    }

    // Save version/tag/commit in the release
    // Note that we can't store branches because _release is supposed to be
    // an unique id of this ref.
    meta._release = version ||
                    this._resolution.tag ||
                    this._resolution.commit;

    // Save resolution to be used in hasNew later
    meta._resolution = this._resolution;

    return Resolver.prototype._savePkgMeta.call(this, meta);
};

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

SvnResolver.versions = function (source, extra) {
    source = SvnResolver.getSource(source);

    var value = this._cache.versions.get(source);

    if (value) {
        return Q.resolve(value)
        .then(function () {
            var versions = this._cache.versions.get(source);

            // If no extra information was requested,
            // resolve simply with the versions
            if (!extra) {
                versions = versions.map(function (version) {
                    return version.version;
                });
            }

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

    value = this.tags(source)
    .then(function (tags) {
        var tag;
        var version;
        var versions = [];

        // For each tag
        for (tag in tags) {
            version = semver.clean(tag);
            if (version) {
                versions.push({ version: version, tag: tag, commit: tags[tag] });
            }
        }

        // Sort them by DESC order
        versions.sort(function (a, b) {
            return semver.rcompare(a.version, b.version);
        });

        this._cache.versions.set(source, versions);

        // Call the function again to keep it DRY
        return this.versions(source, extra);
    }.bind(this));

    // Store the promise to be reused until it resolves
    // to a specific value
    this._cache.versions.set(source, value);

    return value;
};

SvnResolver.tags = function (source) {
    source = SvnResolver.getSource(source);

    var value = this._cache.tags.get(source);

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

    value = cmd('svn', ['list', source + '/tags', '--verbose', '--non-interactive'])
    .spread(function (stout) {
        var tags = SvnResolver.parseSubversionListOutput(stout.toString());

        this._cache.tags.set(source, tags);
        return tags;

    }.bind(this));

    // Store the promise to be reused until it resolves
    // to a specific value
    this._cache.tags.set(source, value);

    return value;
};

SvnResolver.branches = function (source) {
    source = SvnResolver.getSource(source);

    var value = this._cache.branches.get(source);

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

    value = cmd('svn', ['list', source + '/branches', '--verbose', '--non-interactive'])
    .spread(function (stout) {
        var branches = SvnResolver.parseSubversionListOutput(stout.toString());

        // trunk is a branch!
        branches.trunk = '*';

        this._cache.branches.set(source, branches);
        return branches;

    }.bind(this));

    // Store the promise to be reused until it resolves
    // to a specific value
    this._cache.branches.set(source, value);

    return value;
};

SvnResolver.parseSubversionListOutput = function (stout) {

    var entries = {};
    var lines = stout
        .trim()
        .split(/[\r\n]+/);

    // For each line in the refs, match only the branches
    lines.forEach(function (line) {
        var match = line.match(/\s+([0-9]+)\s.+\s([\w.$-]+)\//i);

        if (match && match[2] !== '.') {
            entries[match[2]] = match[1];
        }
    });

    return entries;
};

SvnResolver.clearRuntimeCache = function () {
    // Reset cache for branches, tags, etc
    mout.object.forOwn(SvnResolver._cache, function (lru) {
        lru.reset();
    });
};

SvnResolver._cache = {
    branches: new LRU({ max: 50, maxAge: 5 * 60 * 1000 }),
    tags: new LRU({ max: 50, maxAge: 5 * 60 * 1000 }),
    versions: new LRU({ max: 50, maxAge: 5 * 60 * 1000 })
};

module.exports = SvnResolver;
