FieldDBObject.js

/* globals alert, confirm, prompt, navigator, Android, FieldDB */
"use strict";

var Diacritics = require("diacritics");
var Q = require("q");
var packageJson;
try {
  packageJson = require("./../package.json");
} catch (e) {
  console.log("failed to load package.json", e);
  packageJson = {
    version: "x.x.x"
  };
}
// var FieldDBDate = function FieldDBDate(options) {
//   // this.debug("In FieldDBDate ", options);
//   Object.apply(this, arguments);
//   if (options) {
//     this.timestamp = options;
//   }
// };

// FieldDBDate.prototype = Object.create(Object.prototype, /** @lends FieldDBDate.prototype */ {
//   constructor: {
//     value: FieldDBDate
//   },

//   timestamp: {
//     get: function() {
//       return this._timestamp || 0;
//     },
//     set: function(value) {
//       if (value === this._timestamp) {
//         return;
//       }
//       if (!value) {
//         delete this._timestamp;
//         return;
//       }
//       if (value.replace) {
//         try {
//           value = value.replace(/["\\]/g, "");
//           value = new Date(value);
//           /* Use date modified as a timestamp if it isnt one already */
//           value = value.getTime();
//         } catch (e) {
//           this.warn("Upgraded timestamp" + value);
//         }
//       }
//       this._timestamp = value;
//     }
//   },

//   toJSON: {
//     value: function(includeEvenEmptyAttributes, removeEmptyAttributes) {
//       var result = this._timestamp;

//       if (includeEvenEmptyAttributes) {
//         result = this._timestamp || 0;
//       }

//       if (removeEmptyAttributes && !this._timestamp) {
//         result = 0;
//       }
//       return result;
//     }
//   }
// });

/**
 * @class An extendable object which can recieve new parameters on creation.
 *
 * @param {Object} options Optional json initialization object
 * @property {String} dbname This is the identifier of the corpus, it is set when
 *           a corpus is created. It must be a file save name, and be a permitted
 *           name in CouchDB which means it is [a-z] with no uppercase letters or
 *           symbols, by convention it cannot contain -, but _ is acceptable.

 * @extends Object
 * @tutorial tests/FieldDBObjectTest.js
 */
var FieldDBObject = function FieldDBObject(json) {
  if (json && (json instanceof this.constructor || json.constructor.toString() === this.constructor.toString())) {
    json.debug("This was already the right type, not converting it.");
    return json;
  }
  // if (!this._fieldDBtype) {
  //   this._fieldDBtype = "FieldDBObject";
  // }
  if (json && json.id) {
    this.useIdNotUnderscore = true;
  }
  if (json && json.api && this.api) {
    if (json.api !== this.api) {
      console.log("Using " + this.api + " when the api of the incoming model was " + json.api);
    }
    delete json.api;
  }
  this.verbose("In parent an json", json);
  // Set the confidential first, so the rest of the fields can be encrypted
  // if (json && json.corpus) {
  //   this.corpus = json.corpus;
  // }
  if (json && json.confidential && this.INTERNAL_MODELS["confidential"]) {
    this.confidential = new this.INTERNAL_MODELS["confidential"](json.confidential);
  }
  if (this.INTERNAL_MODELS) {
    this.debug("parsing with ", this.INTERNAL_MODELS);
  }
  var simpleModels = [];
  for (var member in json) {
    if (!json.hasOwnProperty(member)) {
      continue;
    }
    this.debug("JSON: " + member);
    if (json[member] &&
      this.INTERNAL_MODELS &&
      this.INTERNAL_MODELS[member] &&
      typeof this.INTERNAL_MODELS[member] === "function" &&
      !(json[member] instanceof this.INTERNAL_MODELS[member]) &&
      !(this.INTERNAL_MODELS[member].compatibleWithSimpleStrings && typeof json[member] === "string")) {

      json[member] = new this.INTERNAL_MODELS[member](json[member]);

    } else {
      simpleModels.push(member);
    }
    try {
      this[member] = json[member];
    } catch (e) {
      this.warn(e.stack);
    }
  }
  if (simpleModels.length > 0) {
    this.debug("simpleModels", simpleModels.join(", "));
  }
  Object.apply(this, arguments);
  // if (!this._rev) {
  if (!this.id && !this._dateCreated) {
    this.dateCreated = Date.now();
  }

};
FieldDBObject.internalAttributesToNotJSONify = [
  "$$hashKey",
  "application",
  "bugMessage",
  "confirmMessage",
  "confirmMergePromises",
  "contextualizer",
  "corpus",
  "currentDoc",
  "currentSession",
  "datalist",
  "database",
  "db",
  "debugMessages",
  "decryptedMode",
  "dontRecurse",
  "fetching",
  "fieldsInColumns",
  "fossil",
  "loaded",
  "loading",
  "newDatum",
  "parent",
  "perObjectAlwaysConfirmOkay",
  "perObjectDebugMode",
  "promptMessage",
  "saved",
  "saving",
  "selected",
  "temp",
  "unsaved",
  "useIdNotUnderscore",
  "warnMessage",
  "whenReady"
];

FieldDBObject.internalAttributesToAutoMerge = FieldDBObject.internalAttributesToNotJSONify.concat([
  "appVersionWhenCreated",
  "authServerVersionWhenCreated",
  "created_at",
  "dateCreated",
  "dateModified",
  "fieldDBtype",
  "modifiedByUser",
  "rev",
  "roles",
  "updated_at",
  "version"
]);

FieldDBObject.ignore = function(property, ignorelist) {
  if (!ignorelist) {
    throw new Error("missing the list of ignores");
  }
  if (ignorelist.indexOf(property) > -1 || ignorelist.indexOf(property.replace(/^_/, "")) > -1) {
    return true;
  }
};
FieldDBObject.software = {};
FieldDBObject.hardware = {};

FieldDBObject.DEFAULT_STRING = "";
FieldDBObject.DEFAULT_OBJECT = {};
FieldDBObject.DEFAULT_ARRAY = [];
FieldDBObject.DEFAULT_COLLECTION = [];
FieldDBObject.DEFAULT_VERSION = "v" + packageJson.version;
FieldDBObject.DEFAULT_DATE = 0;

FieldDBObject.render = function(options) {
  this.debug("Rendering, but the render was not injected for this " + this.fieldDBtype, options);
};

FieldDBObject.verbose = function(message, message2, message3, message4) {
  try {
    if (navigator && navigator.appName === "Microsoft Internet Explorer") {
      return;
    }
  } catch (e) {
    //do nothing, we are in node or some non-friendly browser.
  }
  if (this.verboseMode) {
    var type = this.fieldDBtype || this._id || "UNKNOWNTYPE";
    console.log(type.toUpperCase() + " VERBOSE: " + message);

    if (message2) {
      console.log(message2);
    }
    if (message3) {
      console.log(message3);
    }
    if (message4) {
      console.log(message4);
    }
  }
};
FieldDBObject.debugMode = false;
FieldDBObject.debug = function(message, message2, message3, message4) {
  try {
    if (navigator && navigator.appName === "Microsoft Internet Explorer") {
      return;
    }
  } catch (e) {
    //do nothing, we are in node or some non-friendly browser.
  }
  if (this.debugMode) {
    var type = this.fieldDBtype || this._id || "UNKNOWNTYPE";
    console.log(type.toUpperCase() + " DEBUG: " + message);

    if (message2) {
      console.log(message2);
    }
    if (message3) {
      console.log(message3);
    }
    if (message4) {
      console.log(message4);
    }
  }
};

FieldDBObject.todo = function(message, message2, message3, message4) {
  var type = this.fieldDBtype || this._id || "UNKNOWNTYPE";
  console.warn(type.toUpperCase() + " TODO: " + message);
  if (message2) {
    console.warn(message2);
  }
  if (message3) {
    console.warn(message3);
  }
  if (message4) {
    console.warn(message4);
  }
};

FieldDBObject.popup = function(message) {
  try {
    alert(message);
  } catch (e) {
    this.warn(" Couldn't tell user about a popup: " + message);
    // console.log("Alert is not defined, this is strange.");
  }
  var type = this.fieldDBtype || this._id || "UNKNOWNTYPE";
  console.log(type.toUpperCase() + " POPUP: " + message);
};

FieldDBObject.bug = function(message) {
  try {
    alert(message);
  } catch (e) {
    this.warn(" Couldn't tell user about a bug: " + message);
    // console.log("Alert is not defined, this is strange.");
  }
  var type = this.fieldDBtype || this._id || "UNKNOWNTYPE";
  //outputing a stack trace
  console.error(type.toUpperCase() + " BUG: " + message);
};

FieldDBObject.warn = function(message, message2, message3, message4) {
  var type = this.fieldDBtype || this._id || "UNKNOWNTYPE";
  // putting out a stacktrace
  console.warn(type.toUpperCase() + " WARN: " + message);
  if (message2) {
    console.warn(message2);
  }
  if (message3) {
    console.warn(message3);
  }
  if (message4) {
    console.warn(message4);
  }
};

