var Response = require("../net/Response.js").Response;
var knox     = require('knox');
var xml2js   = require('xml2js');

/**
 * @class S3Client
 * @classdesc S3 client
 * @param {Object} configuration S3 connection configuration
 * @param {Object} configuration.key        access key ID
 * @param {Object} configuration.secret     secret access key
 * @param {Object} configuration.bucket     bucket
 * @param {Object} options connection options
 * @param {Object} options.timeout  timeout before aborting request
 * @param {Object} options.retry    number of retries when connection problem occurs
 */
var S3Client = function (configuration, options) {
    this.retry   = options.retry || 0;
    this.timeout = options.timeout || 10000;

    /* odwolanie przez bucket */
    configuration.style = 'path';

    this._client = knox.createClient(configuration);
};

/**
 * Callback fired after receiving response from server.
 *
 * When connection error occured {error} object will be passed as first parameter.
 *
 * @callback S3Client~callback
 * @param {error} error
 * @param {response} response Response object
 */

/**
 * Callback fired after receiving response from server.
 *
 * When connection error occured {error} object will be passed as first parameter.
 *
 * @callback S3Client~callbackAcl
 * @param {error} error
 * @param {object} acl requested object's ACLs
 */

/**
 * Adds object to S3
 *
 * @param {string} key key name of the object
 * @param {object} params object properties
 * @param {buffer|string} params.body object content
 * @param {object} params.headers headers
 * @param {string} params.acl ACL applied to object
 * @param {S3Client~callback} callback Called when response from the server is returned.
 */
S3Client.prototype.set = function (key, params, callback) {
    var headers = params.headers;
    var body    = params.body;

    if (!body) {
        return callback(this._getError(S3Client.Error.ENOBODY), null);
    }

    if (!key) {
        return callback(this._getError(S3Client.Error.ENOKEY), null);
    }

    if (!headers) {
        return callback(this._getError(S3Client.Error.ENOHEADERS), null);
    }

    if (params.acl) {
        headers['x-amz-acl'] = params.acl;
    }

    this._spawnFunction(this._set.bind(this, key, headers, body), callback);
};

/**
 * Retrieves object from S3.
 *
 * When retry count specified, callback is called once after all tries.
 *
 * @param {string} key
 * @param {S3Client~callback} callback Called when response from the server is returned.
 */
S3Client.prototype.get = function (key, params, callback) {
    params = params || {};

    var headers = {};

    if (params.range) {
        headers.range = params.range;
    }

    this._spawnFunction(this._get.bind(this, key, headers), callback);
};


/**
 * Retrieves headers of object from S3.
 *
 * When retry count specified, callback is called once after all tries.
 *
 * @param {string} key
 * @param {S3Client~callback} callback Called when response from the server is returned.
 */
S3Client.prototype.head = function (key, headers, callback) {
    this._spawnFunction(this._head.bind(this, key, headers), callback);
};


/**
 * Removes object from S3.
 *
 * When retry count specified, callback is called once after all tries.
 *
 * @param {string} key
 * @param {S3Client~callback} callback Called when response from the server is returned.
 */
S3Client.prototype.del = function (key, callback) {
    this._spawnFunction(this._del.bind(this, key), callback);
};

/**
 * Copy object in S3 around the same bucket.
 *
 * When retry count specified, callback is called once after all tries.
 *
 * @param {string} keySrc
 * @param {string} keyDst
 * @param {string} headers
 * @param {S3Client~callback} callback Called when response from the server is returned.
 */
S3Client.prototype.copy = function (keySrc, keyDst, headers, callback) {
    this._spawnFunction(this._copy.bind(this, keySrc, keyDst, headers), callback);
};

/**
 * List object in S3 bucket.
 *
 * When retry count specified, callback is called once after all tries.
 *
 * @param {string} params
 * @param {string} headers
 * @param {S3Client~callback} callback Called when response from the server is returned.
 */
S3Client.prototype.list = function (params, headers, callback) {
    this._client.list(params, headers, callback);
};


/**
 * Retrieves object's ACL.
 *
 * When retry count specified, callback is called once after all tries.
 *
 * @param {type} key
 * @param {S3Client~callbackAcl} callback Called when response from the server is returned.
 */
S3Client.prototype.getAcl = function (key, callback) {
    key = key + '?acl';
    this._spawnFunction(this._get.bind(this, key, null), function (err, response) {
        if (err) {
            return callback(err, response);
        }

        if (response.getStatusCode() != 200) {
            var error = new Error('Error fetching ACLs - status code: ' + response.getStatusCode());
            return callback(error, null);
        }

        var xml = response.getBody().toBuffer().toString();
        xml2js.parseString(xml, {explicitArray: false}, function (err, acl) {
            if (err) {
                var error = new Error('Error parsing XML response');
                return callback(error, null);
            }

            callback(null, acl.AccessControlPolicy);
        });
    });
};

/**
 * Wraps knox request and manages request events
 *
 * @private
 * @param {string} key
 * @param {Object} headers
 * @param {Buffer} body
 * @param {function} callback
 */
