import { Elements, EventBusEvent, KeyMatcher } from '@ryusei/code';
import { Component } from '../../classes/Component/Component';
import { EVENT_KEYDOWN, EVENT_KEYMAP } from '../../constants/events';
import { KEYMAP, MODIFIER_KEYS } from '../../constants/keymap';
import { assign, forOwn, includes, isArray, isMac, isString, matchesKey, normalizeKey, toArray } from '../../utils';


/**
 * The component for detecting keyboard shortcuts and distributing them as internal events.
 *
 * @since 0.1.0
 */
export class Keymap extends Component {
  /**
   * Stores the target keys.
   */
  protected keys: string[] = [];

  /**
   * The collection of shortcuts.
   */
  protected keymap: Record<string, KeyMatcher| KeyMatcher[] | null | false>;

  /**
   * Initializes the component.
   *
   * @param elements - A collection of essential elements.
   */
  mount( elements: Elements ): void {
    super.mount( elements );

    this.keymap = assign( {}, KEYMAP, this.options.keymap );

    forOwn( this.keymap, matchers => {
      if ( matchers ) {
        this.keys.push( ...toArray( matchers, true ).map( matcher => {
          return matcher[ 0 ].toUpperCase();
        } ) );
      }
    } );

    this.on( EVENT_KEYDOWN, this.onKeydown, this, 0 );
  }

  /**
   * Called when any key is pressed.
   *
   * @param e  - An EventBusEvent object.
   * @param ke - A KeyboardEvent object.
   */
  protected onKeydown( e: EventBusEvent, ke: KeyboardEvent ): void {
    if ( ! this.Editor.readOnly ) {
      if ( includes( this.keys, normalizeKey( ke.key ).toUpperCase() ) ) {
        const action = this.find( ke );

        if ( action ) {
          this.emit( `${ EVENT_KEYMAP }:${ action }`, ke, action );
        }
      }
    }
  }

  /**
   * Finds the shortcut action from keymap definition.
   *
   * @param e - A KeyboardEvent object.
   *
   * @return A found action.
   */
  protected find( e: KeyboardEvent ): string {
    let action  = '';

    forOwn( this.keymap, ( matchers, id ) => {
      if ( this.matches( e, id ) ) {
        action = id;
        return false;
      }
    } );

    return action;
  }

  /**
   * Checks if the keyboard event matches keys of the provided ID or not.
   *
   * @param e  - A KeyboardEvent object.
   * @param id - An ID.
   *
   * @return `true` if the keyboard event matches keys of the ID, or otherwise `false`.
   */
  matches( e: KeyboardEvent, id: string ): boolean {
    const matchers = this.keymap[ id ];
    return matchers && matchesKey( e, matchers );
  }

  /**
   * Builds a shortcut that describes keys of the provided keymap ID or a KeyMatcher object.
   * For example, `undo` or `[ 'Z', true ]` will be `Ctrl + Z`.
   *
   * @param id - An ID in a keymap or a KeyMatcher object.
   *
   * @return A built shortcut as a string.
   */
  getShortcut( id: string | KeyMatcher ): string {
    const matchers = isString( id ) ? this.keymap[ id ] : id;

    if ( matchers ) {
      const matcher = isArray( matchers[ 0 ] ) ? matchers[ 0 ] : matchers as KeyMatcher;

      if ( matcher ) {
        const modifiers = MODIFIER_KEYS[ isMac() ? 'mac' : 'default' ];
        const keys      = matcher.slice( 1 ).map( ( use, index ) => use && modifiers[ index ] ).filter( Boolean );
        return keys.concat( matcher[ 0 ] ).join( '+' );
      }
    }

    return '';
  }
}