import { Controller } from '@hotwired/stimulus';
import React, { lazy, Suspense, FunctionComponent } from 'react';
import { render, unmountComponentAtNode } from 'react-dom';
import invariant from 'tiny-invariant';

type Props = Record<string, unknown>;
type Loader = () => Promise<{ default: FunctionComponent<Props> }>;
const componentsRegistry = new Map<string, FunctionComponent<Props>>();
const components = import.meta.glob('../components/*.tsx');

for (const [path, loader] of Object.entries(components)) {
  const [filename] = path.split('/').reverse();
  const componentClassName = filename.replace(/\.(ts|tsx)$/, '');
  console.debug(
    `Registered lazy default export for "${componentClassName}" component`
  );
  componentsRegistry.set(
    componentClassName,
    LoadableComponent(loader as Loader)
  );
}

// Initialize React components when their markup appears into the DOM.
//
// Example:
//   <div data-controller="react" data-react-component-value="ComboMultiple" data-react-props-value="{}"></div>
//
export class ReactController extends Controller {
  static values = {
    component: String,
    props: Object
  };

  declare readonly componentValue: string;
  declare readonly propsValue: Props;

  connect(): void {
    this.mountComponent(this.element as HTMLElement);
  }

  disconnect(): void {
    unmountComponentAtNode(this.element as HTMLElement);
  }

  private mountComponent(node: HTMLElement): void {
    const componentName = this.componentValue;
    const props = this.propsValue;
    const Component = this.getComponent(componentName);

    invariant(
      Component,
      `Cannot find a React component with class "${componentName}"`
    );
    render(<Component {...props} />, node);
  }

  private getComponent(componentName: string): FunctionComponent<Props> | null {
    return componentsRegistry.get(componentName) ?? null;
  }
}

const Spinner = () => <div className="spinner left" />;

function LoadableComponent(loader: Loader): FunctionComponent<Props> {
  const LazyComponent = lazy(loader);
  const Component: FunctionComponent<Props> = (props: Props) => (
    <Suspense fallback={<Spinner />}>
      <LazyComponent {...props} />
    </Suspense>
  );
  return Component;
}