/* eslint react/prop-types: off */

import React from "react";
import { View, StyleSheet } from "react-native";
import * as R from "ramda";
import { v4 } from "uuid";

type Props = {
  Component: React.ElementType;
  onLayout: (LayoutChangeEvent) => void;
  componentProps?: Record<any, any>;
};

const isError = (possibleError): boolean => possibleError instanceof Error;

const styles = StyleSheet.create({
  container: {
    position: "absolute",
    opacity: 0,
    top: 0,
    left: 0,
    right: 0,
    bottom: 0,
  },
  wrapper: {},
});

const MeasurementsPortal = ({ Component, onLayout, componentProps }: Props) => {
  return (
    <View style={styles.container} testID="MeasurementsPortal">
      <View style={styles.wrapper} onLayout={onLayout} key={v4()}>
        <Component {...componentProps} />
      </View>
    </View>
  );
};

type MeasurementPortalContextType = {
  measureComponent?: (
    comp: React.ElementType,
    props: Record<any, any>
  ) => Promise<any>;
  measuringInProgress?: boolean;
};

const MeasurementPortalContext =
  React.createContext<null | MeasurementPortalContextType>(null);

const MeasurementsPortalContextProvider = ({ children }) => {
  const Component = React.useRef(View);
  const [measuringInProgress, setMeasuringInProgress] = React.useState(false);

  const measureComponentCallback = React.useRef(null);
  const componentProps = React.useRef(null);
  const onLoadFinishedIsCalled = React.useRef(null);

  const setComponent = (comp) => {
    Component.current = comp;
  };

  const setMeasureComponentCallback = (cb) => {
    measureComponentCallback.current = cb;
  };

  const finalize = ({ width, height }) => {
    measureComponentCallback?.current?.({
      width,
      height,
      index: componentProps.current.index,
    });

    setMeasureComponentCallback(null);
    setMeasuringInProgress(false);
    onLoadFinishedIsCalled.current = false;
  };

  const setComponentProps = (props) => {
    const handleOnLoadFinish = (...args) => {
      const error = R.path([0, "error"], args);

      if (isError(error)) {
        finalize({ width: 0, height: 0 });
      } else {
        onLoadFinishedIsCalled.current = true;
        props.onLoadFinished(...args);
      }
    };

    componentProps.current = {
      ...props,
      onLoadFinished: handleOnLoadFinish,
    };
  };

  const measureComponent = React.useCallback(async (comp, props) => {
    return new Promise((resolve) => {
      setMeasureComponentCallback(resolve);
      setMeasuringInProgress(true);

      setComponentProps(props);
      setComponent(comp);
    });
  }, []);

  const onLayout = ({
    nativeEvent: {
      layout: { width, height },
    },
  }) => {
    if (measureComponentCallback.current && onLoadFinishedIsCalled.current) {
      finalize({
        width,
        height,
      });
    }
  };

  return (
    <>
      <MeasurementsPortal
        Component={Component.current}
        componentProps={componentProps.current}
        onLayout={onLayout}
      />
      <MeasurementPortalContext.Provider
        value={{
          measureComponent,
          measuringInProgress,
        }}
      >
        {children}
      </MeasurementPortalContext.Provider>
    </>
  );
};

export {
  MeasurementsPortal,
  MeasurementsPortalContextProvider,
  MeasurementPortalContext,
};
