/**
 * davclient.js - XHTTPRequest Low-level WebDAV Client API.
 *
 * WebDAV Ajax client api by XhrIo and XhrManager of google closure library.
 * (HTTP: GET, PUT, HEAD, OPTIONS, MKCOL, PROPFIND, PROPPATCH, LOCK, UNLOCK)
 *
 * @license Copyright 2011 The xhrdavclient library authors.
 * All rights reserved.
 */

goog.provide('xhrdav.Client');
goog.require('goog.Uri');
goog.require('goog.dom');
goog.require('goog.net.XhrIo');
goog.require('xhrdav.Conf');


/**
 * WebDAV Client library by Google Closure library.
 *
 * @constructor
 * @param {{scheme:string=, domain:stirng=, port:nubmer=, auth:string=}=} opt_uri
 *     URI Parameters(opt_uri: scheme, domain, port)
 * @see #initialize_
 */
xhrdav.Client = function(opt_uri) {
  this.initialize_(opt_uri);
};

/**
 * WebDAV Client initialize
 *
 * @private
 * @param {{scheme:string=, domain:stirng=, port:nubmer=, auth:string=}=} opt_uri
 *     URI Parameters(opt_uri: scheme, domain, port)
 */
xhrdav.Client.prototype.initialize_ = function(opt_uri) {
  if (!goog.isDefAndNotNull(opt_uri)) {
    opt_uri = {};
  }
  var locationUrl = goog.Uri.parse(location);
  /** @type {string} */
  this.scheme_ = opt_uri.scheme || locationUrl.getScheme() || 'http';
  /** @type {string} */
  this.domain_ = opt_uri.domain || locationUrl.getDomain();
  /** @type {number} */
  this.port_ = opt_uri.port || locationUrl.getPort();
  /** @type {?string} */
  this.auth_ = opt_uri.auth;
};

/**
 * Has Authorization credentials
 *
 * @return {boolean}  stored auth data(Basic/Digest auth hash string).
 */
xhrdav.Client.prototype.hasAuthCredentials = function() {
  return !!this.auth_;
};

/**
 * Get Authorization credentials
 *
 * @return {string} stored auth data.
 */
xhrdav.Client.prototype.getAuthCredentials = function() {
  return this.auth_;
};

/**
 * Set Authorization credentials
 *
 * @param {string} auth set auth data.
 */
xhrdav.Client.prototype.setAuthCredentials = function(auth) {
  this.auth_ = auth;
};

/**
 * Clear Authorization credentials
 */
xhrdav.Client.prototype.clearAuthCredentials = function() {
  this.auth_ = null;
};

/**
 * Parse Response All Headers
 *
 * @private
 * @param {string} headerStrings Response all header strings.
 * @return {Object} converted header object(associated array).
 */
xhrdav.Client.prototype.parseHeaders_ = function(headerStrings) {
  var headers = {};
  var headerListWithoutEmpty = goog.array.filter(
    headerStrings.split(/\n/), function(v, i) {
      return !(goog.string.isEmptySafe(v)); }
  );
  goog.array.forEach(headerListWithoutEmpty, function(v, i) {
    var chunks = v.split(': ');
    var key = goog.string.trim(chunks.shift());
    var obj = goog.string.trim(chunks.join(': '));
    headers[key] = obj;
  });
  return headers;
};

/**
 * Have xml parse function? <goog.mixin(this, parseFunc)>
 *
 * @return {boolean} true: can parse xml, false: can't parse xml.
 */
xhrdav.Client.prototype.canParseXml = function() {
  return (goog.isDef(this.parseXml)) ? true : false;
};

/**
 * Set xml parser function Object.
 *
 * @param {Object} funcObj
 *     Xml Parse function Object(defined function: parseXml).
 */
xhrdav.Client.prototype.setXmlParseFunction = function(funcObj) {
  goog.mixin(this, funcObj);
};

/**
 * Callback XHTTPRequest Processing
 *
 * @private
 * @param {Function} handler Callback chain function.
 * @param {Function} onXhrComplete onXhrComplete callback function.
 * @param {Object} event XHR Event Object.
 */
