import { Subject, Observable } from 'rxjs';
import { share } from 'rxjs/operators';
const dynamics = require('dynamics.js');

export type MoveType =
  | 'spring'
  | 'bounce'
  | 'forceWithGravity'
  | 'gravity'
  | 'easeInOut'
  | 'easeIn'
  | 'easeOut'
  | 'linear'
  | 'bezier';

export type MoveObservable = Observable<MoveTargetProps>;
export type MoveTargetProps = { [key: string]: number | string };
export interface IMoveOptions {
  type: MoveType;
  duration: number;
  frequency?: number;
  friction?: number;
  bounciness?: number;
  delay?: number;
  anticipationSize?: number;
  anticipationStrength?: number;
}

export interface IMoveObservableOptions extends IMoveOptions {
  target: MoveTargetProps;
  current: () => object;
}

/**
 * Animates an HtmlElement or object to the given set of property values.
 * See:
 *    https://github.com/michaelvillar/dynamics.js#usage
 *
 * Example:
 *
 *     private animate(target: { value1: number; value2: number }) {
 *       this.stop$.next();  // Stop currently executing animation (if any).
 *       const current = () => this.state;
 *       const duration = 200;
 *       animation
 *         .start({ target, current, duration, type: 'easeInOut' })
 *         .takeUntil(this.stop$)
 *         .subscribe({
 *           next: data => this.setState(data as any),
 *           complete: () => {
 *              // Done.
 *           },
 *         });
 *     }
 *
 */
function animate(
  target: MoveTargetProps | HTMLElement,
  props: object,
  options: IMoveOptions,
) {
  return new Promise((resolve, reject) => {
    const type = dynamics[options.type];
    const args = {
      ...options,
      type,
      complete: () => resolve(),
    };
    dynamics.animate(target, props, args);
  });
}

/**
 * Animates to a target set of values via an Observable.
 */
export function start(options: IMoveObservableOptions) {
  const { target } = options;
  const subject = new Subject<MoveTargetProps>();

  const obj = {};
  const define = (key: string) => {
    Object.defineProperty(obj, key, {
      get: () => options.current()[key],
      set: (value: any) => subject.next({ [key]: value }),
    });
  };
  Object.keys(target).forEach(key => define(key));

  setTimeout(async () => {
    // NB: Allow for the Observable to be returned to
    //     the call-site before starting the animation.
    await animate(obj, target, options);
    subject.complete();
  }, 0);
  return subject.pipe(share()) as MoveObservable;
}
