import cx from "classnames";
import React, { ReactNode, useRef, useState } from "react";
import { renderToStaticMarkup } from "react-dom/server";
import reactElementToJSXString from "react-element-to-jsx-string";
import unescape from "unescape-html";
import { formatHTML } from "../../utils/formatCode";
import { Button } from "../Button";
import Code from "../Code/";
import { getElementDisplayName } from "./getElementDisplayName";

interface CodeExampleProps {
  codeJSXOptions?: Record<string, any>;
  codeTypes?: string[];
  children?: ReactNode;
  htmlThemeClass?: string;
  jsxCode?: string; // Pre-generated JSX string (from server-side PreviewAuto)
  htmlCode?: ReactNode; // Code for HTML rendering
  [key: string]: any;
}

const addClassToOpeningTag = (tag: string, className: string): string => {
  if (tag.includes(' class="')) {
    return tag.replace(/ class="([^"]*)"/, (_, existingClassName) => {
      const mergedClassName = [existingClassName, className]
        .filter(Boolean)
        .join(" ");
      return ` class="${mergedClassName}"`;
    });
  }

  return tag.replace(/^<([\w-]+)/, `<$1 class="${className}"`);
};

const applyThemeClassToMarkup = (
  markup: string,
  themeClass?: string,
  wrapMultipleRoots = false,
): string => {
  if (!themeClass || !markup.trim()) {
    return markup;
  }

  if (wrapMultipleRoots) {
    return `<div class="${themeClass}">${markup}</div>`;
  }

  return markup.replace(/^<[^>]+>/, (openingTag) =>
    addClassToOpeningTag(openingTag, themeClass),
  );
};

const renderMarkupToHtml = (markup: ReactNode, themeClass?: string): string => {
  if (!markup) {
    return "";
  }

  if (typeof markup === "string") {
    return markup;
  }

  if (Array.isArray(markup)) {
    const renderedMarkup = markup
      .map((markupItem) =>
        renderToStaticMarkup(markupItem as React.ReactElement),
      )
      .join("");

    return applyThemeClassToMarkup(
      renderedMarkup,
      themeClass,
      markup.length > 1,
    );
  }

  return applyThemeClassToMarkup(
    renderToStaticMarkup(markup as React.ReactElement),
    themeClass,
  );
};

/**
 * Remove props that are undefined or null
 * Don't show React.Fragment in code example
 */
const cleanUpCode = (markup: any): any => {
  const markupProps = markup.props || {};

  return Object.keys(markupProps).reduce((acc: any, curr: string) => {
    const currProp = markupProps[curr];

    let newProp;
    // clean up child code
    if (curr === "children" && typeof currProp !== "string") {
      newProp = React.Children.map(currProp, (child: any) => {
        // hide fragments if containing just strings
        const isFragmentString =
          child.type === React.Fragment &&
          child.props &&
          typeof child.props.children === "string";

        return isFragmentString
          ? child.props.children
          : {
              ...child,
              props: cleanUpCode(child),
            };
      });
    }

    // hide undefined or null props
    const isNotDefined = [undefined, null].indexOf(currProp) !== -1;
    return {
      ...acc,
      ...(isNotDefined
        ? {}
        : {
            [curr]: newProp || currProp,
          }),
    };
  }, {});
};

const getJSXAsStringFromMarkup = (
  markup: any,
  options?: Record<string, any>,
): string => {
  const { cleanProps, filterProps = [], ...otherOptions } = options || {};

  const reactElementToJSXStringOptions = {
    showDefaultProps: false,
    showFunctions: true,
    functionValue: (fn: any) => fn.name,
    displayName: getElementDisplayName,
    filterProps: ["mdxType", "originalType", ...filterProps],
    ...otherOptions,
  };

  if (cleanProps) {
    markup = {
      ...markup,
      props: cleanUpCode(markup),
    };
  }

  // valid element can be passed to reactElementToJSXString directly
  if (React.isValidElement(markup)) {
    return reactElementToJSXString(markup, reactElementToJSXStringOptions);
  }

  // if it's array, we need to pass elemenets one by one
  if (Array.isArray(markup)) {
    return markup
      .map((markupItem: any) =>
        reactElementToJSXString(markupItem, reactElementToJSXStringOptions),
      )
      .join("\n");
  }

  // if it's pure text, return it
  if (typeof markup === "string") {
    return markup;
  }

  return "";
};