// TODO: Testcase of HTTP Request Error
xhrdav.Client.prototype.processRequest_ = function(
  handler, onXhrComplete, event) {
  if (onXhrComplete && goog.isFunction(onXhrComplete)) onXhrComplete(event);

  var xhr = event.target;
  var xssGuard = 'while(1);';
  var statusCode = xhr.getStatus() || 0;
  var headers = {};
  var content = xhr.getResponse(xssGuard);

  if (!xhr.isSuccess() && xhr.getStatus() != xhrdav.HttpStatus.MULTI_STATUS) {
    xhrdav.Conf.logging({'name': 'Client#processRequest_',
      'uri': xhr.getLastUri(),
      'errStatus': xhr.getLastErrorCode(),
      'errStatusText':
        goog.net.ErrorCode.getDebugMessage(xhr.getLastErrorCode()),
      'errMessage': xhr.getLastError()}, 'warning');
  } else {
    headers = this.parseHeaders_(xhr.getAllResponseHeaders());
    if (goog.string.contains(headers['Content-Type'], 'xml')) {
      var xml = xhr.getResponseXml(xssGuard);
      // HACK: Convert native Document in IE9.
      if (typeof Document !== 'undefined') {
        if (!(xml instanceof Document)) {
          // IE9.
          xml = goog.dom.xml.loadXml(content);
        }
      }
      if (this.canParseXml()) {
        xml = this.parseXml(xml);
      }
      if (!goog.isDefAndNotNull(xml)) {
        statusCode = 500;
      }
      content = xml;
    }
  }
  if (handler) handler((statusCode < 1) ? 500 : statusCode, content, headers);
};

/**
 * Generate URL from path and location
 *
 * @private
 * @param {string} path Path(<code>/foo</code>, <code>/foo/bar.txt</code>).
 * @return {goog.Uri} Generated URL object.
 */
xhrdav.Client.prototype.generateUrl_ = function(path) {
  xhrdav.Conf.logging({'name': 'Client#generateUrl_', 'RequestURL': path});
  // scheme, userinfo, domain, port, path, query, fragment
  return goog.Uri.create(
    this.scheme_,
    null, // username:password(Basic認証?)
    this.domain_,
    this.port_,
    path,
    null, // query(a=1&b2)
    null);
};

/**
 * Set Query parameters for url.
 *
 * @private
 * @param {goog.net.Uri} url  Uri object.
 * @param {Object=} query Json/Hash object for query.
 * @return {goog.net.Uri} created uri object.
 */
xhrdav.Client.prototype.setParameters_ = function(url, query) {
  if (goog.isDefAndNotNull(query) && !goog.object.isEmpty(query)) {
    goog.object.forEach(query, function(val, key) {
      if (goog.isArray(val) && !goog.array.isEmpty(val)) {
        url.setParameterValues(
            xhrdav.utils.string.camelize(key, {with_dasherize: true}), val);
      } else if (!goog.string.isEmptySafe(val)) {
        url.setParameterValue(
            xhrdav.utils.string.camelize(key, {with_dasherize: true}), val);
      }
    });
  }
  return url;
};

/**
 * convert headers keys.
 *
 * @private
 * @param {Object=} headers HTTP headers object.
 * @return {Object} converted HTTP headers object.
 */
xhrdav.Client.prototype.convertHeadersKeys_ = function(headers) {
  var converted = {};
  if (goog.isDefAndNotNull(headers) && !goog.object.isEmpty(headers)) {
    goog.object.forEach(headers, function(val, key) {
      var convKey = xhrdav.utils.string.camelize(key, {with_dasherize: true});
      goog.object.set(converted, convKey, val);
    });
  }
  return converted;
};

/**
 * Send XHTTPRequest
 *
 * @private
 * @param {string} method HTTP method(GET, POST, etc).
 * @param {string} url request url.
 * @param {Function} handler option function of processing after XHTTPRequest.
 * @param {{xhrMgr:goog.net.XhrManager, xhrId, headers:Object, query:Object}=}
 *     opt_request  Option params(xhrId, xhrManager, etc);
 * @param {Function=} onXhrComplete onXhrComplete callback function.
 */