FieldDBObject.prompt = function(message, optionalLocale, providedInput) {
  var deferred = Q.defer(),
    self = this;

  Q.nextTick(function() {
    var response;

    if (self.alwaysReplyToPrompt !== undefined) {
      response = providedInput || self.alwaysReplyToPrompt;
      console.warn(self.fieldDBtype.toUpperCase() + " NOT PROMPTING USER: " + message + " \nThe code decided that they would probably reply `" + response + "` and it wasnt worth prompting.");
    } else {
      try {
        response = prompt(message, providedInput);

        // Let the user enter info, even JSON
        if (response === "yes") {
          response = providedInput;
        } else if (response !== null) {
          if (typeof providedInput !== "string" && typeof providedInput !== "number") {
            try {
              var parsed = JSON.parse(response);
              response = parsed;
            } catch (e) {
              FieldDB.FieldDBObject.bug("There was a problem parsing your input.").then(function() {
                FieldDB.FieldDBObject.prompt(message, optionalLocale, providedInput);
              });
            }
          }
        }

      } catch (e) {
        response = null;
        console.warn(self.fieldDBtype.toUpperCase() + " UNABLE TO PROMPT USER: " + message + " pretending they said `" + response + "`");
      }
    }
    if (response !== null && response !== undefined && typeof response.trim === "function") {
      response = response.trim();
    }
    if (response) {
      deferred.resolve({
        message: message,
        optionalLocale: optionalLocale,
        response: response
      });
    } else {
      deferred.reject({
        message: message,
        optionalLocale: optionalLocale,
        response: response
      });
    }

  });
  return deferred.promise;
};

FieldDBObject.confirm = function(message, optionalLocale) {
  var deferred = Q.defer(),
    self = this;

  Q.nextTick(function() {
    var response;

    if (self.alwaysConfirmOkay) {
      console.warn(self.fieldDBtype.toUpperCase() + " NOT ASKING USER: " + message + " \nThe code decided that they would probably yes and it wasnt worth asking.");
      response = self.alwaysConfirmOkay;
    } else {
      try {
        response = confirm(message);
      } catch (e) {
        console.warn(self.fieldDBtype.toUpperCase() + " UNABLE TO ASK USER: " + message + " pretending they said " + self.alwaysConfirmOkay);
        response = self.alwaysConfirmOkay;
      }
    }

    if (response) {
      deferred.resolve({
        message: message,
        optionalLocale: optionalLocale,
        response: response
      });
    } else {
      deferred.reject({
        message: message,
        optionalLocale: optionalLocale,
        response: response
      });
    }

  });
  return deferred.promise;
};
/* set the application if you want global state (ie for checking if a user is authorized) */
// FieldDBObject.application = {}

/**
 * The uuid generator uses a "GUID" like generation to create a unique string.
 *
 * @returns {String} a string which is likely unique, in the format of a
 *          Globally Unique ID (GUID)
 */
FieldDBObject.uuidGenerator = function() {
  var S4 = function() {
    return (((1 + Math.random()) * 0x10000) | 0).toString(16).substring(1);
  };
  return Date.now() + (S4() + S4() + S4() + S4() + S4() + S4() + S4() + S4());
};

