# av.js

    const _ = require('underscore');
    const userAgent = require('./ua');
    const {
      inherits,
      parseDate,
    } = require('./utils');

    const AV = global.AV || {};

    // All internal configuration items
    AV._config = {
      serverURLs: {},
      useMasterKey: false,
      production: null,
      realtime: null,
      requestTimeout: null,
    };

    // configs shared by all AV instances
    AV._sharedConfig = {
      userAgent,
      liveQueryRealtime: null,
    };

    /**
     * Contains all AV API classes and functions.
     * @namespace AV
     */

    /**
     * Returns prefix for localStorage keys used by this instance of AV.
     * @param {String} path The relative suffix to append to it.
     *     null or undefined is treated as the empty string.
     * @return {String} The full key name.
     * @private
     */
    AV._getAVPath = function(path) {
      if (!AV.applicationId) {
        throw new Error("You need to call AV.initialize before using AV.");
      }
      if (!path) {
        path = "";
      }
      if (!_.isString(path)) {
        throw new Error("Tried to get a localStorage path that wasn't a String.");
      }
      if (path[0] === "/") {
        path = path.substring(1);
      }
      return "AV/" + AV.applicationId + "/" + path;
    };

    const hexOctet = () => Math.floor((1 + Math.random()) * 0x10000).toString(16).substring(1);
    const uuid = () => `${hexOctet()}${hexOctet()}-${hexOctet()}-${hexOctet()}-${hexOctet()}-${hexOctet()}${hexOctet()}${hexOctet()}`;

    /**
     * Returns the unique string for this app on this machine.
     * Gets reset when localStorage is cleared.
     * @private
     */
    AV._installationId = null;
    AV._getInstallationId = () => {
      // See if it's cached in RAM.
      if (AV._installationId) {
        return AV.Promise.resolve(AV._installationId);
      }

      // Try to get it from localStorage.
      const path = AV._getAVPath('installationId');
      return AV.localStorage.getItemAsync(path).then((_installationId) => {
        AV._installationId = _installationId;
        if (!AV._installationId) {
          // It wasn't in localStorage, so create a new one.
          AV._installationId = _installationId = uuid();
          return AV.localStorage.setItemAsync(path, _installationId).then(() => _installationId);
        }
        return _installationId;
      });
    };

    AV._subscriptionId = null;
    AV._refreshSubscriptionId = (path = AV._getAVPath('subscriptionId')) => {
      const subscriptionId = AV._subscriptionId = uuid();
      return AV.localStorage.setItemAsync(path, subscriptionId).then(() => subscriptionId);
    };
    AV._getSubscriptionId = () => {
      // See if it's cached in RAM.
      if (AV._subscriptionId) {
        return AV.Promise.resolve(AV._subscriptionId);
      }

      // Try to get it from localStorage.
      const path = AV._getAVPath('subscriptionId');
      return AV.localStorage.getItemAsync(path).then((_subscriptionId) => {
        AV._subscriptionId = _subscriptionId;
        if (!AV._subscriptionId) {
          // It wasn't in localStorage, so create a new one.
          _subscriptionId = AV._refreshSubscriptionId(path);
        }
        return _subscriptionId;
      });
    };

    AV._parseDate = parseDate;

    // A self-propagating extend function.
    AV._extend = function(protoProps, classProps) {
      var child = inherits(this, protoProps, classProps);
      child.extend = this.extend;
      return child;
    };

    /**
     * Converts a value in a AV Object into the appropriate representation.
     * This is the JS equivalent of Java's AV.maybeReferenceAndEncode(Object)
     * if seenObjects is falsey. Otherwise any AV.Objects not in
     * seenObjects will be fully embedded rather than encoded
     * as a pointer.  This array will be used to prevent going into an infinite
     * loop because we have circular references.  If <seenObjects>
     * is set, then none of the AV Objects that are serialized can be dirty.
     * @private
     */
    AV._encode = function(value, seenObjects, disallowObjects, full = true) {
      if (value instanceof AV.Object) {
        if (disallowObjects) {
          throw new Error("AV.Objects not allowed here");
        }
        if (!seenObjects || _.include(seenObjects, value) || !value._hasData) {
          return value._toPointer();
        }
        return value._toFullJSON(seenObjects.concat(value), full);
      }
      if (value instanceof AV.ACL) {
        return value.toJSON();
      }
      if (_.isDate(value)) {
        return full ? { "__type": "Date", "iso": value.toJSON() } : value.toJSON();
      }
      if (value instanceof AV.GeoPoint) {
        return value.toJSON();
      }
      if (_.isArray(value)) {
        return _.map(value, function(x) {
          return AV._encode(x, seenObjects, disallowObjects, full);
        });
      }
      if (_.isRegExp(value)) {
        return value.source;
      }
      if (value instanceof AV.Relation) {
        return value.toJSON();
      }
      if (value instanceof AV.Op) {
        return value.toJSON();
      }
      if (value instanceof AV.File) {
        if (!value.url() && !value.id) {
          throw new Error("Tried to save an object containing an unsaved file.");
        }
        return value._toFullJSON(seenObjects, full);
      }
      if (_.isObject(value)) {
        return _.mapObject(value, (v, k) => AV._encode(v, seenObjects, disallowObjects, full));
      }
      return value;
    };

    /**
     * The inverse function of AV._encode.
     * @private
     */
    AV._decode = function(value, key) {
      if (!_.isObject(value) || _.isDate(value)) {
        return value;
      }
      if (_.isArray(value)) {
        return _.map(value, v => AV._decode(v));
      }
      if (value instanceof AV.Object) {
        return value;
      }
      if (value instanceof AV.File) {
        return value;
      }
      if (value instanceof AV.Op) {
        return value;
      }
      if (value instanceof AV.GeoPoint) {
        return value;
      }
      if (value instanceof AV.ACL) {
        return value;
      }
      if (key === 'ACL') {
        return new AV.ACL(value);
      }
      if (value.__op) {
        return AV.Op._decode(value);
      }
      var className;
      if (value.__type === "Pointer") {
        className = value.className;
        var pointer = AV.Object._create(className);
        if(Object.keys(value).length > 3) {
            const v = _.clone(value);
            delete v.__type;
            delete v.className;
            pointer._finishFetch(v, true);
        }else{
            pointer._finishFetch({ objectId: value.objectId }, false);
        }
        return pointer;
      }
      if (value.__type === "Object") {
        // It's an Object included in a query result.
        className = value.className;
        const v = _.clone(value);
        delete v.__type;
        delete v.className;
        var object = AV.Object._create(className);
        object._finishFetch(v, true);
        return object;
      }
      if (value.__type === "Date") {
        return AV._parseDate(value.iso);
      }
      if (value.__type === "GeoPoint") {
        return new AV.GeoPoint({
          latitude: value.latitude,
          longitude: value.longitude
        });
      }
      if (value.__type === "Relation") {
        if (!key) throw new Error('key missing decoding a Relation');
        var relation = new AV.Relation(null, key);
        relation.targetClassName = value.className;
        return relation;
      }
      if (value.__type === 'File') {
        var file = new AV.File(value.name);
        const v = _.clone(value);
        delete v.__type;
        file._finishFetch(v);
        return file;
      }
      return _.mapObject(value, AV._decode);
    };

    /**
     * The inverse function of {@link AV.Object#toFullJSON}.
     * @since 2.0.0
     * @param {Object}
     * return {AV.Object|AV.File|any}
     */
    AV.parseJSON = AV._decode;

    AV._encodeObjectOrArray = function(value) {
      var encodeAVObject = function(object) {
        if (object && object._toFullJSON){
          object = object._toFullJSON([]);
        }

        return _.mapObject(object, function(value) {
          return AV._encode(value, []);
        });
      };

      if (_.isArray(value)) {
        return value.map(function(object) {
          return encodeAVObject(object);
        });
      } else {
        return encodeAVObject(value);
      }
    };

    AV._arrayEach = _.each;

    /**
     * Does a deep traversal of every item in object, calling func on every one.
     * @param {Object} object The object or array to traverse deeply.
     * @param {Function} func The function to call for every item. It will
     *     be passed the item as an argument. If it returns a truthy value, that
     *     value will replace the item in its parent container.
     * @returns {} the result of calling func on the top-level object itself.
     * @private
     */
    AV._traverse = function(object, func, seen) {
      if (object instanceof AV.Object) {
        seen = seen || [];
        if (_.indexOf(seen, object) >= 0) {
          // We've already visited this object in this call.
          return;
        }
        seen.push(object);
        AV._traverse(object.attributes, func, seen);
        return func(object);
      }
      if (object instanceof AV.Relation || object instanceof AV.File) {
        // Nothing needs to be done, but we don't want to recurse into the
        // object's parent infinitely, so we catch this case.
        return func(object);
      }
      if (_.isArray(object)) {
        _.each(object, function(child, index) {
          var newChild = AV._traverse(child, func, seen);
          if (newChild) {
            object[index] = newChild;
          }
        });
        return func(object);
      }
      if (_.isObject(object)) {
        AV._each(object, function(child, key) {
          var newChild = AV._traverse(child, func, seen);
          if (newChild) {
            object[key] = newChild;
          }
        });
        return func(object);
      }
      return func(object);
    };

    /**
     * This is like _.each, except:
     * * it doesn't work for so-called array-like objects,
     * * it does work for dictionaries with a "length" attribute.
     * @private
     */
    AV._objectEach = AV._each = function(obj, callback) {
      if (_.isObject(obj)) {
        _.each(_.keys(obj), function(key) {
          callback(obj[key], key);
        });
      } else {
        _.each(obj, callback);
      }
    };

    module.exports = AV;