import { Result } from 'space-lift'

/**
 * Either a Promise value or an Error thrown by the promise.
 */
export type PromiseResult<T> =
  { type: 'success', value: T } |
  { type: 'failure', error: {} }


export interface Observable<T> {

  __T: T

  /**
   * Subscribes to this observable values and returns a function that may be used to unsubscribe.
   */
  subscribe: (onValue: (val: T) => void) => () => void | void
  /**
   * Gives a debug name to the observable.
   * names are inherited by observable transforms unless a new name is defined downstream.
   */
  named: (name: string) => this

  /**
   * Reads the current value of the observable or returns undefined if no value was ever pushed in the observable.
   */
  (): T | undefined

  /**
   * Delays values until a certain amount of silence has passed.
   * Values in between silence periods are discarded.
   */
  debounce(wait: number): Observable<T>

  /**
   * Delays all values by a fixed time offset
   */
  delay(delay: number): Observable<T>

  /**
   * Creates a new Observable with adjacent repeated values removed.
   * A compare function can optionally be passed to implement user-defined equality instead of strict reference equality.
   * The functions should return true if the two values are equal.
   */
  distinct(compareFunction?: (previousValue: T, currentValue: T) => boolean): Observable<T>

  /**
   * Drops 'count' initial values.  
   * Note: This can also be used to drop the initial value when subscribing to an Observable, if it had seen a value previously.
   */
  drop(count: number): Observable<T>

  /**
   * Filters the values of this observable.
   */
  filter(predicate: (t: T) => boolean): Observable<T>

  /*
   * Maps and flattens this observable then only publish values from the observable mapped last.
   */
  flatMapLatest<B>(mapFn: (t: T) => Observable<B>): Observable<B>

  /**
   * Maps the values of this observable.
   */
  map<B>(mapFn: (t: T) => B): Observable<B>

  /**
   * Partitions this observable into two observables based on a predicate
   */
  partition<T>(predicate: (value: T) => boolean): [Observable<T>, Observable<T>]

  /**
   * Groups values in fixed size blocks (of size 2) by passing a "sliding window" over them.
   * An array becomes the value of the new observable. It will have a size of 1 the first time a value is produced,
   * then a size of 2 for subsequent values.
   * The newest value is always found at the index 0 of the Array for convenience and type safety.
   */
  sliding2(): Observable<[T, T | undefined]>

  /**
   * Groups values in fixed size blocks by passing a "sliding window" over them.
   * An array becomes the value of the new observable. It will have a size of 1 the first time a value is produced,
   * then an increasing size of max (maxWindowSize parameter) for subsequent values.
   * The newest value is always found at the index 0 of the Array for convenience (...rest parameters).
   */
  sliding(maxWindowSize: number): Observable<Array<T>>

  /**
   * Take the first 'count' values of this observable.
   */
  take(count: number): Observable<T>

  /**
   * Delays values so that values are produced at most once per every 'time' milliseconds
   */
  throttle(time: number): Observable<T>
}

interface SourceObservable<T> extends Observable<T> {
  /**
   * Sets the value of this observable.
   * The observable is returned for convenient construction with an initial value.
   */
  (value: T): SourceObservable<T>
}

/**
 * A special observable that is garanteed to have an initial value.
 */
export interface ObservableWithInitialValue<T> extends Observable<T> {
  /**
   * Reads the current value of the observable.
   */
  (): T
}

type UnsubFunction = void | (() => void)

export interface ObservableObject {
  /** Creates a new Observable */
  <T>(activate?: (add: (t: T) => void) => UnsubFunction): SourceObservable<T>

