var http         = require('http')
  , https        = require('https')
  , url          = require('url')
  , Serializer   = require('./serializer')
  , Deserializer = require('./deserializer')
  , Cookies      = require('./cookies')

/**
 * Creates a Client object for making XML-RPC method calls.
 *
 * @constructor
 * @param {Object|String} options - Server options to make the HTTP request to.
 *                                  Either a URI string
 *                                  (e.g. 'http://localhost:9090') or an object
 *                                  with fields:
 *   - {String} host              - (optional)
 *   - {Number} port
 *   - {String} url               - (optional) - may be used instead of host/port pair
 *   - {Boolean} cookies          - (optional) - if true then cookies returned by server will be stored and sent back on the next calls.
 *                                  Also it will be possible to access/manipulate cookies via #setCookie/#getCookie methods
 * @param {Boolean} isSecure      - True if using https for making calls,
 *                                  otherwise false.
 * @return {Client}
 */
function Client(options, isSecure) {

  // Invokes with new if called without
  if (false === (this instanceof Client)) {
    return new Client(options, isSecure)
  }

  // If a string URI is passed in, converts to URI fields
  if (typeof options === 'string') {
    options = url.parse(options)
    options.host = options.hostname
    options.path = options.pathname
  }

  if (typeof options.url !== 'undefined') {
    var parsedUrl = url.parse(options.url);
    options.host = parsedUrl.hostname;
    options.path = parsedUrl.pathname;
    options.port = parsedUrl.port;
  }

  // Set the HTTP request headers
  var headers = {
    'User-Agent'     : 'NodeJS XML-RPC Client'
  , 'Content-Type'   : 'text/xml'
  , 'Accept'         : 'text/xml'
  , 'Accept-Charset' : 'UTF8'
  , 'Connection'     : 'Keep-Alive'
  }
  options.headers = options.headers || {}

  if (options.headers.Authorization == null &&
      options.basic_auth != null &&
      options.basic_auth.user != null &&
      options.basic_auth.pass != null)
  {
    var auth = options.basic_auth.user + ':' + options.basic_auth.pass
    options.headers['Authorization'] = 'Basic ' + new Buffer(auth).toString('base64')
  }

  for (var attribute in headers) {
    if (options.headers[attribute] === undefined) {
      options.headers[attribute] = headers[attribute]
    }
  }

  options.method = 'POST'
  this.options = options

  this.isSecure = isSecure
  this.headersProcessors = {
    processors: [],
    composeRequest: function(headers) {
      this.processors.forEach(function(p) {p.composeRequest(headers);})
    },
    parseResponse: function(headers) {
      this.processors.forEach(function(p) {p.parseResponse(headers);})
    }
  };
  if (options.cookies) {
    this.cookies = new Cookies();
    this.headersProcessors.processors.unshift(this.cookies);
  }
}

/**
 * Makes an XML-RPC call to the server specified by the constructor's options.
 *
 * @param {String} method     - The method name.
 * @param {Array} params      - Params to send in the call.
 * @param {Function} callback - function(error, value) { ... }
 *   - {Object|null} error    - Any errors when making the call, otherwise null.
 *   - {mixed} value          - The value returned in the method response.
 */
Client.prototype.methodCall = function methodCall(method, params, callback) {
  var options   = this.options
  var xml       = Serializer.serializeMethodCall(method, params, options.encoding)
  var transport = this.isSecure ? https : http

  options.headers['Content-Length'] = Buffer.byteLength(xml, 'utf8')
  this.headersProcessors.composeRequest(options.headers)
  var request = transport.request(options, function(response) {

    var body = []
    response.on('data', function (chunk) { body.push(chunk) })

    function __enrichError (err) {
      Object.defineProperty(err, 'req', { value: request })
      Object.defineProperty(err, 'res', { value: response })
      Object.defineProperty(err, 'body', { value: body.join('') })
      return err
    }

    if (response.statusCode == 404) {
      callback(__enrichError(new Error('Not Found')))
    }
    else {
      this.headersProcessors.parseResponse(response.headers)

      var deserializer = new Deserializer(options.responseEncoding)

      deserializer.deserializeMethodResponse(response, function(err, result) {
        if (err) {
          err = __enrichError(err)
        }
        callback(err, result)
      })
    }
  }.bind(this))

  request.on('error', callback)
  request.write(xml, 'utf8')
  request.end()
}

/**
 * Gets the cookie value by its name. The latest value received from servr with 'Set-Cookie' header is returned
 * Note that method throws an error if cookies were not turned on during client creation (see comments for constructor)
 *
 * @param {String} name name of the cookie to be obtained or changed
 * @return {*} cookie's value
 */
Client.prototype.getCookie = function getCookie(name) {
  if (!this.cookies) {
    throw 'Cookies support is not turned on for this client instance';
  }
  return this.cookies.get(name);
}

/**
 * Sets the cookie value by its name. The cookie will be sent to the server during the next xml-rpc call.
 * The method returns client itself, so it is possible to chain calls like the following:
 *
 * <code>
 *   client.cookie('login', 'alex').cookie('password', '123');
 * </code>
 *
 * Note that method throws an error if cookies were not turned on during client creation (see comments for constructor)
 *
 * @param {String} name name of the cookie to be changed
 * @param {String} value value to be set.
 * @return {*} client object itself
 */
Client.prototype.setCookie = function setCookie(name, value) {
  if (!this.cookies) {
    throw 'Cookies support is not turned on for this client instance';
  }
  this.cookies.set(name, value);
  return this;
}

module.exports = Client