xhrdav.Client.prototype.request_ = function(
  method, url, handler, opt_request, onXhrComplete) {
  if (!goog.isDefAndNotNull(opt_request)) opt_request = {};
  if (!goog.isDefAndNotNull(opt_request.headers)) opt_request.headers = {};
  var auth = goog.object.get(opt_request.headers, 'Authorization');
  if (!goog.isDefAndNotNull(auth)) {
    goog.object.set(opt_request.headers, 'Authorization', this.auth_);
  }
  if (goog.isDefAndNotNull(opt_request.xhrMgr)) {
    opt_request.xhrMgr.send(
      opt_request.xhrId || goog.string.createUniqueString(),
      url,
      method,
      opt_request.body,
      opt_request.headers,
      opt_request.priority || 0,
      goog.bind(this.processRequest_, this, handler, onXhrComplete),
      opt_request.maxRetries || 1);
  } else {
    goog.net.XhrIo.send(
      url,
      goog.bind(this.processRequest_, this, handler, onXhrComplete),
      method,
      opt_request.body,
      opt_request.headers);
  }
};

/**
 * Find out which HTTP methods are understood by the server.(WebDAV: OPTIONS)
 *
 * @param {string} path Path(<code>/foo/</code>, <code>/foo/bar.txt</code>).
 * @param {Function} handler Callback chain after request processing.
 * @param {{xhrMgr:goog.net.XhrManager, xhrId, headers:Object, query:Object}=}
 *     opt_request  Option params(xhrId, xhrManager, etc);
 * @param {Function=} onXhrComplete onXhrComplete callback function.
 */
xhrdav.Client.prototype.options = function(
  path, handler, opt_request, onXhrComplete) {
  if (!goog.isDefAndNotNull(opt_request)) opt_request = {};
  var url = this.generateUrl_(goog.string.urlDecode(path || ''));
  this.setParameters_(url, opt_request.query);
  opt_request.headers = this.convertHeadersKeys_(opt_request.headers || {});
  goog.object.extend(opt_request.headers, {'Cache-Control': 'no-cache'});
  this.request_('OPTIONS', url, handler, opt_request, onXhrComplete);
};

/**
 * Check Resource(WebDAV: HEAD)
 *
 * @param {string} path Path(<code>/foo/</code>, <code>/foo/bar.txt</code>).
 * @param {Function} handler Callback chain after request processing.
 * @param {{xhrMgr:goog.net.XhrManager, xhrId, headers:Object, query:Object}=}
 *     opt_request  Option params(xhrId, xhrManager, etc);
 * @param {Function=} onXhrComplete onXhrComplete callback function.
 */
xhrdav.Client.prototype.head = function(
  path, handler, opt_request, onXhrComplete) {
  if (!goog.isDefAndNotNull(opt_request)) opt_request = {};

  var url = this.generateUrl_(goog.string.urlDecode(path || ''));
  this.setParameters_(url, opt_request.query);

  opt_request.headers = this.convertHeadersKeys_(opt_request.headers || {});
  goog.object.extend(opt_request.headers, {'Cache-Control': 'no-cache'});

  this.request_('HEAD', url, handler, opt_request, onXhrComplete);
};

/**
 * Get Resource(WebDAV: GET)
 *
 * @param {string} path
 *     Path(<code>/foo/bar.xml</code>, <code>/foo/bar.txt</code>).
 * @param {Function} handler Callback chain after request processing.
 * @param {{xhrMgr:goog.net.XhrManager, xhrId, headers:Object, query:Object}=}
 *     opt_request  Option params(xhrId, xhrManager, etc);
 * @param {Function=} onXhrComplete onXhrComplete callback function.
 */
