text: // Licensed under the Apache License, Version 2.0 (the 'License'); you may not
  // use this file except in compliance with the License. You may obtain a copy of
  // the License at
  //
  //   http://www.apache.org/licenses/LICENSE-2.0
  //
  // Unless required by applicable law or agreed to in writing, software
  // distributed under the License is distributed on an 'AS IS' BASIS, WITHOUT
  // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
  // License for the specific language governing permissions and limitations under
  // the License.
  
  'use strict';
  
  var u = require('url');
  var assert = require('assert');
  var querystring = require('querystring');
  var request = require('request');
  var errs = require('errs');
  var _ = require('underscore');
  var follow = require('follow');
  var logger = require('./logger');
  
  var nano;
  
  module.exports = exports = nano = function dbScope(cfg) {
    var serverScope = {};
  
    if (typeof cfg === 'string') {
      cfg = {url: cfg};
    }
  
    assert.equal(typeof cfg, 'object',
      'You must specify the endpoint url when invoking this module');
    assert.ok(/^https?:/.test(cfg.url), 'url is not valid');
  
    cfg = _.clone(cfg);
  
    serverScope.config = cfg;
    cfg.requestDefaults = cfg.requestDefaults || {jar: false};
  
    var httpAgent = (typeof cfg.request === 'function') ? cfg.request :
      request.defaults(cfg.requestDefaults);
    var followAgent = (typeof cfg.follow === 'function') ? cfg.follow : follow;
    var log = typeof cfg.log === 'function' ? cfg.log : logger(cfg);
    var parseUrl = 'parseUrl' in cfg ? cfg.parseUrl : true;
  
    function maybeExtractDatabaseComponent() {
      if (!parseUrl) {
        return;
      }
  
      var path = u.parse(cfg.url);
      var pathArray = path.pathname.split('/').filter(function(e) { return e; });
      var db = pathArray.pop();
      var rootPath = path.pathname.replace(/\/?$/, '/..');
  
      if (db) {
        cfg.url = urlResolveFix(cfg.url, rootPath).replace(/\/?$/, '');
        return db;
      }
    }
  
    function scrub(str) {
      if (str) {
        str = str.replace(/\/\/(.*)@/,"//XXXXXX:XXXXXX@");
      }
      return str;
    }
  
    function relax(opts, callback) {
      if (typeof opts === 'function') {
        callback = opts;
        opts = {path: ''};
      }
  
      if (typeof opts === 'string') {
        opts = {path: opts};
      }
  
      if (!opts) {
        opts = {path: ''};
        callback = null;
      }
  
      var qs = _.extend({}, opts.qs);
  
      var headers = {
        'content-type': 'application/json',
        accept: 'application/json'
      };
  
      var req = {
        method: (opts.method || 'GET'),
        headers: headers,
        uri: cfg.url
      };
  
      var parsed;
      var rh;
  
      // https://github.com/mikeal/request#requestjar
      var isJar = opts.jar || cfg.jar;
  
      if (isJar) {
        req.jar = isJar;
      }
  
      // http://wiki.apache.org/couchdb/HTTP_database_API#Naming_and_Addressing
      if (opts.db) {
        req.uri = urlResolveFix(req.uri, encodeURIComponent(opts.db));
      }
  
      if (opts.multipart) {
        req.multipart = opts.multipart;
      }
  
      req.headers = _.extend(req.headers, opts.headers, cfg.defaultHeaders);
  
      if (opts.path) {
        req.uri += '/' + opts.path;
      } else if (opts.doc) {
        if (!/^_design/.test(opts.doc)) {
          // http://wiki.apache.org/couchdb/HTTP_Document_API#Naming.2FAddressing
          req.uri += '/' + encodeURIComponent(opts.doc);
        } else {
          // http://wiki.apache.org/couchdb/HTTP_Document_API#Document_IDs
          req.uri += '/' + opts.doc;
        }
  
        // http://wiki.apache.org/couchdb/HTTP_Document_API#Attachments
        if (opts.att) {
          req.uri += '/' + opts.att;
        }
      }
  
      // prevent bugs where people set encoding when piping
      if (opts.encoding !== undefined && callback) {
        req.encoding = opts.encoding;
        delete req.headers['content-type'];
        delete req.headers.accept;
      }
  
      if (opts.contentType) {
        req.headers['content-type'] = opts.contentType;
        delete req.headers.accept;
      }
  
      // http://guide.couchdb.org/draft/security.html#cookies
      if (cfg.cookie) {
        req.headers['X-CouchDB-WWW-Authenticate'] = 'Cookie';
        req.headers.cookie = cfg.cookie;
      }
  
      // http://wiki.apache.org/couchdb/HTTP_view_API#Querying_Options
      if (typeof opts.qs === 'object' && !_.isEmpty(opts.qs)) {
        ['startkey', 'endkey', 'key', 'keys'].forEach(function(key) {
          if (key in opts.qs) {
            qs[key] = JSON.stringify(opts.qs[key]);
          }
        });
        req.qs = qs;
      }
  
      if (opts.body) {
        if (Buffer.isBuffer(opts.body) || opts.dontStringify) {
          req.body = opts.body;
        } else {
          req.body = JSON.stringify(opts.body, function(key, value) {
            // don't encode functions
            if (typeof(value) === 'function') {
              return value.toString();
            } else {
              return value;
            }
          });
        }
      }
  
      if (opts.form) {
        req.headers['content-type'] =
          'application/x-www-form-urlencoded; charset=utf-8';
        req.body = querystring.stringify(opts.form).toString('utf8');
      }
  
      log(req);
  
      if (!callback) {
        return httpAgent(req);
      }
  
      return httpAgent(req, function(e, h, b) { 
        rh = h && h.headers || {};
        rh.statusCode = h && h.statusCode || 500;
        rh.uri = req.uri;
        if (e) {
          log({err: 'socket', body: b, headers: rh});
          return callback(errs.merge(e, {
            message: 'error happened in your connection',
            scope: 'socket',
            errid: 'request'
          }));
        }
  
        delete rh.server;
        delete rh['content-length'];
  
        if (opts.dontParse) {
          parsed = b;
        } else {
          try { parsed = JSON.parse(b); } catch (err) { parsed = b; }
        }
  
        if (rh.statusCode >= 200 && rh.statusCode < 400) {
          log({err: null, body: parsed, headers: rh});
          return callback(null, parsed, rh);
        }
  
        log({err: 'couch', body: parsed, headers: rh});
  
        // cloudant stacktrace
        if (typeof parsed === 'string') {
          parsed = {message: parsed};
        }
  
        if (!parsed.message && (parsed.reason || parsed.error)) {
          parsed.message = (parsed.reason || parsed.error);
        }
  
        // fix cloudant issues where they give an erlang stacktrace as js
        delete parsed.stack;
  
        // scrub credentials
        req.uri = scrub(req.uri);
        rh.uri = scrub(rh.uri);
        if (req.headers.cookie) {
          req.headers.cookie = "XXXXXXX";
        }
  
        callback(errs.merge({
          message: 'couch returned ' + rh.statusCode,
          scope: 'couch',
          statusCode: rh.statusCode,
          request: req,
          headers: rh,
          errid: 'non_200'
        }, errs.create(parsed)));
      });
    }
  
    // http://docs.couchdb.org/en/latest/api/server/authn.html#cookie-authentication
    function auth(username, password, callback) {
      return relax({
        method: 'POST',
        db: '_session',
        form: {
          name: username,
          password: password
        }
      }, callback);
    }
  
    // http://docs.couchdb.org/en/latest/api/server/authn.html#post--_session
    function session(callback) {
      return relax({db: '_session'}, callback);
    }
  
    // http://docs.couchdb.org/en/latest/api/server/common.html#get--_db_updates
    function updates(qs, callback) {
      if (typeof qs === 'function') {
        callback = qs;
        qs = {};
      }
      return relax({
        db: '_db_updates',
        qs: qs
      }, callback);
    }
  
    function followUpdates(qs, callback) {
      return followDb('_db_updates', qs, callback);
    }
  
    // http://docs.couchdb.org/en/latest/api/database/common.html#put--db
    function createDb(dbName, callback) {
      return relax({db: dbName, method: 'PUT'}, callback);
    }
  
    // http://docs.couchdb.org/en/latest/api/database/common.html#delete--db
    function destroyDb(dbName, callback) {
      return relax({db: dbName, method: 'DELETE'}, callback);
    }
  
    // http://docs.couchdb.org/en/latest/api/database/common.html#get--db
    function getDb(dbName, callback) {
      return relax({db: dbName}, callback);
    }
  
    // http://docs.couchdb.org/en/latest/api/server/common.html#get--_all_dbs
    function listDbs(callback) {
      return relax({db: '_all_dbs'}, callback);
    }
  
    // http://docs.couchdb.org/en/latest/api/database/compact.html#post--db-_compact
    function compactDb(dbName, ddoc, callback) {
      if (typeof ddoc === 'function') {
        callback = ddoc;
        ddoc = null;
      }
      return relax({
        db: dbName,
        doc: '_compact',
        att: ddoc,
        method: 'POST'
      }, callback);
    }
  
    // http://docs.couchdb.org/en/latest/api/database/changes.html#get--db-_changes
    function changesDb(dbName, qs, callback) {
      if (typeof qs === 'function') {
        callback = qs;
        qs = {};
      }
      return relax({db: dbName, path: '_changes', qs: qs}, callback);
    }
  
    function followDb(dbName, qs, callback) {
      if (typeof qs === 'function') {
        callback = qs;
        qs = {};
      }
  
      qs.db = urlResolveFix(cfg.url, encodeURIComponent(dbName));
  
      if (typeof callback === 'function') {
        return followAgent(qs, callback);
      } else {
        return new followAgent.Feed(qs);
      }
    }
  
    function _serializeAsUrl(db) {
      if (typeof db === 'object' && db.config && db.config.url && db.config.db) {
        return urlResolveFix(db.config.url, encodeURIComponent(db.config.db));
      } else {
        return db;
      }
    }
  
    // http://docs.couchdb.org/en/latest/api/server/common.html#post--_replicate
    function replicateDb(source, target, opts, callback) {
      if (typeof opts === 'function') {
        callback = opts;
        opts = {};
      }
  
      opts.source = _serializeAsUrl(source);
      opts.target = _serializeAsUrl(target);
  
      return relax({db: '_replicate', body: opts, method: 'POST'}, callback);
    }
  
    // http://docs.couchdb.org/en/latest/api/server/common.html#uuids
    function uuids(count, callback) {
      if (typeof count === 'function') {
        callback = count;
        count = 1;
      }
  
      return relax({ method: 'GET', path: '_uuids', qs: {count: count}}, callback);
    }
  
    function docModule(dbName) {
      var docScope = {};
      dbName = decodeURIComponent(dbName);
  
      // http://docs.couchdb.org/en/latest/api/document/common.html#put--db-docid
      // http://docs.couchdb.org/en/latest/api/database/common.html#post--db
      function insertDoc(doc, qs, callback) {
        var opts = {db: dbName, body: doc, method: 'POST'};
  
        if (typeof qs === 'function') {
          callback = qs;
          qs = {};
        }
  
        if (typeof qs === 'string') {
          qs = {docName: qs};
        }
  
        if (qs) {
          if (qs.docName) {
            opts.doc = qs.docName;
            opts.method = 'PUT';
            delete qs.docName;
          }
          opts.qs = qs;
        }
  
        return relax(opts, callback);
      }
  
      // http://docs.couchdb.org/en/latest/api/document/common.html#delete--db-docid
      function destroyDoc(docName, rev, callback) {
        if(!docName) {
          if(callback)
            callback("Invalid doc id", null);
        }
        else {
          return relax({
            db: dbName,
            doc: docName,
            method: 'DELETE',
            qs: {rev: rev}
          }, callback);
        }
      }
  
      // http://docs.couchdb.org/en/latest/api/document/common.html#get--db-docid
      function getDoc(docName, qs, callback) {
        if (typeof qs === 'function') {
          callback = qs;
          qs = {};
        }
  
        return relax({db: dbName, doc: docName, qs: qs}, callback);
      }
  
      // http://docs.couchdb.org/en/latest/api/document/common.html#head--db-docid
      function headDoc(docName, callback) {
        return relax({
          db: dbName,
          doc: docName,
          method: 'HEAD',
          qs: {}
        }, callback);
      }
  
      // http://docs.couchdb.org/en/latest/api/document/common.html#copy--db-docid
      function copyDoc(docSrc, docDest, opts, callback) {
        if (typeof opts === 'function') {
          callback = opts;
          opts = {};
        }
  
        var qs = {
          db: dbName,
          doc: docSrc,
          method: 'COPY',
          headers: {'Destination': docDest}
        };
  
        if (opts.overwrite) {
          return headDoc(docDest, function(e, b, h) {
            if (e && e.statusCode !== 404) {
              return callback(e);
            }
            if (h.etag) {
              qs.headers.Destination += '?rev=' +
                h.etag.substring(1, h.etag.length - 1);
            }
            return relax(qs, callback);
          });
        } else {
          return relax(qs, callback);
        }
      }
  
      // http://docs.couchdb.org/en/latest/api/database/bulk-api.html#get--db-_all_docs
      function listDoc(qs, callback) {
        if (typeof qs === 'function') {
          callback = qs;
          qs = {};
        }
  
        return relax({db: dbName, path: '_all_docs', qs: qs}, callback);
      }
  
      // http://docs.couchdb.org/en/latest/api/database/bulk-api.html#post--db-_all_docs
      function fetchDocs(docNames, qs, callback) {
        if (typeof qs === 'function') {
          callback = qs;
          qs = {};
        }
  
        qs = qs || {};
        qs['include_docs'] = true;
  
        return relax({
          db: dbName,
          path: '_all_docs',
          method: 'POST',
          qs: qs,
          body: docNames
        }, callback);
      }
  
      function fetchRevs(docNames, qs, callback) {
        if (typeof qs === 'function') {
          callback = qs;
          qs = {};
        }
        return relax({
          db: dbName,
          path: '_all_docs',
          method: 'POST',
          qs: qs,
          body: docNames
        }, callback);
      }
  
      function view(ddoc, viewName, meta, qs, callback) {
        if (typeof qs === 'function') {
          callback = qs;
          qs = {};
        }
  
        var viewPath = '_design/' + ddoc + '/_' + meta.type + '/'  + viewName;
  
        // Several search parameters must be JSON-encoded; but since this is an
        // object API, several parameters need JSON endoding.
        var paramsToEncode = ['counts', 'drilldown', 'group_sort', 'ranges', 'sort'];
        paramsToEncode.forEach(function(param) {
          if (param in qs) {
            if (typeof qs[param] !== 'string') {
              qs[param] = JSON.stringify(qs[param]);
            } else {
              // if the parameter is not already encoded, encode it
              try {
                JSON.parse(qs[param]);
              } catch(e) {
                qs[param] = JSON.stringify(qs[param]);
              }
            }
          }
        });
  
        if (qs && qs.keys) {
          var body = {keys: qs.keys};
          delete qs.keys;
          return relax({
            db: dbName,
            path: viewPath,
            method: 'POST',
            qs: qs,
            body: body
          }, callback);
        } else {
          var req = {
            db: dbName,
            method: meta.method || 'GET',
            path: viewPath,
            qs: qs
          };
  
          if (meta.body) {
            req.body = meta.body;
          }
  
          return relax(req, callback);
        }
      }
  
      // http://docs.couchdb.org/en/latest/api/ddoc/views.html#post--db-_design-ddoc-_view-view
      function viewDocs(ddoc, viewName, qs, callback) {
        return view(ddoc, viewName, {type: 'view'}, qs, callback);
      }
  
      // geocouch
      function viewSpatial(ddoc, viewName, qs, callback) {
        return view(ddoc, viewName, {type: 'spatial'}, qs, callback);
      }
  
      // cloudant
      function viewSearch(ddoc, viewName, qs, callback) {
        return view(ddoc, viewName, {type: 'search'}, qs, callback);
      }
  
      // http://docs.couchdb.org/en/latest/api/ddoc/render.html#get--db-_design-ddoc-_show-func
      function showDoc(ddoc, viewName, docName, qs, callback) {
        return view(ddoc, viewName + '/' + docName, {type: 'show'}, qs, callback);
      }
  
      // http://docs.couchdb.org/en/latest/api/ddoc/render.html#put--db-_design-ddoc-_update-func-docid
      function updateWithHandler(ddoc, viewName, docName, body, callback) {
        if (typeof body === 'function') {
            callback = body;
            body = undefined;
        }
        return view(ddoc, viewName + '/' + encodeURIComponent(docName), {
          type: 'update',
          method: 'PUT',
          body: body
        }, callback);
      }
  
      function viewWithList(ddoc, viewName, listName, qs, callback) {
        return view(ddoc, listName + '/' + viewName, {
          type: 'list'
        }, qs, callback);
      }
  
      // http://docs.couchdb.org/en/latest/api/database/bulk-api.html#post--db-_bulksDoc
      function bulksDoc(docs, qs, callback) {
        if (typeof qs === 'function') {
          callback = qs;
          qs = {};
        }
  
        return relax({
          db: dbName,
          path: '_bulk_docs',
          body: docs,
          method: 'POST',
          qs: qs
        }, callback);
      }
  
      // http://docs.couchdb.org/en/latest/api/document/common.html#creating-multiple-attachments
      function insertMultipart(doc, attachments, qs, callback) {
        if (typeof qs === 'string') {
          qs = {docName: qs};
        }
  
        var docName = qs.docName;
        delete qs.docName;
  
        doc = _.extend({_attachments: {}}, doc);
  
        var multipart = [];
  
        attachments.forEach(function(att) {
          doc._attachments[att.name] = {
            follows: true,
            length: Buffer.isBuffer(att.data) ?
              att.data.length : Buffer.byteLength(att.data),
            /* jscs:disable requireCamelCaseOrUpperCaseIdentifiers */
            'content_type': att.content_type
          };
          multipart.push({body: att.data});
        });
  
        multipart.unshift({
          'content-type': 'application/json',
          body: JSON.stringify(doc)
        });
  
        return relax({
          db: dbName,
          method: 'PUT',
          contentType: 'multipart/related',
          doc: docName,
          qs: qs,
          multipart: multipart
        }, callback);
      }
  
      function getMultipart(docName, qs, callback) {
        if (typeof qs === 'function') {
          callback = qs;
          qs = {};
        }
  
        qs.attachments = true;
  
        return relax({
          db: dbName,
          doc: docName,
          encoding: null,
          contentType: 'multipart/related',
          qs: qs
        }, callback);
      }
  
      function insertAtt(docName, attName, att, contentType, qs, callback) {
        if (typeof qs === 'function') {
          callback = qs;
          qs = {};
        }
  
        return relax({
          db: dbName,
          att: attName,
          method: 'PUT',
          contentType: contentType,
          doc: docName,
          qs: qs,
          body: att,
          dontStringify: true
        }, callback);
      }
  
      function getAtt(docName, attName, qs, callback) {
        if (typeof qs === 'function') {
          callback = qs;
          qs = {};
        }
  
        return relax({
          db: dbName,
          att: attName,
          doc: docName,
          qs: qs,
          encoding: null,
          dontParse: true
        }, callback);
      }
  
      function destroyAtt(docName, attName, qs, callback) {
        return relax({
          db: dbName,
          att: attName,
          method: 'DELETE',
          doc: docName,
          qs: qs
        }, callback);
      }
  
      // db level exports
      docScope = {
        info: function(cb) {
          return getDb(dbName, cb);
        },
        replicate: function(target, opts, cb) {
          return replicateDb(dbName, target, opts, cb);
        },
        compact: function(cb) {
          return compactDb(dbName, cb);
        },
        changes: function(qs, cb) {
          return changesDb(dbName, qs, cb);
        },
        follow: function(qs, cb) {
          return followDb(dbName, qs, cb);
        },
        auth: auth,
        session: session,
        insert: insertDoc,
        get: getDoc,
        head: headDoc,
        copy: copyDoc,
        destroy: destroyDoc,
        bulk: bulksDoc,
        list: listDoc,
        fetch: fetchDocs,
        fetchRevs: fetchRevs,
        config: {url: cfg.url, db: dbName},
        multipart: {
          insert: insertMultipart,
          get: getMultipart
        },
        attachment: {
          insert: insertAtt,
          get: getAtt,
          destroy: destroyAtt
        },
        show: showDoc,
        atomic: updateWithHandler,
        updateWithHandler: updateWithHandler,
        search: viewSearch,
        spatial: viewSpatial,
        view: viewDocs,
        viewWithList: viewWithList
      };
  
      docScope.view.compact = function(ddoc, cb) {
        return compactDb(dbName, ddoc, cb);
      };
  
      return docScope;
    }
  
    // server level exports
    serverScope = _.extend(serverScope, {
      db: {
        create: createDb,
        get: getDb,
        destroy: destroyDb,
        list: listDbs,
        use: docModule,
        scope: docModule,
        compact: compactDb,
        replicate: replicateDb,
        changes: changesDb,
        follow: followDb,
        followUpdates: followUpdates,
        updates: updates
      },
      use: docModule,
      scope: docModule,
      request: relax,
      relax: relax,
      dinosaur: relax,
      auth: auth,
      session: session,
      updates: updates,
      followUpdates: followUpdates,
      uuids: uuids
    });
  
    var db = maybeExtractDatabaseComponent();
  
    return db ? docModule(db) : serverScope;
  };
  
  /*
   * and now an ascii dinosaur
   *              _
   *            / _) ROAR! i'm a vegan!
   *     .-^^^-/ /
   *  __/       /
   * /__.|_|-|_|
   *
   * thanks for visiting! come again!
   */
  nano.version = require('../package.json').version;
  nano.path    = __dirname;
  
  function urlResolveFix(couchUrl, dbName) {
    if (/[^\/]$/.test(couchUrl)) {
      couchUrl += '/';
    }
    return u.resolve(couchUrl, dbName);
  }
  