/*
 * JIO extension for resource global identifier management.
 * Copyright (C) 2013  Nexedi SA
 *
 * This program is free software: you can Use, Study, Modify and Redistribute
 * it under the terms of the GNU General Public License version 3, or (at your
 * option) any later version, as published by the Free Software Foundation.
 *
 * You can also Link and Combine this program with other software covered by
 * the terms of any of the Free Software licenses or any of the Open Source
 * Initiative approved licenses and Convey the resulting work. Corresponding
 * source of such a combination shall include the source code for all other
 * software used.
 *
 * This program is distributed WITHOUT ANY WARRANTY; without even the implied
 * warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
 *
 * See COPYING file for full licensing terms.
 * See https://www.nexedi.com/licensing for rationale and options.
 */

/*jslint indent: 2, maxlen: 80, sloppy: true, nomen: true */
/*global define, jIO */

/**
 * JIO GID Storage. Type = 'gid'.
 * Identifies document with their global identifier representation
 *
 * Sub storages must support queries and include_docs options.
 *
 * Storage Description:
 *
 *     {
 *       "type": "gid",
 *       "sub_storage": {<storage description>},
 *       "constraints": {
 *         "default": {
 *           "identifier": "list",    // ['a', 1]
 *           "type": "DCMIType",      // 'Text'
 *           "title": "string"        // 'something blue'
 *         },
 *         "Text": {
 *           "format": "contentType"  // contains 'text/plain;charset=utf-8'
 *         },
 *         "Image": {
 *           "version": "json"        // value as is
 *         }
 *       }
 *     }
 */