xhrdav.Client.prototype.get = function(
  path, handler, opt_request, onXhrComplete) {
  if (!goog.isDefAndNotNull(opt_request)) opt_request = {};

  var url = this.generateUrl_(goog.string.urlDecode(path || ''));
  this.setParameters_(url, opt_request.query);
  opt_request.headers = this.convertHeadersKeys_(opt_request.headers || {});

  this.request_('GET', url, handler, opt_request, onXhrComplete);
};

/**
 * Upload Resource(WebDAV: PUT)
 *
 * @param {string} path
 *     Path(<code>/foo/bar.xml</code>, <code>/foo/bar.txt</code>).
 * @param {Object} data Upload filedata(text OR binary).
 * @param {Function} handler Callback chain after request processing.
 * @param {{xhrMgr:goog.net.XhrManager, xhrId, headers:Object, query:Object}=}
 *     opt_request  Option params(xhrId, xhrManager, etc);
 * @param {Function=} onXhrComplete onXhrComplete callback function.
 */
xhrdav.Client.prototype.put = function(
  path, data, handler, opt_request, onXhrComplete) {
  if (!goog.isDefAndNotNull(opt_request)) opt_request = {};

  var decodedPath = goog.string.urlDecode(
    xhrdav.utils.path.removeLastSlash(path || ''));
  var url = this.generateUrl_(decodedPath);
  this.setParameters_(url, opt_request.query);

  opt_request.headers = this.convertHeadersKeys_(opt_request.headers || {});
  goog.object.extend(opt_request.headers, {'Cache-Control': 'no-cache'});

  opt_request.body = data;

  this.request_('PUT', url, handler, opt_request, onXhrComplete);
};

/**
 * Get Collection list and Resource property(WebDAV: PROPFIND)
 *
 * @param {string} path Path(<code>/foo/</code>, <code>/foo/bar.txt</code>).
 * @param {Function} handler Callback chain after request processing.
 * @param {{xhrMgr:goog.net.XhrManager, xhrId, headers:Object, query:Object}=}
 *     opt_request  Option params(xhrId, xhrManager, etc);
 * @param {Function=} onXhrComplete onXhrComplete callback function.
 */
xhrdav.Client.prototype.propfind = function(
  path, handler, opt_request, onXhrComplete) {
  if (!goog.isDefAndNotNull(opt_request)) opt_request = {};

  var url = this.generateUrl_(goog.string.urlDecode(path || ''));
  this.setParameters_(url, opt_request.query);

  opt_request.headers = this.convertHeadersKeys_(opt_request.headers || {});
  // 0(path only) or 1(current directory)
  goog.object.extend(opt_request.headers, {
    'Content-Type': 'text/xml',
    'Depth': goog.isDefAndNotNull(opt_request.headers['Depth']) ?
      opt_request.headers['Depth'] : 0});

  goog.object.extend(opt_request, {body:
    '<?xml version="1.0" encoding="UTF-8"?>' +
    '<D:propfind xmlns:D="DAV:"><D:allprop /></D:propfind>'});

  this.request_('PROPFIND', url, handler, opt_request, onXhrComplete);
};

/**
 * Set Collection list and Resource property(WebDAV: PROPPATCH)
 *
 * @param {string} path Path(<code>/foo/</code>, <code>/foo/bar.txt</code>).
 * @param {Function} handler Callback chain after request processing.
 * @param {{xhrMgr:goog.net.XhrManager, xhrId, headers:Object, query:Object}=}
 *     opt_request  Option params(xhrId, xhrManager, etc);
 * @param {Function=} onXhrComplete onXhrComplete callback function.
 */
// TODO: UNFIXED
xhrdav.Client.prototype.proppatch = function(
  path, handler, opt_request, onXhrComplete) {
  if (!goog.isDefAndNotNull(opt_request)) opt_request = {};

  var url = this.generateUrl_(goog.string.urlDecode(path || ''));
  this.setParameters_(url, opt_request.query);

  opt_request.headers = this.convertHeadersKeys_(opt_request.headers || {});
};

