"use strict";

var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");

Object.defineProperty(exports, "__esModule", {
  value: true
});
exports.Store = void 0;

var _toConsumableArray2 = _interopRequireDefault(require("@babel/runtime/helpers/toConsumableArray"));

var _objectWithoutProperties2 = _interopRequireDefault(require("@babel/runtime/helpers/objectWithoutProperties"));

var _classCallCheck2 = _interopRequireDefault(require("@babel/runtime/helpers/classCallCheck"));

var _createClass2 = _interopRequireDefault(require("@babel/runtime/helpers/createClass"));

var _tsFns = require("ts-fns");

var _utils = require("./shared/utils.js");

var _excluded = ["key"];

var Store = /*#__PURE__*/function () {
  function Store() {
    var params = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
    (0, _classCallCheck2.default)(this, Store);
    this.data = null;
    this.state = null;
    this.editable = true;
    this.silent = false;
    this.context = this.getContext();
    this._watchers = [];
    this._updateData = {};
    this._updator = null;
    this._descriptors = {};
    this._deps = {};
    this._dep = [];
    this._observers = [];
    this.init(params);
  }

  (0, _createClass2.default)(Store, [{
    key: "runSilent",
    value: function runSilent(fn) {
      var latestSilent = this.silent;
      this.silent = true;

      for (var _len = arguments.length, args = new Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) {
        args[_key - 1] = arguments[_key];
      }

      var res = fn.apply(void 0, args);
      this.silent = latestSilent;
      return res;
    }
  }, {
    key: "getContext",
    value: function getContext() {
      return null;
    }
  }, {
    key: "init",
    value: function init(params) {
      var _this = this;

      this.runSilent(function () {
        // remove binders if exist
        (0, _tsFns.each)(_this._descriptors, function (_, key) {
          return _this.del(key);
        }); // reset to empty object

        _this._descriptors = {};
        _this._deps = {};
        _this._dep = []; // data & state

        _this.data = {};

        var traps = _this._traps({
          get: function get(keyPath, active) {
            var key = keyPath[0]; // keep store in state

            if ((0, _tsFns.isSymbol)(key) && (0, _tsFns.getSymbolContent)(key) === 'STORE') {
              return _this;
            } // auto observe at the first time


            _this._observers.forEach(function (observer) {
              var value = (0, _tsFns.parse)(_this.data, keyPath);
              var nodes = observer.nodes,
                  trap = observer.trap;
              var match = trap.match,
                  register = trap.register;
              var existing = nodes.find(function (item) {
                return item.value === value && (0, _utils.isKeyPathEqual)(item.key, keyPath);
              });

              if (!existing && (match(value) || match(active))) {
                register(keyPath, value, active);
              }
            });

            _this._depend(keyPath);

            return active;
          },
          set: function set(keyPath, value) {
            // computed property
            var key = keyPath[0];
            var descriptor = _this._descriptors[key];

            if (keyPath.length === 1 && descriptor) {
              // call setter
              if (descriptor.set) {
                descriptor.set.call(_this.state, value);
              }
            } // support change computed property


            return value;
          },
          del: function del(keyPath) {
            // computed property
            if (keyPath.length === 1 && _this._descriptors[keyPath[0]]) {
              var key = keyPath[0]; // remove existing binders

              var descriptor = _this._descriptors[key];

              if (descriptor && descriptor.binders) {
                descriptor.binders.forEach(function (item) {
                  item.store.unwatch(item.on, item.fn);
                });
              } // remove computed property


              delete _this._descriptors[key]; // remove dependencies

              if (_this._deps[key]) {
                (0, _tsFns.each)(_this._deps[key], function (observe, key) {
                  _this.unwatch(key, observe);
                });
                delete _this._deps[key];
              }
            }
          },
          dispatch: function dispatch(_ref, force) {
            var keyPath = _ref.keyPath,
                value = _ref.value,
                next = _ref.next,
                prev = _ref.prev,
                active = _ref.active,
                invalid = _ref.invalid;

            _this.dispatch(keyPath, {
              value: value,
              next: next,
              prev: prev,
              active: active,
              invalid: invalid
            }, force);
          },
          writable: function writable() {
            // chould not change the value any more
            return _this.editable;
          }
        });

        _this.state = (0, _tsFns.createProxy)(_this.data, traps); // descriptors

        (0, _tsFns.each)(params, function (descriptor, key) {
          // make value patch to data, so that the data has initialized value which is needed in compute
          _this.state[key] = params[key]; // now all keys have been generated
          // collect descriptors

          if (descriptor.get || descriptor.set) {
            _this._descriptors[key] = descriptor;
          }
        }, true); // collecting dependencies
        // this should be executed after all values have been set to state, or computers will throw out errors

        (0, _tsFns.each)(_this._descriptors, function (item, key) {
          if (item.get) {
            _this._collect(key);

            _this._refine(key, true);
          }
        });
      });
    }
  }, {
    key: "_traps",
    value: function _traps(traps) {
      return traps;
    }
  }, {
    key: "get",
    value: function get(keyPath) {
      return (0, _tsFns.parse)(this.state, keyPath);
    }
  }, {
    key: "set",
    value: function set(keyPath, value, silent) {
      var _this2 = this;

      if (silent) {
        this.runSilent(function () {
          (0, _tsFns.assign)(_this2.state, keyPath, value);
        });
      } else {
        (0, _tsFns.assign)(this.state, keyPath, value);
      }

      return value;
    }
  }, {
    key: "del",
    value: function del(keyPath) {
      (0, _tsFns.remove)(this.state, keyPath);
    }
  }, {
    key: "update",
    value: function update() {
      var _this3 = this;

      var data = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
      var async = arguments.length > 1 ? arguments[1] : undefined;
      var silent = arguments.length > 2 ? arguments[2] : undefined;

      if (async) {
        Object.assign(this._updateData, data);

        if (this._updator) {
          return this._updator;
        }

        var updator = Promise.resolve().then(function () {
          _this3.update(_this3._updateData);
        }).then(function () {
          _this3._updateData = {};
          _this3._updator = null;
          return _this3.state;
        });
        this._updator = updator;
        return updator;
      }

      (0, _tsFns.each)(data, function (value, key) {
        _this3.set(key, value, silent);
      });
      return Promise.resolve(this.state);
    } // define a computed property

  }, {
    key: "define",
    value: function define(key, options) {
      var descriptor = (0, _tsFns.isFunction)(options) ? {
        get: options
      } : options;
      this.del(key);
      this._descriptors[key] = descriptor;

      if (descriptor.get) {
        this._collect(key);

        this._refine(key, true);
      }

      return this.state[key];
    }
    /**
     * bind a key to another store, when the store's certain key changed, re-compute key
     * @param {*} key
     * @example
     * store1.bind('age')(store2, 'age') // bind store1's age to store2's age, when store2's age changed, store1's age will re-compute
     */

  }, {
    key: "bind",
    value: function bind(key) {
      var _this4 = this;

      var fn = function fn() {
        _this4._refine(key);
      };

      var bind = function bind(store, on) {
        var descriptor = _this4._descriptors[key];

        if (!descriptor) {
          return bind;
        }

        store.watch(on, fn, true);
        descriptor.binders = descriptor.binders = [];
        descriptor.binders.push({
          store: store,
          on: on,
          fn: fn
        });
        return bind;
      };

      return bind;
    }
    /**
     *
     * @param {function|any} target if function, return true to match
     * @param {function} subscribe target => dispatch => [unsubscribe]
     * @param {function} [unsubscribe] target => dispatch => !
     * @example
     * const disconnect = store.observe(
     *  model,
     *  model => dispatch => model.watch('*', dispatch, true),
     *  model => dispatch => model.unwatch('*', dispatch),
     * )
     * store.set(keyPath, model) // should must after observe
     */

  }, {
    key: "observe",
    value: function observe(target, subscribe, unsubscribe) {
      var _this5 = this;

      var match = function match(v) {
        if ((0, _tsFns.isFunction)(target)) {
          return target(v);
        } else {
          return v === target;
        }
      };

      var nodes = [];

      var register = function register(key, value, active) {
        var dispatch = function dispatch(_ref2) {
          var k = _ref2.key,
              info = (0, _objectWithoutProperties2.default)(_ref2, _excluded);

          _this5.dispatch([].concat((0, _toConsumableArray2.default)(key), (0, _toConsumableArray2.default)(k)), info, true);
        };

        var unsubscribe = subscribe(value)(dispatch);
        nodes.push({
          key: key,
          value: value,
          active: active,
          dispatch: dispatch,
          unsubscribe: unsubscribe
        });
      };

      var watch = function watch(info) {
        var key = info.key,
            value = info.value,
            next = info.next,
            prev = info.prev,
            active = info.active,
            invalid = info.invalid;

        if (next === prev) {
          return;
        }

        if (active === invalid) {
          return;
        } // unsubscribe


        var index = nodes.findIndex(function (item) {
          return item.value === prev && (0, _utils.isKeyPathEqual)(item.key, key);
        });

        if (index > -1) {
          var _nodes$index = nodes[index],
              dispatch = _nodes$index.dispatch,
              _unsubscribe = _nodes$index.unsubscribe,
              _value = _nodes$index.value;

          if ((0, _tsFns.isFunction)(_unsubscribe)) {
            _unsubscribe();
          }

          if (unsubscribe) {
            unsubscribe(_value)(dispatch);
          }

          nodes.splice(index, 1); // delete the item
        } // subscribe


        if (match(value) || match(active)) {
          register(key, value, active);
        }
      };

      this.watch('*', watch, true);
      var trap = {
        match: match,
        register: register,
        watch: watch
      };
      var observer = {
        nodes: nodes,
        trap: trap
      };

      this._observers.push(observer);

      var disconnect = function disconnect() {
        _this5.unwatch('*', watch);

        var index = _this5._observers.indexOf(observer);

        if (index > -1) {
          _this5._observers.splice(index, 1);
        }
      };

      return disconnect;
    } // watch change of dependencies

  }, {
    key: "_depend",
    value: function _depend(keyPath) {
      var _this6 = this;

      var chain = (0, _tsFns.isArray)(keyPath) ? keyPath : (0, _tsFns.makeKeyChain)(keyPath);

      if (chain.length > 1) {
        return;
      }

      var key = chain[0];
      var dep = this._dep;
      var by = dep[dep.length - 1]; // without begin

      if (!by) {
        return;
      } // donot depend on self


      if (by === key) {
        return;
      }

      var descriptor = this._descriptors[by];

      if (!descriptor || !descriptor.get) {
        return;
      }

      var deps = this._deps;
      var depsOfBy = deps[by] = deps[by] || {};
      var depInBy = depsOfBy[key]; // has been collected

      if (depInBy) {
        return;
      }

      var reactive = function reactive() {
        _this6._collect(by);

        _this6._refine(by);
      };

      this.watch(key, reactive, true);
      depsOfBy[key] = reactive; // ignore nested dependencies, for example: a->b+c && b->c => a->b

      var depsOfKey = deps[key];

      if (depsOfKey) {
        var keys = Object.keys(depsOfBy);

        for (var i = 0, len = keys.length; i < len; i++) {
          var k = keys[i]; // if nested dependencies, remove existing dependency.
          // k is in depsOfBy and also in depsOfKey

          if (depsOfKey[k]) {
            this.unwatch(k, depsOfBy[k]);
            delete depsOfBy[k];
          }
        }
      }

      this._collect(key);
    }
  }, {
    key: "_refine",
    value: function _refine(key, silent) {
      var descriptor = this._descriptors[key];

      if (!descriptor || !descriptor.get) {
        return;
      }

      var value = this._compute(key);

      this.set(key, value, silent);
    } // collect dependencies, re-compute value

  }, {
    key: "_collect",
    value: function _collect(key) {
      this._dep.push(key);

      var descriptor = this._descriptors[key];

      if (!descriptor || !descriptor.get) {
        this._dep.pop();

        return;
      }

      this._compute(key);

      this._dep.pop();
    }
  }, {
    key: "_compute",
    value: function _compute(key) {
      var descriptor = this._descriptors[key];

      if (!descriptor || !descriptor.get) {
        return;
      }

      var value = descriptor.get.call(this.state);
      return value;
    }
  }, {
    key: "watch",
    value: function watch(keyPath, fn) {
      var deep = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : false;
      var once = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : false;
      var items = this._watchers;
      var key = (0, _tsFns.isArray)(keyPath) ? keyPath : (0, _tsFns.makeKeyChain)(keyPath);
      items.push({
        key: key,
        fn: fn,
        deep: deep,
        once: once
      });
      return this;
    }
  }, {
    key: "unwatch",
    value: function unwatch(keyPath, fn) {
      var items = this._watchers;
      var key = (0, _tsFns.isArray)(keyPath) ? keyPath : (0, _tsFns.makeKeyChain)(keyPath);
      items.forEach(function (item, i) {
        if ((0, _utils.isKeyPathEqual)(item.key, key) && (item.fn === fn || (0, _tsFns.isUndefined)(fn))) {
          items.splice(i, 1);
        }
      });
      return this;
    }
  }, {
    key: "dispatch",
    value: function dispatch(keyPath, _ref3) {
      var _this7 = this;

      var value = _ref3.value,
          next = _ref3.next,
          prev = _ref3.prev,
          active = _ref3.active,
          invalid = _ref3.invalid,
          compute = _ref3.compute;
      var force = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : false;

      if (this.silent) {
        return false;
      }

      if (!force && next === prev) {
        return false;
      }

      if (!force && (0, _tsFns.isEqual)(next, prev)) {
        return false;
      }

      var key = (0, _tsFns.isArray)(keyPath) ? (0, _toConsumableArray2.default)(keyPath) : (0, _tsFns.makeKeyChain)(keyPath);
      var watchers = this._watchers;
      var items = watchers.filter(function (item) {
        if ((0, _utils.isKeyPathEqual)(item.key, key)) {
          return true;
        }

        if (item.deep && (0, _utils.isKeyPathEqual)(item.key, key.slice(0, item.key.length))) {
          return true;
        }

        return false;
      }); // watchers which watch any change

      var anys = watchers.filter(function (item) {
        if (item.key[0] && item.key[0][0] === '!') {
          return false;
        }

        if (!(0, _utils.isKeyPathEqual)(item.key, ['*'])) {
          return false;
        } // if watch('*', fn, false), only top level will be watched


        if (!item.deep && key.length > 1) {
          return false;
        }

        return true;
      });
      items.push.apply(items, (0, _toConsumableArray2.default)(anys));
      items.forEach(function (item) {
        var target = item.key;
        var once = item.once;
        item.fn.call(_this7.context || _this7.state, {
          target: target,
          key: key,
          value: value,
          next: next,
          prev: prev,
          active: active,
          invalid: invalid,
          compute: compute
        });

        if (once) {
          _this7.unwatch(item.key, item.fn);
        }
      });
      return true;
    }
  }, {
    key: "forceDispatch",
    value: function forceDispatch() {
      var _this8 = this;

      var keyPath = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : '!';

      for (var _len2 = arguments.length, args = new Array(_len2 > 1 ? _len2 - 1 : 0), _key2 = 1; _key2 < _len2; _key2++) {
        args[_key2 - 1] = arguments[_key2];
      }

      if (this.silent) {
        return false;
      }

      var key = (0, _tsFns.isArray)(keyPath) ? (0, _toConsumableArray2.default)(keyPath) : (0, _tsFns.makeKeyChain)(keyPath); // pad '!'

      if (key[0][0] !== '!') {
        key[0] = '!' + key[0];
      }

      var watchers = this._watchers;
      var items = watchers.filter(function (item) {
        if (!(item.key[0] && item.key[0][0] === '!')) {
          return false;
        }

        if (item.key[0] === '!') {
          return false;
        }

        if ((0, _utils.isKeyPathEqual)(item.key, key)) {
          return true;
        }

        if (item.deep && (0, _utils.isKeyPathEqual)(item.key, key.slice(0, item.key.length))) {
          return true;
        }

        return false;
      }); // watchers which watch any change

      var anys = watchers.filter(function (item) {
        if (!(0, _utils.isKeyPathEqual)(item.key, ['!'])) {
          return false;
        } // if watch('!', fn, false), only top level will be watched


        if (!item.deep && key.length > 1) {
          return false;
        }

        return true;
      });
      items.push.apply(items, (0, _toConsumableArray2.default)(anys));
      items.forEach(function (item) {
        var _item$fn;

        (_item$fn = item.fn).call.apply(_item$fn, [_this8.context || _this8.state, key].concat(args));
      });
      return true;
    }
  }]);
  return Store;
}();

exports.Store = Store;