import {subscriberCollection} from 'aurelia-binding';
import {TaskQueue} from 'aurelia-task-queue';

/**
* An implementation of Aurelia's Observer interface that is used to back bindable properties defined on a behavior.
*/
@subscriberCollection()
export class BehaviorPropertyObserver {

  /** @internal */
  taskQueue: TaskQueue;

  /** @internal */
  obj: object;

  /** @internal */
  propertyName: string;

  /** @internal */
  notqueued: boolean;

  /** @internal */
  publishing: boolean;

  /** @internal */
  selfSubscriber: Function;

  /** @internal */
  private currentValue: any;

  /** @internal */
  private oldValue: any;

  /**
  * Creates an instance of BehaviorPropertyObserver.
  * @param taskQueue The task queue used to schedule change notifications.
  * @param obj The object that the property is defined on.
  * @param propertyName The name of the property.
  * @param selfSubscriber The callback function that notifies the object which defines the properties, if present.
  * @param initialValue The initial value of the property.
  */
  constructor(taskQueue: TaskQueue, obj: object, propertyName: string, selfSubscriber: Function, initialValue?: any) {
    this.taskQueue = taskQueue;
    this.obj = obj;
    this.propertyName = propertyName;
    this.notqueued = true;
    this.publishing = false;
    this.selfSubscriber = selfSubscriber;
    this.currentValue = this.oldValue = initialValue;
  }

  /**
  * Gets the property's value.
  */
  getValue(): any {
    return this.currentValue;
  }

  /**
  * Sets the property's value.
  * @param newValue The new value to set.
  */
  setValue(newValue: any): void {
    let oldValue = this.currentValue;

    if (!Object.is(newValue, oldValue)) {
      this.oldValue = oldValue;
      this.currentValue = newValue;

      if (this.publishing && this.notqueued) {
        if (this.taskQueue.flushing) {
          this.call();
        } else {
          this.notqueued = false;
          this.taskQueue.queueMicroTask(this);
        }
      }
    }
  }

  /**
  * Invoked by the TaskQueue to publish changes to subscribers.
  */
  call(): void {
    let oldValue = this.oldValue;
    let newValue = this.currentValue;

    this.notqueued = true;

    if (Object.is(newValue, oldValue)) {
      return;
    }

    if (this.selfSubscriber) {
      this.selfSubscriber(newValue, oldValue);
    }

    this.callSubscribers(newValue, oldValue);
    this.oldValue = newValue;
  }

  // eslint-disable-next-line @typescript-eslint/no-unused-vars
  callSubscribers(newValue: any, oldValue: any) {
    throw new Error('Method not implemented.');
  }

  /**
  * Subscribes to the observerable.
  * @param context A context object to pass along to the subscriber when it's called.
  * @param callable A function or object with a "call" method to be invoked for delivery of changes.
  */
  subscribe(context: any, callable: Function): void {
    this.addSubscriber(context, callable);
  }

  // eslint-disable-next-line @typescript-eslint/no-unused-vars
  addSubscriber(context: any, callable: Function) {
    throw new Error('Method not implemented.');
  }

  /**
  * Unsubscribes from the observerable.
  * @param context The context object originally subscribed with.
  * @param callable The callable that was originally subscribed with.
  */
  unsubscribe(context: any, callable: Function): void {
    this.removeSubscriber(context, callable);
  }
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
  removeSubscriber(context: any, callable: Function) {
    throw new Error('Method not implemented.');
  }
}
