import { createElement, forwardRef } from "react";

/**
 * `DynamicComponent` is a generic React component that can dynamically render
 * different HTML and SVG elements based on the `tag` prop.
 * The `tag` prop supports all standard HTML and SVG tags defined in `JSX.IntrinsicElements`.
 *
 * Reference: https://stackoverflow.com/a/76193787
 */

// `JSX.IntrinsicElements` contains all possible native JSX tags
export type ValidTags = keyof JSX.IntrinsicElements;

// Conditionally determines the correct props based on the given tag type.
// Example: If tag is "button", DynamicProps<T> will resolve to
// `React.HTMLProps<HTMLAnchorElement>`, allowing all valid properties for an anchor tag.
export type DynamicProps<T extends ValidTags> =
  T extends keyof HTMLElementTagNameMap
    ? React.HTMLProps<HTMLElementTagNameMap[T]> // If the tag is part of HTML
    : T extends keyof SVGElementTagNameMap // If not HTML, check if SVG
      ? React.SVGProps<SVGElementTagNameMap[T]>
      : // If neither HTML nor SVG, use empty object type representation
        Record<string, never>;

// Conditionally maps the correct DOM element type based on the given tag type.
// Example: If tag is "button", RefType<T> will resolve to `HTMLAnchorElement`,
// which informs TS of the type of DOM element the ref will point to.
type RefType<T extends ValidTags> = T extends keyof HTMLElementTagNameMap
  ? HTMLElementTagNameMap[T]
  : T extends keyof SVGElementTagNameMap
    ? SVGElementTagNameMap[T]
    : never;

interface DynamicComponentProps<T extends ValidTags> {
  tag?: T | React.ComponentType;
  children?: React.ReactNode;
}

// Use const assertion on default tag for easy type inference of both the
// default generic parameter and the `tag` prop.
const DEFAULT_TAG = "div" as const;

const DynamicComponent = forwardRef(
  <T extends ValidTags>(
    {
      // Cast `tag` to a valid tag name to prevent TS from inferring as a string
      tag = DEFAULT_TAG as T,
      children,
      ...elementProps
    }: DynamicComponentProps<T> & DynamicProps<T>,
    ref: React.ForwardedRef<RefType<T>>,
  ) => {
    return createElement(tag, { ...elementProps, ref }, children);
  },
);
DynamicComponent.displayName = "DynamicComponent";

export default DynamicComponent;