// define([module_name], [dependencies], module);
(function (dependencies, module) {
  "use strict";
  if (typeof define === 'function' && define.amd) {
    return define(dependencies, module);
  }
  module(jIO);
}(['jio'], function (jIO) {
  "use strict";

  var dcmi_types, metadata_actions, content_type_re, tool;
  dcmi_types = {
    'Collection': 'Collection',
    'Dataset': 'Dataset',
    'Event': 'Event',
    'Image': 'Image',
    'InteractiveResource': 'InteractiveResource',
    'MovingImage': 'MovingImage',
    'PhysicalObject': 'PhysicalObject',
    'Service': 'Service',
    'Software': 'Software',
    'Sound': 'Sound',
    'StillImage': 'StillImage',
    'Text': 'Text'
  };
  metadata_actions = {
    /**
     * Returns the metadata value
     */
    json: function (value) {
      return value;
    },
    /**
     * Returns the metadata if there is a string
     */
    string: function (value) {
      if (!Array.isArray(value)) {
        if (typeof value === 'object') {
          return value.content;
        }
        return value;
      }
    },
    /**
     * Returns the metadata in a array format
     */
    list: function (value) {
      var i, new_value = [];
      if (Array.isArray(value)) {
        for (i = 0; i < value.length; i += 1) {
          if (typeof value[i] === 'object') {
            new_value[new_value.length] = value[i].content;
          } else {
            new_value[new_value.length] = value[i];
          }
        }
      } else if (value !== undefined) {
        value = [value];
      }
      return value;
    },
    /**
     * Returns the metadata if there is a string equal to a DCMIType
     */
    DCMIType: function (value) {
      var i;
      if (!Array.isArray(value)) {
        value = [value];
      }
      for (i = 0; i < value.length; i += 1) {
        if (typeof value[i] === 'object' && dcmi_types[value[i].content]) {
          return value[i].content;
        }
        if (dcmi_types[value[i]]) {
          return value[i];
        }
      }
    },
    /**
     * Returns the metadata content type if exist
     */
    contentType: function (value) {
      var i;
      if (!Array.isArray(value)) {
        value = [value];
      }
      for (i = 0; i < value.length; i += 1) {
        if (value[i] === 'object') {
          if (content_type_re.test(value[i].content)) {
            return value[i].content;
          }
        } else {
          if (content_type_re.test(value[i])) {
            return value[i];
          }
        }
      }
    },
    /**
     * Returns the metadata if it is a date
     */
    date: function (value) {
      var d;
      if (!Array.isArray(value)) {
        if (typeof value === 'object') {
          d = new Date(value.content);
          value = value.content;
        } else {
          d = new Date(value);
        }
      }
      if (Object.prototype.toString.call(d) === "[object Date]") {
        if (!isNaN(d.getTime())) {
          return value;
        }
      }
    }
  };
  content_type_re = new RegExp(
    '^([a-z]+\\/[a-zA-Z0-9\\+\\-\\.]+)' +
      '((?:\\s*;\\s*[a-zA-Z\\+\\-\\.]+\\s*=' +
      '\\s*[a-zA-Z0-9\\-\\+\\.,]+)*)$'
  );
  tool = {
    "deepClone": jIO.util.deepClone
  };

  /**
   * Creates a gid from metadata and constraints.
   *
   * @param  {Object} metadata The metadata to use
   * @param  {Object} constraints The constraints
   * @return {String} The gid or undefined if metadata doesn't respect the
   *   constraints
   */
  function gidFormat(metadata, constraints) {
    var types, i, j, meta_key, result = [], tmp, constraint, actions;
    types = (metadata_actions.list(metadata.type) || []).slice();
    types.unshift('default');
    for (i = 0; i < types.length; i += 1) {
      constraint = constraints[types[i]];
      for (meta_key in constraint) {
        if (constraint.hasOwnProperty(meta_key)) {
          actions = constraint[meta_key];
          if (!Array.isArray(actions)) {
            actions = [actions];
          }
          for (j = 0; j < actions.length; j += 1) {
            tmp = metadata_actions[
              actions[j]
            ](metadata[meta_key]);
            if (tmp === undefined) {
              return;
            }
          }
          result[result.length] = [meta_key, tmp];
        }
      }
    }
    // sort dict keys to make gid universal
    result.sort(function (a, b) {
      return a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0;
    });
    tmp = {};
    for (i = 0; i < result.length; i += 1) {
      tmp[result[i][0]] = result[i][1];
    }
    return JSON.stringify(tmp);
  }

  /**
   * Convert a gid to a jio query.
   *
   * @param  {Object,String} gid The gid
   * @return {Object} A jio serialized query
   */
  function gidToJIOQuery(gid) {
    var k, i, result = [], meta, content;
    if (typeof gid === 'string') {
      gid = JSON.parse(gid);
    }
    for (k in gid) {
      if (gid.hasOwnProperty(k)) {
        meta = gid[k];
        if (!Array.isArray(meta)) {
          meta = [meta];
        }
        for (i = 0; i < meta.length; i += 1) {
          content = meta[i];
          if (typeof content === 'object') {
            content = content.content;
          }
          result[result.length] = {
            "type": "simple",
            "operator": "=",
            "key": k,
            "value": content
          };
        }
      }
    }
    return {
      "type": "complex",
      "operator": "AND",
      "query_list": result
    };
  }

  /**
   * Parse the gid and returns a metadata object containing gid keys and values.
   *
   * @param  {String} gid The gid to convert
   * @param  {Object} constraints The constraints
   * @return {Object} The gid metadata
   */
  function gidParse(gid, constraints) {
    var object;
    try {
      object = JSON.parse(gid);
    } catch (e) {
      return;
    }
    if (gid !== gidFormat(object, constraints)) {
      return;
    }
    return object;
  }

  /**
   * The gid storage used by JIO.
   *
   * This storage change the id of a document with its global id. A global id
   * is representation of a document metadata used to define it as uniq. The way
   * to generate global ids can be define in the storage description. It allows
   * us use duplicating storage with different sub storage kind.
   *
   * @class GidStorage
   */
  function GidStorage(spec) {
    var that = this, priv = {};

    priv.sub_storage = spec.sub_storage;
    priv.constraints = spec.constraints || {
      "default": {
        "type": "DCMIType",
        "title": "string"
      }
    };

    // JIO Commands

    /**
     * Generic command for post or put one.
     *
     * This command will check if the document already exist with an allDocs
     * and a jio query. If exist, then post will fail. Put will update the
     * retrieved document thanks to its real id. If no documents are found, post
     * and put will create a new document with the sub storage id generator.
     *
     * @method putOrPost
     * @private
     * @param  {Command} command The JIO command
     * @param  {String} method The command method
     */
    priv.putOrPost = function (command, metadata, method) {
      var gid, jio_query, doc = tool.deepClone(metadata);
      gid = gidFormat(doc, priv.constraints);
      if (gid === undefined || (doc._id && gid !== doc._id)) {
        return command.error(
          "bad_request",
          "metadata should respect constraints",
          "Cannot " + method + " document"
        );
      }
      jio_query = gidToJIOQuery(gid);
      command.storage(priv.sub_storage).allDocs({
        "query": jio_query
      }).then(function (response) {
        var update_method = method;
        response = response.data;
        if (response.total_rows !== 0) {
          if (method === 'post') {
            return command.error(
              "conflict",
              "Document already exists",
              "Cannot " + method + " document"
            );
          }
          doc = tool.deepClone(metadata);
          doc._id = response.rows[0].id;
        } else {
          doc = tool.deepClone(metadata);
          delete doc._id;
          update_method = 'post';
        }
        command.storage(priv.sub_storage)[update_method](
          doc
        ).then(function (response) {
          response.id = gid;
          command.success(response);
        }, function (err) {
          err.message = "Cannot " + method + " document";
          command.error(err);
        });
      }, function (err) {
        err.message = "Cannot " + method + " document";
        command.error(err);
      });
    };

    /**
     * Generic command for putAttachment, getAttachment or removeAttachment.
     *
     * This command will check if the document exist with an allDocs and a
     * jio query. If not exist, then it returns 404. Otherwise the
     * action will be done on the attachment of the found document.
     *
     * @method putGetOrRemoveAttachment
     * @private
     * @param  {Object} command The JIO command
     * @param  {Object} doc The command parameters
     * @param  {String} method The command method
     */
    priv.putGetOrRemoveAttachment = function (command, doc, method) {
      var gid_object, jio_query;
      gid_object = gidParse(doc._id, priv.constraints);
      if (gid_object === undefined) {
        return command.error(
          "bad_request",
          "metadata should respect constraints",
          "Cannot " + method + " attachment"
        );
      }
      jio_query = gidToJIOQuery(gid_object);
      command.storage(priv.sub_storage).allDocs({
        "query": jio_query
      }).then(function (response) {
        response = response.data;
        if (response.total_rows === 0) {
          return command.error(
            "not_found",
            "Document already exists",
            "Cannot " + method + " attachment"
          );
        }
        gid_object = doc._id;
        doc._id = response.rows[0].id;
        command.storage(priv.sub_storage)[method + "Attachment"](
          doc
        ).then(function (response) {
          response.id = gid_object;
          command.success(response);
        }, function (err) {
          err.message = "Cannot " + method + " attachment";
          command.error(err);
        });
      }, function (err) {
        err.message = "Cannot " + method + " attachment";
        command.error(err);
      });
    };

    /**
     * See {{#crossLink "gidStorage/putOrPost:method"}}{{/#crossLink}}.
     *
     * @method post
     * @param  {Command} command The JIO command
     */
    that.post = function (command, metadata) {
      priv.putOrPost(command, metadata, 'post');
    };

    /**
     * See {{#crossLink "gidStorage/putOrPost:method"}}{{/#crossLink}}.
     *
     * @method put
     * @param  {Command} command The JIO command
     */
    that.put = function (command, metadata) {
      priv.putOrPost(command, metadata, 'put');
    };

    /**
     * Puts an attachment to a document thank to its gid, a sub allDocs and a
     * jio query.
     *
     * @method putAttachment
     * @param  {Command} command The JIO command
     */
    that.putAttachment = function (command, param) {
      priv.putGetOrRemoveAttachment(command, param, 'put');
    };

    /**
     * Gets a document thank to its gid, a sub allDocs and a jio query.
     *
     * @method get
     * @param  {Object} command The JIO command
     */
    that.get = function (command, param) {
      var gid_object, jio_query;
      gid_object = gidParse(param._id, priv.constraints);
      if (gid_object === undefined) {
        return command.error(
          "bad_request",
          "metadata should respect constraints",
          "Cannot get document"
        );
      }
      jio_query = gidToJIOQuery(gid_object);
      command.storage(priv.sub_storage).allDocs({
        "query": jio_query,
        "include_docs": true
      }).then(function (response) {
        response = response.data;
        if (response.total_rows === 0) {
          return command.error(
            "not_found",
            "missing",
            "Cannot get document"
          );
        }
        response.rows[0].doc._id = param._id;
        return command.success({"data": response.rows[0].doc});
      }, function (err) {
        err.message = "Cannot get document";
        return command.error(err);
      });
    };

    /**
     * Gets an attachment from a document thank to its gid, a sub allDocs and a
     * jio query.
     *
     * @method getAttachment
     * @param  {Command} command The JIO command
     */
    that.getAttachment = function (command, param) {
      priv.putGetOrRemoveAttachment(command, param, 'get');
    };

    /**
     * Remove a document thank to its gid, sub allDocs and a jio query.
     *
     * @method remove
     * @param  {Command} command The JIO command.
     */
    that.remove = function (command, doc) {
      var gid_object, jio_query;
      gid_object = gidParse(doc._id, priv.constraints);
      if (gid_object === undefined) {
        return command.error(
          "bad_request",
          "metadata should respect constraints",
          "Cannot remove document"
        );
      }
      jio_query = gidToJIOQuery(gid_object);
      command.storage(priv.sub_storage).allDocs({
        "query": jio_query
      }).then(function (response) {
        response = response.data;
        if (response.total_rows === 0) {
          return command.error(
            "not_found",
            "missing",
            "Cannot remove document"
          );
        }
        gid_object = doc._id;
        doc = {"_id": response.rows[0].id};
        command.storage(priv.sub_storage).remove(
          doc
        ).then(function (response) {
          response.id = gid_object;
          command.success(response);
        }, function (err) {
          err.message = "Cannot remove document";
          command.error(err);
        });
      }, function (err) {
        err.message = "Cannot remove document";
        command.error(err);
      });
    };

    /**
     * Removes an attachment to a document thank to its gid, a sub allDocs and a
     * jio query.
     *
     * @method removeAttachment
     * @param  {Command} command The JIO command
     */
    that.removeAttachment = function (command, param) {
      priv.putGetOrRemoveAttachment(command, param, 'remove');
    };

    /**
     * Retrieve a list of document which respect gid constraints.
     *
     * @method allDocs
     * @param  {Command} command The JIO command
     */
    that.allDocs = function (command, param, options) {
      /*jslint unparam: true */
      var include_docs;
      include_docs = options.include_docs;
      options.include_docs = true;
      command.storage(priv.sub_storage).allDocs(
        options
      ).then(function (response) {
        /*jslint ass: true */
        var result = [], doc_gids = {}, row, gid;
        response = response.data;
        while ((row = response.rows.shift()) !== undefined) {
          gid = gidFormat(row.doc, priv.constraints);
          if (gid !== undefined) {
            if (!doc_gids[gid]) {
              doc_gids[gid] = true;
              row.id = gid;
              delete row.key;
              result[result.length] = row;
              if (include_docs === true) {
                row.doc._id = gid;
              } else {
                delete row.doc;
              }
            }
          }
        }
        doc_gids = undefined; // free memory
        row = undefined;
        command.success({"data": {
          "total_rows": result.length,
          "rows": result
        }});
      }, function (err) {
        err.message = "Cannot get all documents";
        return command.error(err);
      });

    };

    that.check = function (command, param, options) {
      return that.repair(command, param, options, "check");
    };

    that.repair = function (command, param, options, action) {
      var gid_object, jio_query, sub_storage;
      if (typeof param._id !== "string" || !param._id) {
        return command.error("bad_request", "document id must be provided");
      }
      if (action === undefined) {
        action = "repair";
      }
      gid_object = gidParse(param._id, priv.constraints);
      if (gid_object === undefined) {
        return command.error(
          "bad_request",
          "metadata should respect constraints",
          "Cannot " + action + " document"
        );
      }
      jio_query = gidToJIOQuery(gid_object);
      sub_storage = command.storage(priv.sub_storage);
      sub_storage.allDocs({
        "query": jio_query
      }).then(function (response) {
        response = response.data;
        if (response.total_rows === 0) {
          // document not found, nothing to repair or check
          return command.success();
        }
        return sub_storage[action]({"_id": response.rows[0].id}, options);
      }).then(command.success, command.error, command.notify);
    };
  }

  jIO.addStorage('gid', GidStorage);

}));
