import React from 'react';
import {EnNavigationApi} from 'ernnavigation-api';
import AppNavigator from './app-navigator';

/**
 * @class Component
 * @description
 * <u><b>NOTE</b></u>:<br>
 * If overriding <code>componentWillUnmount</code> or <code>componentWillUpdate</code>, you <u><b>must</b></u> call the
 * appropriate super method - <code>super.componentWillUnmount()</code> or
 * <code>super.componentWillUpdate(nextProps, nextState)</code>, respectively.
 * @extends React.Component
 * @property autoReset=true {boolean} - (static) Whether to automatically reset the navigation bar upon component display. (defaults to <code>true</code>)
 * @property navigationOptions {NavigationBar} - (static) The navigation bar for this component.  Defaults to a title of "Untitled" with no right {@link Button}s.
 * @example
 * import {Component} from 'ern-navigation';
 *
 * export default class MainScreenComponent extends Component {
 *   static displayName = 'Main Screen';
 *   static autoReset = true;
 *   static navigationOptions = {
 *     title: 'My Application',
 *     buttons: [
 *       {
 *         icon: Image.resolveAssetSource(exitIcon).uri,
 *         id: 'exit',
 *         location: 'right',
 *         adaLabel: 'Exit this app',
 *       },
 *     ],
 *   };
 *
 *   onNavButtonPress(buttonId) {
 *     switch (buttonId) {
 *       case 'exit':
 *         this.finish();
 *         break;
 *       default:
 *         console.warn(
 *           `'${buttonId}' not handled in '${MainScreenComponent.getRegisteredRoute()}'`,
 *         );
 *         break;
 *     }
 *   }
 * }
 */

/**
 * @typedef {Object} NavigationBar
 * @property {string} title - The title for the navigation bar.
 * @property {?boolean} overlay - (optional) Show this page as an overlay (navigate only).
 * @property {?boolean} hide - (optional) Hide this page's navigation bar.
 * @property {Button[]} buttons - The {@link Button}s to display on the right side of the navigation bar.
 * @property {?LeftButton} leftButton - The {@link LeftButton} to display on the left side of the navigation bar.
 */

/**
 * @typedef {Object} RouteOptions
 * @property {?boolean} overlay - (optional) Show this page as an overlay (navigate only).
 * @property {?boolean} replace - (optional) Replace the current page with this page in the stack.
 */

/**
 * @typedef {Object} Button
 * @property {?string} icon - The location of the icon (use <code>Image.resolveAssetSource(iconFile).uri</code>)
 * or the name of a built-in icon.
 * @property {?string} title - The title for the button; will be used in case of missing or invalid icon.
 * @property {!string} id - The ID of the button; will be used in header button events.  Cannot contain '.'.
 * @property {?string} adaLabel - The text to read out with screen-reader technology.
 */

/**
 * @typedef {Object} LeftButton
 * @property {?string} icon - The location of the icon (use <code>Image.resolveAssetSource(iconFile).uri</code>)
 * or the name of a built-in icon.
 * @property {?string} title - The title for the button (iOS only).
 * @property {?string} id - The ID of the button; will be used in header button events.  If set, the press event
 * must be handled on the JavaScript side, as native will no longer handle the back press.  Cannot contain '.'.
 * @property {?string} adaLabel - The text to read out with screen-reader technology.
 */

/**
 * @typedef {Object} NavigationEvent
 * @property {('BUTTON_CLICK' | 'DID_FOCUS' | 'DID_BLUR' | 'APP_DATA')} eventType - The type of the event.
 * @property {string} viewId - The UUID for the view on which the event was fired.
 * @property {?string} jsonPayload - The payload for the event as stringified JSON.
 */

export const errors = {
  invalidAppNavigator: new Error(
    'The appNavigator has not been set or is not a valid instance of AppNavigator.  Internal navigation is not available.',
  ),
  noValidScreens: new Error(
    'No valid screens have been set.  Internal navigation is not available.',
  ),
  noScreenName: new Error('The screenName is required'),
  invalidScreenName: (screenName) =>
    new Error(`'${screenName}' is not a valid screen name.`),
};

class Component extends React.Component {
  static appNavigator = undefined;
  static headerListener = undefined;
  static displayName = 'Component';
  static route = '';
  static autoReset = true;
  static navigationOptions = {
    title: 'Untitled',
    buttons: [],
  };

  constructor(props) {
    super(props);
    this.viewId = this.props.viewId;
    this.jsonProps = props.jsonPayload ? JSON.parse(props.jsonPayload) : {};
    if (this.viewId && !this.headerListener) {
      this.headerListener = EnNavigationApi.events().addNavEventEventListener(
        this.constructor._handleNavEvent.bind(this),
      );
    }
    if (this.constructor.autoReset) {
      this.resetNavigationBar();
    }
  }

  componentWillUnmount() {
    if (this.headerListener) {
      EnNavigationApi.events().removeNavEventEventListener(this.headerListener);
    }
  }

