import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
import { R, React } from './common';
import { IStore } from './types';
import { Connector, ConnectorLoadedEvent } from './components/Connector';

interface IConnectWrapperState {
  store?: IStore<any, any>;
}
export interface IConnectProps {
  store?: IStore<any, any>;
}

/**
 * HoC factory for connecting a component to a store.
 */
export function connect<P>(
  component: React.SFC<any> | React.ComponentClass<any> | string,
  options: {
    autoRender?: boolean;
    path?: string;
  } = {},
): React.ComponentClass<P> {
  // Options.
  const autoRender =
    options.autoRender === undefined ? true : options.autoRender;

  // HoC.
  class ConnectWrapper extends React.PureComponent<
    P & IConnectProps,
    IConnectWrapperState
  > {
    private loaded: ConnectorLoadedEvent;
    public state: IConnectWrapperState = {};
    private unmounted$ = new Subject();

    private get store(): IStore<any, any> | undefined {
      if (this.props.store) {
        // An explicit store was passed in as prop, use this instead of the one from the <Provider>.
        return this.props.store;
      }

      // Read store that was passed via the <Provider>.
      const store = this.loaded && this.loaded.store;
      const { path } = options;
      const result = path ? store.get(path) : store;
      return R.is(Object, result) ? result : undefined;
    }

    private init = (e: ConnectorLoadedEvent) => {
      this.loaded = e;
    };

    public componentDidMount() {
      const store = this.store;
      this.setState({ store });
      if (store && autoRender) {
        store.state$.pipe(takeUntil(this.unmounted$)).subscribe(() => {
          try {
            this.forceUpdate();
          } catch (error) {
            this.unmounted$.next();
          }
        });
      }
    }

    public componentWillUnmount() {
      this.unmounted$.next();
    }

    public render() {
      return (
        <Connector onLoaded={this.init}>{this.renderComponent()}</Connector>
      );
    }

    private renderComponent() {
      const { store } = this.state;
      if (!store) {
        // Don't render anything if the store has not been retrieved yet.
        // Ensure the first render of the component has a store.
        return '';
      }

      const props: any = {
        ...(this.props as object),
        store,
      };
      return React.createElement(component, props);
    }
  }

  // Finish up.
  return ConnectWrapper as React.ComponentClass<P>;
}
