var fs = require('../../util/fs');
var path = require('path');
var Q = require('q');
var tmp = require('tmp');
var mkdirp = require('mkdirp');
var rimraf = require('../../util/rimraf');
var readJson = require('../../util/readJson');
var createError = require('../../util/createError');
var removeIgnores = require('../../util/removeIgnores');
var md5 = require('md5-hex');

tmp.setGracefulCleanup();

function Resolver(decEndpoint, config, logger) {
    this._source = decEndpoint.source;
    this._target = decEndpoint.target || '*';
    this._name = decEndpoint.name || path.basename(this._source);

    this._config = config;
    this._logger = logger;

    this._guessedName = !decEndpoint.name;
}

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

Resolver.prototype.getSource = function() {
    return this._source;
};

Resolver.prototype.getName = function() {
    return this._name;
};

Resolver.prototype.getTarget = function() {
    return this._target;
};

Resolver.prototype.getTempDir = function() {
    return this._tempDir;
};

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

Resolver.prototype.hasNew = function(pkgMeta) {
    var promise;
    var that = this;

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

    this._working = true;

    // Avoid reading the package meta if already given
    promise = this._hasNew(pkgMeta);

    return promise.fin(function() {
        that._working = false;
    });
};

Resolver.prototype.resolve = function() {
    var that = this;

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

    this._working = true;

    // Create temporary dir
    return (
        this._createTempDir()
            // Resolve self
            .then(this._resolve.bind(this))
            // Read json, generating the package meta
            .then(this._readJson.bind(this, null))
            // Apply and save package meta
            .then(function(meta) {
                return that
                    ._applyPkgMeta(meta)
                    .then(that._savePkgMeta.bind(that, meta));
            })
            .then(
                function() {
                    // Resolve with the folder
                    return that._tempDir;
                },
                function(err) {
                    // If something went wrong, unset the temporary dir
                    that._tempDir = null;
                    throw err;
                }
            )
            .fin(function() {
                that._working = false;
            })
    );
};

Resolver.prototype.isCacheable = function() {
    // Bypass cache for local dependencies
    if (
        this._source &&
        /^(?:file:[\/\\]{2}|[A-Z]:)?\.?\.?[\/\\]/.test(this._source)
    ) {
        return false;
    }

    // We don't want to cache moving targets like branches
    if (
        this._pkgMeta &&
        this._pkgMeta._resolution &&
        this._pkgMeta._resolution.type === 'branch'
    ) {
        return false;
    }

    return true;
};

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

// Abstract functions that must be implemented by concrete resolvers
Resolver.prototype._resolve = function() {
    throw new Error('_resolve not implemented');
};

// Abstract functions that can be re-implemented by concrete resolvers
// as necessary
Resolver.prototype._hasNew = function(pkgMeta) {
    return Q.resolve(true);
};

Resolver.isTargetable = function() {
    return true;
};

Resolver.versions = function(source) {
    return Q.resolve([]);
};

Resolver.clearRuntimeCache = function() {};

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

Resolver.prototype._createTempDir = function() {
    return Q.nfcall(mkdirp, this._config.tmp)
        .then(
            function() {
                return Q.nfcall(tmp.dir, {
                    template: path.join(
                        this._config.tmp,
                        md5(this._name) + '-' + process.pid + '-XXXXXX'
                    ),
                    mode: 0777 & ~process.umask(),
                    unsafeCleanup: true
                });
            }.bind(this)
        )
        .then(
            function(dir) {
                // nfcall may return multiple callback arguments as an array
                return (this._tempDir = Array.isArray(dir) ? dir[0] : dir);
            }.bind(this)
        );
};

Resolver.prototype._cleanTempDir = function() {
    var tempDir = this._tempDir;

    if (!tempDir) {
        return Q.resolve();
    }

    // Delete and create folder
    return Q.nfcall(rimraf, tempDir)
        .then(function() {
            return Q.nfcall(mkdirp, tempDir, 0777 & ~process.umask());
        })
        .then(function() {
            return tempDir;
        });
};

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

    dir = dir || this._tempDir;
    return readJson(dir, {
        assume: { name: this._name },
        logger: that._logger
    }).spread(function(json, deprecated) {
        if (deprecated) {
            that._logger.warn(
                'deprecated',
                'Package ' +
                    that._name +
                    ' is using the deprecated ' +
                    deprecated
            );
        }

        return json;
    });
};

Resolver.prototype._applyPkgMeta = function(meta) {
    // 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._guessedName) {
        this._name = meta.name;
    }

    // Handle ignore property, deleting all files from the temporary directory
    // If no ignores were specified, simply resolve
    if (!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;
    });
};

Resolver.prototype._savePkgMeta = function(meta) {
    var that = this;
    var contents;

    // Store original source & target
    meta._source = this._source;
    meta._target = this._target;

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

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

module.exports = Resolver;