/**
 * Lock resource(WebDAV: LOCK)
 *
 * @param {string} path Path(<code>/foo/</code>, <code>/foo/bar.txt</code>).
 * @param {Function} handler Callback chain after request processing.
 * @param {{xhrMgr:goog.net.XhrManager, xhrId, headers:Object, query:Object}=}
 *     opt_request  Option params(xhrId, xhrManager, etc);
 * @param {Function=} onXhrComplete onXhrComplete callback function.
 */
// TODO: UNFIXED
xhrdav.Client.prototype.lock = function(
  path, handler, opt_request, onXhrComplete) {
  if (!goog.isDefAndNotNull(opt_request)) opt_request = {};

  var url = this.generateUrl_(goog.string.urlDecode(path || ''));
  this.setParameters_(url, opt_request.query);

  opt_request.headers = this.convertHeadersKeys_(opt_request.headers || {});
  goog.object.extend(opt_request.headers, {
    'Content-Type': 'text/xml',
    'Depth': goog.isDefAndNotNull(opt_request.headers['Depth']) ?
      opt_request.headers['Depth'] : 0});

  goog.object.extend(opt_request, {body:
    '<?xml version="1.0" encoding="UTF-8"?>' +
    '<D:lockinfo xmlns:D="DAV:">\n' +
    '<D:lockscope><D:' + (opt_request.scope || 'exclusive') +
    ' /></D:lockscope>\n' +
    '<D:locktype><D:' + (opt_request.type || 'write') + ' /></D:locktype>\n' +
    '<D:owner></D:owner>\n</D:lockinfo>\n'});

  this.request_('LOCK', url, handler, opt_request, onXhrComplete);
};

/**
 * Create Collection(WebDAV: MKCOL)
 *
 * @param {string} path Path(<code>/foo/</code>, <code>/foo/bar/</code>).
 * @param {Function} handler Callback chain after request processing.
 * @param {{xhrMgr:goog.net.XhrManager, xhrId, headers:Object, query:Object}=}
 *     opt_request  Option params(xhrId, xhrManager, etc);
 * @param {Function=} onXhrComplete onXhrComplete callback function.
 */
xhrdav.Client.prototype.mkcol = function(
  path, handler, opt_request, onXhrComplete) {
  if (!goog.isDefAndNotNull(opt_request)) opt_request = {};

  path = goog.string.endsWith(path, '/') ? path : path + '/'; // Preserve GET
  var url = this.generateUrl_(goog.string.urlDecode(path || ''));
  this.setParameters_(url, opt_request.query);

  opt_request.headers = this.convertHeadersKeys_(opt_request.headers || {});

  this.request_('MKCOL', url, handler, opt_request, onXhrComplete);
};

/**
 * Delete Collection or Resource(WebDAV: DELETE)
 *
 * @param {string} path Path(<code>/foo/</code>, <code>/foo/bar.txt</code>).
 * @param {Function} handler Callback chain after request processing.
 * @param {{xhrMgr:goog.net.XhrManager, xhrId, headers:Object, query:Object}=}
 *     opt_request  Option params(xhrId, xhrManager, etc);
 * @param {Function=} onXhrComplete onXhrComplete callback function.
 */
xhrdav.Client.prototype._delete = function(
  path, handler, opt_request, onXhrComplete) {
  if (!goog.isDefAndNotNull(opt_request)) opt_request = {};

  var url = this.generateUrl_(goog.string.urlDecode(path || ''));
  this.setParameters_(url, opt_request.query);

  opt_request.headers = this.convertHeadersKeys_(opt_request.headers || {});

  this.request_('DELETE', url, handler, opt_request, onXhrComplete);
};

/**
 * Private method of Copy or Move Collection(WebDAV: COPY/MOVE)
 *
 * @private
 * @param {string} method HTTP method of WebDAV.
 * @param {string} path Source Path(<code>/foo/</code>).
 * @param {string} dstPath Destination Path(<code>/bar/</code>).
 * @param {Function} handler Callback chain after request processing.
 * @param {{xhrMgr:goog.net.XhrManager, xhrId, headers:Object, query:Object}=}
 *     opt_request  Option params(xhrId, xhrManager, etc);
 * @param {Function=} onXhrComplete onXhrComplete callback function.
 */
