/**
 * Owl Observer
 *
 * This code contains the logic that allows Owl to observe and react to state
 * changes.
 *
 * This is a Observer class that can observe any JS values.  The way it works
 * can be summarized thusly:
 * - primitive values are not observed at all
 * - Objects and arrays are observed by replacing them with a Proxy
 * - each object/array metadata are tracked in a weakmap, and keep a revision
 *   number
 *
 * Note that this code is loosely inspired by Vue.
 */

//------------------------------------------------------------------------------
// Observer
//------------------------------------------------------------------------------
export class Observer {
  rev: number = 1;
  allowMutations: boolean = true;
  dirty: boolean = false;
  weakMap: WeakMap<any, any> = new WeakMap();

  notifyCB() {}
  async notifyChange() {

    this.dirty = true;
    await Promise.resolve();
    if (this.dirty) {
      this.dirty = false;
      this.notifyCB();
    }
  }

  observe(value: any, parent?: any): any {
    if (value === null || typeof value !== "object" || value instanceof Date) {
      // fun fact: typeof null === 'object'
      return value;
    }
    let metadata = this.weakMap.get(value) || this._observe(value, parent);
    return metadata.proxy;
  }

  revNumber(value): number {
    const metadata = this.weakMap.get(value);
    return metadata ? metadata.rev : 0;
  }
  deepRevNumber(value): number {
    const metadata = this.weakMap.get(value);
    return metadata ? metadata.deepRev : 0;
  }

  _observe(value, parent) {
    var self = this;

    const proxy = new Proxy(value, {
      get(target, k) {
        const targetValue = target[k];
        return self.observe(targetValue, value);
      },
      set(target, key: string, newVal): boolean {
        const value = target[key];
        if (newVal !== value) {
          if (!self.allowMutations) {
            throw new Error(
              `Observed state cannot be changed here! (key: "${key}", val: "${newVal}")`
            );
          }
          self._updateRevNumber(target);
          target[key] = newVal;
          self.notifyChange();
        }
        return true;
      },
      deleteProperty(target, key) {
        if (key in target) {
          delete target[key];
          self._updateRevNumber(target);
          self.notifyChange();
        }
        return true;
      }
    });

    const metadata = {
      value,
      proxy,
      rev: this.rev,
      deepRev: this.rev,
      parent
    };

    this.weakMap.set(value, metadata);
    this.weakMap.set(metadata.proxy, metadata);
    return metadata;
  }

  _updateRevNumber(target: any) {
    this.rev++;
    let metadata = this.weakMap.get(target);
    metadata.rev!++;
    let parent = target;
    do {
      metadata = this.weakMap.get(parent);
      metadata.deepRev++;
    } while ((parent = metadata.parent) && parent !== target);
  }
}