  /**
   * Listens for DOM events at the specified parent element.
   * a childSelector can optionally be passed to listen to delegated events instead of direct events.
   */
  fromEvent(name: 'blur', el: Element | EventTarget, childSelector?: string): Observable<FocusEvent>
  fromEvent(name: 'click', el: Element | EventTarget, childSelector?: string): Observable<MouseEvent>
  fromEvent(name: 'dblclick', el: Element | EventTarget, childSelector?: string): Observable<MouseEvent>
  fromEvent(name: 'drag', el: Element | EventTarget, childSelector?: string): Observable<DragEvent>
  fromEvent(name: 'dragend', el: Element | EventTarget, childSelector?: string): Observable<DragEvent>
  fromEvent(name: 'dragenter', el: Element | EventTarget, childSelector?: string): Observable<DragEvent>
  fromEvent(name: 'dragexit', el: Element | EventTarget, childSelector?: string): Observable<DragEvent>
  fromEvent(name: 'dragleave', el: Element | EventTarget, childSelector?: string): Observable<DragEvent>
  fromEvent(name: 'dragover', el: Element | EventTarget, childSelector?: string): Observable<DragEvent>
  fromEvent(name: 'dragstart', el: Element | EventTarget, childSelector?: string): Observable<DragEvent>
  fromEvent(name: 'drop', el: Element | EventTarget, childSelector?: string): Observable<DragEvent>
  fromEvent(name: 'focus', el: Element | EventTarget, childSelector?: string): Observable<FocusEvent>
  fromEvent(name: 'keydown', el: Element | EventTarget, childSelector?: string): Observable<KeyboardEvent>
  fromEvent(name: 'keyup', el: Element | EventTarget, childSelector?: string): Observable<KeyboardEvent>
  fromEvent(name: 'mousedown', el: Element | EventTarget, childSelector?: string): Observable<MouseEvent>
  fromEvent(name: 'mouseenter', el: Element | EventTarget, childSelector?: string): Observable<MouseEvent>
  fromEvent(name: 'mouseleave', el: Element | EventTarget, childSelector?: string): Observable<MouseEvent>
  fromEvent(name: 'mousemove', el: Element | EventTarget, childSelector?: string): Observable<MouseEvent>
  fromEvent(name: 'mouseout', el: Element | EventTarget, childSelector?: string): Observable<MouseEvent>
  fromEvent(name: 'mouseup', el: Element | EventTarget, childSelector?: string): Observable<MouseEvent>
  fromEvent(name: 'mousewheel', el: Element | EventTarget, childSelector?: string): Observable<MouseWheelEvent>
  fromEvent(name: 'scroll', el: Element | EventTarget, childSelector?: string): Observable<UIEvent>
  fromEvent(name: 'touchcancel', el: Element | EventTarget, childSelector?: string): Observable<TouchEvent>
  fromEvent(name: 'touchend', el: Element | EventTarget, childSelector?: string): Observable<TouchEvent>
  fromEvent(name: 'touchmove', el: Element | EventTarget, childSelector?: string): Observable<TouchEvent>
  fromEvent(name: 'touchstart', el: Element | EventTarget, childSelector?: string): Observable<TouchEvent>
  fromEvent(name: string, el: Element | EventTarget, childSelector?: string): Observable<Event>

  /**
   * Creates a new Observable from a Promise. The observable will produce only one value:
   * Either an Ok, or an Err (see space-lift/result) object based on the result of the Promise.
   */
  fromPromise<T, E = never>(promise: Promise<T>): Observable<Result<E, T>>

  /**
   * Creates a new observable that produces undefined values at the provided interval in milliseconds.
   */
  interval(time: number): Observable<undefined>

   /**
    * Merges all the observables into one
    */
  merge<T>(...obss: Array<Observable<T>>): Observable<T>

   /**
    * Creates a new observable that produces one value immediately.
    */
  pure<A>(value: A): Observable<A>
}

/**
 * Creates a new observable.
 *
 * The fist argument function is called every time the observable goes
 * from inactive (no subscribers) to active (the first subscriber just registered)
 *
 * This function can optionally return a function that will be called when the last subscriber unregisters itself from the observable.
 * Use this as an opportunity to clean up the observable.
 */
export const Observable: ObservableObject