/**
 * @license
 * Copyright 2021 Google LLC
 * SPDX-License-Identifier: Apache-2.0
 */
import {BehaviorSubject, Observable, Subscription} from 'rxjs';
import {Finalizable} from '../services/registry';
import {deepEqual} from '../utils/deep-util';

/**
 * A Model stores a value <T> and controls changes to that value via `subject$`
 * while allowing others to subscribe to value updates via the `state$`
 * Observable.
 *
 * Typically a given Model subclass will provide:
 *   1. An initial value. If there is no good default to start with, then
 *      include `undefined` in the type `T`.
 *   2. "reducers": functions for users to request changes to the value
 *   3. "selectors": convenient sub-Observables that only contain updates for a
 *      nested property from the value
 *
 *  Any new subscriber will immediately receive the current value.
 */
export abstract class Model<T> implements Finalizable {
  /**
   * rxjs does not like `next()` being called on a subject during processing of
   * another `next()` call. So make sure that state updates complete before
   * starting another one.
   */
  private stateUpdateInProgress = false;

  private subject$: BehaviorSubject<T>;

  public state$: Observable<T>;

  protected subscriptions: Subscription[] = [];

  constructor(initialState: T) {
    this.subject$ = new BehaviorSubject(initialState);
    this.state$ = this.subject$.asObservable();
  }

  getState() {
    return this.subject$.getValue();
  }

  setState(state: T) {
    if (this.stateUpdateInProgress) {
      setTimeout(() => this.setState(state));
      return;
    }
    if (deepEqual(state, this.getState())) return;
    try {
      this.stateUpdateInProgress = true;
      this.subject$.next(state);
    } finally {
      this.stateUpdateInProgress = false;
    }
  }

  updateState(state: Partial<T>) {
    if (this.stateUpdateInProgress) {
      setTimeout(() => this.updateState(state));
      return;
    }
    this.setState({...this.getState(), ...state});
  }

  finalize() {
    this.subject$.complete();
    for (const s of this.subscriptions) {
      s.unsubscribe();
    }
    this.subscriptions = [];
  }
}
