/*
 * Copyright 2013, Nexedi SA
 * Released under the LGPL license.
 * http://www.gnu.org/licenses/lgpl.html
 */

/*jslint indent: 2, maxlen: 80, nomen: true, regexp: true, unparam: true */
/*global define, window, jIO, RSVP, btoa, DOMParser, Blob */

// JIO Dav Storage Description :
// {
//   type: "dav",
//   url: {string}
//   // No Authentication Here
// }

// {
//   type: "dav",
//   url: {string},
//   basic_login: {string} // Basic authentication
// }

// NOTE: to get the authentication type ->
// curl --verbose  -X OPTION http://domain/
// In the headers: "WWW-Authenticate: Basic realm="DAV-upload"

// URL Characters convertion:
// If I want to retrieve the file which id is -> http://100%.json
// http://domain/collection/http://100%.json cannot be applied
// - '/' is col separator,
// - '?' is url/parameter separator
// - '%' is special char
// - '.' document and attachment separator
// http://100%.json will become
// - http:%2F%2F100%25.json to avoid bad request ('/', '%' -> '%2F', '%25')
// - http:%2F%2F100%25_.json to avoid ids conflicts ('.' -> '_.')
// - http:%252F%252F100%2525_.json to avoid request bad interpretation
//   ('%', '%25')
// The file will be saved as http:%2F%2F100%25_.json

