1/*
2 * Copyright (c) 2020 Huawei Device Co., Ltd.
3 * Licensed under the Apache License, Version 2.0 (the "License");
4 * you may not use this file except in compliance with the License.
5 * You may obtain a copy of the License at
6 *
7 *     http://www.apache.org/licenses/LICENSE-2.0
8 *
9 * Unless required by applicable law or agreed to in writing, software
10 * distributed under the License is distributed on an "AS IS" BASIS,
11 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 * See the License for the specific language governing permissions and
13 * limitations under the License.
14 */
15import {
16  ObserverStack,
17  SYMBOL_OBSERVABLE,
18  canObserve,
19  defineProp
20} from './utils';
21
22
23/**
24 * Subject constructor
25 * @param {any} target the target object to be observed
26 */
27export function Subject(target) {
28  const subject = this;
29  subject._hijacking = true;
30  defineProp(target, SYMBOL_OBSERVABLE, subject);
31
32  if (Array.isArray(target)) {
33    hijackArray(target);
34  }
35
36  Object.keys(target).forEach(key => hijack(target, key, target[key]));
37}
38
39Subject.of = function(target) {
40  if (!target || !canObserve(target)) {
41    return target;
42  }
43  if (target[SYMBOL_OBSERVABLE]) {
44    return target[SYMBOL_OBSERVABLE];
45  }
46  return new Subject(target);
47};
48
49Subject.is = function(target) {
50  return target && target._hijacking;
51};
52Subject.prototype.attach = function(key, observer) {
53  if (typeof key === 'undefined' || !observer) {
54    return void 0;
55  }
56  if (!this._obsMap) {
57    this._obsMap = {};
58  }
59  if (!this._obsMap[key]) {
60    this._obsMap[key] = new Set();
61  }
62  const observers = this._obsMap[key];
63  if (!observers.has(observer)) {
64    observers.add(observer);
65    return function() {
66      observers.delete(observer);
67    };
68  }
69  return void 0;
70};
71
72Subject.prototype.notify = function(key) {
73  if (
74    typeof key === 'undefined' ||
75    !this._obsMap ||
76    !this._obsMap[key]
77  ) {
78    return void 0;
79  }
80  this._obsMap[key].forEach(observer => observer.update());
81  return void 1;
82};
83
84Subject.prototype.setParent = function(parent, key) {
85  this._parent = parent;
86  this._key = key;
87};
88
89Subject.prototype.notifyParent = function() {
90  this._parent && this._parent.notify(this._key);
91};
92
93const ObservedMethods = {
94  PUSH: 'push',
95  POP: 'pop',
96  UNSHIFT: 'unshift',
97  SHIFT: 'shift',
98  SORT: 'sort',
99  SPLICE: 'splice',
100  REVERSE: 'reverse'
101};
102
103const OBSERVED_METHODS = Object.keys(ObservedMethods).map(
104    key => ObservedMethods[key]
105);
106
107/**
108 * observe the change of array
109 * @param {Array} target a plain JavaScript array to be observed
110 */
111function hijackArray(target) {
112  OBSERVED_METHODS.forEach(key => {
113    const originalMethod = target[key];
114
115    defineProp(target, key, function() {
116      // eslint-disable-next-line
117      const args = Array.prototype.slice.call(arguments);
118      // eslint-disable-next-line
119      originalMethod.apply(this, args);
120
121      let inserted;
122      if (ObservedMethods.PUSH === key || ObservedMethods.UNSHIFT === key) {
123        inserted = args;
124      } else if (ObservedMethods.SPLICE === key) {
125        inserted = args.slice(2);
126      }
127
128      if (inserted && inserted.length) {
129        inserted.forEach(Subject.of);
130      }
131
132      const subject = target[SYMBOL_OBSERVABLE];
133      if (subject) {
134        subject.notifyParent();
135      }
136    });
137  });
138}
139
140/**
141 * observe object
142 * @param {any} target the object to be observed
143 * @param {String} key the key to be observed
144 * @param {any} cache the cached value
145 */
146function hijack(target, key, cache) {
147  const subject = target[SYMBOL_OBSERVABLE];
148
149  Object.defineProperty(target, key, {
150    enumerable: true,
151    get() {
152      const observer = ObserverStack.top();
153      if (observer) {
154        observer.subscribe(subject, key);
155      }
156
157      const subSubject = Subject.of(cache);
158      if (Subject.is(subSubject)) {
159        subSubject.setParent(subject, key);
160      }
161
162      return cache;
163    },
164    set(value) {
165      cache = value;
166      subject.notify(key);
167    }
168  });
169}
170