import { MEDIA_PREFERS_REDUCED_MOTION } from '../../constants/media';
import { CREATED, DESTROYED } from '../../constants/states';
import { EventBinder } from '../../constructors';
import { Splide } from '../../core/Splide/Splide';
import { BaseComponent, Components, Options } from '../../types';
import { merge, omit, ownKeys } from '../../utils';
import { EVENT_UPDATED } from '../../constants/events';


/**
 * The interface for the Media component.
 *
 * @since 4.0.0
 */
export interface MediaComponent extends BaseComponent {
  /** @internal */
  reduce( reduced: boolean ): void;
  set( options: Options, base?: boolean, notify?: boolean ): void;
}

/**
 * The component for observing media queries and updating options if necessary.
 * This used to be the Options component.
 *
 * @since 4.0.0
 *
 * @param Splide     - A Splide instance.
 * @param Components - A collection of components.
 * @param options    - Options.
 *
 * @return A Media component object.
 */
export function Media( Splide: Splide, Components: Components, options: Options ): MediaComponent {
  const { state } = Splide;
  const breakpoints   = options.breakpoints || {};
  const reducedMotion = options.reducedMotion || {};
  const binder        = EventBinder();

  /**
   * Stores options and MediaQueryList object.
   */
  const queries: Array<[ Options, MediaQueryList ]> = [];

  /**
   * Called when the component is constructed.
   */
  function setup(): void {
    const isMin = options.mediaQuery === 'min';

    ownKeys( breakpoints )
      .sort( ( n, m ) => isMin ? +n - +m : +m - +n )
      .forEach( key => {
        register( breakpoints[ key ], `(${ isMin ? 'min' : 'max' }-width:${ key }px)` );
      } );

    register( reducedMotion, MEDIA_PREFERS_REDUCED_MOTION );
    update();
  }

  /**
   * Destroys the component.
   *
   * @param completely - Will be `true` for complete destruction.
   */
  function destroy( completely: boolean ): void {
    if ( completely ) {
      binder.destroy();
    }
  }

  /**
   * Registers entries as [ Options, media query string ].
   *
   * @param options - Options merged to current options when the document matches the query.
   * @param query   - A query string.
   */
  function register( options: Options, query: string ): void {
    const queryList = matchMedia( query );
    binder.bind( queryList, 'change', update );
    queries.push( [ options, queryList ] );
  }

  /**
   * Checks all media queries in entries and updates options.
   */
  function update(): void {
    const destroyed = state.is( DESTROYED );
    const direction = options.direction;
    const merged = queries.reduce<Options>( ( merged, entry ) => {
      return merge( merged, entry[ 1 ].matches ? entry[ 0 ] : {} );
    }, {} );

    omit( options );
    set( merged );

    if ( options.destroy ) {
      Splide.destroy( options.destroy === 'completely' );
    } else if ( destroyed ) {
      destroy( true );
      Splide.mount();
    } else {
      direction !== options.direction && Splide.refresh();
    }
  }

  /**
   * Disables or enables `reducedMotion` options.
   * This method does nothing when the document does not match the query.
   *
   * @internal
   *
   * @param enable - Determines whether to apply `reducedMotion` options or not.
   */
  function reduce( enable: boolean ): void {
    if ( matchMedia( MEDIA_PREFERS_REDUCED_MOTION ).matches ) {
      enable ? merge( options, reducedMotion ) : omit( options, ownKeys( reducedMotion ) );
    }
  }

  /**
   * Sets current options or base options (prototype).
   * If changing base options, always emits the `updated` event.
   *
   * @internal
   *
   * @param opts   - New options.
   * @param base   - Optional. Determines whether to also update base options or not.
   * @param notify - Optional. If `true`, always emits the `update` event.
   */
  function set( opts: Options, base?: boolean, notify?: boolean ): void {
    merge( options, opts );
    base && merge( Object.getPrototypeOf( options ), opts );

    if ( notify || ! state.is( CREATED ) ) {
      Splide.emit( EVENT_UPDATED, options );
    }
  }

  return {
    setup,
    destroy,
    reduce,
    set,
  };
}
