import * as React from 'react';
import * as PropTypes from 'prop-types';
import { ConnectHost } from './ConnectHost';
import { ConnectIframeInterface } from './ConnectIframeInterface';
import { AppOptions } from '../../definitions/AppContext';
import { ConnectIframeProvider, ConnectIframeContext } from './ConnectIframeProvider';
import { logger } from '../../adaptors/logger/LoggerAdaptor';
import { analytics } from '../../adaptors/analytics/AnalyticsAdaptor';
import IFrameLifecycleEventManager from './IFrameLifecycleEventManager';
import { allowedStrings, AnalyticsCategories, AnalyticsActions } from '../../adaptors/analytics/AnalyticsConstants';

export const LoadingState = Object.freeze({
  INIT: Symbol('init'),
  LOADED: Symbol('loaded'),
  FAILED: Symbol('failed'),
  TIMEOUT: Symbol('timeout'),
  LOADING: Symbol('loading'),
  RESOLVING: Symbol('resolving')
});

module ConnectIframeDefinitions {

  export interface Props {

    connectHost: ConnectHost;
    appKey: string;
    moduleKey: string;
    iframeContainer: new () => React.PureComponent<{ width: string | number | undefined; height: string | number | undefined }, {}>;
    loadingIndicator: typeof React.PureComponent;
    failedToLoadIndicator: typeof React.PureComponent;
    timeoutIndicator: new() => React.PureComponent<{failedCallback: Function}, {}>;
    url: string;
    width: string;
    height: string;
    options: AppOptions;
    connectIframeProvider: ConnectIframeProvider;

  }

  export interface State {

    // State attributes are marked as optional so we can call setState with a subset of attributes.
    width?: string;
    height?: string;
    hostFrameOffset?: number;
    loadingState?: symbol;

  }

}

/**
 * ConnectIframe represents an add-on within a view.
 */
class ConnectIframe extends React.PureComponent<ConnectIframeDefinitions.Props, ConnectIframeDefinitions.State> {

  unmountCallbacks: Function[] = [];
  iframeContext: ConnectIframeContext;
  iframeAttributes: ConnectIframeInterface;
  iframeLifecycleEventManager: IFrameLifecycleEventManager;

  static propTypes: object = {
    connectHost: PropTypes.object.isRequired,
    appKey: PropTypes.string.isRequired,
    moduleKey: PropTypes.string.isRequired,
    iframeContainer: PropTypes.func,
    loadingIndicator: PropTypes.func,
    failedToLoadIndicator: PropTypes.func,
    timeoutIndicator: PropTypes.func,
    url: PropTypes.string,
    width: PropTypes.string,
    height: PropTypes.string,
    options: PropTypes.object,
    connectIframeProvider: PropTypes.object
  };

  static defaultProps: object = {
    options: {} as AppOptions,
    connectIframeProvider: {} as ConnectIframeProvider,
    iframeContainer: ({
      width,
      height,
      children
    }: {
      width: string | number | undefined;
      height: string | number | undefined;
      children: React.ReactNode;
    }) => (
      <div
        style={{
          position: 'relative' as 'relative',
          width: width === '100%' ? '100%' : 'auto',
          height: height === '100%' ? '100%' : 'auto'
        }}>
        {children}
      </div>
    ),
    failedToLoadIndicator: () => null,
    loadingIndicator: () => null,
    timeoutIndicator: () => null
  };

  constructor(props: ConnectIframeDefinitions.Props) {
    super(props);
    this.state = {
      width: props.width,
      height: props.height,
      loadingState: LoadingState.INIT,
      hostFrameOffset: props.options.hostFrameOffset || 1
    };
    this.iframeContext = {
      url: props.url,
      appKey: props.appKey,
      moduleKey: props.moduleKey,
      options: props.options
    } as ConnectIframeContext;
  }

  resize = (width: string, height: string): void => {
    this.setState({width: width, height: height});
  }

  sizeToParent = (): void => {
    this.setState({width: '100%', height: '100%'});
  }

  registerUnmountCallback = (callback: Function): void => {
    this.unmountCallbacks.push(callback);
  }

  hideInlineDialog = (): void => {
    if (this.props.connectIframeProvider.onHideInlineDialog) {
      this.props.connectIframeProvider.onHideInlineDialog();
    }
  }

  getId = (): string|null => {
    if (this.iframeAttributes) {
      return this.iframeAttributes.id;
    } else {
      return null;
    }
  }

  getIFrameLifecycleEventManager = (): IFrameLifecycleEventManager | undefined => {
    return this.iframeLifecycleEventManager;
  }

  iframeEstablishedCallback = (): void => {
    this.setState({loadingState: LoadingState.LOADED});
  }

  iframeFailedToLoadCallback = (): void => {
    this.setState({loadingState: LoadingState.FAILED});
  }

  iframeTimeoutCallback = (): void => {
    this.setState({loadingState: LoadingState.TIMEOUT});
  }

  _createIFrameLifecycleManager = (): void => {
    this.iframeLifecycleEventManager = new IFrameLifecycleEventManager(this);
  }

  _unregisterIFrameLifecycleManager = (): void => {
    if (this.iframeLifecycleEventManager) {
      this.iframeLifecycleEventManager.unregister(this);
    }
  }

