/*jslint indent: 2,
    maxlen: 80,
    nomen: true
*/
/*global
    define: true,
    jIO: true,
    jQuery: true,
    XMLHttpRequest: true,
    Blob: true,
    FormData: true,
    window: true,
    setTimeout: true
*/
/**
 * JIO XWiki Storage. Type = 'xwiki'.
 * XWiki Document/Attachment storage.
 */
(function () {
  "use strict";

  var $, store;
  store = function (spec, my) {

    spec = spec || {};
    var that, priv, xwikistorage;

    that = my.basicStorage(spec, my);
    priv = {};

    /**
     * Get the Space and Page components of a documkent ID.
     *
     * @param id the document id.
     * @return a map of { 'space':<Space>, 'page':<Page> }
     */
    priv.getParts = function (id) {
      if (id.indexOf('/') === -1) {
        return {
          space: 'Main',
          page: id
        };
      }
      return {
        space: id.substring(0, id.indexOf('/')),
        page: id.substring(id.indexOf('/') + 1)
      };
    };

    /**
     * Get the Anti-CSRF token and do something with it.
     *
     * @param andThen function which is called with (formToken, err)
     *                as parameters.
     */
    priv.doWithFormToken = function (andThen) {
      $.ajax({
        url: priv.formTokenPath,
        type: "GET",
        async: true,
        dataType: 'text',
        success: function (html) {
          var m, token;
          // this is unreliable
          //var token = $('meta[name=form_token]', html).attr("content");
          m = html.match(/<meta name="form_token" content="(\w*)"\/>/);
          token = (m && m[1]) || null;
          if (!token) {
            andThen(null, {
              "status": 404,
              "statusText": "Not Found",
              "error": "err_form_token_not_found",
              "message": "Anti-CSRF form token was not found in page",
              "reason": "XWiki main page did not contain expected " +
                        "Anti-CSRF form token"
            });
          } else {
            andThen(token, null);
          }
        },
        error: function (jqxhr, err, cause) {
          andThen(null, {
            "status": jqxhr.status,
            "statusText": jqxhr.statusText,
            "error": err,
            "message": "Could not get Anti-CSRF form token from [" +
                priv.xwikiurl + "]",
            "reason": cause
          });
        }
      });
    };

    /**
     * Get the REST read URL for a document.
     *
     * @param docId the id of the document.
     * @return the REST URL for accessing this document.
     */
    priv.getDocRestURL = function (docId) {
      var parts = priv.getParts(docId);
      return priv.xwikiurl + '/rest/wikis/'
        + priv.wiki + '/spaces/' + parts.space + '/pages/' + parts.page;
    };

    /**
     * Make an HTML5 Blob object.
     * Equivilant to the `new Blob()` constructor.
     * Will fall back on deprecated BlobBuilder if necessary.
     */
    priv.makeBlob = function (contentArray, options) {
      var i, bb, BB;
      try {
        // use the constructor if possible.
        return new Blob(contentArray, options);
      } catch (err) {
        // fall back on the blob builder.
        BB = (window.MozBlobBuilder || window.WebKitBlobBuilder
          || window.BlobBuilder);
        bb = new BB();
        for (i = 0; i < contentArray.length; i += 1) {
          bb.append(contentArray[i]);
        }
        return bb.getBlob(options ? options.type : undefined);
      }
    };

    priv.isBlob = function (potentialBlob) {
      return potentialBlob !== undefined &&
        potentialBlob.toString() === "[object Blob]";
    };

    /*
     * Wrapper for the xwikistorage based on localstorage JiO store.
     */
    xwikistorage = {
      /**
       * Get content of an XWikiDocument.
       *
       * @param docId the document ID.
       * @param andThen a callback taking (doc, err), doc being the document
       *                json object and err being the error if any.
       */
      getItem: function (docId, andThen) {

        var success = function (jqxhr) {
          var out, xd;
          out = {};
          try {
            xd = $(jqxhr.responseText);
            xd.find('modified').each(function () {
              out._last_modified = Date.parse($(this).text());
            });
            xd.find('created').each(function () {
              out._creation_date = Date.parse($(this).text());
            });
            xd.find('title').each(function () { out.title = $(this).text(); });
            xd.find('parent').each(function () {
              out.parent = $(this).text();
            });
            xd.find('syntax').each(function () {
              out.syntax = $(this).text();
            });
            xd.find('content').each(function () {
              out.content = $(this).text();
            });
            out._id = docId;
            andThen(out, null);
          } catch (err) {
            andThen(null, {
              status: 500,
              statusText: "internal error",
              error: err,
              message: err.message,
              reason: ""
            });
          }
        };

        $.ajax({
          url: priv.getDocRestURL(docId),
          type: "GET",
          async: true,
          dataType: 'xml',

          // Use complete instead of success and error because phantomjs
          // sometimes causes error to be called with html return code 200.
          complete: function (jqxhr) {
            if (jqxhr.status === 404) {
              andThen(null, null);
              return;
            }
            if (jqxhr.status !== 200) {
              andThen(null, {
                "status": jqxhr.status,
                "statusText": jqxhr.statusText,
                "error": "",
                "message": "Failed to get document [" + docId + "]",
                "reason": ""
              });
              return;
            }
            success(jqxhr);
          }
        });
      },

      /**
       * Get content of an XWikiAttachment.
       *
       * @param attachId the attachment ID.
       * @param andThen a callback taking (attach, err), attach being the
       *                attachment blob and err being the error if any.
       */
      getAttachment: function (docId, fileName, andThen) {
        var xhr, parts, url;
        // need to do this manually, jquery doesn't support returning blobs.
        xhr = new XMLHttpRequest();
        parts = priv.getParts(docId);
        url = priv.xwikiurl + '/bin/download/' + parts.space +
            "/" + parts.page + "/" + fileName + '?cb=' + Math.random();
        xhr.open('GET', url, true);
        if (priv.useBlobs) {
          xhr.responseType = 'blob';
        } else {
          xhr.responseType = 'text';
        }

        xhr.onload = function () {
          if (xhr.status === 200) {
            var contentType = xhr.getResponseHeader("Content-Type");
            if (contentType.indexOf(';') > -1) {
              contentType = contentType.substring(0, contentType.indexOf(';'));
            }
            andThen(xhr.response);
          } else {
            andThen(null, {
              "status": xhr.status,
              "statusText": xhr.statusText,
              "error": "err_network_error",
              "message": "Failed to get attachment ["
                  + docId + "/" + fileName + "]",
              "reason": "Error getting data from network"
            });
          }
        };

        xhr.send();
      },

      /**
       * Store an XWikiDocument.
       *
       * @param id the document identifier.
       * @param doc the document JSON object containing
       *            "parent", "title", "content", and/or "syntax" keys.
       * @param andThen a callback taking (err), err being the error if any.
       */
      setItem: function (id, doc, andThen) {
        priv.doWithFormToken(function (formToken, err) {
          if (err) {
            that.error(err);
            return;
          }
          var parts = priv.getParts(id);
          $.ajax({
            url: priv.xwikiurl + "/bin/preview/" +
              parts.space + '/' + parts.page,
            type: "POST",
            async: true,
            dataType: 'text',
            data: {
              parent: doc.parent || '',
              title: doc.title || '',
              xredirect: '',
              language: 'en',
  //            RequiresHTMLConversion: 'content',
  //            content_syntax: doc.syntax || 'xwiki/2.1',
              content: doc.content || '',
              xeditaction: 'edit',
              comment: 'Saved by JiO',
              action_saveandcontinue: 'Save & Continue',
              syntaxId: doc.syntax || 'xwiki/2.1',
              xhidden: 0,
              minorEdit: 0,
              ajax: true,
              form_token: formToken
            },
            success: function () {
              andThen(null);
            },
            error: function (jqxhr, err, cause) {
              andThen({
                "status": jqxhr.status,
                "statusText": jqxhr.statusText,
                "error": err,
                "message": "Failed to store document [" + id + "]",
                "reason": cause
              });
            }
          });
        });
      },

      /**
       * Store an XWikiAttachment.
       *
       * @param docId the ID of the document to attach to.
       * @param fileName the attachment file name.
       * @param mimeType the MIME type of the attachment content.
       * @param content the attachment content.
       * @param andThen a callback taking one parameter, the error if any.
       */
      setAttachment: function (docId, fileName, mimeType, content, andThen) {
        priv.doWithFormToken(function (formToken, err) {
          var parts, blob, fd, xhr;
          if (err) {
            that.error(err);
            return;
          }
          parts = priv.getParts(docId);
          blob = priv.isBlob(content)
            ? content
            : priv.makeBlob([content], {type: mimeType});
          fd = new FormData();
          fd.append("filepath", blob, fileName);
          fd.append("form_token", formToken);
          xhr = new XMLHttpRequest();
          xhr.open('POST', priv.xwikiurl + "/bin/upload/" +
                           parts.space + '/' + parts.page, true);
          xhr.onload = function () {
            if (xhr.status === 302 || xhr.status === 200) {
              andThen(null);
            } else {
              andThen({
                "status": xhr.status,
                "statusText": xhr.statusText,
                "error": "err_network_error",
                "message": "Failed to store attachment ["
                    + docId + "/" + fileName + "]",
                "reason": "Error posting data"
              });
            }
          };
          xhr.send(fd);
        });
      },

      removeItem: function (id, andThen) {
        priv.doWithFormToken(function (formToken, err) {
          if (err) {
            that.error(err);
            return;
          }
          var parts = priv.getParts(id);
          $.ajax({
            url: priv.xwikiurl + "/bin/delete/" +
              parts.space + '/' + parts.page,
            type: "POST",
            async: true,
            dataType: 'text',
            data: {
              confirm: '1',
              form_token: formToken
            },
            success: function () {
              andThen(null);
            },
            error: function (jqxhr, err, cause) {
              andThen({
                "status": jqxhr.status,
                "statusText": jqxhr.statusText,
                "error": err,
                "message": "Failed to delete document [" + id + "]",
                "reason": cause
              });
            }
          });
        });
      },

      removeAttachment: function (docId, fileName, andThen) {
        var parts = priv.getParts(docId);
        priv.doWithFormToken(function (formToken, err) {
          if (err) {
            that.error(err);
            return;
          }
          $.ajax({
            url: priv.xwikiurl + "/bin/delattachment/" + parts.space + '/' +
                parts.page + '/' + fileName,
            type: "POST",
            async: true,
            dataType: 'text',
            data: {
              ajax: '1',
              form_token: formToken
            },
            success: function () {
              andThen(null);
            },
            error: function (jqxhr, err, cause) {
              andThen({
                "status": jqxhr.status,
                "statusText": jqxhr.statusText,
                "error": err,
                "message": "Failed to delete attachment ["
                    + docId + '/' + fileName + "]",
                "reason": cause
              });
            }
          });
        });
      }
    };

    // ==================== Tools ====================
    /**
     * Update [doc] the document object and remove [doc] keys
     * which are not in [new_doc]. It only changes [doc] keys not starting
     * with an underscore.
     * ex: doc:     {key:value1,_key:value2} with
     *     new_doc: {key:value3,_key:value4} updates
     *     doc:     {key:value3,_key:value2}.
     * @param  {object} doc The original document object.
     * @param  {object} new_doc The new document object
     */
    priv.documentObjectUpdate = function (doc, new_doc) {
      var k;
      for (k in doc) {
        if (doc.hasOwnProperty(k)) {
          if (k[0] !== '_') {
            delete doc[k];
          }
        }
      }
      for (k in new_doc) {
        if (new_doc.hasOwnProperty(k)) {
          if (k[0] !== '_') {
            doc[k] = new_doc[k];
          }
        }
      }
    };

    /**
     * Checks if an object has no enumerable keys
     * @method objectIsEmpty
     * @param  {object} obj The object
     * @return {boolean} true if empty, else false
     */
    priv.objectIsEmpty = function (obj) {
      var k;
      for (k in obj) {
        if (obj.hasOwnProperty(k)) {
          return false;
        }
      }
      return true;
    };

    // ==================== attributes ====================
    // the wiki to store stuff in
    priv.wiki = spec.wiki || 'xwiki';

    // unused
    priv.username = spec.username;
    priv.language = spec.language;

    // URL location of the wiki, unused since
    // XWiki doesn't currently allow cross-domain requests.
    priv.xwikiurl = spec.xwikiurl ||
       window.location.href.replace(/\/xwiki\/bin\//, '/xwiki\n')
         .split('\n')[0];
    // should be: s@/xwiki/bin/.*$@/xwiki@
    // but jslint gets in the way.

    // Which URL to load for getting the Anti-CSRF form token, used for testing.
    priv.formTokenPath = spec.formTokenPath || priv.xwikiurl;

    // If true then Blob objects will be returned by
    // getAttachment() rather than strings.
    priv.useBlobs = spec.useBlobs || false;

    // If true then Blob objects will be returned by
    // getAttachment() rather than strings.
    priv.useBlobs = spec.useBlobs || false;


    that.specToStore = function () {
      return {
        "username": priv.username,
        "language": priv.language,
        "xwikiurl": priv.xwikiurl
      };
    };

    // can't fo wrong since no parameters are required.
    that.validateState = function () {
      return '';
    };

    // ==================== commands ====================
    /**
     * Create a document in local storage.
     * @method post
     * @param  {object} command The JIO command
     */
    that.post = function (command) {
      var docId = command.getDocId();
      if (!(typeof docId === "string" && docId !== "")) {
        setTimeout(function () {
          that.error({
            "status": 405,
            "statusText": "Method Not Allowed",
            "error": "method_not_allowed",
            "message": "Cannot create document which id is undefined",
            "reason": "Document id is undefined"
          });
        });
        return;
      }
      xwikistorage.getItem(docId, function (doc, err) {
        if (err) {
          that.error(err);
        } else if (doc === null) {
          // the document does not exist
          xwikistorage.setItem(command.getDocId(),
                               command.cloneDoc(),
                               function (err) {
              if (err) {
                that.error(err);
              } else {
                that.success({
                  "ok": true,
                  "id": command.getDocId()
                });
              }
            });
        } else {
          // the document already exists
          that.error({
            "status": 409,
            "statusText": "Conflicts",
            "error": "conflicts",
            "message": "Cannot create a new document",
            "reason": "Document already exists (use 'put' to modify it)"
          });
        }
      });
    };

    /**
     * Create or update a document in local storage.
     * @method put
     * @param  {object} command The JIO command
     */
    that.put = function (command) {
      xwikistorage.getItem(command.getDocId(), function (doc, err) {
        if (err) {
          that.error(err);
        } else if (doc === null) {
          doc = command.cloneDoc();
        } else {
          priv.documentObjectUpdate(doc, command.cloneDoc());
        }
        // write
        xwikistorage.setItem(command.getDocId(), doc, function (err) {
          if (err) {
            that.error(err);
          } else {
            that.success({
              "ok": true,
              "id": command.getDocId()
            });
          }
        });
      });
    };

    /**
     * Add an attachment to a document
     * @method  putAttachment
     * @param  {object} command The JIO command
     */
    that.putAttachment = function (command) {
      xwikistorage.getItem(command.getDocId(), function (doc, err) {
        if (err) {
          that.error(err);
        } else if (doc === null) {
          //  the document does not exist
          that.error({
            "status": 404,
            "statusText": "Not Found",
            "error": "not_found",
            "message": "Impossible to add attachment",
            "reason": "Document not found"
          });
        } else {
          // Document exists, upload attachment.
          xwikistorage.setAttachment(command.getDocId(),
                                     command.getAttachmentId(),
                                     command.getAttachmentMimeType(),
                                     command.getAttachmentData(),
                                     function (err) {
              if (err) {
                that.error(err);
              } else {
                that.success({
                  "ok": true,
                  "id": command.getDocId() + "/" + command.getAttachmentId()
                });
              }
            });
        }
      });
    };

    /**
     * Get a document or attachment
     * @method get
     * @param  {object} command The JIO command
     */
    that.get = that.getAttachment = function (command) {
      if (typeof command.getAttachmentId() === "string") {
        // seeking for an attachment
        xwikistorage.getAttachment(command.getDocId(),
                                   command.getAttachmentId(),
                                   function (attach, err) {
            if (err) {
              that.error(err);
            } else if (attach !== null) {
              that.success(attach);
            } else {
              that.error({
                "status": 404,
                "statusText": "Not Found",
                "error": "not_found",
                "message": "Cannot find the attachment",
                "reason": "Attachment does not exist"
              });
            }
          });
      } else {
        // seeking for a document
        xwikistorage.getItem(command.getDocId(), function (doc, err) {
          if (err) {
            that.error(err);
          } else if (doc !== null) {
            that.success(doc);
          } else {
            that.error({
              "status": 404,
              "statusText": "Not Found",
              "error": "not_found",
              "message": "Cannot find the document",
              "reason": "Document does not exist"
            });
          }
        });
      }
    };

    /**
     * Remove a document or attachment
     * @method remove
     * @param  {object} command The JIO command
     */
    that.remove = that.removeAttachment = function (command) {
      var objId, complete;
      // notFoundError = function (word) {
      //   that.error({
      //     "status": 404,
      //     "statusText": "Not Found",
      //     "error": "not_found",
      //     "message": word + " not found",
      //     "reason": "missing"
      //   });
      // };

      objId = command.getDocId();
      complete = function (err) {
        if (err) {
          that.error(err);
        } else {
          that.success({
            "ok": true,
            "id": objId
          });
        }
      };
      if (typeof command.getAttachmentId() === "string") {
        objId += '/' + command.getAttachmentId();
        xwikistorage.removeAttachment(command.getDocId(),
                                      command.getAttachmentId(),
                                      complete);
      } else {
        xwikistorage.removeItem(objId, complete);
      }
    };

    /**
     * Get all filenames belonging to a user from the document index
     * @method allDocs
     * @param  {object} command The JIO command
     */
    that.allDocs = function () {
      setTimeout(function () {
        that.error({
          "status": 405,
          "statusText": "Method Not Allowed",
          "error": "method_not_allowed",
          "message": "Your are not allowed to use this command",
          "reason": "xwikistorage forbids AllDocs command executions"
        });
      });
    };

    return that;
  };

  if (typeof define === 'function' && define.amd) {
    define(['jquery', 'jio'], function (jquery, jIO) {
      $ = jquery;
      jIO.addStorageType('xwiki', store);
    });
  } else {
    jIO.addStorageType('xwiki', store);
    $ = jQuery;
  }

}());