// define([module_name], [dependencies], module);
(function (dependencies, module) {
  "use strict";
  if (typeof define === 'function' && define.amd) {
    return define(dependencies, module);
  }
  window.dav_storage = {};
  module(window.dav_storage, RSVP, jIO);
}(['exports', 'rsvp', 'jio'], function (exports, RSVP, jIO) {
  "use strict";

  /**
   * Removes the last character if it is a "/". "/a/b/c/" become "/a/b/c"
   *
   * @param  {String} string The string to modify
   * @return {String} The modified string
   */
  function removeLastSlashes(string) {
    return string.replace(/\/*$/, '');
  }

  /**
   * Tool to create a ready to use JIO storage description for Dav Storage
   *
   * @param  {String} url The url
   * @param  {String} [auth_type] The authentication type: 'none', 'basic' or
   *   'digest'
   * @param  {String} [realm] The realm
   * @param  {String} [username] The username
   * @param  {String} [password] The password
   * @return {Object} The dav storage description
   */
  function createDescription(url, auth_type, realm, username, password) {
    if (typeof url !== 'string') {
      throw new TypeError("dav_storage.createDescription(): URL: " +
                          "Argument 1 is not of type string");
    }

    function checkUserAndPwd(username, password) {
      if (typeof username !== 'string') {
        throw new TypeError("dav_storage.createDescription(): Username: " +
                            "Argument 4 is not of type string");
      }
      if (typeof password !== 'string') {
        throw new TypeError("dav_storage.createDescription(): Password: " +
                            "Argument 5 is not of type string");
      }
    }

    switch (auth_type) {
    case 'none':
      return {
        "type": "dav",
        "url": removeLastSlashes(url)
      };
    case 'basic':
      checkUserAndPwd(username, password);
      return {
        "type": "dav",
        "url": removeLastSlashes(url),
        "basic_login": btoa(username + ":" + password)
      };
    case 'digest':
      // XXX
      realm.toString();
      throw new Error("Not Implemented");
    default:
      throw new TypeError("dav_storage.createDescription(): " +
                          "Authentication type: " +
                          "Argument 2 is not 'none', 'basic' nor 'digest'");
    }
  }
  exports.createDescription = createDescription;

  /**
   * sequence(thens): Promise
   *
   * Executes a sequence of *then* callbacks. It acts like
   * `smth().then(callback).then(callback)...`. The first callback is called
   * with no parameter.
   *
   * Elements of `thens` array can be a function or an array contaning at most
   * three *then* callbacks: *onFulfilled*, *onRejected*, *onNotified*.
   *
   * When `cancel()` is executed, each then promises are cancelled at the same
   * time.
   *
   * @param  {Array} thens An array of *then* callbacks
   * @return {Promise} A new promise
   */
  function sequence(thens) {
    var promises = [];
    return new RSVP.Promise(function (resolve, reject, notify) {
      var i;
      promises[0] = new RSVP.Promise(function (resolve) {
        resolve();
      });
      for (i = 0; i < thens.length; i += 1) {
        if (Array.isArray(thens[i])) {
          promises[i + 1] = promises[i].
            then(thens[i][0], thens[i][1], thens[i][2]);
        } else {
          promises[i + 1] = promises[i].then(thens[i]);
        }
      }
      promises[i].then(resolve, reject, notify);
    }, function () {
      var i;
      for (i = 0; i < promises.length; i += 1) {
        promises[i].cancel();
      }
    });
  }

  /**
   * Changes spaces to %20, / to %2f, % to %25 and ? to %3f
   *
   * @param  {String} name The name to secure
   * @return {String} The secured name
   */
  function secureName(name) {
    return encodeURI(name).replace(/\//g, '%2F').replace(/\?/g, '%3F');
  }

  /**
   * Restores the original name from a secured name
   *
   * @param  {String} secured_name The secured name to restore
   * @return {String} The original name
   */
  function restoreName(secured_name) {
    return decodeURI(secured_name.replace(/%3F/ig, '?').replace(/%2F/ig, '/'));
  }

  /**
   * Convert document id and attachment id to a file name
   *
   * @param  {String} doc_id The document id
   * @param  {String} attachment_id The attachment id (optional)
   * @return {String} The file name
   */
  function idsToFileName(doc_id, attachment_id) {
    doc_id = secureName(doc_id).replace(/\./g, '_.');
    if (typeof attachment_id === "string") {
      attachment_id = secureName(attachment_id);
      return doc_id + "." + attachment_id;
    }
    return doc_id;
  }

  /**
   * Convert a file name to a document id (and attachment id if there)
   *
   * @param  {String} file_name The file name to convert
   * @return {Array} ["document id", "attachment id"] or ["document id"]
   */
  function fileNameToIds(file_name) {
    /*jslint regexp: true */
    file_name = /^((?:_\.|[^\.])*)(?:\.(.*))?$/.exec(file_name);
    if (file_name === null ||
        (file_name[1] &&
         file_name[1].length === 0)) {
      return [];
    }
    if (file_name[2]) {
      if (file_name[2].length > 0) {
        return [restoreName(file_name[1].replace(/_\./g, '.')),
                restoreName(file_name[2])];
      }
      return [];
    }
    return [restoreName(file_name[1].replace(/_\./g, '.'))];
  }

  function promiseSucceed(promise) {
    return new RSVP.Promise(function (resolve, reject, notify) {
      promise.then(resolve, reject, notify);
    }, function () {
      promise.cancel();
    });
  }

  /**
   * An ajax object to do the good request according to the auth type
   */
  var ajax = {
    "none": function (method, type, url, data) {
      var headers = {};
      if (method === "PROPFIND") {
        headers.Depth = "1";
      }
      return jIO.util.ajax({
        "type": method,
        "url": url,
        "dataType": type,
        "data": data,
        "headers": headers
      });
    },
    "basic": function (method, type, url, data, login) {
      var headers = {"Authorization": "Basic " + login};
      if (method === "PROPFIND") {
        headers.Depth = "1";
      }
      return jIO.util.ajax({
        "type": method,
        "url": url,
        "dataType": type,
        "data": data,
        "headers": headers
      });
    },
    "digest": function () {
      // XXX
      throw new TypeError("DavStorage digest not implemented");
    }
  };

  /**
   * The JIO WebDAV Storage extension
   *
   * @class DavStorage
   * @constructor
   */
  function DavStorage(spec) {
    if (typeof spec.url !== 'string') {
      throw new TypeError("DavStorage 'url' is not of type string");
    }
    this._url = removeLastSlashes(spec.url);
    // XXX digest login
    if (typeof spec.basic_login === 'string') {
      this._auth_type = 'basic';
      this._login = spec.basic_login;
    } else {
      this._auth_type = 'none';
    }
  }

  DavStorage.prototype._put = function (metadata) {
    return ajax[this._auth_type](
      "PUT",
      "text",
      this._url + '/' + idsToFileName(metadata._id) + "?_=" + Date.now(),
      JSON.stringify(metadata),
      this._login
    );
  };

  DavStorage.prototype._putAttachment = function (param) {
    return ajax[this._auth_type](
      "PUT",
      null,
      this._url + '/' + idsToFileName(param._id, param._attachment) +
        "?_=" + Date.now(),
      param._blob,
      this._login
    );
  };

  DavStorage.prototype._get = function (param) {
    return ajax[this._auth_type](
      "GET",
      "text",
      this._url + '/' + idsToFileName(param._id),
      null,
      this._login
    ).then(function (e) {
      try {
        return {"target": {
          "status": e.target.status,
          "statusText": e.target.statusText,
          "response": JSON.parse(e.target.responseText)
        }};
      } catch (err) {
        throw {"target": {
          "status": 0,
          "statusText": "Parse error"
        }};
      }
    });
  };

  DavStorage.prototype._getAttachment = function (param) {
    return ajax[this._auth_type](
      "GET",
      "blob",
      this._url + '/' + idsToFileName(param._id, param._attachment),
      null,
      this._login
    );
    // .then(function (v) { // for sinon js compatibility
    //   return {"target": {
    //     "status": v.target.status,
    //     "statusText": v.target.statusText,
    //     "response": new Blob([v.target.responseText])
    //   }};
    // });
  };

  DavStorage.prototype._remove = function (param) {
    return ajax[this._auth_type](
      "DELETE",
      null,
      this._url + '/' + idsToFileName(param._id) + "?_=" + Date.now(),
      null,
      this._login
    );
  };

  DavStorage.prototype._removeAttachment = function (param) {
    return ajax[this._auth_type](
      "DELETE",
      null,
      this._url + '/' + idsToFileName(param._id, param._attachment) +
        "?_=" + Date.now(),
      null,
      this._login
    );
  };

  DavStorage.prototype._allDocs = function (param) {
    return ajax[this._auth_type](
      "PROPFIND",
      "text",
      this._url + '/',
      null,
      this._login
    ).then(function (e) {
      var i, rows = [], row, responses = new DOMParser().parseFromString(
        e.target.responseText,
        "text/xml"
      ).querySelectorAll(
        "D\\:response, response"
      );
      if (responses.length === 1) {
        return {"target": {"response": {
          "total_rows": 0,
          "rows": []
        }, "status": 200}};
      }
      // exclude parent folder and browse
      for (i = 1; i < responses.length; i += 1) {
        row = {
          "id": "",
          "value": {}
        };
        row.id = responses[i].querySelector("D\\:href, href").
          textContent.split('/').slice(-1)[0];
        row.id = fileNameToIds(row.id);
        if (row.id.length !== 1) {
          row = undefined;
        } else {
          row.id = row.id[0];
        }
        if (row !== undefined) {
          rows[rows.length] = row;
        }
      }
      return {"target": {"response": {
        "total_rows": rows.length,
        "rows": rows
      }, "status": 200}};
    });
  };

  // JIO COMMANDS //

  // wedDav methods rfc4918 (short summary)
  // COPY     Reproduces single resources (files) and collections (directory
  //          trees). Will overwrite files (if specified by request) but will
  //          respond 209 (Conflict) if it would overwrite a tree
  // DELETE   deletes files and directory trees
  // GET      just the vanilla HTTP/1.1 behaviour
  // HEAD     ditto
  // LOCK     locks a resources
  // MKCOL    creates a directory
  // MOVE     Moves (rename or copy) a file or a directory tree. Will
  //          'overwrite' files (if specified by the request) but will respond
  //          209 (Conflict) if it would overwrite a tree.
  // OPTIONS  If WebDAV is enabled and available for the path this reports the
  //          WebDAV extension methods
  // PROPFIND Retrieves the requested file characteristics, DAV lock status
  //          and 'dead' properties for individual files, a directory and its
  //          child files, or a directory tree
  // PROPPATCHset and remove 'dead' meta-data properties
  // PUT      Update or create resource or collections
  // UNLOCK   unlocks a resource

  // Notes: all Ajax requests should be CORS (cross-domain)
  // adding custom headers triggers preflight OPTIONS request!
  // http://remysharp.com/2011/04/21/getting-cors-working/

  DavStorage.prototype.postOrPut = function (method, command, metadata) {
    metadata._id = metadata._id || jIO.util.generateUuid();
    var that = this, o = {
      error_message: "DavStorage, unable to get metadata.",
      notify_message: "Getting metadata",
      percentage: [0, 30],
      notifyProgress: function (e) {
        command.notify({
          "method": method,
          "message": o.notify_message,
          "loaded": e.loaded,
          "total": e.total,
          "percentage": (e.loaded / e.total) *
            (o.percentage[1] - o.percentage[0]) +
            o.percentage[0]
        });
      },
      putMetadata: function (e) {
        metadata._attachments = e.target.response._attachments;
        o.notify_message = "Updating metadata";
        o.error_message = "DavStorage, unable to update document.";
        o.percentage = [30, 100];
        that._put(metadata).then(o.success, o.reject, o.notifyProgress);
      },
      errorDocumentExists: function (e) {
        command.error(
          "conflict",
          "Document exists",
          "DavStorage, cannot overwrite document metadata."
        );
      },
      putMetadataIfPossible: function (e) {
        if (e.target.status !== 404) {
          return command.reject(
            e.target.status,
            e.target.statusText,
            o.error_message
          );
        }
        o.percentage = [30, 100];
        o.notify_message = "Updating metadata";
        o.error_message = "DavStorage, unable to create document.";
        that._put(metadata).then(o.success, o.reject, o.notifyProgress);
      },
      success: function (e) {
        command.success(e.target.status, {"id": metadata._id});
      },
      reject: function (e) {
        command.reject(
          e.target.status,
          e.target.statusText,
          o.error_message
        );
      }
    };

    this._get(metadata).then(
      method === 'post' ? o.errorDocumentExists : o.putMetadata,
      o.putMetadataIfPossible,
      o.notifyProgress
    );
  };

  /**
   * Creates a new document if not already exists
   *
   * @method post
   * @param  {Object} command The JIO command
   * @param  {Object} metadata The metadata to put
   * @param  {Object} options The command options
   */
  DavStorage.prototype.post = function (command, metadata) {
    this.postOrPut('post', command, metadata);
  };


  /**
   * Creates or updates a document
   *
   * @method put
   * @param  {Object} command The JIO command
   * @param  {Object} metadata The metadata to post
   * @param  {Object} options The command options
   */
  DavStorage.prototype.put = function (command, metadata) {
    this.postOrPut('put', command, metadata);
  };

  /**
   * Add an attachment to a document
   *
   * @method putAttachment
   * @param  {Object} command The JIO command
   * @param  {Object} param The command parameters
   * @param  {Object} options The command options
   */
  DavStorage.prototype.putAttachment = function (command, param) {
    var that = this, o = {
      error_message: "DavStorage unable to put attachment",
      percentage: [0, 30],
      notify_message: "Getting metadata",
      notifyProgress: function (e) {
        command.notify({
          "method": "putAttachment",
          "message": o.notify_message,
          "loaded": e.loaded,
          "total": e.total,
          "percentage": (e.loaded / e.total) *
            (o.percentage[1] - o.percentage[0]) +
            o.percentage[0]
        });
      },
      putAttachmentAndReadBlob: function (e) {
        o.percentage = [30, 70];
        o.notify_message = "Putting attachment";
        o.remote_metadata = e.target.response;
        return RSVP.all([
          that._putAttachment(param),
          jIO.util.readBlobAsBinaryString(param._blob)
        ]).then(null, null, function (e) {
          // propagate only putAttachment progress
          if (e.index === 0) {
            return e.value;
          }
          throw null;
        });
      },
      putMetadata: function (answers) {
        o.percentage = [70, 100];
        o.notify_message = "Updating metadata";
        o.remote_metadata._id = param._id;
        o.remote_metadata._attachments = o.remote_metadata._attachments || {};
        o.remote_metadata._attachments[param._attachment] = {
          "length": param._blob.size,
          "digest": jIO.util.makeBinaryStringDigest(answers[1].target.result),
          "content_type": param._blob.type
        };
        return that._put(o.remote_metadata);
      },
      success: function (e) {
        command.success(e.target.status, {
          "digest": o.remote_metadata._attachments[param._attachment].digest
        });
      },
      reject: function (e) {
        command.reject(
          e.target.status,
          e.target.statusText,
          o.error_message
        );
      }
    };

    this._get(param).
      then(o.putAttachmentAndReadBlob).
      then(o.putMetadata).
      then(o.success, o.reject, o.notifyProgress);
  };

  /**
   * Retrieve metadata
   *
   * @method get
   * @param  {Object} command The JIO command
   * @param  {Object} param The command parameters
   * @param  {Object} options The command options
   */
  DavStorage.prototype.get = function (command, param) {
    var o = {
      notifyGetProgress: function (e) {
        command.notify({
          "method": "get",
          "message": "Getting metadata",
          "loaded": e.loaded,
          "total": e.total,
          "percentage": (e.loaded / e.total) * 100 // 0% to 100%
        });
      },
      success: function (e) {
        command.success(e.target.status, {"data": e.target.response});
      },
      reject: function (e) {
        command.reject(
          e.target.status,
          e.target.statusText,
          "DavStorage, unable to get document."
        );
      }
    };

    this._get(param).then(o.success, o.reject, o.notifyGetProgress);
  };

  /**
   * Retriev a document attachment
   *
   * @method getAttachment
   * @param  {Object} command The JIO command
   * @param  {Object} param The command parameters
   * @param  {Object} options The command options
   */
  DavStorage.prototype.getAttachment = function (command, param) {
    var that = this, o = {
      error_message: "DavStorage, unable to get attachment.",
      percentage: [0, 30],
      notify_message: "Getting metedata",
      "404": "missing document", // Not Found
      notifyProgress: function (e) {
        command.notify({
          "method": "getAttachment",
          "message": o.notify_message,
          "loaded": e.loaded,
          "total": e.total,
          "percentage": (e.loaded / e.total) *
            (o.percentage[1] - o.percentage[0]) +
            o.percentage[0]
        });
      },
      getAttachment: function (e) {
        var attachment = e.target.response._attachments &&
          e.target.response._attachments[param._attachment];
        delete o["404"];
        if (typeof attachment !== 'object' || attachment === null) {
          throw {"target": {
            "status": 404,
            "statusText": "missing attachment"
          }};
        }
        o.type = attachment.content_type || "application/octet-stream";
        o.notify_message = "Retrieving attachment";
        o.percentage = [30, 100];
        o.digest = attachment.digest;
        return that._getAttachment(param);
      },
      success: function (e) {
        command.success(e.target.status, {
          "data": new Blob([e.target.response], {"type": o.type}),
          "digest": o.digest
        });
      },
      reject: function (e) {
        command.reject(
          e.target.status,
          o[e.target.status] || e.target.statusText,
          o.error_message
        );
      }
    };

    this._get(param).
      then(o.getAttachment).
      then(o.success, o.reject, o.notifyProgress);
  };

  /**
   * Remove a document
   *
   * @method remove
   * @param  {Object} command The JIO command
   * @param  {Object} param The command parameters
   * @param  {Object} options The command options
   */
  DavStorage.prototype.remove = function (command, param) {
    var that = this, o = {
      error_message: "DavStorage, unable to get metadata.",
      notify_message: "Getting metadata",
      percentage: [0, 70],
      notifyProgress: function (e) {
        if (e === null) {
          return;
        }
        command.notify({
          "method": "remove",
          "message": o.notify_message,
          "loaded": e.loaded,
          "total": e.total,
          "percentage": (e.loaded / e.total) *
            (o.percentage[1] - o.percentage[0]) + o.percentage[0]
        });
      },
      removeDocument: function (e) {
        o.get_result = e;
        o.percentage = [70, 80];
        o.notify_message = "Removing document";
        o.error_message = "DavStorage, unable to remove document";
        return that._remove(param);
      },
      removeAllAttachments: function (e) {
        var k, requests = [], attachments;
        attachments = o.get_result.target.response._attachments;
        o.remove_result = e;
        if (typeof attachments === 'object' && attachments !== null) {
          for (k in attachments) {
            if (attachments.hasOwnProperty(k)) {
              requests[requests.length] = promiseSucceed(
                that._removeAttachment({
                  "_id": param._id,
                  "_attachment": k
                })
              );
            }
          }
        }
        if (requests.length === 0) {
          return;
        }
        o.count = 0;
        o.nb_requests = requests.length;
        return RSVP.all(requests).then(null, null, function (e) {
          if (e.value.loaded === e.value.total) {
            o.count += 1;
            command.notify({
              "method": "remove",
              "message": "Removing all associated attachments",
              "loaded": o.count,
              "total": o.nb_requests,
              "percentage": Math.min(
                o.count / o.nb_requests * 20 + 80,
                100
              )
            });
          }
          return null;
        });
      },
      success: function () {
        command.success(o.remove_result.target.status);
      },
      reject: function (e) {
        return command.reject(
          e.target.status,
          e.target.statusText,
          o.error_message
        );
      }
    };

    this._get(param).
      then(o.removeDocument).
      then(o.removeAllAttachments).
      then(o.success, o.reject, o.notifyProgress);
  };

  /**
   * Remove an attachment
   *
   * @method removeAttachment
   * @param  {Object} command The JIO command
   * @param  {Object} param The command parameters
   * @param  {Object} options The command options
   */
  DavStorage.prototype.removeAttachment = function (command, param) {
    var that = this, o = {
      error_message: "DavStorage, an error occured while getting metadata.",
      percentage: [0, 40],
      notify_message: "Getting metadata",
      notifyProgress: function (e) {
        command.notify({
          "method": "remove",
          "message": o.notify_message,
          "loaded": e.loaded,
          "total": e.total,
          "percentage": (e.loaded / e.total) *
            (o.percentage[1] - o.percentage[0]) +
            o.percentage[0]
        });
      },
      updateMetadata: function (e) {
        var k, doc = e.target.response, attachment;
        attachment = doc._attachments && doc._attachments[param._attachment];
        o.error_message = "DavStorage, document attachment not found.";
        if (typeof attachment !== 'object' || attachment === null) {
          throw {"target": {
            "status": 404,
            "statusText": "missing attachment"
          }};
        }
        delete doc._attachments[param._attachment];
        for (k in doc._attachments) {
          if (doc._attachments.hasOwnProperty(k)) {
            break;
          }
        }
        if (k === undefined) {
          delete doc._attachments;
        }
        o.percentage = [40, 80];
        o.notify_message = "Updating metadata";
        o.error_message = "DavStorage, an error occured " +
          "while updating metadata.";
        return that._put(doc);
      },
      removeAttachment: function () {
        o.percentage = [80, 100];
        o.notify_message = "Removing attachment";
        o.error_message = "DavStorage, an error occured " +
          "while removing attachment.";
        return that._removeAttachment(param);
      },
      success: function (e) {
        command.success(e.status);
      },
      reject: function (e) {
        return command.reject(
          e.target.status,
          e.target.statusText,
          o.error_message
        );
      }
    };

    this._get(param).
      then(o.updateMetadata).
      then(o.removeAttachment).
      then(o.success, o.reject, o.notifyProgress);
  };

  /**
   * Retrieve a list of present document
   *
   * @method allDocs
   * @param  {Object} command The JIO command
   * @param  {Object} param The command parameters
   * @param  {Object} options The command options
   * @param  {Boolean} [options.include_docs=false]
   *   Also retrieve the actual document content.
   */
  DavStorage.prototype.allDocs = function (command, param, options) {
    var that = this, o = {
      error_message: "DavStorage, an error occured while " +
        "retrieving document list",
      max_percentage: options.include_docs === true ? 20 : 100,
      notifyAllDocsProgress: function (e) {
        command.notify({
          "method": "remove",
          "message": "Retrieving document list",
          "loaded": e.loaded,
          "total": e.total,
          "percentage": (e.loaded / e.total) * o.max_percentage
        });
      },
      getAllMetadataIfNecessary: function (e) {
        var requests = [];
        o.alldocs_result = e;
        if (options.include_docs !== true ||
            e.target.response.rows.length === 0) {
          return;
        }

        e.target.response.rows.forEach(function (row) {
          requests[requests.length] = that._get({"_id": row.id}).
            then(function (e) {
              row.doc = e.target.response;
            });
        });

        o.count = 0;
        o.nb_requests = requests.length;
        o.error_message = "DavStorage, an error occured while " +
          "getting document metadata";
        return RSVP.all(requests).then(null, null, function (e) {
          if (e.value.loaded === e.value.total) {
            o.count += 1;
            command.notify({
              "method": "allDocs",
              "message": "Getting all documents metadata",
              "loaded": o.count,
              "total": o.nb_requests,
              "percentage": Math.min(
                o.count / o.nb_requests * 80 + 20,
                100
              )
            });
          }
          throw null;
        });
      },
      success: function () {
        command.success(o.alldocs_result.target.status, {
          "data": o.alldocs_result.target.response
        });
      },
      reject: function (e) {
        return command.reject(
          e.target.status,
          e.target.statusText,
          o.error_message
        );
      }
    };

    this._allDocs(param, options).
      then(o.getAllMetadataIfNecessary).
      then(o.success, o.reject, o.notifyProgress);
  };

  /**
   * Check the storage or a specific document
   *
   * @method check
   * @param  {Object} command The JIO command
   * @param  {Object} param The command parameters
   * @param  {Object} options The command options
   */
  DavStorage.prototype.check = function (command, param) {
    this.genericRepair(command, param, false);
  };

  /**
   * Repair the storage or a specific document
   *
   * @method repair
   * @param  {Object} command The JIO command
   * @param  {Object} param The command parameters
   * @param  {Object} options The command options
   */
  DavStorage.prototype.repair = function (command, param) {
    this.genericRepair(command, param, true);
  };

  /**
   * A generic method that manage check or repair command
   *
   * @method genericRepair
   * @param  {Object} command The JIO command
   * @param  {Object} param The command parameters
   * @param  {Boolean} repair If true then repair else just check
   */
  DavStorage.prototype.genericRepair = function (command, param, repair) {

    var that = this, repair_promise, command_promise;

    // returns a jio object
    function getAllFile() {
      return sequence([function () {
        return ajax[that._auth_type](
          "PROPFIND",
          "text",
          that._url + '/',
          null,
          that._login
        );
      }, [function (e) { // on success
        var i, length, rows = new DOMParser().parseFromString(
          e.target.responseText,
          "text/xml"
        ).querySelectorAll(
          "D\\:response, response"
        );
        if (rows.length === 1) {
          return {"status": 200, "data": []};
        }
        // exclude parent folder and browse
        rows = [].slice.call(rows);
        rows.shift();
        length = rows.length;
        for (i = 0; i < length; i += 1) {
          rows[i] = rows[i].querySelector("D\\:href, href").
            textContent.split('/').slice(-1)[0];
        }
        return {"data": rows, "status": 200};
        // rows -> [
        //   'file_path_1',
        //   ...
        // ]
      }, function (e) { // on error
        // convert into jio error object
        // then propagate
        throw {"status": e.target.status,
               "reason": e.target.statusText};
      }]]);
    }

    // returns jio object
    function repairOne(shared, repair) {
      var modified = false, document_id = shared._id;
      return sequence([function () {
        return that._get({"_id": document_id});
      }, [function (event) {
        var attachment_id, metadata = event.target.response;

        // metadata should be an object
        if (typeof metadata !== 'object' || metadata === null ||
            Array.isArray(metadata)) {
          if (!repair) {
            throw {
              "status": "conflict",
              "reason": "corrupted",
              "message": "Bad metadata found in document \"" +
                document_id + "\""
            };
          }
          return {};
        }

        // check metadata content
        if (!repair) {
          if (!(new jIO.Metadata(metadata).check())) {
            return {
              "status": "conflict",
              "reason": "corrupted",
              "message": "Some metadata might be lost"
            };
          }
        } else {
          modified = (
            jIO.util.uniqueJSONStringify(metadata) !==
              jIO.util.uniqueJSONStringify(
                new jIO.Metadata(metadata).format()._dict
              )
          );
        }

        // check metadata id
        if (metadata._id !== document_id) {
          // metadata id is different than file
          // this is not a critical thing
          modified = true;
          metadata._id = document_id;
        }

        // check attachment metadata container
        if (metadata._attachments &&
            (typeof metadata._attachments !== 'object' ||
             Array.isArray(metadata._attachments))) {
          // is not undefined nor object
          if (!repair) {
            throw {
              "status": "conflict",
              "reason": "corrupted",
              "message": "Bad attachment metadata found in document \"" +
                document_id + "\""
            };
          }
          delete metadata._attachments;
          modified = true;
        }

        // check every attachment metadata
        if (metadata._attachments) {
          for (attachment_id in metadata._attachments) {
            if (metadata._attachments.hasOwnProperty(attachment_id)) {
              // check attachment metadata type
              if (typeof metadata._attachments[attachment_id] !== 'object' ||
                  metadata._attachments[attachment_id] === null ||
                  Array.isArray(metadata._attachments[attachment_id])) {
                // is not object
                if (!repair) {
                  throw {
                    "status": "conflict",
                    "reason": "corrupted",
                    "message": "Bad attachment metadata found in document \"" +
                      document_id + "\", attachment \"" +
                      attachment_id + "\""
                  };
                }
                metadata._attachments[attachment_id] = {};
                modified = true;
              }
              // check attachment existency if all attachment are listed
              if (shared.referenced_dict) {
                if (shared.unreferenced_dict[metadata._id] &&
                    shared.unreferenced_dict[metadata._id][attachment_id]) {
                  // attachment seams to exist but is not referenced
                  shared.referenced_dict[metadata._id] =
                    shared.referenced_dict[metadata._id] || {};
                  shared.referenced_dict[metadata._id][attachment_id] = true;
                  delete shared.unreferenced_dict[metadata._id][attachment_id];
                } else if (
                  !(shared.referenced_dict[metadata._id] &&
                    shared.referenced_dict[metadata._id][attachment_id])
                ) {
                  // attachment doesn't exist, remove attachment id
                  if (!repair) {
                    throw {
                      "status": "conflict",
                      "reason": "attachment missing",
                      "message": "Attachment \"" +
                        attachment_id + "\" from document \"" +
                        document_id + "\" is missing"
                    };
                  }
                  delete metadata._attachments[attachment_id];
                  modified = true;
                }
              }
            }
          }
        }
        return {
          "modified": modified,
          "metadata": metadata
        };
      }, function (event) { // on error
        // convert into jio error object
        // then propagate
        throw {"status": event.target.status,
               "reason": event.target.statustext};
      }], function (dict) {
        if (dict.modified) {
          return this._put(dict.metadata);
        }
        return null;
      }, function () {
        return "no_content";
      }]);
    }

    // returns jio object
    function repairAll(shared, repair) {
      return sequence([function () {
        return getAllFile();
      }, function (answer) {
        var index, data = answer.data, length = data.length, id_list,
          document_list = [];
        for (index = 0; index < length; index += 1) {
          // parsing all files
          id_list = fileNameToIds(data[index]);
          if (id_list.length === 1) {
            // this is a document
            document_list[document_list.length] = id_list[0];
          } else if (id_list.length === 2) {
            // this is an attachment
            // reference it
            shared.unreferenced_dict[id_list[0]] =
              shared.unreferenced_dict[id_list[0]] || {};
            shared.unreferenced_dict[id_list[0]][id_list[1]] = true;
          } else {
            shared.unknown_file_list.push(data[index]);
          }
        }
        length = document_list.length;
        for (index = 0; index < length; index += 1) {
          shared._id = document_list[index];
          document_list[index] = repairOne(shared, repair);
        }

        function fileRemover(name) {
          return function () {
            return ajax[that._auth_type](
              "DELETE",
              null,
              that._url + '/' + name + "?_=" + Date.now(),
              null,
              that._login
            );
          };
        }

        function errorEventConverter(event) {
          throw {"status": event.target.status,
                 "reason": event.target.statusText};
        }

        length = shared.unknown_file_list.length;
        for (index = 0; index < length; index += 1) {
          document_list.push(sequence([
            fileRemover(shared.unknown_file_list[index]),
            [null, errorEventConverter]
          ]));
        }

        return RSVP.all(document_list);
      }, function () {
        return "no_content";
      }]);
    }

    if (typeof param._id === 'string') {
      repair_promise = repairOne(param, repair);
    } else {
      param.referenced_attachment_path_dict = {};
      param.unreferenced_attachment_path_dict = {};
      param.unknown_file_list = [];
      repair_promise = repairAll(param, repair);
    }

    command_promise = sequence([function () {
      return repair_promise;
    }, [command.success, command.error]]);

    command.oncancel = function () {
      command_promise.cancel();
    };
  };

  jIO.addStorage('dav', DavStorage);

}));