  _createExtension = (): void => {
    const simpleXdmExtension = this.props.connectHost.createExtension({
      addon_key: this.iframeContext.appKey,
      key: this.iframeContext.moduleKey,
      url: this.iframeContext.url,
      options: Object.assign({}, this.iframeContext.options, {
        resize: this.resize.bind(this),
        sizeToParent: this.sizeToParent.bind(this),
        registerUnmountCallback: this.registerUnmountCallback.bind(this),
        _contextualOperations: {
          hideInlineDialog: this.hideInlineDialog.bind(this)
        }
      })
    });
    this.iframeAttributes = simpleXdmExtension.iframeAttributes;
    logger.debug('Created iframe for add-on ', this.iframeContext.appKey, this.iframeAttributes);
    analytics.trigger(
      AnalyticsCategories.iframe,
      analytics.markAsSafe(...allowedStrings)(AnalyticsActions.create),
      analytics.dangerouslyCreateSafeString(this.iframeContext.appKey),
      {}
    );
  }

  _destroyExtension = (): void => {
    if (this.iframeAttributes) {
      this.props.connectHost.destroy(this.iframeAttributes.id);
    }
  }

  _isSubIframe = (): boolean => {
    return (this.state.hostFrameOffset && this.state.hostFrameOffset > 1) as boolean;
  }

  _initialise = (): void => {
    if (this.props.children && this.props.connectIframeProvider.onStoreNestedIframeJSON) {
      const childComponent = this.props.children as React.Component<any, any>;
      const childProps = childComponent.props as ConnectIframeDefinitions.Props;
      this.props.connectIframeProvider.onStoreNestedIframeJSON(childProps);
    }

    if (this._isSubIframe()) {
      this.setState({loadingState: LoadingState.LOADING});
      return;
    }

    this._createIFrameLifecycleManager();

    if (!this.props.connectIframeProvider.resolveIframeContext) {
      this._createExtension();
      this.setState({loadingState: LoadingState.LOADING});
      return;
    }

    this.props.connectIframeProvider.resolveIframeContext(this.iframeContext, this.props.connectHost)
      .then(iframeContext => {
        const propsMutated = (iframeContext.appKey !== this.props.appKey) || (iframeContext.moduleKey !== this.props.moduleKey);
        if (propsMutated) {
          logger.warn('Iframe context changed during resolution');
          return;
        }

        this.iframeContext = iframeContext;
        this._createExtension();
        this.setState({loadingState: LoadingState.LOADING});
      });
  }

  componentWillReceiveProps(nextProps: ConnectIframeDefinitions.Props): void {
    if (
      (this.props.appKey !== nextProps.appKey) ||
      (this.props.moduleKey !== nextProps.moduleKey)
    ){
      this._unregisterIFrameLifecycleManager();
      this._destroyExtension();
      this.setState({
        width: nextProps.width,
        height: nextProps.height,
        hostFrameOffset: nextProps.options.hostFrameOffset || 1,
        loadingState: LoadingState.INIT
      });
    }
  }

  componentWillUnmount(): void {
    this.unmountCallbacks.forEach(cb => cb());
    this._unregisterIFrameLifecycleManager();
    this._destroyExtension();
  }

  render(): React.ReactElement<any> | null {
    switch (this.state.loadingState) {
      case LoadingState.LOADED:
      case LoadingState.LOADING:
      case LoadingState.TIMEOUT:
        let iframeStyles;
        // Ask the product for the styles to use...
        if (this.props.connectIframeProvider.buildIframeStyles) {
          iframeStyles = this.props.connectIframeProvider.buildIframeStyles(this.state.loadingState, LoadingState);
        }
        // if iframeStyles is undefined and loadingState is loading/timeout, set the default style
        if (!iframeStyles && (this.state.loadingState === LoadingState.LOADING || this.state.loadingState === LoadingState.TIMEOUT)) {
          iframeStyles = {opacity: 0.0};
        }
        return (
          <this.props.iframeContainer width={this.state.width} height={this.state.height}>
            {this.state.loadingState === LoadingState.TIMEOUT ?
              <this.props.timeoutIndicator failedCallback={this.iframeFailedToLoadCallback.bind(this)} /> : null
            }
            {this.state.loadingState === LoadingState.LOADING ?
              <this.props.loadingIndicator /> : null
            }
            {this._isSubIframe() ? this.props.children :
              <iframe
                frameBorder="0"
                width={this.state.width}
                height={this.state.height}
                style={iframeStyles}
                {...this.iframeAttributes}
                {...{referrerPolicy: 'no-referrer'}}
              >
                {this.props.children}
              </iframe>
            }
          </this.props.iframeContainer>
        );
      case LoadingState.FAILED:
        return <div style={{height: this.state.height}}><this.props.failedToLoadIndicator /></div>;
      case LoadingState.INIT:
      case LoadingState.RESOLVING:
      default:
        return null;
    }
  }

  componentDidMount(): void {
    if (this.state.loadingState === LoadingState.INIT) {
      this.setState({loadingState: LoadingState.RESOLVING});
      this._initialise();
    }
  }

  componentDidUpdate(): void {
    if (this.state.loadingState === LoadingState.INIT) {
      this.setState({loadingState: LoadingState.RESOLVING});
      this._initialise();
    }
  }

}

export {ConnectIframeDefinitions, ConnectIframe};