xhrdav.Client.prototype.copyOrMovePath_ = function(
  method, path, dstPath, handler, opt_request, onXhrComplete) {

  if (!goog.isDefAndNotNull(opt_request)) opt_request = {};

  var url = this.generateUrl_(goog.string.urlDecode(path || ''));
  this.setParameters_(url, opt_request.query);

  opt_request.headers = this.convertHeadersKeys_(opt_request.headers || {});

  goog.object.extend(opt_request.headers, {
    'Cache-Control': 'no-cache',
    'Destination': this.generateUrl_(goog.string.urlDecode(dstPath || '')),
    'Overwrite': !!opt_request.headers['Overwrite'] ? 'T' : 'F'});

  this.request_(method, url, handler, opt_request, onXhrComplete);
};

/**
 * Move Collection(WebDAV: MOVE)
 *
 * @see #copyOrMovePath_
 */
xhrdav.Client.prototype.move = function(
  path, dstPath, handler, opt_request, onXhrComplete) {
  this.copyOrMovePath_('MOVE',
    path, dstPath, handler, opt_request, onXhrComplete);
};

/**
 * Copy Collection(WebDAV: COPY)
 *
 * @see #copyOrMovePath_
 */
xhrdav.Client.prototype.copy = function(
  path, dstPath, handler, opt_request, onXhrComplete) {
  this.copyOrMovePath_('COPY',
    path, dstPath, handler, opt_request, onXhrComplete);
};

/**
 * Error Handler of exception[Mix-in function]
 *
 * @param {Error} e Error object.
 */
xhrdav.Client.prototype.errorHandler = function(e) {
  xhrdav.Conf.logging({'Client#errorHandler': e.name,
    'message': e.message}, 'warning');
  xhrdav.Conf.getInstance().getLogger().warning(e.name, e);
};


/* Entry Point for closure compiler */
goog.exportSymbol('xhrdav.Client', xhrdav.Client);
goog.exportProperty(xhrdav.Client.prototype, 'hasAuthCredentials',
  xhrdav.Client.prototype.hasAuthCredentials);
goog.exportProperty(xhrdav.Client.prototype, 'getAuthCredentials',
  xhrdav.Client.prototype.getAuthCredentials);
goog.exportProperty(xhrdav.Client.prototype, 'setAuthCredentials',
  xhrdav.Client.prototype.setAuthCredentials);
goog.exportProperty(xhrdav.Client.prototype, 'clearAuthCredentials',
  xhrdav.Client.prototype.clearAuthCredentials);
goog.exportProperty(xhrdav.Client.prototype, 'canParseXml',
  xhrdav.Client.prototype.canParseXml);
goog.exportProperty(xhrdav.Client.prototype, 'setXmlParseFunction',
  xhrdav.Client.prototype.setXmlParseFunction);
goog.exportProperty(xhrdav.Client.prototype, 'options',
  xhrdav.Client.prototype.options);
goog.exportProperty(xhrdav.Client.prototype, 'head',
  xhrdav.Client.prototype.head);
goog.exportProperty(xhrdav.Client.prototype, 'get',
  xhrdav.Client.prototype.get);
goog.exportProperty(xhrdav.Client.prototype, 'put',
  xhrdav.Client.prototype.put);
goog.exportProperty(xhrdav.Client.prototype, 'propfind',
  xhrdav.Client.prototype.propfind);
goog.exportProperty(xhrdav.Client.prototype, 'mkcol',
  xhrdav.Client.prototype.mkcol);
goog.exportProperty(xhrdav.Client.prototype, '_delete',
  xhrdav.Client.prototype._delete);
goog.exportProperty(xhrdav.Client.prototype, 'move',
  xhrdav.Client.prototype.move);
goog.exportProperty(xhrdav.Client.prototype, 'copy',
  xhrdav.Client.prototype.copy);
goog.exportProperty(xhrdav.Client.prototype, 'errorHandler',
  xhrdav.Client.prototype.errorHandler);