  componentWillUpdate(nextProps) {
    if (this.props.jsonPayload !== nextProps.jsonPayload) {
      this.jsonProps = nextProps.jsonPayload
        ? JSON.parse(nextProps.jsonPayload)
        : {};
    }
  }

  /**
   * Set the registered route for this component.
   *
   * @static
   * @param {string} route - The registered route for this component.
   */
  static setRegisteredRoute(route) {
    if (!this.route) {
      this.route = route;
    } else {
      console.warn(
        `This component has already been registered as '${this.route}'.  Not re-registering as '${route}'.`,
      );
    }
  }

  /**
   * Get the registered route for this component.
   *
   * @static
   * @returns {string} A string containing the registered route for this component.
   */
  static getRegisteredRoute() {
    return this.route;
  }

  /**
   * Set the {@link AppNavigator} for this component.
   *
   * @param {AppNavigator} appNavigator - The {@link AppNavigator} for this component.
   */
  static setAppNavigator(appNavigator) {
    this.appNavigator = appNavigator;
  }

  /**
   * Get the {@link AppNavigator} for this component.
   *
   * @returns {AppNavigator} The {@link AppNavigator} for this component.
   */
  static getAppNavigator() {
    return this.appNavigator;
  }

  /**
   * Dispatch events for this component.  This is called automatically whenever an event
   * is fired by the OnNavEventEventListener.
   *
   * @private
   * @param {NavigationEvent} event - The navigation event.
   */
  static _handleNavEvent(event) {
    if (event && event.viewId === this.viewId) {
      const payload = event.jsonPayload ? JSON.parse(event.jsonPayload) : {};
      switch (event.eventType) {
        case 'BUTTON_CLICK':
          this.constructor._handleButtonEvent.bind(this)(payload);
          break;
        case 'DID_FOCUS':
          this.constructor._handleFocusEvent.bind(this)(payload);
          break;
        case 'DID_BLUR':
          this.constructor._handleBlurEvent.bind(this)(payload);
          break;
        case 'APP_DATA':
          this.constructor._handleAppDataEvent.bind(this)(payload);
          break;
        default:
          console.warn('Received invalid event: ', event);
          break;
      }
    }
  }

  /**
   * Make a call to <code>onNavButtonPress(buttonId)</code> (if available) whenever a button is
   * pressed in the navigation bar.  This is called automatically whenever an event
   * is fired by the OnNavEventEventListener.
   *
   * If a subclassed instance contains the <code>onNavButtonPress</code> method, that will be
   * called on button press events, otherwise the class's static method will be called.
   *
   * @private
   * @param {string} payload - The stringified JSON payload for the event.
   */
  static _handleButtonEvent(payload) {
    const handler = this.onNavButtonPress || this.constructor.onNavButtonPress;
    if (handler) {
      handler.bind(this)(payload.id);
    }
  }

  /**
   * Make a call to <code>onFocus()</code> (if available) whenever this view receives focus.
   *
   * If a subclassed instance contains the <code>onFocus</code> method, it will be
   * called on focus events.
   *
   * @private
   * @param {string} payload - The stringified JSON payload for the event.
   */
  static _handleFocusEvent(payload) {
    if (this.onFocus) {
      this.onFocus.bind(this)();
    }
  }

  /**
   * Make a call to <code>onBlur()</code> (if available) whenever this view loses focus.
   *
   * If a subclassed instance contains the <code>onBlur</code> method, it will be
   * called on blur events.
   *
   * @private
   * @param {string} payload - The stringified JSON payload for the event.
   */
  static _handleBlurEvent(payload) {
    if (this.onBlur) {
      this.onBlur.bind(this)();
    }
  }

  /**
   * Make a call to <code>onAppData()</code> (if available) whenever this view receives data.
   *
   * If a subclassed instance contains the <code>onAppData</code> method, it will be
   * called on appData events.
   *
   * @private
   * @param {string} payload - The stringified JSON payload for the event.
   */
  static _handleAppDataEvent(payload) {
    if (this.onAppData) {
      this.onAppData.bind(this)(payload);
    }
  }

  /**
   * Get the navigation bar for the given route.
   *
   * @private
   * @static
   * @param {Object} jsonPayload - The JSON payload for the current route.
   * @returns {NavigationBar} A {@link NavigationBar} object for the given route.
   */
  static _getNavigationBar(jsonPayload) {
    return {
      ...this.navigationOptions,
      title:
        this.getDynamicTitle(jsonPayload) ||
        (this.navigationOptions || {}).title,
    };
  }

  /**
   * Calculate the title for the current route based on the JSON payload.
   * Must be overridden in subclasses.
   *
   * @abstract
   * @static
   * @param {Object} jsonPayload - The JSON payload for the current route.
   */
  static getDynamicTitle(jsonPayload) {}

  /**
   * Handle button press events.
   * Must be overridden in subclasses.
   *
   * @abstract
   * @static
   * @param {string} buttonId - The ID of the button which was pressed.
   */
  static onNavButtonPress(buttonId) {
    console.warn(
      `\`onNavButtonPress(buttonId)\` was not overridden in ${this.constructor.name}, but a button press event was fired.`,
      {buttonId},
    );
  }

