import {
  RenderOnce,
  SkipRender,
  Slot,
  component$,
  implicit$FirstArg,
  isBrowser,
  isServer,
  noSerialize,
  useSignal,
  useStore,
  useStylesScoped$,
  useTask$,
  type NoSerialize,
  type QRL,
} from '@qwik.dev/core';
import { _getContextContainer, _getContextHostElement } from '@qwik.dev/core/internal';

import type { FunctionComponent as ReactFC } from 'react';
import type { Root } from 'react-dom/client';
import * as client from './client';
import { renderFromServer } from './server-render';
import { getHostProps, main, mainExactProps, useWakeupSignal } from './slot';
import type { QwikProjectionState } from './slot';
import type { Internal, QwikifyOptions, QwikifyProps } from './types';

export function qwikifyQrl<PROPS extends Record<any, any>>(
  reactCmp$: QRL<ReactFC<PROPS & { children?: any }>>,
  opts?: QwikifyOptions
) {
  return component$((props: QwikifyProps<PROPS>) => {
    const { scopeId } = useStylesScoped$(
      `q-slot{display:none} q-slotc,q-slotc>q-slot{display:contents}`
    );
    const hostRef = useSignal<Element>();
    const slotRef = useSignal<Element>();
    const internalState = useSignal<NoSerialize<Internal<PROPS>>>();
    const [signal, isClientOnly] = useWakeupSignal(props, opts);
    const hydrationKeys = useStore({});
    const TagName = opts?.tagName ?? ('qwik-react' as any);

    // Task takes cares of updates and partial hydration
    useTask$(async ({ track }) => {
      const trackedProps = track(() => ({ ...props }));
      track(signal);

      if (!isBrowser) {
        return;
      }

      // Capture projection state for reactify$ components in the React tree
      let projectionState: QwikProjectionState | null = null;
      const container = _getContextContainer();
      const hostVNode = _getContextHostElement();
      if (container && hostVNode) {
        projectionState = { parentVNode: hostVNode, container };
      }

      // Update
      if (internalState.value) {
        if (internalState.value.root) {
          internalState.value.root.render(
            main(
              slotRef.value,
              scopeId,
              internalState.value.cmp,
              trackedProps,
              internalState.value.projectionState
            )
          );
        }
      } else {
        let root: Root | undefined = undefined;
        const Cmp = await reactCmp$.resolve();
        const hostElement = hostRef.value;
        if (hostElement) {
          // hydration
          if (isClientOnly) {
            root = client.createRoot(hostElement);
          } else {
            root = client.flushSync(() => {
              return client.hydrateRoot(
                hostElement,
                mainExactProps(slotRef.value, scopeId, Cmp, hydrationKeys, projectionState)
              );
            });
          }
          if (isClientOnly || signal.value === false) {
            root.render(main(slotRef.value, scopeId, Cmp, trackedProps, projectionState));
          }
        }
        internalState.value = noSerialize({
          cmp: Cmp,
          root,
          projectionState,
        });
      }
    });

    useTask$(({ track, cleanup }) => {
      track(signal);

      if (isBrowser) {
        cleanup(() => {
          const root = internalState.value?.root;
          if (root) {
            // Defer React unmount so Qwik's diff can finish processing VNode
            // references before DOM elements are removed by React
            queueMicrotask(() => root.unmount());
          }
        });
      }
    });

    if (isServer && !isClientOnly) {
      const jsx = renderFromServer(
        TagName,
        reactCmp$,
        scopeId,
        props,
        hostRef,
        slotRef,
        hydrationKeys
      );
      return <RenderOnce key={2}>{jsx}</RenderOnce>;
    }

    return (
      <>
        <TagName
          {...getHostProps(props)}
          ref={(el: Element) => {
            if (isBrowser) {
              queueMicrotask(() => {
                const internalData = internalState.value;
                if (internalData && !internalData.root) {
                  const root = (internalData.root = client.createRoot(el));
                  root.render(
                    main(
                      slotRef.value,
                      scopeId,
                      internalData.cmp,
                      props,
                      internalData.projectionState
                    )
                  );
                }
              });
            } else {
              hostRef.value = el;
            }
          }}
        >
          {SkipRender}
        </TagName>
        <q-slot ref={slotRef}>
          <Slot></Slot>
        </q-slot>
      </>
    );
  });
}

export const qwikify$ = /*#__PURE__*/ implicit$FirstArg(qwikifyQrl);