const CodeExample: React.FC<CodeExampleProps> = ({
  children,
  codeJSXOptions,
  codeTypes = ["html", "jsx"],
  htmlThemeClass,
  jsxCode,
  htmlCode,
  ...other
}) => {
  const [codePreviewType, setCodePreviewType] = useState<string>(
    (codeTypes && codeTypes[0]) || "html",
  );
  const [copyButtonText, setCopyButtonText] =
    useState<string>("Copy to clipboard");
  const [copyButtonClass, setCopyButtonClass] = useState<string>("");

  const codeBlockRef = useRef<HTMLDivElement>(null);

  const handleCodePreviewTypeToggle = (e: React.MouseEvent, type: string) => {
    setCodePreviewType(type);
  };

  const handleCopyCode = (
    e: React.MouseEvent,
    element: HTMLDivElement | null,
  ) => {
    if (!element) return;

    const selection = window.getSelection();
    const range = document.createRange();

    range.selectNodeContents(element);
    selection?.removeAllRanges();
    selection?.addRange(range);

    let newText = "Copied!";
    let newClass = "success";

    try {
      document.execCommand("copy");
    } catch {
      newText = "Error! Press Ctrl + C";
      newClass = "error";
    }

    const originalText = copyButtonText;
    const originalClass = copyButtonClass;

    setCopyButtonText(newText);
    setCopyButtonClass(newClass);

    setTimeout(() => {
      setCopyButtonText(originalText);
      setCopyButtonClass(originalClass);
    }, 1200);
  };

  let codeToShow: string;
  switch (codePreviewType) {
    case "html":
      if (htmlCode) {
        codeToShow =
          typeof htmlCode === "string"
            ? unescape(htmlCode)
            : unescape(
                formatHTML(renderMarkupToHtml(htmlCode, htmlThemeClass)),
              );
      } else {
        codeToShow =
          typeof children === "string"
            ? unescape(children)
            : unescape(
                formatHTML(renderMarkupToHtml(children, htmlThemeClass)),
              );
      }
      break;
    case "jsx":
      if (jsxCode) {
        // Use pre-generated JSX string (from server-side PreviewAuto)
        codeToShow = jsxCode;
      } else {
        // Fallback to client-side generation (may not work well with MDX)
        codeToShow = getJSXAsStringFromMarkup(children, codeJSXOptions);
      }
      break;
    default:
      codeToShow = "";
  }

  return (
    <div className="wrapper" {...other}>
      {codeTypes.map((codeType) => (
        <Button
          key={codeType}
          role="button"
          onClick={(e) => handleCodePreviewTypeToggle(e, codeType)}
          data-testid="code-type-toggle"
          className={cx("codeTypeToggle", {
            isActive: codePreviewType === codeType,
          })}
        >
          {codeType.toUpperCase()}
        </Button>
      ))}

      <Code
        inline={false}
        ref={codeBlockRef}
        data-testid={codePreviewType}
        language={codePreviewType === "html" ? "markup" : codePreviewType}
      >
        {codeToShow}
      </Code>
      <div className="d-flex justify-content-end">
        <Button
          className={copyButtonClass}
          onClick={(e) => handleCopyCode(e, codeBlockRef.current)}
        >
          {copyButtonText}
        </Button>
      </div>
    </div>
  );
};

CodeExample.displayName = "CodeExample";

export default CodeExample;
