"use strict";

exports.__esModule = true;
exports.default = undefined;

var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }();

var _es6Symbol = require("es6-symbol");

var _es6Symbol2 = _interopRequireDefault(_es6Symbol);

var _lodash = require("lodash.flatten");

var _lodash2 = _interopRequireDefault(_lodash);

var _lodash3 = require("lodash.has");

var _lodash4 = _interopRequireDefault(_lodash3);

var _lodash5 = require("lodash.sortby");

var _lodash6 = _interopRequireDefault(_lodash5);

var _akutils = require("akutils");

var _Shadow2 = require("../Shadow");

var _Shadow3 = _interopRequireDefault(_Shadow2);

var _CollectionOptions = require("./CollectionOptions");

function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }

function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }

function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; }

function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; }

var _cache = (0, _es6Symbol2.default)("_cache");
var _keysArray = (0, _es6Symbol2.default)('_keysArray');
var _valuesArray = (0, _es6Symbol2.default)('_valuesArray');

/**
	Shadow api for the `CollectionProperty`.

	## Basic functions

	<ul>
		<li>`add(state, mergeOp)`</li>
		<li>`all()`</li>
		<li>`clear()</li>
		<li>`create(model)`</li>
		<li>`destroy(id)`</li>
		<li>`fetch(filter, mergeOp)`</li>
		<li>`find(id)`</li>
		<li>`get(id)`</li>
		<li>`has(id)`</li>
		<li>`remove(id)`</li>
	</ul>


	## Utility functions

	<ul>
		<li>`entries()`</li>
		<li>`every(iteratee, context)`</li>
		<li>`filter(iteratee, context)`</li>
		<li>`groupBy(callback, context)`</li>
		<li>`keys()`</li>
		<li>`map(iteratee, context)`</li>
		<li>`reduce(iteratee, acc, context)`</li>
		<li>`some(iteratee, context)`</li>
		<li>`sortBy(...iteratee)`</li>
		<li>`values()`</li>
	</ul>

	## Paging function

	<ul>
		<li>`fetchNext()` - gets the next group of models</li>
		<li>`isPaging()` - gets if a paging request is outstanding (only one paging request per
				collection allowed at one time)</li>
		<li>`hasMorePages()` - does the endpoint have additional models</li>
		<li>`nextOffset()` - gets the offset for the next paging request</li>
		<li>`resetPaging()` - resets the internal paging variables</li>
		<li>`setLimit()` - change the number of models requested with each `fetchNext()` request</li>
	</ul>


	@see {@link CollectionProperty}
*/