S3Client.prototype._set = function (key, headers, body, callback) {
    var request = this._client.put(key, headers, body);
    if (this.timeout) {
        request.setTimeout(this.timeout);
    }
    request.on('response', this._handleResponse.bind(this, callback));
    request.on('error', function (error) {
        callback(error, null);
    });
    request.once('timeout', function () {
        request.abort();
    });
    request.end(body);
};


/**
 * Wraps knox request and manages request events
 *
 * @private
 * @param {string} keySrc
 * @param {Object} keyDst
 * @param {Buffer} headers
 * @param {function} callback
 */
S3Client.prototype._copy = function (keySrc, keyDst, headers, callback) {
    var request = this._client.copy(keySrc, keyDst, headers);
    if (this.timeout) {
        request.setTimeout(this.timeout);
    }
    request.on('response', this._handleResponse.bind(this, callback));
    request.on('error', function (error) {
        callback(error, null);
    });
    request.once('timeout', function () {
        request.abort();
    });
    request.end();
};


/**
 * Wraps knox request and manages request events
 *
 * @private
 * @param {string} key
 * @param {function} callback
 */
S3Client.prototype._get = function (key, headers, callback) {
    var request = this._client.get(key, headers);
    if (this.timeout) {
        request.setTimeout(this.timeout);
    }
    request.on('response', this._handleResponse.bind(this, callback));
    request.on('error', function (error) {
        callback(error, null);
    });
    request.once('timeout', function () {
        request.abort();
    });
    request.end();
};


/**
 * Wraps knox request and manages request events
 *
 * @private
 * @param {string} key
 * @param {function} callback
 */
S3Client.prototype._head = function (key, headers, callback) {
    var request = this._client.head(key, headers);
    if (this.timeout) {
        request.setTimeout(this.timeout);
    }
    request.on('response', this._handleResponse.bind(this, callback));
    request.on('error', function (error) {
        callback(error, null);
    });
    request.once('timeout', function () {
        request.abort();
    });
    request.end();
};


/**
 * Wraps knox request and manages request events
 *
 * @private
 * @param {string} key
 * @param {function} callback
 */
S3Client.prototype._del = function (key, callback) {
    var request = this._client.del(key);
    if (this.timeout) {
        request.setTimeout(this.timeout);
    }
    request.on('response', this._handleResponse.bind(this, callback));
    request.on('error', function (error) {
        callback(error, null);
    });
    request.once('timeout', function () {
        request.abort();
    });
    request.end();
};

/**
 * @private
 * @param {function} callback
 * @param {http.response} res
 */
S3Client.prototype._handleResponse = function (callback, res) {
    var dataLength = 0;
    var buffers = [];

    res.on('data', function (data) {
        buffers.push(data);
        dataLength += data.length;
    });
    res.on('end', function () {
        var resultBuffer   = new Buffer(dataLength);
        var bufferPosition = 0;
        for (var i = 0, l = buffers.length; i < l; i++) {
            buffers[i].copy(resultBuffer, bufferPosition);
            bufferPosition += buffers[i].length;
        }
        var response = new Response();
        response.setStatusCode(res.statusCode);
        response.setHeaders(res.headers);
        response._setBody(resultBuffer);

        callback(null, response);
    });
};

/**
 * Function for handling retries
 *
 * @private
 * @param {function} fnc function to call
 * @param {function} callback callback fired when specified number
 *                      of retries reached or response recieved
 */
S3Client.prototype._spawnFunction = function (fnc, callback) {
    var maxRetry = this.retry;

    fnc(function (err, data) {
        if (err && maxRetry--) {
            console.log('S3Client retrying operation...', maxRetry);
            fnc(arguments.callee);
        } else {
            callback(err, data);
        }
    });
};

S3Client.prototype._getError = function (err) {
    var error = new Error(err.msg);
    error.code = err.code;

    return error;
};

/**
 * Checks if passed ACL allows for public read.
 *
 * @param {Object} acl ACL
 * @returns {Boolean}  True or false depending on ACL configuration
 */
S3Client.isPublicRead = function (acl) {
    try {
        var grants = acl.AccessControlList.Grant;
        var grant;
        for (var i = 0, len = grants.length; i < len; i++) {
            grant = grants[i];

            if (grant.Grantee.URI == 'http://acs.amazonaws.com/groups/global/AllUsers') {
                if (['READ', 'FULL_CONTROL'].indexOf(grant.Permission) != -1) {
                    return true;
                }
            }
        }
        return false;
    } catch (ex) {
        console.warn('S3Client error processing ACLs', ex);
        return false;
    }
};


S3Client.Error = {};
S3Client.Error.ENOBODY    = {code: 'ENOBODY',     msg: 'No body given'};
S3Client.Error.ENOKEY     = {code: 'ENOKEY',      msg: 'No key give'};
S3Client.Error.ENOHEADERS = {code: 'ENOHEADERS',  msg: 'No headers given'};

exports.S3Client = S3Client;