  /**
   * Handle focus events.
   *
   * @abstract
   * @static
   */
  static onFocus() {}

  /**
   * Handle blur events.
   *
   * @abstract
   * @static
   */
  static onBlur() {}

  /**
   * Handle appData events.
   *
   * @abstract
   * @static
   */
  static onAppData() {}

  /**
   * Reset the navigation bar for the current screen to its defaults.
   *
   * @async
   * @instance
   * @return {Promise} A <code>Promise</code> which will resolve or reject upon attempting to
   * reset the navigation bar.
   */
  resetNavigationBar() {
    return this.updateNavigationBar(
      this.constructor._getNavigationBar(this.jsonProps),
    );
  }

  /**
   * Update the navigation bar for the current screen.
   *
   * @async
   * @instance
   * @param {NavigationBar} navigationBar - The {@link NavigationBar} object.
   * @return {Promise} A <code>Promise</code> which will resolve or reject upon attempting to
   * update the navigation bar.
   */
  updateNavigationBar(navigationBar) {
    const routePayload = {
      path: this.constructor.route,
      navigationBar,
    };
    return EnNavigationApi.requests().update(routePayload);
  }

  /**
   * Navigate to a given route.
   *
   * @async
   * @instance
   * @param {Object} route - The route object that details where to navigate next.
   * @return {Promise} A <code>Promise</code> which will resolve or reject upon attempting to
   * navigate to the given route.
   */
  navigate(route) {
    return EnNavigationApi.requests().navigate(route);
  }

  /**
   * Navigate to an internal screen.
   *
   * @async
   * @instance
   * @param {string} screenName - The name of the screen to navigate to; these names
   * should be defined in the initial {@link AppNavigator} setup.
   * @param {Object} [jsonPayload] - (optional) The JSON payload with props to send to the new
   * screen.
   * @param {RouteOptions} [options] - (optional) Additional route options.
   * @return {Promise} A <code>Promise</code> which will resolve or reject upon attempting to
   * navigate to the new screen.
   */
  navigateInternal(screenName, jsonPayload, options = {}) {
    if (!this.constructor.getAppNavigator()) {
      throw errors.invalidAppNavigator;
    }
    if (!(this.constructor.getAppNavigator() instanceof AppNavigator)) {
      throw errors.invalidAppNavigator;
    }
    if (
      !this.constructor.getAppNavigator().screens ||
      Object.keys(this.constructor.getAppNavigator().screens).length < 1
    ) {
      throw errors.noValidScreens;
    }
    if (!screenName) {
      throw errors.noScreenName;
    }
    if (
      !Object.keys(this.constructor.getAppNavigator().screens).includes(
        screenName,
      )
    ) {
      throw errors.invalidScreenName(screenName);
    }

    const nav = this.constructor.getAppNavigator().screens[screenName];
    const {overlay, ...navigationBar} = nav._getNavigationBar(jsonPayload);
    const routePayload = {
      path: nav.getRegisteredRoute(),
      overlay,
      navigationBar,
      ...options,
      jsonPayload: JSON.stringify(jsonPayload || {}),
    };
    return EnNavigationApi.requests().navigate(routePayload);
  }

  /**
   * Go back to a specified screen.
   *
   * @async
   * @instance
   * @param {string} screenName - The name of the screen to navigate to; these names
   * should be defined in the initial {@link AppNavigator} setup.
   * @return {Promise} A <code>Promise</code> which will resolve or reject upon attempting to
   * go back to the specified screen.
   */
  backTo(screenName) {
    if (!this.constructor.getAppNavigator()) {
      throw errors.invalidAppNavigator;
    }
    if (!(this.constructor.getAppNavigator() instanceof AppNavigator)) {
      throw errors.invalidAppNavigator;
    }
    if (
      !this.constructor.getAppNavigator().screens ||
      Object.keys(this.constructor.getAppNavigator().screens).length < 1
    ) {
      throw errors.noValidScreens;
    }
    if (!screenName) {
      throw errors.noScreenName;
    }
    if (
      !Object.keys(this.constructor.getAppNavigator().screens).includes(
        screenName,
      )
    ) {
      throw errors.invalidScreenName(screenName);
    }

    return EnNavigationApi.requests().back({
      path: this.constructor
        .getAppNavigator()
        .screens[screenName].getRegisteredRoute(),
    });
  }

  /**
   * Go back one screen.
   *
   * @async
   * @instance
   * @return {Promise} A <code>Promise</code> which will resolve or reject upon attempting to
   * go back one screen.
   */
  back() {
    return EnNavigationApi.requests().back();
  }

  /**
   * Finish this flow.
   *
   * @async
   * @instance
   * @param {Object} [payload] - (optional) The JSON payload to send to the native activity or view
   * controller that launched the flow.
   * @return {Promise} A <code>Promise</code> which will resolve or reject upon attempting to
   * finish the current flow.
   */
  finish(payload) {
    return EnNavigationApi.requests().finish(JSON.stringify(payload || {}));
  }
}

export default Component;