var CollectionShadow = function (_Shadow) {
	_inherits(CollectionShadow, _Shadow);

	function CollectionShadow(impl) {
		_classCallCheck(this, CollectionShadow);

		var _this = _possibleConstructorReturn(this, Object.getPrototypeOf(CollectionShadow).call(this, impl));

		_this[_cache] = {};
		return _this;
	}

	/**
 	Gets the endpoint for accessing the remote data source.
 		@return {Object}
 */


	_createClass(CollectionShadow, [{
		key: "clearEndpoint",


		/**
  	Disconnects the collection from the remote data source.
  */
		value: function clearEndpoint() {
			return this.$$().clearEndpoint();
		}

		/**
  	Fetches the next set of models from the endpoint. This method differs from
  	{@link CollectionProperty#fetch} by passing `offset` and `limit` filter criteria to the
  	endpoint.
  		@param {string} [mergeOp=REPLACE_OPTION] - one of `DEFAULTS_OPTION`, `MERGE_OPTION`, or
  		`REPLACE_OPTION` and specifies how to combine an existing model with a matching ID
  		with a newly retrieved model.
  		@return {Promise} - resolves with a single argument of an array of model json objects as
  		returned from the endpoint.
  */

	}, {
		key: "fetchNext",
		value: function fetchNext() {
			var mergeOp = arguments.length <= 0 || arguments[0] === undefined ? _CollectionOptions.REPLACE_OPTION : arguments[0];

			return this.$$().fetchNext(mergeOp);
		}

		/**
  	Gets if additional paging calls will return additional results.
  */

	}, {
		key: "hasMorePages",
		value: function hasMorePages() {
			return this.$$().hasMorePages(this);
		}

		/**
  	Sets the number of models to request with each paging request. The default paging size is
  	50.
  		@param {number} limit
  */

	}, {
		key: "setLimit",
		value: function setLimit(limit) {
			this.$$().setLimit(limit);
		}

		//------------------------------------------------------------------------------------------------------
		// Autocheckpointing API
		//------------------------------------------------------------------------------------------------------

		/**
  	Sets whether auto-checkpointing is enabled.
  		@return {boolean}
  */

	}, {
		key: "isAutocheckpoint",
		value: function isAutocheckpoint() {
			return this.$$().isAutocheckpoint();
		}

		/**
  	Sets whether auto-checkpointing should be enabled.
  		@param {boolean} auto - `true` to enable.
  */

	}, {
		key: "setAutocheckpoint",
		value: function setAutocheckpoint(auto) {
			this.$$().setAutocheckpoint(auto);
		}

		/**
  	Resets all dirty models to their last checkpoint and removes all unsaved models.
  */

	}, {
		key: "resetToLastEndpointState",
		value: function resetToLastEndpointState() {
			var auto = this.isAutocheckpoint();

			this.forEach(function (m) {
				var m$ = m.$();

				if (m$.isNew()) {
					m$.destroy();
				} else if (auto && m$.isDirty()) {
					m$.resetToCheckpoint();
				}
			});
		}

		//------------------------------------------------------------------------------------------------------
		// Stable API
		//------------------------------------------------------------------------------------------------------

		/**
  	Synchronously adds a new model object to the collection. Call `model.$().save()` to persist the newly
  	added object.
  		@param {object} state - the json model to add to the collection
  	@param {string} [mergeOp=REPLACE_OPTION] - one of `DEFAULTS_OPTION`, `MERGE_OPTION`, or
  		`REPLACE_OPTION` and specifies how to combine an existing model with a matching ID
  		with a newly retrieved model.
  		@return the object's ID. An ID is assigned if the 'id' parameter was not set and it could not
  		be found in the `state` parameter.
  */

	}, {
		key: "add",
		value: function add(state) {
			var mergeOp = arguments.length <= 1 || arguments[1] === undefined ? _CollectionOptions.REPLACE_OPTION : arguments[1];

			return this.$$().addModel(state, mergeOp);
		}

		/*
  	@deprecated use {@link CollectionShadow#add}
  */

	}, {
		key: "addModel",
		value: function addModel(state) {
			var mergeOp = arguments.length <= 1 || arguments[1] === undefined ? _CollectionOptions.REPLACE_OPTION : arguments[1];

			return this.$$().addModel(state, mergeOp);
		}

		/**
  	Bulk adds multiple models. Models must have an ID as it is assumed they have been previously
  	saved.
  		@param {array} models - array of model values
  	@param {string} [mergeOp=REPLACE_OPTION] - one of `DEFAULTS_OPTION`, `MERGE_OPTION`, or
  		`REPLACE_OPTION` and specifies how to combine an existing model with a matching ID
  		with a newly retrieved model.
  	@param {boolean} [syncOp=true] - sets the `synced` flag to true if this parameter is true
  */

	}, {
		key: "addModels",
		value: function addModels(models) {
			var mergeOp = arguments.length <= 1 || arguments[1] === undefined ? _CollectionOptions.REPLACE_OPTION : arguments[1];
			var syncOp = arguments[2];

			this.$$().addModels(models, mergeOp, syncOp);
		}

		/**
  	Gets the shadow models currently managed by the collection.
  		@return {array} all models being managed by the collection
  */

	}, {
		key: "all",
		value: function all() {
			return this.valuesArray();
		}

		/**
  	Removes all models from the collection and marks the collection as having not synched with the
  	endpoint.
  */

	}, {
		key: "clear",
		value: function clear() {
			this.$$().removeAllModels();
		}

		/**
  	Combines an {@link CollectionShadow#add} and {@link CollectionShadow#save} actions.
  		@param {Object} model - the json data
  		@return {Promise} resolves with the f.lux shadow state for the new model
  */

	}, {
		key: "create",
		value: function create(model) {
			return this.$$().create(model);
		}

		/**
  	Permantently deletes the model with the endpoint and removes it from the collection.
  		@param id - the model ID to delete
  		@return {Promise} resolves with the ID for the removed mdoel
  */

	}, {
		key: "destroy",
		value: function destroy(id) {
			return this.$$().destroy(id);
		}

		/**
  	Gets an iterator with [ID, model] paris.
  		@return {Array}
  */

	}, {
		key: "entries",
		value: function entries() {
			return this.$$().modelEntries(this);
		}

		/**
  	Tests whether all elements in the array pass the test implemented by the provided function.
  */

	}, {
		key: "every",
		value: function every(iteratee, context) {
			var keys = this.keysArray();
			var key, value;

			for (var i = 0, len = keys.length; i < len; i++) {
				key = keys[i];
				value = this.get(key);

				if (!iteratee.call(context, value, key, this)) {
					return false;
				}
			}

			return true;
		}

		/**
  	Fetches all the models from the endpoint.
  		@param [filter=null] - filter object created using the endpoint.
  	@param {string} [mergeOp=REPLACE_OPTION] - one of `DEFAULTS_OPTION`, `MERGE_OPTION`, or
  		`REPLACE_OPTION` and specifies how to combine an existing model with a matching ID
  		with a newly retrieved model.
  		@return {Promise} resolves with the json models from the endpoint
  */

	}, {
		key: "fetch",
		value: function fetch() {
			var filter = arguments.length <= 0 || arguments[0] === undefined ? null : arguments[0];
			var mergeOp = arguments.length <= 1 || arguments[1] === undefined ? _CollectionOptions.REPLACE_OPTION : arguments[1];

			return this.$$().fetch(filter, mergeOp);
		}

		/**
  	Creates an array with all elements that pass the test implemented by the provided function.
  */

	}, {
		key: "filter",
		value: function filter(iteratee, context) {
			var keys = this.keysArray();
			var acc = [];
			var key, value;

			for (var i = 0, len = keys.length; i < len; i++) {
				key = keys[i];
				value = this.get(key);

				if (iteratee.call(context, value, key, this)) {
					acc.push(value);
				}
			}

			return acc;
		}

		/**
  	Gets a model by ID. The method looks for a matching model in the collection. If one
  	is not found then one is requested from the endpoint.
  		@param id - the model ID
  	@param force - determines if locally stored model is returned if found using get().
  		true=refetch model
  		@return {Promise} reolves with the model or undefined if one is not found
  */

	}, {
		key: "find",
		value: function find(id, force) {
			return this.$$().find(id, force);
		}

		/**
  	Fetches all the models from the endpoint and adds them to the collection.
  		@param [filter] - filter object created using the endpoint.
  		@return {Promise} resolves with the json models from the endpoint
  */

	}, {
		key: "search",
		value: function search(filter) {
			return this.$$().search(filter);
		}

		/**
  	Gets the first model that satisfies the provided testing function.
  		@param iteratee - Function to execute on each value in the array, taking three arguments:
  		element, ID, and the collection reference.
  	@param {object} [context] - object to use as `this` when executing callback
  		@return the model or undefined if one is not found
  */

	}, {
		key: "findModel",
		value: function findModel(iteratee, context) {
			var keys = this.keysArray();
			var key, value;

			for (var i = 0, len = keys.length; i < len; i++) {
				key = keys[i];
				value = this.get(key);

				if (iteratee.call(context, value, key, this)) {
					return value;
				}
			}
		}

		/**
  	Executes a provided function once for each array element.
  		@param iteratee - Function to execute on each value in the array, taking three arguments:
  		element, ID, and the collection reference.
  	@param {object} [context] - object to use as `this` when executing callback
  */

	}, {
		key: "forEach",
		value: function forEach(iteratee, context) {
			var keys = this.keysArray();
			var key;

			for (var i = 0, len = keys.length; i < len; i++) {
				key = keys[i];

				iteratee.call(context, this.get(key), key, this);
			}
		}

		/**
  	Gets a model by ID. The method looks for a matching model in the collection and never
  	looks for one remotely through the endpoint.
  		@param id - the model ID
  		@return {Object} the model or undefined if one is not found
  */

	}, {
		key: "get",
		value: function get(id) {
			return this.$$().getModel(id, this);
		}

		/**
  	Inspired by [Lodash groupBy()](https://lodash.com/docs/4.17.4#groupBy) method.
  		Creates an object composed of keys generated from the results of running each element of
  	collection thru iteratee. The order of grouped values is determined by the order they occur
  	in collection. The corresponding value of each key is an array of elements responsible for
  	generating the key. The iteratee is invoked with one argument: (value).
  		@see https://lodash.com/docs/4.17.4#groupBy
  */

	}, {
		key: "groupBy",
		value: function groupBy(callback, context) {
			var keys = this.keysArray();
			var result = {};
			var key, value, groupId;

			for (var i = 0, len = keys.length; i < len; i++) {
				key = keys[i];
				value = this.get(key);
				groupId = callback.call(context, value, key, this);

				if ((0, _lodash4.default)(result, groupId)) {
					result[groupId].push(value);
				} else {
					result[groupId] = [value];
				}
			}

			return result;
		}

		/**
  	Gets if the collection contains a matching model.
  		@param id - the model ID
  		@return {boolean}
  */

	}, {
		key: "has",
		value: function has(id) {
			return this.$$().hasModel(id, this);
		}

		/**
  	Gets if the collection is paging, fetching, or restoring data.
  */

	}, {
		key: "isBusy",
		value: function isBusy() {
			return this.paging || this.fetching || this.restoring;
		}

		/**
  	Gets if the collection is active (has a shadow) and an endpoint.
  */

	}, {
		key: "isConnected",
		value: function isConnected() {
			return this.$$().isConnected();
		}

		/**
  	Gets if a model has never been persisted to the endpoint.
  */

	}, {
		key: "isNew",
		value: function isNew(id) {
			return this.$$().isNew(id, this);
		}

		/**
  	Gets an iterator containing each model IDs.
  		@return {Iterator}
  */

	}, {
		key: "keys",
		value: function keys() {
			return this.$$().modelKeys(this);
		}

		/**
  	Gets an array containing each model iDs.
  		@return {Array}
  */

	}, {
		key: "keysArray",
		value: function keysArray() {
			if (!this[_cache][_keysArray]) {
				this[_cache][_keysArray] = this.$$().modelKeysArray(this);
			}

			return this[_cache][_keysArray];
		}

		/**
  	Creates a new array with the results of calling a provided function on every element in
  	this collection.
  		@param iteratee - Function that produces an element of the new array, taking three arguments:
  		element, ID, and the collection reference.
  	@param {object} [context] - object to use as `this` when executing callback
  */

	}, {
		key: "map",
		value: function map(iteratee, context) {
			var keys = this.keysArray();
			var acc = [];
			var key;

			for (var i = 0, len = keys.length; i < len; i++) {
				key = keys[i];

				acc.push(iteratee.call(context, this.get(key), key, this));
			}

			return acc;
		}

		/**
  	Applies a function against an accumulator and each value of the collection to reduce it
  	to a single value.
  		@param iteratee - Function to execute on each value in the array, taking four arguments:
  		accumulator, element, ID, and the collection reference.
  	@param acc - the initial accumulator value
  	@param {object} [context] - object to use as `this` when executing callback
  */

	}, {
		key: "reduce",
		value: function reduce(iteratee, acc, context) {
			var keys = this.keysArray();
			var key, value;

			for (var i = 0, len = keys.length; i < len; i++) {
				key = keys[i];
				value = this.get(key);

				acc = iteratee.call(context, acc, value, key, this);
			}

			return acc;
		}

		/**
  	Synchronously removes the model from the collection without performing an endpoint operation.
  		@param id - the model id or cid
  */

	}, {
		key: "remove",
		value: function remove(id) {
			return this.$$().remove(id);
		}

		/**
  	Experimental feature to update values with any modifications from the server.
  		Question: should this be delegated to the endpoint?
  		@experimental
  	@ignore
  */

	}, {
		key: "resync",
		value: function resync() {
			return this.$$().resync();
		}

		/**
  	Sets the {@link CollectionProperty#endpoint}.
  */

	}, {
		key: "setEndpoint",
		value: function setEndpoint(endPoint) {
			this.$$().setEndpoint(endPoint);
		}

		/**
  	Bulk replaces current models with an array of new models.
  		@param {array} models - array of model values
  */

	}, {
		key: "setModels",
		value: function setModels(models) {
			this.$$().setModels(models);
		}

		/**
  	Tests whether some element in the array passes the test implemented by the provided function.
  		@param iteratee - Function to test for each element, taking three arguments:
  		element, ID, and the collection reference.
  	@param {object} [context] - object to use as `this` when executing callback
  */

	}, {
		key: "some",
		value: function some(iteratee, context) {
			var keys = this.keysArray();
			var key, value;

			for (var i = 0, len = keys.length; i < len; i++) {
				key = keys[i];
				value = this.get(key);

				if (iteratee.call(context, value, key, this)) {
					return true;
				}
			}

			return false;
		}

		/*
  	Sorts using the same arguments as [Lodash sortBy()](https://lodash.com/docs#sortBy).
  */

	}, {
		key: "sortBy",
		value: function sortBy() {
			var values = this.valuesArray();

			// use flatten in case caller incloses sort criteria in an array

			for (var _len = arguments.length, iteratee = Array(_len), _key = 0; _key < _len; _key++) {
				iteratee[_key] = arguments[_key];
			}

			return _lodash6.default.apply(null, [values, (0, _lodash2.default)(iteratee)]);
		}
	}, {
		key: "sort",
		value: function sort(comparator) {
			var values = this.valuesArray();

			return values.sort(comparator);
		}

		/**
  	Gets an iterator containing each f.lux shadow state model.
  		@return {Iterator}
  */

	}, {
		key: "values",
		value: function values() {
			return this.$$().modelValues(this);
		}

		/**
  	Gets an array containing each model iDs.
  		@return {Array}
  */

	}, {
		key: "valuesArray",
		value: function valuesArray() {
			if (!this[_cache][_valuesArray]) {
				this[_cache][_valuesArray] = this.$$().modelsArray(this);
			}

			return this[_cache][_valuesArray];
		}
	}, {
		key: _es6Symbol2.default.iterator,
		value: function value() {
			return this.$$().modelEntries(this);
		}
	}, {
		key: "endpoint",
		get: function get() {
			return this._endpoint;
		}

		/**
  	Gets the endpoint for accessing the remote data source.
  		@return {Object}
  */

	}, {
		key: "endpointId",
		get: function get() {
			var ep = this._endpoint;

			return ep && ep.id;
		}

		/**
  	Gets if a fetching operation that will replace ALL models is in progress.
  */

	}, {
		key: "fetching",
		get: function get() {
			return this.$$().isFetching();
		}

		/**
  	Gets if the collection has synced or had its data restored.
  */

	}, {
		key: "filled",
		get: function get() {
			return this.synced || this.restored;
		}

		/**
  	Gets the endpoint ID, which for {@link RestEndpointProperty} is its URL.
  		@return {string}
  */

	}, {
		key: "id",
		get: function get() {
			return this._endpoint && this._endpoint.id;
		}

		/** Gets the number of models being managed by the collection */

	}, {
		key: "size",
		get: function get() {
			return this._models.size;
		}

		//------------------------------------------------------------------------------------------------------
		// Paging API
		//------------------------------------------------------------------------------------------------------

		/**
  	Gets the number of models to fetch with each paging request.
  */

	}, {
		key: "limit",
		get: function get() {
			return this._limit;
		}

		/**
  	Gets the offset for the next paging request.
  */

	}, {
		key: "nextOffset",
		get: function get() {
			return this.$$().nextOffset();
		}

		/**
  	Gets if a paging operation ({@link CollectionProperty#fetchNext}) call is in progress.
  */

	}, {
		key: "paging",
		get: function get() {
			return this.$$().isPaging();
		}
	}, {
		key: "restoring",
		get: function get() {
			return this.$$().isRestoring();
		}
	}]);

	return CollectionShadow;
}(_Shadow3.default);

exports.default = CollectionShadow;