text: var querystring = require('querystring'),
    http = require('./http'),
    fs = require('fs'),
    path = require('path'),
    url = require('url'),
    stream = require('readable-stream'),
    HttpDuplex = require('./http_duplex'),
    debug = require('debug')('modem'),
    util = require('util'),
    splitca = require('split-ca'),
    JSONStream = require('JSONStream'),
    isWin = require('os').type() === 'Windows_NT';
  
  var defaultOpts = function() {
    var split;
    var opts = {};
  
    if (!process.env.DOCKER_HOST) {
      // Windows socket path: //./pipe/docker_engine ( Windows 10 )
      // Linux & Darwin socket path: /var/run/docker.sock
      opts.socketPath = isWin ? '//./pipe/docker_engine' : '/var/run/docker.sock';
    } else if (process.env.DOCKER_HOST.indexOf('unix://') === 0) {
      // Strip off unix://, fall back to default of /var/run/docker.sock if
      // unix:// was passed without a path
      opts.socketPath = process.env.DOCKER_HOST.substring(7) || '/var/run/docker.sock';
    } else {
      split = /(?:tcp:\/\/)?(.*?):([0-9]+)/g.exec(process.env.DOCKER_HOST);
  
      if (!split || split.length !== 3) {
        throw new Error('DOCKER_HOST env variable should be something like tcp://localhost:1234');
      }
  
      opts.port = split[2];
  
      if (process.env.DOCKER_TLS_VERIFY === '1' || opts.port === '2376') {
        opts.protocol = 'https';
      } else {
        opts.protocol = 'http';
      }
  
      opts.host = split[1];
  
      if (process.env.DOCKER_CERT_PATH) {
        opts.ca = splitca(path.join(process.env.DOCKER_CERT_PATH, 'ca.pem'));
        opts.cert = fs.readFileSync(path.join(process.env.DOCKER_CERT_PATH, 'cert.pem'));
        opts.key = fs.readFileSync(path.join(process.env.DOCKER_CERT_PATH, 'key.pem'));
      }
    }
  
    return opts;
  };
  
  var Modem = function(opts) {
    if (!opts) {
      opts = defaultOpts();
    }
  
    this.socketPath = opts.socketPath;
    this.host = opts.host;
    this.port = opts.port;
    this.version = opts.version;
    this.key = opts.key;
    this.cert = opts.cert;
    this.ca = opts.ca;
    this.timeout = opts.timeout;
    this.checkServerIdentity = opts.checkServerIdentity;
  
    if (this.key && this.cert && this.ca) {
      this.protocol = 'https';
    }
    this.protocol = opts.protocol || this.protocol || 'http';
  };
  
  Modem.prototype.dial = function(options, callback) {
    var opts, address, data;
    var self = this;
  
    if (options.options) {
      opts = options.options;
    }
  
    // Prevent credentials from showing up in URL
    if (opts && opts.authconfig) {
      delete opts.authconfig;
    }
  
    if (this.version) {
      options.path = '/' + this.version + options.path;
    }
  
    if (this.host) {
      var parsed = url.parse(self.host);
      address = url.format({
        'protocol': parsed.protocol || self.protocol,
        'hostname': parsed.hostname || self.host,
        'port': self.port
      });
      address = url.resolve(address, options.path);
    } else {
      address = options.path;
    }
  
    if (options.path.indexOf('?') !== -1) {
      if (opts && Object.keys(opts).length > 0) {
        address += this.buildQuerystring(opts._query || opts);
      } else {
        address = address.substring(0, address.length - 1);
      }
    }
  
    var optionsf = {
      path: address,
      method: options.method,
      headers: options.headers || {},
      key: self.key,
      cert: self.cert,
      ca: self.ca
    };
  
    if (this.checkServerIdentity) {
      optionsf.checkServerIdentity = this.checkServerIdentity;
    }
  
    if (options.authconfig) {
      optionsf.headers['X-Registry-Auth'] = options.authconfig.key || options.authconfig.base64 ||
        new Buffer(JSON.stringify(options.authconfig)).toString('base64');
    }
  
    if (options.registryconfig) {
      optionsf.headers['X-Registry-Config'] = options.registryconfig.base64 ||
        new Buffer(JSON.stringify(options.registryconfig)).toString('base64');
    }
  
    if (options.file) {
      if (typeof options.file === 'string') {
        data = fs.readFileSync(path.resolve(options.file));
      } else {
        data = options.file;
      }
      optionsf.headers['Content-Type'] = 'application/tar';
    } else if (opts && options.method === 'POST') {
      data = JSON.stringify(opts._body || opts);
      optionsf.headers['Content-Type'] = 'application/json';
    }
  
    if (typeof data === "string") {
      optionsf.headers['Content-Length'] = Buffer.byteLength(data);
    } else if (Buffer.isBuffer(data) === true) {
      optionsf.headers['Content-Length'] = data.length;
    } else {
      optionsf.headers['Transfer-Encoding'] = 'chunked';
    }
  
    if (options.hijack) {
      optionsf.headers.Connection = 'Upgrade';
      optionsf.headers.Upgrade = 'tcp';
    }
  
    if (this.socketPath) {
      optionsf.socketPath = this.socketPath;
    } else {
      var urlp = url.parse(address);
      optionsf.hostname = urlp.hostname;
      optionsf.port = urlp.port;
      optionsf.path = urlp.path;
    }
  
    this.buildRequest(optionsf, options, data, callback);
  };
  
  Modem.prototype.buildRequest = function(options, context, data, callback) {
    var self = this;
    var req = http[self.protocol].request(options, function() {});
  
    debug('Sending: %s', util.inspect(options, {
      showHidden: true,
      depth: null
    }));
  
    if (self.timeout) {
      req.on('socket', function(socket) {
        socket.setTimeout(self.timeout);
        socket.on('timeout', function() {
          debug('Timeout of %s ms exceeded', self.timeout);
          req.abort();
        });
      });
    }
  
    if (context.hijack === true) {
      req.on('upgrade', function(res, sock, head) {
        return callback(null, sock);
      });
    }
  
    req.on('response', function(res) {
      if (context.isStream === true) {
        self.buildPayload(null, context.isStream, context.statusCodes, context.openStdin, req, res, null, callback);
      } else {
        var chunks = '';
        res.on('data', function(chunk) {
          chunks += chunk;
        });
  
        res.on('end', function() {
          debug('Received: %s', chunks);
  
          var json;
          try {
            json = JSON.parse(chunks);
          } catch (e) {
            json = chunks;
          }
          self.buildPayload(null, context.isStream, context.statusCodes, false, req, res, json, callback);
        });
      }
    });
  
    req.on('error', function(error) {
      self.buildPayload(error, context.isStream, context.statusCodes, false, {}, {}, null, callback);
    });
  
    if (typeof data === "string" || Buffer.isBuffer(data)) {
      req.write(data);
    } else if (data) {
      data.pipe(req);
    }
  
    if (!context.hijack && !context.openStdin && (typeof data === "string" || data === undefined || Buffer.isBuffer(data))) {
      req.end();
    }
  };
  
  Modem.prototype.buildPayload = function(err, isStream, statusCodes, openStdin, req, res, json, cb) {
    if (err) return cb(err, null);
  
    if (statusCodes[res.statusCode] !== true) {
      getCause(isStream, res, json, function(err, cause) {
        var msg = new Error(
          '(HTTP code ' + res.statusCode + ') ' +
          (statusCodes[res.statusCode] || 'unexpected') + ' - ' +
          (cause.message || cause) + ' '
        );
        msg.reason = statusCodes[res.statusCode];
        msg.statusCode = res.statusCode;
        msg.json = json;
        cb(msg, null);
      });
    } else {
      if (openStdin) {
        cb(null, new HttpDuplex(req, res));
      } else if (isStream) {
        cb(null, res);
      } else {
        cb(null, json);
      }
    }
  
    function getCause(isStream, res, json, callback) {
      var chunks = '';
      if (isStream) {
        res.on('data', function(chunk) {
          chunks += chunk;
        });
        res.on('end', function() {
          callback(null, chunks);
        });
      } else {
        callback(null, json);
      }
    }
  };
  
  Modem.prototype.demuxStream = function(stream, stdout, stderr) {
    var header = null;
  
    stream.on('readable', function() {
      header = header || stream.read(8);
      while (header !== null) {
        var type = header.readUInt8(0);
        var payload = stream.read(header.readUInt32BE(4));
        if (payload === null) break;
        if (type == 2) {
          stderr.write(payload);
        } else {
          stdout.write(payload);
        }
        header = stream.read(8);
      }
    });
  };
  
  Modem.prototype.followProgress = function(stream, onFinished, onProgress) {
    var parser = JSONStream.parse(),
      output = [];
  
    parser.on('root', onStreamEvent);
    parser.on('error', onStreamError);
    parser.on('end', onStreamEnd);
  
    stream.pipe(parser);
  
    function onStreamEvent(evt) {
      if (!(evt instanceof Object)) {
        evt = {};
      }
  
      output.push(evt);
  
      if (evt.error) {
        return onStreamError(evt.error);
      }
  
      if (onProgress) {
        onProgress(evt);
      }
    }
  
    function onStreamError(err) {
      parser.removeListener('root', onStreamEvent);
      parser.removeListener('error', onStreamError);
      parser.removeListener('end', onStreamEnd);
      onFinished(err, output);
    }
  
    function onStreamEnd() {
      onFinished(null, output);
    }
  };
  
  Modem.prototype.buildQuerystring = function(opts) {
    var clone = {};
  
    // serialize map values as JSON strings, else querystring truncates.
    Object.keys(opts).map(function(key, i) {
      clone[key] = (opts[key] && typeof opts[key] === 'object') ?
        JSON.stringify(opts[key]) : opts[key];
    });
  
    return querystring.stringify(clone);
  };
  
  module.exports = Modem;
  