import React from "react";
import * as ReactDOM from "react-dom";
import { BehaviorSubject, Observable, Subscription } from "rxjs";
import uuid from 'uuid/v1';

export interface PortalProviderApi {
  render(children: React.ReactNode, container: Element): void;
  destroy(container: Element): void;
}

export interface Props {
  children: (api: PortalProviderApi) => React.ReactNode;
}

type DomToReactNodeMap = ReadonlyMap<Element, React.ReactNode>;

export class PortalProvider extends React.Component<Props> {
  private readonly domToReactNodeMap = new Map<Element, React.ReactNode>();
  private readonly domToReactNodeMapSubject = new BehaviorSubject<DomToReactNodeMap>(this.domToReactNodeMap);
  private readonly domToReactNodeMapObservable = this.domToReactNodeMapSubject.asObservable();
  private readonly childrenApi: PortalProviderApi = {
    render: (children, container) => {
      this.domToReactNodeMap.set(container, children);
      this.domToReactNodeMapSubject.next(this.domToReactNodeMap);
    },
    destroy: container => {
      this.domToReactNodeMap.delete(container);
      this.domToReactNodeMapSubject.next(this.domToReactNodeMap);
    }
  };

  public render() {
    return (
      <>
        {this.props.children(this.childrenApi)}
        <RenderPortals spec={this.domToReactNodeMapObservable} />
      </>
    );
  }
}

interface RenderPortalsProps {
  spec: Observable<DomToReactNodeMap>;
}

interface RenderPortalsState {
  spec?: DomToReactNodeMap;
}

const containerKeys = new WeakMap<Element, string>();

const getContainerKey = (el: Element): string => {
  if(!containerKeys.has(el)) {
    containerKeys.set(el, uuid())
  }
  return containerKeys.get(el)!
}

class RenderPortals extends React.Component<RenderPortalsProps, RenderPortalsState> {
  public readonly state: RenderPortalsState = {};

  private subscription!: Subscription;

  public componentWillMount() {
    this.subscription = this.props.spec.subscribe(spec => {
      this.setState({ spec });
    });
  }

  public componentWillUnmount() {
    this.subscription.unsubscribe();
  }

  public render() {
    const { spec } = this.state;
    return spec !== undefined
      ? Array.from(spec.entries()).map(([container, children]) => ReactDOM.createPortal(children, container, getContainerKey(container)))
      : null;
  }
}