FieldDBObject.regExpEscape = function(s) {
  return String(s).replace(/([-()\[\]{}+?*.$\^|,:#<!\\])/g, "\\$1").
  replace(/\x08/g, "\\x08");
};

FieldDBObject.getHumanReadableTimestamp = function() {
  var today = new Date();
  var year = today.getFullYear();
  var month = today.getMonth() + 1;
  var day = today.getDate();
  var hour = today.getHours();
  var minute = today.getMinutes();

  if (month < 10) {
    month = "0" + month;
  }
  if (day < 10) {
    day = "0" + day;
  }
  if (hour < 10) {
    hour = "0" + hour;
  }
  if (minute < 10) {
    minute = "0" + minute;
  }

  return year + "-" + month + "-" + day + "_" + hour + "." + minute;
};

FieldDBObject.guessType = function(doc) {
  if (!doc || JSON.stringify(doc) === {}) {
    return "FieldDBObject";
  }
  FieldDBObject.debug("Guessing type " + doc._id);
  var guessedType = doc.previousFieldDBtype || doc.jsonType || doc.collection || "FieldDBObject";
  if (doc.api && doc.api.length > 0) {
    FieldDBObject.debug("using api" + doc.api);
    guessedType = doc.api[0].toUpperCase() + doc.api.substring(1, doc.api.length);
  }
  guessedType = guessedType.replace(/s$/, "");
  guessedType = guessedType[0].toUpperCase() + guessedType.substring(1, guessedType.length);
  if (guessedType === "Datalist") {
    guessedType = "DataList";
  }
  if (guessedType === "FieldDBObject") {
    if (doc.session) {
      guessedType = "Datum";
      if (doc.fields && doc.fields[0] === "judgement") {
        guessedType = "LanguageDatum";
      }
    } else if (doc.datumFields && doc.sessionFields) {
      guessedType = "Corpus";
    } else if (doc.collection === "sessions" && doc.sessionFields) {
      guessedType = "Session";
    } else if (doc.text && doc.username && doc.timestamp && doc.gravatar) {
      guessedType = "Comment";
    } else if (doc.symbol && doc.tipa !== undefined) {
      guessedType = "UnicodeSymbol";
    }
  }

  FieldDBObject.debug("Guessed type " + doc._id + " is a " + guessedType);
  return guessedType;
};

FieldDBObject.convertDocIntoItsType = function(doc, clone) {
  // this.debugMode = true;
  var guessedType,
    typeofAnotherObjectsProperty = Object.prototype.toString.call(doc);

  if (clone) {
    var cloneDoc;
    if (typeof doc.clone === "function") {
      // if (doc instanceof FieldDBObject || typeof doc.fuzzyFind === "function") {
      // wasnt able to make it what it should be, but it was at least some extension of FieldDBObject or Collection
      cloneDoc = doc.clone();
      doc = new doc.constructor(cloneDoc);
    } else
    // Return a clone of simple types, or a new clone of the json of this object
    if (typeofAnotherObjectsProperty === "[object Boolean]") {
      return !!doc;
    } else if (typeofAnotherObjectsProperty === "[object String]") {
      return doc + "";
    } else if (typeofAnotherObjectsProperty === "[object Number]") {
      return doc + 0;
    } else if (typeofAnotherObjectsProperty === "[object Date]") {
      return new Date(doc);
    } else if (typeofAnotherObjectsProperty === "[object Array]") {
      return doc.concat([]);
    } else {
      clone = doc.toJSON ? doc.toJSON() : doc;
      doc = new doc.constructor(clone);
    }
  } else {
    // Return the doc if its a simple type
    if (typeofAnotherObjectsProperty === "[object Boolean]") {
      return doc;
    } else if (typeofAnotherObjectsProperty === "[object String]") {
      return doc;
    } else if (typeofAnotherObjectsProperty === "[object Number]") {
      return doc;
    } else if (typeofAnotherObjectsProperty === "[object Date]") {
      return doc;
    } else if (typeofAnotherObjectsProperty === "[object Array]") {
      return doc;
    }
  }

  if (typeof doc.debug === "function" && doc.constructor !== FieldDBObject) {
    // if (doc instanceof FieldDBObject || typeof doc.fuzzyFind === "function") {
    // wasnt able to make it what it should be, but it was at least some extension of FieldDBObject or Collection
    return doc;
  }

  try {
    guessedType = doc.fieldDBtype;
    if (!guessedType || guessedType === "FieldDBObject") {
      FieldDBObject.debug(" requesting guess type ");
      guessedType = FieldDBObject.guessType(doc);
      FieldDBObject.debug("request complete");
    }
    FieldDBObject.debug("Converting doc into type " + guessedType);

    if (FieldDB && FieldDB[guessedType]) {
      if (doc instanceof FieldDB[guessedType]) {
        return doc;
      }
      doc = new FieldDB[guessedType](doc);
      // FieldDBObject.warn("Converting doc into guessed type " + guessedType);
    } else {
      doc = new FieldDBObject(doc);
      FieldDBObject.debug("This doc does not have a type than is known to the FieldDB system. It might display oddly ", doc);
    }
  } catch (e) {
    FieldDBObject.debug("Couldn't convert this doc to its type " + guessedType + ", it will be a base FieldDBObject: " + JSON.stringify(doc));
    FieldDBObject.debug(" error: ", e);
    var checkPreviousTypeWithoutS = doc.previousFieldDBtype ? doc.previousFieldDBtype.replace(/s$/, "") : "";
    if (guessedType !== "FieldDBObject" && guessedType !== checkPreviousTypeWithoutS) {
      doc.previousFieldDBtype = doc.previousFieldDBtype || "";
      doc.previousFieldDBtype = doc.previousFieldDBtype + guessedType;
    }
    doc = new FieldDBObject(doc);
  }
  return doc;
};

/** @lends FieldDBObject.prototype */
FieldDBObject.prototype = Object.create(Object.prototype, {
  constructor: {
    value: FieldDBObject
  },

  fieldDBtype: {
    configurable: true,
    get: function() {
      return this._fieldDBtype || "FieldDBObject";
    },
    set: function(value) {
      if (value !== this.fieldDBtype) {
        this.debug("Using type " + this.fieldDBtype + " when the incoming object was " + value);
      }
    }
  },

  /**
   * Can be set to true to debug all objects, or false to debug no objects and true only on the instances of objects which
   * you want to debug.
   *
   * @type {Boolean}
   */
  debugMode: {
    get: function() {
      if (this.perObjectDebugMode === undefined) {
        return false;
      } else {
        return this.perObjectDebugMode;
      }
    },
    set: function(value) {
      if (value === this.perObjectDebugMode) {
        return;
      }
      if (value === null || value === undefined) {
        delete this.perObjectDebugMode;
        return;
      }
      this.perObjectDebugMode = value;
    }
  },
  debug: {
    value: function( /* message, message2, message3, message4 */ ) {
      if (this.debugMode) {
        FieldDBObject.debug.apply(this, arguments);
      }
    }
  },
  verboseMode: {
    get: function() {
      if (this.perObjectVerboseMode === undefined) {
        return false;
      } else {
        return this.perObjectVerboseMode;
      }
    },
    set: function(value) {
      if (value === this.perObjectVerboseMode) {
        return;
      }
      if (value === null || value === undefined) {
        delete this.perObjectVerboseMode;
        return;
      }
      this.perObjectVerboseMode = value;
    }
  },
  verbose: {
    value: function( /* message, message2, message3, message4 */ ) {
      if (this.verboseMode) {
        FieldDBObject.verbose.apply(this, arguments);
      }
    }
  },
  bug: {
    value: function(message) {
      if (this.bugMessage) {
        if (this.bugMessage.indexOf(message) > -1) {
          this.warn("Not repeating bug message: " + message);
          return;
        }
        this.bugMessage += ";;; ";
      } else {
        this.bugMessage = "";
      }

      this.bugMessage = this.bugMessage + message;
      FieldDBObject.bug.apply(this, arguments);
    }
  },
  popup: {
    value: function(message) {
      if (this.popupMessage) {
        if (this.popupMessage.indexOf(message) > -1) {
          this.warn("Not repeating popup message: " + message);
          return;
        }
        this.popupMessage += ";;; ";
      } else {
        this.popupMessage = "";
      }

      this.popupMessage = this.popupMessage + message;
      FieldDBObject.popup.apply(this, arguments);
    }
  },
  alwaysConfirmOkay: {
    get: function() {
      if (this.perObjectAlwaysConfirmOkay === undefined) {
        return false;
      } else {
        return this.perObjectAlwaysConfirmOkay;
      }
    },
    set: function(value) {
      if (value === this.perObjectAlwaysConfirmOkay) {
        return;
      }
      if (value === null || value === undefined) {
        delete this.perObjectAlwaysConfirmOkay;
        return;
      }
      this.perObjectAlwaysConfirmOkay = value;
    }
  },
  prompt: {
    value: function(message) {
      if (this.promptMessage) {
        this.promptMessage += "\n";
      } else {
        this.promptMessage = "";
      }
      this.promptMessage = this.promptMessage + message;

      return FieldDBObject.prompt.apply(this, arguments);
    }
  },
  confirm: {
    value: function(message) {
      if (this.confirmMessage) {
        this.confirmMessage += "\n";
      } else {
        this.confirmMessage = "";
      }
      this.confirmMessage = this.confirmMessage + message;

      return FieldDBObject.confirm.apply(this, arguments);
    }
  },
  warn: {
    value: function(message) {
      if (this.warnMessage) {
        this.warnMessage += ";;; ";
      } else {
        this.warnMessage = "";
      }
      this.warnMessage = this.warnMessage + message;
      FieldDBObject.warn.apply(this, arguments);
    }
  },

  todo: {
    value: function( /* message, message2, message3, message4 */ ) {
      FieldDBObject.todo.apply(this, arguments);
    }
  },

  decryptedMode: {
    get: function() {
      if (this.application) {
        return this.application.decryptedMode;
      }
      // if not running in an app, dont need to demonstrate a mask the data if its decryptable
      return this._decryptedMode;
    },
    set: function(value) {
      if (this.application) {
        this.application.decryptedMode = value;
      } else {
        this._decryptedMode = value;
      }
    }
  },

  render: {
    configurable: true,
    writable: true,
    value: function(options) {
      this.debug("Calling render with options", options);
      FieldDBObject.render.apply(this, arguments);
    }
  },

  ensureSetViaAppropriateType: {
    value: function(propertyname, value, optionalInnerPropertyName) {
      this.debug("ensureSetViaAppropriateType on " + propertyname, this.INTERNAL_MODELS);
      if (!propertyname) {
        console.error("Invalid call to ensureSetViaAppropriateType", value);
        throw new Error("Invalid call to ensureSetViaAppropriateType");
      }

      optionalInnerPropertyName = optionalInnerPropertyName || "_" + propertyname;

      if (value === this[optionalInnerPropertyName]) {
        return this[optionalInnerPropertyName];
      }
      if (!value) {
        delete this[optionalInnerPropertyName];
        return;
      }

      if (this.INTERNAL_MODELS &&
        this.INTERNAL_MODELS[propertyname] &&
        typeof this.INTERNAL_MODELS[propertyname] === "function" &&
        !(value instanceof this.INTERNAL_MODELS[propertyname]) &&
        !(this.INTERNAL_MODELS[propertyname].compatibleWithSimpleStrings && typeof value === "string")) {

        this.debug("Converting this into  type for " + propertyname, value.constructor.toString());
        value = new this.INTERNAL_MODELS[propertyname](value);

      }

      // This trims all strings in the system...
      if (typeof value.trim === "function") {
        value = value.trim();
      }

      this[optionalInnerPropertyName] = value;
      return this[optionalInnerPropertyName];
    }
  },

  unsaved: {
    get: function() {
      return this._unsaved;
    },
    set: function(value) {
      this._unsaved = !!value;
    }
  },

  calculateUnsaved: {
    value: function() {
      if (!this.fossil) {
        this._unsaved = true;
        return;
      }

      var previous = new this.constructor(this.fossil);
      var current = new this.constructor(this.toJSON());

      current.debugMode = this.debugMode;
      if (previous.equals(current)) {
        this.warn("The " + this.id + " didnt actually change. Not marking as edited");
        this._unsaved = false;
      } else {
        this._unsaved = true;
      }
      return this._unsaved;
    }
  },

  createSaveSnapshot: {
    value: function(selfOrSnapshot, optionalUserWhoSaved) {
      var self = this;

      selfOrSnapshot = this;

      this.debug("    Running snapshot...");
      //update to selfOrSnapshot version
      selfOrSnapshot.version = FieldDBObject.DEFAULT_VERSION;

      try {
        FieldDBObject.software = FieldDBObject.software || {};
        FieldDBObject.software.appCodeName = navigator.appCodeName;
        FieldDBObject.software.appName = navigator.appName;
        FieldDBObject.software.appVersion = navigator.appVersion;
        FieldDBObject.software.cookieEnabled = navigator.cookieEnabled;
        FieldDBObject.software.doNotTrack = navigator.doNotTrack;
        FieldDBObject.software.hardwareConcurrency = navigator.hardwareConcurrency;
        FieldDBObject.software.language = navigator.language;
        FieldDBObject.software.languages = navigator.languages;
        FieldDBObject.software.maxTouchPoints = navigator.maxTouchPoints;
        FieldDBObject.software.onLine = navigator.onLine;
        FieldDBObject.software.platform = navigator.platform;
        FieldDBObject.software.product = navigator.product;
        FieldDBObject.software.productSub = navigator.productSub;
        FieldDBObject.software.userAgent = navigator.userAgent;
        FieldDBObject.software.vendor = navigator.vendor;
        FieldDBObject.software.vendorSub = navigator.vendorSub;
        if (navigator && navigator.geolocation && typeof navigator.geolocation.getCurrentPosition === "function") {
          navigator.geolocation.getCurrentPosition(function(position) {
            self.debug("recieved position information");
            FieldDBObject.software.location = position.coords;
          });
        }
      } catch (e) {
        this.debug("Error loading software ", e);
        FieldDBObject.software = FieldDBObject.software || {};
        FieldDBObject.software.version = process.version;
        FieldDBObject.software.appVersion = "PhantomJS unknown";

        try {
          var avoidmontagerequire = require;
          var os = avoidmontagerequire("os");
          FieldDBObject.hardware = FieldDBObject.hardware || {};
          FieldDBObject.hardware.endianness = os.endianness();
          FieldDBObject.hardware.platform = os.platform();
          FieldDBObject.hardware.hostname = os.hostname();
          FieldDBObject.hardware.type = os.type();
          FieldDBObject.hardware.arch = os.arch();
          FieldDBObject.hardware.release = os.release();
          FieldDBObject.hardware.totalmem = os.totalmem();
          FieldDBObject.hardware.cpus = os.cpus().length;
        } catch (e) {
          this.debug(" hardware is unknown.", e);
          FieldDBObject.hardware = FieldDBObject.hardware || {};
          FieldDBObject.software.appVersion = "Device unknown";
        }
      }
      if (!optionalUserWhoSaved) {
        optionalUserWhoSaved = {
          name: "",
          username: "unknown"
        };
        try {
          if (this.corpus && this.corpus.connectionInfo && this.corpus.connectionInfo.userCtx) {
            optionalUserWhoSaved.username = this.corpus.connectionInfo.userCtx.name;
          } else if (FieldDBObject.application && FieldDBObject.application.user && FieldDBObject.application.user.username) {
            optionalUserWhoSaved.username = optionalUserWhoSaved.username || FieldDBObject.application.user.username;
            optionalUserWhoSaved.gravatar = optionalUserWhoSaved.gravatar || FieldDBObject.application.user.gravatar;
          }
        } catch (e) {
          this.warn("Can't get the corpus connection info to guess who saved this.", e);
        }
      }
      // optionalUserWhoSaved._name = optionalUserWhoSaved.name || optionalUserWhoSaved.username || optionalUserWhoSaved.browserVersion;
      if (typeof optionalUserWhoSaved.toJSON === "function") {
        var asJson = optionalUserWhoSaved.toJSON();
        asJson.name = optionalUserWhoSaved.name;
        optionalUserWhoSaved = asJson;
      } else {
        optionalUserWhoSaved.name = optionalUserWhoSaved.name;
      }
      // optionalUserWhoSaved.browser = browser;

      this.debug("    Calculating userWhoSaved...");

      var userWhoSaved = {
        username: optionalUserWhoSaved.username,
        name: optionalUserWhoSaved.name,
        lastname: optionalUserWhoSaved.lastname,
        firstname: optionalUserWhoSaved.firstname,
        gravatar: optionalUserWhoSaved.gravatar
      };

      if (!selfOrSnapshot._rev) {
        selfOrSnapshot._dateCreated = Date.now();
        var enteredByUser = selfOrSnapshot.enteredByUser || {};
        if (selfOrSnapshot.fields && selfOrSnapshot.fields.enteredbyuser) {
          enteredByUser = selfOrSnapshot.fields.enteredbyuser;
        } else if (!selfOrSnapshot.enteredByUser) {
          selfOrSnapshot.enteredByUser = enteredByUser;
        }
        enteredByUser.value = userWhoSaved.name || userWhoSaved.username;
        enteredByUser.json = enteredByUser.json || {};
        enteredByUser.json.user = userWhoSaved;
        enteredByUser.json.software = FieldDBObject.software;
        try {
          enteredByUser.json.hardware = Android ? Android.deviceDetails : FieldDBObject.hardware;
        } catch (e) {
          this.debug("Cannot detect the hardware used for selfOrSnapshot save.", e);
          enteredByUser.json.hardware = FieldDBObject.hardware;
        }

      } else {
        selfOrSnapshot._dateModified = Date.now();

        var modifiedByUser = selfOrSnapshot.modifiedByUser || {};
        if (selfOrSnapshot.fields && selfOrSnapshot.fields.modifiedbyuser) {
          modifiedByUser = selfOrSnapshot.fields.modifiedbyuser;
        } else if (!selfOrSnapshot.modifiedByUser) {
          selfOrSnapshot.modifiedByUser = modifiedByUser;
        }
        if (selfOrSnapshot.modifiedByUsers) {
          modifiedByUser = {
            json: {
              users: selfOrSnapshot.modifiedByUsers
            }
          };
          delete selfOrSnapshot.modifiedByUsers;
        }

        modifiedByUser.value = modifiedByUser.value ? modifiedByUser.value + ", " : "";
        modifiedByUser.value += userWhoSaved.name || userWhoSaved.username;
        modifiedByUser.json = modifiedByUser.json || {};
        if (modifiedByUser.users) {
          modifiedByUser.json.users = modifiedByUser.users;
          delete modifiedByUser.users;
        }
        modifiedByUser.json.users = modifiedByUser.json.users || [];
        userWhoSaved.software = FieldDBObject.software;
        userWhoSaved.hardware = FieldDBObject.hardware;
        modifiedByUser.json.users.push(userWhoSaved);
      }

      if (FieldDBObject.software && FieldDBObject.software.location) {
        var location;
        if (selfOrSnapshot.location) {
          location = selfOrSnapshot.location;
        } else if (selfOrSnapshot.fields && selfOrSnapshot.fields.location) {
          location = selfOrSnapshot.fields.location;
        }
        if (location) {
          location.json = location.json || {};
          location.json.previousLocations = location.json.previousLocations || [];
          if (location.json && location.json.location && location.json.location.latitude) {
            location.json.previousLocations.push(location.json.location);
          }
          this.debug("overwriting location ", location);
          location.json.location = FieldDBObject.software.location;
          location.value = location.json.location.latitude + "," + location.json.location.longitude;
        }
      }

      this.debug("    Serializing to send object to selfOrSnapshotbase...");
      // this.debug("snapshot   ", selfOrSnapshot);

      return selfOrSnapshot.toJSON();

      // return selfOrSnapshot.toJSON ? selfOrSnapshot.toJSON() : selfOrSnapshot;
    }
  },

  save: {
    value: function(optionalUserWhoSaved, saveEvenIfSeemsUnchanged, optionalUrl) {
      var deferred = Q.defer(),
        self = this;

      if (this.fetching) {
        self.warn("Fetching is in process, can't save right now...");
        Q.nextTick(function() {
          deferred.reject("Fetching is in process, can't save right now...");
        });
        return deferred.promise;
      }
      if (this.saving) {
        self.warn("Save was already in process...");
        Q.nextTick(function() {
          deferred.reject("Fetching is in process, can't save right now...");
        });
        return deferred.promise;
      }

      if (saveEvenIfSeemsUnchanged) {
        this.debug("Not calculating if this object has changed, assuming it needs to be saved anyway.");
      } else {
        self.debug("    Checking to see if item needs to be saved.", saveEvenIfSeemsUnchanged, this.unsaved);

        if (!this.unsaved && !this.calculateUnsaved()) {
          self.warn("Item hasn't really changed, no need to save...");
          Q.nextTick(function() {
            deferred.resolve(self);
            return self;
          });
          return deferred.promise;
        }
      }
      if (!this.corpus || typeof this.corpus.set !== "function") {
        self.warn("The corpus for this doc isnt acessible, this is probably a bug.", this);
        Q.nextTick(function() {
          self.saving = false;
          deferred.reject({
            status: 406,
            userFriendlyErrors: ["This application has errored. Please notify its developers: Cannot save data, database is not currently opened."]
          });
        });
        return deferred.promise;
      }

      if (this.corpus.dbname !== this.dbname) {
        this.warn("This item belongs in the " + this.dbname + "database, not in the " + this.corpus.dbname + " database.");
        Q.nextTick(function() {
          self.saving = false;
          deferred.reject({
            status: 406,
            userFriendlyErrors: ["This item belongs in the " + self.dbname + "database, not in the " + self.corpus.dbname + " database."]
          });
        });
        return deferred.promise;
      }
      if (optionalUrl) {
        this.todo("Url for save was specified, but it is not being used. optionalUrl", optionalUrl);
      }

      var data = this.createSaveSnapshot();

      if (!data) {
        this.warn(" Can't save " + this.id + " right now. The JSON isnt ready.");
        Q.nextTick(function() {
          self.saving = false;
          deferred.reject({
            status: 406,
            userFriendlyErrors: ["Can't save " + this.id + " right now. Please wait."]
          });
        });
        return deferred.promise;
      }

      self.debug("    Requesting corpus to run save...");
      this.saving = true;
      this.whenReady = deferred.promise;

      // if (!confirm("save this?")) {
      //   this.warn("Pretending we saved, so we can see if load production models works, without affecting them ");
      //   Q.nextTick(function() {
      //     self.saving = false;
      //     self.rev = Date.now() + "notacutallysaved";
      //     deferred.resolve(self);
      //   });
      //   return deferred.promise;
      // }

      this.corpus.set(data).then(function(result) {
          self.saving = false;
          self.debug("    Save completed...");
          self.debug("saved ", result);
          if (!result) {
            deferred.reject({
              status: 400,
              userFriendlyErrors: ["This application has errored. Please notify its developers: Cannot save data."]
            });
            return "self";
          }

          if (!result.id) {
            self.debug("    Rejecting promise, the id was not set by the database ..");
            deferred.reject({
              status: 500,
              userFriendlyErrors: ["This application has errored. Please notify its developers: Save operation returned abnormal results."],
              details: result
            });
            return self;
          }

          self.unsaved = false;

          self.id = result.id;
          self.rev = result.rev;

          self.debug("    Updating fossil...");
          self.fossil = self.toJSON();

          self.debug("    Resolving promise...", self);
          deferred.resolve(self);
          return self;
        },
        function(reason) {
          self.debug(reason);
          self.saving = false;
          deferred.reject(reason);
          return self;
        }).fail(
        function(error) {
          console.error(error.stack, self);
          deferred.reject(error);
        });

      return deferred.promise;
    }
  },

  delete: {
    value: function(reason) {
      return this.trash(reason);
    }
  },

  trash: {
    value: function(reason) {
      this.trashed = "deleted";
      if (reason) {
        this.trashedReason = reason;
      } else {
        this.todo("consider using a confirm to ask for a reason for deleting the item");
      }

      return this.save(null, "forcesavesinceweneedtopersistthischange");
    }
  },

  undelete: {
    value: function(reason) {
      this.trashed = "restored";
      if (reason) {
        this.untrashedReason = reason;
      } else {
        this.todo("consider using a confirm to ask for a reason for undeleting the item");
      }
      return this.save(null, "forcesavesinceweneedtopersistthischange");
    }
  },

  saveToGit: {
    value: function(commit, saveEvenIfSeemsUnchanged) {
      var deferred = Q.defer(),
        self = this;
      Q.nextTick(function() {
        self.todo("If in nodejs, write to file and do a git commit with optional user's email who modified the file and push ot a branch with that user's username", saveEvenIfSeemsUnchanged);
        self.debug("Commit to be used: ", commit);
        deferred.resolve(self);
      });
      return deferred.promise;
    }
  },

  equals: {
    value: function(anotherObject) {
      for (var aproperty in this) {
        if (!this.hasOwnProperty(aproperty) || typeof this[aproperty] === "function" || FieldDBObject.ignore(aproperty, FieldDBObject.internalAttributesToAutoMerge)) {
          this.debug("skipping equality of " + aproperty);
          continue;
        }
        if (!anotherObject) {
          return false;
        }

        if /* use fielddb equality function first */ (this[aproperty] && typeof this[aproperty].equals === "function") {
          if (!this[aproperty].equals(anotherObject[aproperty])) {
            this.debug("  " + aproperty + ": ", this[aproperty], " not equalivalent to ", anotherObject[aproperty]);
            if (this.debugMode) {
              console.error("objects are not equal stactrace");
            }
            return false;
          }
        } /* then try normal equality */
        else if (this[aproperty] === anotherObject[aproperty]) {
          this.debug(aproperty + ": " + this[aproperty] + " equals " + anotherObject[aproperty]);
          // return true;
        } /* then try stringification */
        else if (JSON.stringify(this[aproperty]) === JSON.stringify(anotherObject[aproperty])) {
          this.debug(aproperty + ": " + this[aproperty] + " equals " + anotherObject[aproperty]);
          // return true;
        } else if (anotherObject[aproperty] === undefined && (aproperty !== "_dateCreated" && aproperty !== "perObjectDebugMode")) {
          this.debug(aproperty + " is missing " + this[aproperty] + " on anotherObject " + anotherObject[aproperty]);
          if (this.debugMode) {
            console.error("objects are not equal stactrace");
          }
          return false;
        } else {
          if (aproperty !== "_dateCreated" && aproperty !== "perObjectDebugMode") {
            this.debug(aproperty + ": ", this[aproperty], " not equal ", anotherObject[aproperty]);
            if (this.debugMode) {
              console.error("objects are not equal stactrace");
            }
            return false;
          }
        }
      }
      if (typeof anotherObject.equals === "function") {
        if (this.dontRecurse === undefined) {
          this.dontRecurse = true;
          anotherObject.dontRecurse = true;
          if (!anotherObject.equals(this)) {
            if (this.debugMode) {
              console.error("objects are not equal stactrace");
            }
            return false;
          }
        }
      }
      delete this.dontRecurse;
      delete anotherObject.dontRecurse;
      return true;
    }
  },

  merge: {
    value: function(callOnSelf, anotherObject, optionalOverwriteOrAsk) {
      var anObject,
        resultObject,
        aproperty,
        targetPropertyIsEmpty,
        overwrite,
        localCallOnSelf,
        propertyList = {},
        json;

      // this.debugMode = true;

      if (arguments.length === 0) {
        this.warn("Invalid call to merge, there was no object provided to merge");
        return null;
      }

      if (!anotherObject && !optionalOverwriteOrAsk) {
        resultObject = anObject = this;
        anotherObject = FieldDBObject.convertDocIntoItsType(callOnSelf);
      } else if (callOnSelf === "self") {
        this.debug("Merging properties into myself. ");
        anObject = this;
        resultObject = anObject;
      } else if (callOnSelf && anotherObject) {
        anObject = FieldDBObject.convertDocIntoItsType(callOnSelf);
        anotherObject = FieldDBObject.convertDocIntoItsType(anotherObject);
        resultObject = this;
      } else {
        this.warn("Invalid call to merge, invalid arguments were provided to merge", arguments);
        return null;
      }

      if (!optionalOverwriteOrAsk) {
        optionalOverwriteOrAsk = "";
      }

      if (anObject.id && anotherObject.id && anObject.id !== anotherObject.id) {
        this.warn("Refusing to merge these objects, they have different ids: " + anObject.id + "  and " + anotherObject.id);
        this.debug("Refusing to merge" + anObject.id + "  and " + anotherObject.id, anObject, anotherObject);
        return null;
      }
      if (anObject.dbname && anotherObject.dbname && anObject.dbname !== anotherObject.dbname) {
        if (optionalOverwriteOrAsk.indexOf("keepDBname") > -1) {
          this.warn("Permitting a merge of objects from different databases: " + anObject.dbname + "  and " + anotherObject.dbname);
          this.debug("Merging ", anObject, anotherObject);
        } else if (optionalOverwriteOrAsk.indexOf("changeDBname") === -1) {
          this.warn("Refusing to merge these objects, they come from different databases: " + anObject.dbname + "  and " + anotherObject.dbname);
          this.debug("Refusing to merge" + anObject.dbname + "  and " + anotherObject.dbname, anObject, anotherObject);
          return null;
        }
      }

      for (aproperty in anObject) {
        if (anObject.hasOwnProperty(aproperty) &&
          typeof anObject[aproperty] !== "function" &&
          !FieldDBObject.ignore(aproperty, FieldDBObject.internalAttributesToNotJSONify)) {
          propertyList[aproperty] = true;
        } else {
          this.debug("Not merging " + aproperty, "parent ", anObject.parent ? anObject.parent.length : "");
        }
      }

      for (aproperty in anotherObject) {
        if (anotherObject.hasOwnProperty(aproperty) &&
          typeof anotherObject[aproperty] !== "function" &&
          !FieldDBObject.ignore(aproperty, FieldDBObject.internalAttributesToNotJSONify)) {
          propertyList[aproperty] = true;
        } else {
          this.debug("Not merging " + aproperty, "parent ", anObject.parent ? anObject.parent.length : "");
        }
      }

      this.debug(" Merging properties: " + this.id, propertyList);

      var handleAsyncConfirmMerge = function(self, apropertylocal) {
        var deferred = Q.defer();
        var promptInputText = anotherObject[apropertylocal];
        if (typeof promptInputText === "object") {
          promptInputText = JSON.stringify(anotherObject[apropertylocal]);
        }
        var context = self.id || self._id || "";
        self.prompt(context + " I found a conflict for " + apropertylocal + ", Do you want to overwrite it from " + JSON.stringify(anObject[apropertylocal]) + " -> " + promptInputText, null, promptInputText)
          .then(function(reply) {
            // Let the user enter the value they would like

            if (apropertylocal === "_dbname" && optionalOverwriteOrAsk.indexOf("keepDBname") > -1) {
              // resultObject._dbname = self.dbname;
              self.warn(" Keeping _dbname of " + resultObject.dbname);
            } else {
              self.debug("Async Overwriting contents of " + apropertylocal + " (this may cause disconnection in listeners)");
              self.debug("Async Overwriting  ", anObject[apropertylocal], " ->", reply.response);

              resultObject[apropertylocal] = reply.response;
            }
            deferred.resolve(reply);
          }, function(reason) {
            resultObject[apropertylocal] = anObject[apropertylocal];
            deferred.reject(reason);
          }).fail(function(error) {
            console.error(error.stack, self);
            deferred.reject(error);
          });

        self.confirmMergePromises = self.confirmMergePromises || [];
        self.confirmMergePromises.push(deferred.promise);
      };

      for (aproperty in propertyList) {

        if (typeof anObject[aproperty] === "function" || typeof anotherObject[aproperty] === "function") {
          this.debug("  Ignoring ---" + aproperty + "----");
          continue;
        }
        // if (this.debugMode) {
        this.debug("  Merging ---" + aproperty + "--- \n   :::" + JSON.stringify(resultObject[aproperty]) + ":::\n   :::" + JSON.stringify(anObject[aproperty]) + ":::\n   :::" + JSON.stringify(anotherObject[aproperty]) + ":::");
        // }

        // if the result is missing the property, clone it from anObject or anotherObject
        if (resultObject[aproperty] === undefined || resultObject[aproperty] === null) {
          if (anObject[aproperty] !== undefined && anObject[aproperty] !== null) {
            if (typeof anObject[aproperty] !== "string" && typeof anObject[aproperty].constructor === "function") {
              json = anObject[aproperty].toJSON ? anObject[aproperty].toJSON() : anObject[aproperty];
              resultObject[aproperty] = new anObject[aproperty].constructor(json);
              this.debug(" " + aproperty + " resultObject will have anObject's Cloned contents because it was empty");
            } else {
              /* jshint eqeqeq:false */
              if (resultObject[aproperty] != anObject[aproperty]) {
                resultObject[aproperty] = anObject[aproperty];
              }
              this.debug(" " + aproperty + " resultObject will have anObject's contents because it was empty");
            }
          } else if (anotherObject[aproperty] !== undefined && anotherObject[aproperty] !== null) {
            this.debug("Using a constructor");
            this.todo(" use clone only if not merging self.");
            if (callOnSelf === "self") {
              resultObject[aproperty] = FieldDBObject.convertDocIntoItsType(anotherObject[aproperty]);
            } else {
              resultObject[aproperty] = FieldDBObject.convertDocIntoItsType(anotherObject[aproperty], "clone");
            }
          }
          // dont continue, instead let the iffs run
        }
        if (this.debugMode) {
          this.debug("  Merging ---" + aproperty + "--- \n   :::" + JSON.stringify(resultObject[aproperty]) + ":::\n   :::" + JSON.stringify(anObject[aproperty]) + ":::\n   :::" + JSON.stringify(anotherObject[aproperty]) + ":::");
        }

        /* jshint eqeqeq:false */
        if (anObject[aproperty] == anotherObject[aproperty]) {
          this.debug(aproperty + " were equal or had no conflict.");
          if (resultObject[aproperty] != anObject[aproperty]) {
            resultObject[aproperty] = anObject[aproperty];
          }
          continue;
        }

        // Don't bother with equivalentcy, we will merge the internal elements recursively if they are not equivalent so this doesn't save time.
        // if (anObject[aproperty] && typeof anObject[aproperty].equals === "function" && anObject[aproperty].equals(anotherObject[aproperty])) {
        //   this.debug(aproperty + " were equivalent or had no conflict.");
        //   if (!anObject[aproperty].equals(resultObject[aproperty])) {
        //     if(resultObject[aproperty] != anObject[aproperty]){resultObject[aproperty] = anObject[aproperty];}
        //   }
        //   continue;
        // }

        if ((anotherObject[aproperty] === undefined || anotherObject[aproperty] === null) && resultObject[aproperty] != anObject[aproperty]) {
          this.debug(aproperty + " was missing in new object, using the original");
          if (resultObject[aproperty] != anObject[aproperty]) {
            resultObject[aproperty] = anObject[aproperty];
          }
          continue;
        }

        if (anotherObject[aproperty] && (anObject[aproperty] === undefined || anObject[aproperty] === null || anObject[aproperty] === [] || anObject[aproperty].length === 0 || anObject[aproperty] === {})) {
          targetPropertyIsEmpty = true;
          this.debug(aproperty + " was previously empty, taking the new value");
          resultObject[aproperty] = anotherObject[aproperty];
          continue;
        }

        if ((anObject[aproperty] !== undefined || anObject[aproperty] !== null) && (anotherObject[aproperty] === undefined || anotherObject[aproperty] === null || anotherObject[aproperty] === [] || anotherObject[aproperty].length === 0 || anotherObject[aproperty] === {})) {
          targetPropertyIsEmpty = true;
          this.debug(aproperty + " target is empty, taking the old value");
          if (resultObject[aproperty] != anObject[aproperty]) {
            resultObject[aproperty] = anObject[aproperty];
          }
          continue;
        }

        //  if two arrays: concat
        if (Object.prototype.toString.call(anObject[aproperty]) === "[object Array]" && Object.prototype.toString.call(anotherObject[aproperty]) === "[object Array]") {
          this.debug(aproperty + " was an array, concatinating with the new value", anObject[aproperty], " ->", anotherObject[aproperty]);
          resultObject[aproperty] = anObject[aproperty].concat([]);

          // only add the ones that were missing (dont remove any. merge wont remove stuff, only add.)
          try {
            /* jshint loopfunc:true */
            anotherObject[aproperty].map(function(item) {
              if (resultObject[aproperty].indexOf(item) === -1) {
                resultObject[aproperty].push(item);
              }
            });
          } catch (e) {
            console.warn("problem merging this array " + aproperty, e);
          }
          this.debug("  added members of anotherObject " + aproperty + " to anObject ", resultObject[aproperty]);
          continue;
        }

        // if two objects with merge function: recursively merge
        if (resultObject[aproperty] && typeof resultObject[aproperty].merge === "function") {
          if (callOnSelf === "self") {
            localCallOnSelf = callOnSelf;
          } else {
            localCallOnSelf = anObject[aproperty];
          }
          this.debug("Requesting recursive merge of internal property " + aproperty + " using method: " + localCallOnSelf);
          try {
            var result = resultObject[aproperty].merge(localCallOnSelf, anotherObject[aproperty], optionalOverwriteOrAsk);
            this.debug("after internal merge ", result);
            this.debug("after internal merge ", resultObject[aproperty]);
          } catch (e) {
            console.warn("problem merging this " + aproperty, e.stack);
          }
          continue;
        }

        overwrite = optionalOverwriteOrAsk;
        this.debug("Found conflict for " + aproperty + " Requested with " + optionalOverwriteOrAsk + " " + optionalOverwriteOrAsk.indexOf("overwrite"));
        if (optionalOverwriteOrAsk.indexOf("overwrite") === -1 && !FieldDBObject.ignore(aproperty, FieldDBObject.internalAttributesToAutoMerge)) {
          handleAsyncConfirmMerge(this, aproperty);
        }
        if (overwrite || FieldDBObject.ignore(aproperty, FieldDBObject.internalAttributesToAutoMerge)) {
          if (aproperty === "_dbname" && optionalOverwriteOrAsk.indexOf("keepDBname") > -1) {
            // resultObject._dbname = this.dbname;
            this.warn(" Keeping _dbname of " + resultObject.dbname);
          } else {
            if (!FieldDBObject.ignore(aproperty, FieldDBObject.internalAttributesToAutoMerge)) {
              this.debug("Overwriting contents of " + aproperty + " (this may cause disconnection in listeners)");
            }
            this.debug("Overwriting  ", anObject[aproperty], " ->", anotherObject[aproperty]);

            resultObject[aproperty] = anotherObject[aproperty];
          }
        } else {
          if (resultObject[aproperty] != anObject[aproperty]) {
            resultObject[aproperty] = anObject[aproperty];
          }
        }
      }

      return resultObject;
    }
  },

  fetchRevisions: {
    value: function(optionalUrl) {
      var deferred = Q.defer(),
        self = this;

      if (!this._id) {
        Q.nextTick(function() {
          self.warn("This hasn't been saved before, so there are no previous revisisons to show you.");
          deferred.resolve(self._revisions);
        });
        return deferred.promise;
      }

      if (!this.corpus || typeof this.corpus.fetchRevisions !== "function") {
        Q.nextTick(function() {
          deferred.reject({
            status: 406,
            userFriendlyErrors: ["This application has errored. Please notify its developers: Cannot fetch data if the database is not currently opened."]
          });
        });
        return deferred.promise;
      }

      self.corpus.fetchRevisions(this.id, optionalUrl).then(function(revisions) {
        if (!self._revisions) {
          self._revisions = revisions;
        } else {
          revisions.map(function(revision) {
            if (self._revisions.indexOf(revision) === -1) {
              self._revisions.push(revision);
            }
          });
        }
        self.debug("This " + self.id + " has " + self._revisions.length + " previous revisions");
        deferred.resolve(self._revisions);
      }, function(reason) {
        self.warn("Unable to update list of revisions currently.");
        self.debug(reason);
        deferred.reject(reason);
        return self;
      }).fail(function(error) {
        console.error(error.stack, self);
        deferred.reject(error);
      });

      return deferred.promise;
    }
  },

  fetch: {
    value: function(optionalUrl) {
      var deferred = Q.defer(),
        self = this;

      if (this.fetching && this.whenReady) {
        self.warn("Fetching is in process, don't need to fetch right now...");
        return this.whenReady;
      }

      if (!this.corpus || typeof this.corpus.get !== "function" || !this._id) {
        Q.nextTick(function() {
          self.fetching = self.loading = false;
          deferred.reject({
            status: 406,
            userFriendlyErrors: ["This application has errored. Please notify its developers: Cannot fetch data which has no id, or the if database is not currently opened."]
          });
        });
        return deferred.promise;
      }

      this.todo("Should probably call save before fetch to create a snapshot of the item regardless of whether the save is completeable, ");
      var oldRev = this.rev;

      this.fetching = this.loading = true;
      this.whenReady = deferred.promise;
      self.corpus.get(self.id, optionalUrl).then(function(result) {
        self.fetching = self.loading = false;
        if (!result) {
          deferred.reject({
            status: 400,
            userFriendlyErrors: ["This application has errored. Please notify its developers: Cannot fetch data url."]
          });
          return self;
        }
        self.loaded = true;

        // If this had no revision number before, and it does now, then this is a good fossil point
        if (!oldRev && (result._rev || result.rev)) {
          self.warn(self.id + " was probabbly a placeholder (didnt have .rev before fetch, but now it does) which is now filled in, calling merge with overwrite from server.");

          self.merge("self", FieldDBObject.convertDocIntoItsType(result), "overwrite");
          // Setting the fossil causes
          // A: the merge to count as something which needs to be seaved
          // B: the user's previous changes prior to fetch might get lost
          self.fossil = self.toJSON();
          self.unsaved = false;
        } else {
          self.warn("Cant tell if this was a placeholder, calling merge and asking user if there are merge conflicts.", result);
          self.merge("self", result);
          self.debug("After merge ", self.modifiedByUser);
        }

        deferred.resolve(self);
        return self;
      }, function(reason) {
        self.fetching = self.loading = false;
        self.loaded = false;
        self.debug(reason);
        deferred.reject(reason);
        return self;
      }).fail(function(error) {
        console.error(error.stack, self);
        deferred.reject(error);
      });

      return deferred.promise;
    }
  },

  INTERNAL_MODELS: {
    value: {
      _id: FieldDBObject.DEFAULT_STRING,
      _rev: FieldDBObject.DEFAULT_STRING,
      dbname: FieldDBObject.DEFAULT_STRING,
      version: FieldDBObject.DEFAULT_STRING,
      dateCreated: FieldDBObject.DEFAULT_DATE,
      dateModified: FieldDBObject.DEFAULT_DATE,
      comments: FieldDBObject.DEFAULT_COLLECTION
    }
  },

  application: {
    get: function() {
      return FieldDBObject.application;
    },
    set: function() {}
  },

  corpus: {
    get: function() {
      var db = null;
      // this.debugMode = true;
      if (this.resumeAuthenticationSession && typeof this.resumeAuthenticationSession === "function") {
        db = this;
        this.debug("this " + this._id + " has the functions of a corpus, using it.", db);
      } else if (this._corpus) {
        db = this._corpus;
        this.debug("this " + this._id + " has a _corpus hard coded inside it, using it.", db);
      } else if (FieldDBObject.application && FieldDBObject.application._corpus) {
        db = FieldDBObject.application._corpus;
        this.debug("this " + this._id + " is running in the context where FieldDBObject.application._corpus is defined, using it.", db);
      } else {

        try {
          if (FieldDB && FieldDB["Database"]) {
            // db = FieldDB["Database"].prototype;
            this.debug("  using the Database.prototype to run db calls for " + this._id + ", this could be problematic " + this._id + " .");
            this.debug(" the database", db);
          }
        } catch (e) {
          var message = e ? e.message : " unknown error in getting the corpus";
          if (message !== "FieldDB is not defined") {
            this.warn(this._id + "Cant get the corpus, cant find the Database class.", e);
            if (e) {
              this.warn("  stack trace" + e.stack);
            }
          }
        }
      }
      if (!db) {
        this.warn("Operations that need a corpus/database wont work for the " + this._id + " object");
      }

      return db;
    },
    set: function(value) {
      if (value && value.dbname && this.dbname && value.dbname !== this.dbname) {
        this.warn("The corpus " + value.db + " cant be set on this item, its db is different" + this.dbname);
        return;
      }
      this.debug("setting corpus ", value);
      this._corpus = value;
    }
  },

  id: {
    get: function() {
      return this._id || FieldDBObject.DEFAULT_STRING;
    },
    set: function(value) {
      if (value === this._id) {
        return;
      }
      if (!value) {
        delete this._id;
        return;
      }
      if (value.trim) {
        value = value.trim();
      }
      // var originalValue = value + "";
      // value = this.sanitizeStringForPrimaryKey(value); /*TODO dont do this on all objects */
      // if (value === null) {
      //   this.bug("Invalid id, not using " + originalValue + " id remains as " + this._id);
      //   return;
      // }
      this._id = value;
    }
  },

  rev: {
    get: function() {
      return this._rev || FieldDBObject.DEFAULT_STRING;
    },
    set: function(value) {
      if (value === this._rev) {
        return;
      }
      if (!value) {
        delete this._rev;
        return;
      }
      if (value.trim) {
        value = value.trim();
      }
      this._rev = value;
    }
  },

  dbname: {
    get: function() {
      return this._dbname || FieldDBObject.DEFAULT_STRING;
    },
    set: function(value) {
      if (value === this._dbname) {
        return;
      }
      if (this._dbname && this._dbname !== "default" && this.rev) {
        throw new Error("This is the " + this._dbname + ". You cannot change the dbname of an object in this corpus to " + value + ", you must create a clone of the object first.");
      }
      if (!value) {
        delete this._dbname;
        return;
      }
      if (value.trim) {
        value = value.trim();
      }
      this._dbname = value;
    }
  },

  pouchname: {
    get: function() {
      this.debug("Pouchname is deprecated, use dbname instead.");
      return this.dbname;
    },
    set: function(value) {
      this.debug("Pouchname is deprecated, please use dbname instead.");
      this.dbname = value;
    }
  },

  couchConnection: {
    get: function() {
      console.error("CouchConnection is deprecated, use connection instead " + this.id);
      return this.connection;
    },
    set: function(value) {
      // console.error("CouchConnection is deprecated, use connection instead");
      this.connection = value;
    }
  },

  version: {
    get: function() {
      return this._version || FieldDBObject.DEFAULT_VERSION;
    },
    set: function(value) {
      if (value === this._version) {
        return;
      }
      if (!value) {
        value = FieldDBObject.DEFAULT_VERSION;
      }
      if (value.trim) {
        value = value.trim();
      }
      this._version = value;
    }
  },

  timestamp: {
    get: function() {
      return this._timestamp || this._dateCreated;
    },
    set: function(value) {
      if (value === this._timestamp) {
        return;
      }
      if (!value) {
        // delete this._timestamp;
        return;
      }
      if (value.replace) {
        try {
          value = value.replace(/["\\]/g, "");
          value = new Date(value);
          value = value.getTime();
        } catch (e) {
          this.warn("Upgraded timestamp" + value);
        }
      }
      this._timestamp = value;

      // Use timestamp as date created if there was none, or the timestamp is older.
      if (!this._dateCreated || this._dateCreated > value) {
        this._dateCreated = value;
      }
    }
  },

  dateCreated: {
    get: function() {
      if (this.created_at) {
        this.dateCreated = this.created_at;
      }
      return this._dateCreated || FieldDBObject.DEFAULT_DATE;
    },
    set: function(value) {
      if (value === this._dateCreated) {
        return;
      }
      if (!value) {
        // delete this._dateCreated;
        return;
      }
      if (value.replace) {
        try {
          value = value.replace(/["\\]/g, "");
          value = new Date(value);
          value = value.getTime();
        } catch (e) {
          this.warn("Upgraded dateCreated" + value);
        }
      }
      this._dateCreated = value;
    }
  },

  dateModified: {
    get: function() {
      if (!this._dateModified && this.updated_at) {
        this.dateModified = this.updated_at;
      }
      return this._dateModified || FieldDBObject.DEFAULT_DATE;
    },
    set: function(value) {
      if (value === this._dateModified) {
        return;
      }
      if (!value) {
        delete this._dateModified;
        return;
      }
      if (value.replace) {
        try {
          value = value.replace(/["\\]/g, "");
          value = new Date(value);
          value = value.getTime();
        } catch (e) {
          this.warn("Upgraded dateModified" + value);
        }
      }
      this._dateModified = value;
    }
  },

  /**
   * Shows the differences between revisions of two couchdb docs, TODO not working yet but someday when it becomes a priority..
   */
  showDiffs: {
    value: function(oldrevision, newrevision) {
      this.todo("We haven't implemented the 'diff' tool yet" +
        " (ie, showing the changes, letting you undo changes etc)." +
        " We will do it eventually, when it becomes a priority. " +
        "<a target='blank'  href='https://github.com/FieldDB/FieldDB/issues/124'>" +
        "You can vote for it in our issue tracker</a>.  " +
        "We use the " +
        "<a target='blank' href='" + this.url + "/" + oldrevision + "?rev=" + newrevision + "'>" + "Futon User Interface</a> directly to track revisions in the data, you can too (if your a power user type).", "alert", "Track Changes:");
    }
  },

  comments: {
    get: function() {
      return this._comments || FieldDBObject.DEFAULT_COLLECTION;
    },
    set: function(value) {
      if (value === this._comments) {
        return;
      }
      if (!value) {
        delete this._comments;
        return;
      } else {
        if (typeof this.INTERNAL_MODELS["comments"] === "function" && Object.prototype.toString.call(value) === "[object Array]") {
          value = new this.INTERNAL_MODELS["comments"](value);
        }
      }
      this._comments = value;
    }
  },

  isEmpty: {
    value: function(aproperty) {
      var empty = !this[aproperty] || this[aproperty] === FieldDBObject.DEFAULT_COLLECTION || this[aproperty] === FieldDBObject.DEFAULT_ARRAY || this[aproperty] === FieldDBObject.DEFAULT_OBJECT || this[aproperty] === FieldDBObject.DEFAULT_STRING || this[aproperty] === FieldDBObject.DEFAULT_DATE || (this[aproperty].length !== undefined && this[aproperty].length === 0) || this[aproperty] === {};
      /* TODO also return empty if it matches a default of any version of the model? */
      return empty;
    }
  },

  toJSON: {
    value: function(includeEvenEmptyAttributes, removeEmptyAttributes, attributesToIgnore) {
      try {
        var json = {
            fieldDBtype: this.fieldDBtype
          },
          aproperty,
          underscorelessProperty;

        if (this.fetching) {
          this.warn("Cannot get json while " + this.id + " is fetching itself");
          this.debug(" this is the object", this);
          // return;
          // throw "Cannot get json while object is fetching itself";
        }
        /* this object has been updated to this version */
        this.version = this.version;
        /* force id to be set if possible */
        // this.id = this.id;

        if (this.useIdNotUnderscore) {
          json.id = this.id;
        } else {
          json._id = this.id;
        }

        if (!attributesToIgnore) {
          attributesToIgnore = [];
        }
        attributesToIgnore = attributesToIgnore.concat(FieldDBObject.internalAttributesToNotJSONify);
        this.debug("Ignoring for json ", attributesToIgnore);
        for (aproperty in this) {
          if (this.hasOwnProperty(aproperty) && typeof this[aproperty] !== "function" && !FieldDBObject.ignore(aproperty, attributesToIgnore)) {
            underscorelessProperty = aproperty.replace(/^_/, "");
            if (underscorelessProperty === "id" || underscorelessProperty === "rev") {
              underscorelessProperty = "_" + underscorelessProperty;
            }
            if (!removeEmptyAttributes || (removeEmptyAttributes && !this.isEmpty(aproperty))) {
              if (this[aproperty] && typeof this[aproperty].toJSON === "function") {
                json[underscorelessProperty] = this[aproperty].toJSON(includeEvenEmptyAttributes, removeEmptyAttributes);
              } else {
                json[underscorelessProperty] = this[aproperty];
              }
            }
          }
        }

        /* if the caller requests a complete object include the default for all defauls by calling get on them */
        if (includeEvenEmptyAttributes) {
          for (aproperty in this.INTERNAL_MODELS) {
            if (!json[aproperty] && this.INTERNAL_MODELS) {
              if (this.INTERNAL_MODELS[aproperty] && typeof this.INTERNAL_MODELS[aproperty] === "function" && typeof new this.INTERNAL_MODELS[aproperty]().toJSON === "function") {
                json[aproperty] = new this.INTERNAL_MODELS[aproperty]().toJSON(includeEvenEmptyAttributes, removeEmptyAttributes, attributesToIgnore);
              } else {
                json[aproperty] = this.INTERNAL_MODELS[aproperty];
              }
            }
          }
        }

        if (!json._id) {
          delete json._id;
        }
        if (this.useIdNotUnderscore) {
          delete json._id;
        }

        if (!json._rev) {
          delete json._rev;
        }
        if (json.dbname) {
          json.pouchname = json.dbname;
          this.debug("Serializing Pouchname for backward compatability until prototype can handle dbname");
        }

        for (var uninterestingAttrib in FieldDBObject.internalAttributesToNotJSONify) {
          if (FieldDBObject.internalAttributesToNotJSONify.hasOwnProperty(uninterestingAttrib)) {
            delete json[FieldDBObject.internalAttributesToNotJSONify[uninterestingAttrib]];
            delete json[FieldDBObject.internalAttributesToNotJSONify[uninterestingAttrib].replace(/^_/, "")];
          }
        }

        if (this.collection !== "private_corpora" || this.api !== "private_corpora") {
          delete json.confidential;
          delete json.confidentialEncrypter;
        } else {
          this.warn("serializing confidential in this object " + this._collection);
        }
        if (this.api) {
          json.api = this.api;
        }

        json.fieldDBtype = this.fieldDBtype;
        if (json.previousFieldDBtype && json.fieldDBtype && json.previousFieldDBtype === json.fieldDBtype) {
          delete json.previousFieldDBtype;
        }
        if (attributesToIgnore.indexOf("fieldDBtype") > -1) {
          delete json.fieldDBtype;
        }
        return json;

      } catch (e) {
        console.warn(e);
        console.error(e.stack);
        this.bug("Unable to serialze " + this.id + ". Please report this.");
        return null;
      }
    }
  },

  addRelatedData: {
    value: function(json) {
      var relatedData;
      if (this.fields && this.fields.relatedData) {
        relatedData = this.fields.relatedData.json.relatedData || [];
      } else if (this.relatedData) {
        relatedData = this.relatedData;
      } else {
        this.relatedData = relatedData = [];
      }

      json.relation = "associated file";
      relatedData.push(json);
    }
  },

  /**
   * Creates a deep copy of the object (not a reference)
   * @return {Object} a near-clone of the objcet
   */
  clone: {
    value: function(includeEvenEmptyAttributes) {
      var json = {},
        aproperty,
        underscorelessProperty;
      try {
        json = JSON.parse(JSON.stringify(this.toJSON(includeEvenEmptyAttributes)));
      } catch (e) {
        if (e) {
          console.warn(e);
          console.warn(e.stack);
          console.warn(e.message);
          console.warn(this);
          // throw e;
        }
      }
      // Use clone on internal properties which have a clone function
      for (aproperty in json) {
        underscorelessProperty = aproperty.replace(/^_/, "");
        if (this[aproperty] && typeof this[aproperty].clone === "function") {
          json[underscorelessProperty] = JSON.parse(JSON.stringify(this[aproperty].clone(includeEvenEmptyAttributes)));
        }
      }

      var source = this.id;
      if (this.id && this.rev) {
        var relatedData;
        if (json.fields && json.fields.relatedData) {
          relatedData = json.fields.relatedData.json.relatedData || [];
        } else if (json.relatedData) {
          relatedData = json.relatedData;
        } else {
          json.relatedData = relatedData = [];
        }
        if (this.rev) {
          source = source + "?rev=" + this.rev;
        } else {
          if (this.parent && this.parent._rev) {
            source = "parent" + this.parent._id + "?rev=" + this.parent._rev;
          }
        }
        relatedData.push({
          relation: "clonedFrom",
          URI: source
        });
      }

      /* Clear the current object's info which we shouldnt clone */
      delete json._id;
      delete json._rev;
      delete json.parent;
      delete json.dbname;
      delete json.pouchname;

      return new this.constructor(json);
    }
  },

  contextualizer: {
    get: function() {
      if (this.application && this.application.contextualizer) {
        return this.application.contextualizer;
      }
    },
    set: function() {}
  },

  /**
   *  Cleans a value to become a primary key on an object (replaces punctuation and symbols with underscore)
   *  formerly: item.replace(/[-\""+=?.*&^%,\/\[\]{}() ]/g, "")
   *
   * @param  String value the potential primary key to be cleaned
   * @return String       the value cleaned and safe as a primary key
   */
  sanitizeStringForFileSystem: {
    value: function(value, optionalReplacementCharacter) {
      this.debug("sanitizeStringForPrimaryKey " + value);
      if (!value) {
        return null;
      }
      if (optionalReplacementCharacter === undefined || optionalReplacementCharacter === "-") {
        optionalReplacementCharacter = "_";
      }
      if (value.trim) {
        value = Diacritics.remove(value);
        this.debug("sanitizeStringForPrimaryKey " + value);

        value = value.trim().replace(/[^-a-zA-Z0-9]+/g, optionalReplacementCharacter).replace(/^_/, "").replace(/_$/, "");
        this.debug("sanitizeStringForPrimaryKey " + value);
        return value;
      } else if (typeof value === "number") {
        return parseFloat(value, 10);
      } else {
        return null;
      }
    }
  },

  sanitizeStringForPrimaryKey: {
    value: function(value, optionalReplacementCharacter) {
      this.debug("sanitizeStringForPrimaryKey " + value);
      if (!value) {
        return null;
      }
      if (value.replace) {
        value = value.replace(/-/g, "_");
      }
      value = this.sanitizeStringForFileSystem(value, optionalReplacementCharacter);
      if (value && typeof value !== "number") {
        return this.camelCased(value);
      }
    }
  },

  camelCased: {
    value: function(value) {
      if (!value) {
        return null;
      }
      if (value.replace) {
        value = value.replace(/_([a-zA-Z])/g, function(word) {
          return word[1].toUpperCase();
        });
        value = value[0].toLowerCase() + value.substring(1, value.length);
      }
      return value;
    }
  }

});

exports.FieldDBObject = FieldDBObject;
exports.Document = FieldDBObject;