/*jslint indent: 2, maxlen: 80, sloppy: true, nomen: true */
/*global jIO: true, localStorage: true, setTimeout: true */
/**
 * JIO Index Storage.
 * Manages indexes for specified storages.
 * Description:
 * {
 *     "type": "index",
 *     "indices": [
 *        {"indexA",["field_A"]},
 *        {"indexAB",["field_A","field_B"]}
 *     ],
 *     "storage": [
 *         <sub storage description>,
 *         ...
 *     ]
 * }
 * Index file will contain
 * {
 *   "_id": "ipost_indices.json",
 *   "indexA": {
 *       "keyword_abc": ["some_id","some_other_id",...]
 *   },
 *   "indexAB": {
 *       "keyword_abc": ["some_id"],
 *       "keyword_def": ["some_id"]
 *   }
 * }
 */
jIO.addStorageType('indexed', function (spec, my) {

  "use strict";
  var that, priv = {}, spec;

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

  priv.indices = spec.indices;
  priv.substorage_key = "sub_storage";
  priv.substorage = spec[priv.substorage_key];
  priv.index_indicator = spec.sub_storage.application_name || "index";
  priv.index_suffix = priv.index_indicator + "_indices.json";

  my.env = my.env || spec.env || {};

  that.specToStore = function () {
    var o = {};
    o[priv.substorage_key] = priv.substorage;
    o.env = my.env;
    return o;
  };

  /**
   * Generate a new uuid
   * @method generateUuid
   * @return {string} The new uuid
   */
  priv.generateUuid = function () {
    var S4 = function () {
      var i, string = Math.floor(
        Math.random() * 0x10000 /* 65536 */
      ).toString(16);
      for (i = string.length; i < 4; i += 1) {
        string = "0" + string;
      }
      return string;
    };
    return S4() + S4() + "-" +
      S4() + "-" +
      S4() + "-" +
      S4() + "-" +
      S4() + S4() + S4();
  };

  /**
  * Escape string before storing
  * @method sanitizeValue
  * @param  {string} s The string to be sanitized
  * @return {string} The sanitized string
  */
  priv.sanitizeValue = function (s) {
    return s.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&')
  };

  /**
  * Get number of elements in object
  * @method getObjectSize
  * @param  {object} obj The object to check
  * @return {number} size The amount of elements in the object
  */
  priv.getObjectSize = function (obj) {
    var size = 0, key;
    for (key in obj) {
        if (obj.hasOwnProperty(key)) {
          size += 1;
        }
    }
    return size;
  };

  /**
   * Creates an empty indices array
   * @method createEmptyIndexArray
   * @param  {array} indices An array of indices (optional)
   * @return {object} The new index array
   */
  priv.createEmptyIndexArray = function (indices) {
    var obj, i, j = priv.indices.length,
      new_index_object = {}, new_index_name;

    if (indices === undefined) {
      for (i = 0; i < j; i += 1) {
        new_index_name = priv.indices[i]["name"];
        new_index_object[new_index_name] = {};
      }
    }
    return new_index_object;
  };

  /**
   * Determine if a key/value pair exists in an object by VALUE
   * @method searchObjectByValue
   * @param  {object} indexToSearch The index to search
   * @param  {string} docid The document id to find
   * @param  {string} passback The value that should be returned
   * @return {boolean} true/false
   */
  priv.searchIndexByValue = function (indexToSearch, docid, passback) {
    var key, obj, prop;

    for (key in indexToSearch) {
        obj = indexToSearch[key];
        for (prop in obj) {
          if (obj[prop] === docid) {
            return passback === "bool" ? true : key;
          }
        }
      }
    return false;
  }

  /**
   * Find id in indices
   * @method isDocidInIndex
   * @param  {object} indices The file containing the indeces
   * @param  {object} doc The document which should be added to the index
   * @return {boolean} true/false
   */
  priv.isDocidInIndex = function (indices, doc) {
    var index, i, l = priv.indices.length;

    // loop indices
    for (i = 0; i < l; i += 1) {
      index = {};
      index.reference = priv.indices[i];
      index.name = index.reference["name"];
      index.size = priv.getObjectSize(indices[index.name]);
      index.result_array;

      if (index.size > 0) {
        if (priv.searchIndexByValue(indices[index.name], doc._id, "bool")) {
          return true
        };
      }
    }
    return false;
  }
  priv.cleanIndices = function (indices, doc) {
    var i, j, k, index, key, obj, prop, l = priv.indices.length,
      docid = doc._id;

    // loop indices (indexA, indexAB...)
    for (i = 0; i < l; i += 1) {
      // index object (reference and current-iteration)
      index = {};
      index.reference = priv.indices[i];
      index.name = index.reference["name"];
      index.current = indices[index.name];
      index.current_size = priv.getObjectSize(index.current);

      for (j = 0; j < index.current_size; j++) {
        key = priv.searchIndexByValue(index.current, doc._id, "key");
        index.result_array = index.current[key];
        if (!!key) {
          // if there is more than one docid in the result array,
          // just remove this one and not the whole array
          if (index.result_array.length > 1) {
            index.result_array.splice(k,1);
          } else {
            delete index.current[key];
          }
        }
      }
    }
    return indices;
  }
  /**
   * Adds entries to indices
   * @method createEmptyIndexArray
   * @param  {object} indices The file containing the indeces
   * @param  {object} doc The document which should be added to the index
   */
  priv.updateIndices = function (indices, doc) {
    var i, j, k, m, index,value, label, key, l = priv.indices.length;

    // loop indices
    for (i = 0; i < l; i += 1) {
      // index object (reference and current iteration)
      index = {};
      index.reference = priv.indices[i];
      index.reference_size = index.reference.fields.length;
      index.name = index.reference["name"];
      index.field_array = [];
      index.current = indices[index.name];
      index.current_size = priv.getObjectSize(index.current);

      // build array of values to create entries in index
      for (j = 0; j < index.reference_size; j += 1) {
        label = index.reference.fields[j]
        value = doc[label];
        if (value !== undefined) {
          // add a new entry
          index.field_array.push(value);

          // remove existing entries with same docid
          // because items are stored as "keyword:id" pairs this is tricky
          if (index.current_size > 0) {
            key = priv.searchIndexByValue(indices[index.name], doc._id, "key");
            if (!!key) {
              delete index.current[key];
            }
          }
        }
      }
      // create keyword entries
      if (index.current !== undefined) {
        m = index.field_array.length;
        if (m) {
          for (k = 0; k < m; k += 1) {
            index.current_keyword = [index.field_array[k]];
            if (index.current[index.current_keyword] === undefined) {
              index.current[index.current_keyword] = [];
            }
            index.current[index.current_keyword].push(doc._id);
          }
        }
      }
    }
    return indices;
  };

  /**
   * Post document to substorage and create/update index file(s)
   * @method post
   * @param  {object} command The JIO command
   * @param  {string} source The source of the function call
   */
  priv.postOrput = function (command, source) {
    var f = {}, indices, doc, docid;
    doc = command.cloneDoc();
    docid = command.getDocId();
    if (typeof docid !== "string") {
      doc._id = priv.generateUuid();
      docid = doc._id;
    }
    f.getIndices = function () {
      var option = command.cloneOption();
      if (option.max_retry === 0) {
        option.max_retry = 3;
      }
      that.addJob(
        "get",
        priv.substorage,
        priv.index_suffix,
        option,
        function (response) {
          indices = response;
          f.postDocument("put");
        },
        function (err) {
          switch (err.status) {
          case 404:
            if (source !== 'PUTATTACHMENT') {
              indices = priv.createEmptyIndexArray();
              f.postDocument("post");
            } else {
              that.error({
                "status": 404,
                "statusText": "Not Found",
                "error": "not found",
                "message": "Document not found",
                "reason": "Document not found"
              });
        return;
            }
            break;
          default:
            err.message = "Cannot retrieve index array";
            that.error(err);
            break;
          }
        }
      );
    };
    f.postDocument = function (index_update_method) {
      if (priv.isDocidInIndex(indices, doc) && source === 'POST') {
        // POST the document already exists
        that.error({
          "status": 409,
          "statusText": "Conflicts",
          "error": "conflicts",
          "message": "Cannot create a new document",
          "reason": "Document already exists"
        });
        return;
      } else {
        if (source !== 'PUTATTACHMENT') {
          indices = priv.updateIndices(indices, doc);
        }
        that.addJob(
          source === 'PUTATTACHMENT' ? "putAttachment" : "post",
          priv.substorage,
          doc,
          command.cloneOption(),
          function () {
            if (source !== 'PUTATTACHMENT') {
              f.sendIndices(index_update_method);
            } else {
              docid = docid + '/' + command.getAttachmentId();
              that.success({
                "ok": true,
                "id": docid
              });
            }
          },
          function (err) {
            switch (err.status) {
            case 409:
              // file already exists
              if (source !== 'PUTATTACHMENT') {
                f.sendIndices(index_update_method);
              } else {
                that.success({
                  "ok": true,
                  "id": docid
                });
              }
              break;
            default:
              err.message = "Cannot upload document";
              that.error(err);
              break;
            }
          }
        );
      }
    };
    f.sendIndices = function (method) {
      indices._id = priv.index_suffix;
      that.addJob(
        method,
        priv.substorage,
        indices,
        command.cloneOption(),
        function () {
          that.success({
            "ok": true,
            "id": docid
          });
        },
        function (err) {
          // xxx do we try to delete the posted document ?
          err.message = "Cannot save index file";
          that.error(err);
        }
      );
    };
    f.getIndices();
  };

  /**
   * Update the document metadata and update the index
   * @method put
   * @param  {object} command The JIO command
   */
  that.post = function (command) {
    priv.postOrput(command, 'POST');
  };

  /**
   * Update the document metadata and update the index
   * @method put
   * @param  {object} command The JIO command
   */
  that.put = function (command) {
    priv.postOrput(command, 'PUT');
  };

  /**
   * Add an attachment to a document (no index modification)
   * @method putAttachment
   * @param  {object} command The JIO command
   */
  that.putAttachment = function (command) {
    priv.postOrput(command, 'PUTATTACHMENT');
  };

  /**
   * Get the document metadata or attachment.
   * Options:
   * - {boolean} revs Add simple revision history (false by default).
   * - {boolean} revs_info Add revs info (false by default).
   * - {boolean} conflicts Add conflict object (false by default).
   * @method get
   * @param  {object} command The JIO command
   */
  that.get = function (command) {
    var option, docid;
    option = command.cloneOption();
    if (option.max_retry === 0) {
      option.max_retry = 3;
    }
    if (command.getAttachmentId() !== undefined) {
      docid = command.getDocId() + '/' + command.getAttachmentId();
    } else {
      docid = command.getDocId();
    }
    that.addJob(
      "get",
      priv.substorage,
      docid,
      option,
      function (response) {
        that.success(response);
      },
      function (err) {
        that.error({
          "status": 404,
          "statusText": "Not Found",
          "error": "not_found",
          "message": "Cannot find the attachment",
          "reason": "Document/Attachment not found"
        });
      }
    );
  };

  /**
   * Remove document or attachment - removing documents updates index!.
   * @method remove
   * @param  {object} command The JIO command
   */
  that.remove = function (command) {
    var f = {}, indices, doc, docid, option;

    doc = command.cloneDoc();
    option = command.cloneOption();
    if (option.max_retry === 0) {
      option.max_retry = 3;
    }

    f.removeDocument = function (type) {
      if (type === 'doc') {
        docid = command.getDocId();
      } else {
        docid = command.getDocId() + '/' + command.getAttachmentId();
      }
      that.addJob(
        "remove",
        priv.substorage,
        docid,
        option,
        function (response) {
          that.success(response);
        },
        function (err) {
          that.error({
            "status": 409,
            "statusText": "Conflict",
            "error": "conflict",
            "message": "Document Update Conflict",
            "reason": "Could not delete document or attachment"
          });
        }
      );
    };
    f.getIndices = function () {
      that.addJob(
        "get",
        priv.substorage,
        priv.index_suffix,
        option,
        function (response) {
          // if deleting an attachment
          if (typeof command.getAttachmentId() === 'string'){
            f.removeDocument('attachment')
          } else {
            indices = priv.cleanIndices(response, doc);
            // store update index file
            that.addJob(
              "put",
              priv.substorage,
              indices,
              command.cloneOption(),
              function () {
                // remove actual document
                f.removeDocument('doc');
              },
              function (err) {
                // xxx do we try to delete the posted document ?
                err.message = "Cannot save index file";
                that.error(err);
              }
            );
          }
        },
        function (err) {
          that.error({
            "status": 404,
            "statusText": "Not Found",
            "error": "not_found",
            "message": "Document index not found, please check document ID",
            "reason": "Incorrect document ID"
          });
        return;
        }
      );
    };
    f.getIndices();
  };

 /**
   * Gets a document list.
   * @method allDocs
   */
  /*
  that.allDocs = function (command) {
    var obj = localStorage.getItem(storage_file_object_name),
      success,
      error;

    if (obj) {
      priv.update();
      setTimeout(function () {
        that.success(priv.allDocs(obj));
      });
    } else {
      success = function (val) {
        priv.setFileArray(val.rows);
        that.success(val);
      };
      error = function (err) {
        that.error(err);
      };
      that.addJob('allDocs', priv.sub_storage_spec, null,
        command.cloneOption(), success, error);
    }
  }; // end allDocs
  */
  return that;
});