import React, { useContext, useEffect, useRef, useState } from 'react';
import {
  CheckOutlined,
  LinkOutlined,
  SnippetsOutlined,
  ThunderboltOutlined,
  UpOutlined,
} from '@ant-design/icons';
import type { Project } from '@stackblitz/sdk';
import stackblitzSdk from '@stackblitz/sdk';
import { Alert, Badge, Space, Tooltip } from 'antd';
import { createStyles, css } from 'antd-style';
import classNames from 'classnames';
import { FormattedMessage, useSiteData } from 'dumi';
import LZString from 'lz-string';
import CopyToClipboard from 'react-copy-to-clipboard';

import type { AntdPreviewerProps } from '.';
import useLocation from '../../../hooks/useLocation';
import BrowserFrame from '../../common/BrowserFrame';
import ClientOnly from '../../common/ClientOnly';
import CodePenIcon from '../../common/CodePenIcon';
import CodePreview from '../../common/CodePreview';
import CodeSandboxIcon from '../../common/CodeSandboxIcon';
import EditButton from '../../common/EditButton';
import ExternalLinkIcon from '../../common/ExternalLinkIcon';
import RiddleIcon from '../../common/RiddleIcon';
import type { SiteContextProps } from '../../slots/SiteContext';
import SiteContext from '../../slots/SiteContext';
import { ping } from '../../utils';

const { ErrorBoundary } = Alert;

function compress(string: string): string {
  return LZString.compressToBase64(string)
    .replace(/\+/g, '-') // Convert '+' to '-'
    .replace(/\//g, '_') // Convert '/' to '_'
    .replace(/=+$/, ''); // Remove ending '='
}

const track = ({ type, demo }: { type: string; demo: string }) => {
  if (!window.gtag) {
    return;
  }
  window.gtag('event', 'demo', { event_category: type, event_label: demo });
};

let pingDeferrer: PromiseLike<boolean>;

function useShowRiddleButton() {
  const [showRiddleButton, setShowRiddleButton] = useState(false);

  useEffect(() => {
    pingDeferrer ??= new Promise<boolean>((resolve) => {
      ping((status) => {
        if (status !== 'timeout' && status !== 'error') {
          return resolve(true);
        }

        return resolve(false);
      });
    });
    pingDeferrer.then(setShowRiddleButton);
  }, []);

  return showRiddleButton;
}

const useStyle = createStyles(({ token }) => {
  const { borderRadius } = token;
  return {
    codeHideBtn: css`
      width: 100%;
      height: 40px;
      display: flex;
      justify-content: center;
      align-items: center;
      border-radius: 0 0 ${borderRadius}px ${borderRadius}px;
      border-top: 1px solid ${token.colorSplit};
      color: ${token.colorTextSecondary};
      transition: all 0.2s ease-in-out;
      background-color: ${token.colorBgElevated};
      cursor: pointer;
      &:hover {
        color: ${token.colorPrimary};
      }
      span {
        margin-right: ${token.marginXXS}px;
      }
    `,
  };
});

const CodePreviewer: React.FC<AntdPreviewerProps> = (props) => {
  const {
    asset,
    expand,
    iframe,
    demoUrl,
    children,
    title,
    description,
    originDebug,
    jsx,
    style,
    compact,
    background,
    filename,
    version,
    clientOnly,
    pkgDependencyList,
  } = props;

  const { pkg } = useSiteData();
  const location = useLocation();

  const { styles } = useStyle();

  const entryCode = asset.dependencies['index.tsx'].value;
  const showRiddleButton = useShowRiddleButton();

  const liveDemo = useRef<React.ReactNode>(null);
  const anchorRef = useRef<HTMLAnchorElement>(null);
  const codeSandboxIconRef = useRef<HTMLFormElement>(null);
  const riddleIconRef = useRef<HTMLFormElement>(null);
  const codepenIconRef = useRef<HTMLFormElement>(null);
  const [codeExpand, setCodeExpand] = useState<boolean>(false);
  const [copyTooltipOpen, setCopyTooltipOpen] = useState<boolean>(false);
  const [copied, setCopied] = useState<boolean>(false);
  const [codeType, setCodeType] = useState<string>('tsx');
  const { theme } = useContext<SiteContextProps>(SiteContext);

  const { hash, pathname, search } = location;
  const docsOnlineUrl = `https://ant.design${pathname}${search}#${asset.id}`;

  const [showOnlineUrl, setShowOnlineUrl] = useState<boolean>(false);

  useEffect(() => {
    const regexp = /preview-(\d+)-ant-design/; // matching PR preview addresses
    setShowOnlineUrl(
      process.env.NODE_ENV === 'development' || regexp.test(window.location.hostname),
    );
  }, []);

  const handleCodeExpand = (demo: string) => {
    setCodeExpand((prev) => !prev);
    track({ type: 'expand', demo });
  };

  const handleCodeCopied = (demo: string) => {
    setCopied(true);
    track({ type: 'copy', demo });
  };

  const onCopyTooltipOpenChange = (open: boolean) => {
    setCopyTooltipOpen(open);
    if (open) {
      setCopied(false);
    }
  };

  useEffect(() => {
    if (asset.id === hash.slice(1)) {
      anchorRef.current?.click();
    }
  }, []);

  useEffect(() => {
    setCodeExpand(expand);
  }, [expand]);

  const mergedChildren = !iframe && clientOnly ? <ClientOnly>{children}</ClientOnly> : children;

  if (!liveDemo.current) {
    liveDemo.current = iframe ? (
      <BrowserFrame>
        <iframe
          src={demoUrl}
          height={iframe === true ? undefined : iframe}
          title="demo"
          className="iframe-demo"
        />
      </BrowserFrame>
    ) : (
      mergedChildren
    );
  }

  const codeBoxClass = classNames('code-box', {
    expand: codeExpand,
    'code-box-debug': originDebug,
  });

  const localizedTitle = title;
  const introChildren = <div dangerouslySetInnerHTML={{ __html: description }} />;
  const highlightClass = classNames('highlight-wrapper', {
    'highlight-wrapper-expand': codeExpand,
  });

  const html = `
    <!DOCTYPE html>
      <html lang="en">
        <head>
          <meta charset="utf-8">
          <meta name="viewport" content="width=device-width">
          <meta name="theme-color" content="#000000">
        </head>
        <body>
          <div id="container" style="padding: 24px" />
          <script>const mountNode = document.getElementById('container');</script>
        </body>
      </html>
    `;

  const tsconfig = {
    compilerOptions: {
      target: 'esnext',
      module: 'esnext',
      esModuleInterop: true,
      moduleResolution: 'node',
      jsx: 'react',
      jsxFactory: 'React.createElement',
      jsxFragmentFactory: 'React.Fragment',
    },
  };

  const suffix = codeType === 'tsx' ? 'tsx' : 'js';

  const dependencies: Record<PropertyKey, string> = jsx.split('\n').reduce(
    (acc, line) => {
      const matches = line.match(/import .+? from '(.+)';$/);
      if (matches && matches[1] && !line.includes('antd')) {
        const paths = matches[1].split('/');
        if (paths.length) {
          const dep = paths[0].startsWith('@') ? `${paths[0]}/${paths[1]}` : paths[0];
          acc[dep] = 'latest';
        }
      }
      return acc;
    },
    { antd: pkg.version },
  );

  dependencies['@ant-design/icons'] = 'latest';

  if (suffix === 'tsx') {
    dependencies['@types/react'] = '^18.0.0';
    dependencies['@types/react-dom'] = '^18.0.0';
  }

  dependencies.react = '^18.0.0';
  dependencies['react-dom'] = '^18.0.0';

  const codepenPrefillConfig = {
    title: `${localizedTitle} - antd@${dependencies.antd}`,
    html,
    js: `const { createRoot } = ReactDOM;\n${jsx
      .replace(/import\s+(?:React,\s+)?{(\s+[^}]*\s+)}\s+from\s+'react'/, `const { $1 } = React;`)
      .replace(/import\s+{(\s+[^}]*\s+)}\s+from\s+'antd';/, 'const { $1 } = antd;')
      .replace(/import\s+{(\s+[^}]*\s+)}\s+from\s+'@ant-design\/icons';/, 'const { $1 } = icons;')
      .replace("import moment from 'moment';", '')
      .replace("import React from 'react';", '')
      .replace(/import\s+{\s+(.*)\s+}\s+from\s+'react-router';/, 'const { $1 } = ReactRouter;')
      .replace(
        /import\s+{\s+(.*)\s+}\s+from\s+'react-router-dom';/,
        'const { $1 } = ReactRouterDOM;',
      )
      .replace(/([A-Za-z]*)\s+as\s+([A-Za-z]*)/, '$1:$2')
      .replace(
        /export default/,
        'const ComponentDemo =',
      )}\n\ncreateRoot(mountNode).render(<ComponentDemo />);\n`,
    editors: '001',
    css: '',
    js_external: [
      'react@18/umd/react.development.js',
      'react-dom@18/umd/react-dom.development.js',
      'dayjs@1/dayjs.min.js',
      `antd@${pkg.version}/dist/antd-with-locales.js`,
      `@ant-design/icons/dist/index.umd.js`,
      'react-router-dom/dist/umd/react-router-dom.production.min.js',
      'react-router/dist/umd/react-router.production.min.js',
    ]
      .map((url) => `https://unpkg.com/${url}`)
      .join(';'),
    js_pre_processor: 'typescript',
  };

  const riddlePrefillConfig = {
    title: `${localizedTitle} - antd@${dependencies.antd}`,
    js: `${
      /import React(\D*)from 'react';/.test(jsx) ? '' : `import React from 'react';\n`
    }import { createRoot } from 'react-dom/client';\n${jsx.replace(
      /export default/,
      'const ComponentDemo =',
    )}\n\ncreateRoot(mountNode).render(<ComponentDemo />);\n`,
    css: '',
    json: JSON.stringify({ name: 'antd-demo', dependencies }, null, 2),
  };

  // Reorder source code
  let parsedSourceCode = suffix === 'tsx' ? entryCode : jsx;
  let importReactContent = "import React from 'react';";
  const importReactReg = /import React(\D*)from 'react';/;
  const matchImportReact = parsedSourceCode.match(importReactReg);
  if (matchImportReact) {
    [importReactContent] = matchImportReact;
    parsedSourceCode = parsedSourceCode.replace(importReactReg, '').trim();
  }
  const demoJsContent = `
${importReactContent}
import './index.css';
${parsedSourceCode}
    `.trim();
  const indexCssContent = (style || '')
    .trim()
    .replace(new RegExp(`#${asset.id}\\s*`, 'g'), '')
    .replace('</style>', '')
    .replace('<style>', '');

  const indexJsContent = `import React from 'react';
import { createRoot } from 'react-dom/client';
import Demo from './demo';

createRoot(document.getElementById('container')).render(<Demo />);
  `;

  const codesandboxPackage = {
    title: `${localizedTitle} - antd@${dependencies.antd}`,
    main: 'index.js',
    dependencies: {
      ...dependencies,
      'rc-util': pkgDependencyList['rc-util'],
      react: '^18.0.0',
      'react-dom': '^18.0.0',
      'react-scripts': '^5.0.0',
    },
    devDependencies: {
      typescript: '^5.0.2',
    },
    scripts: {
      start: 'react-scripts start',
      build: 'react-scripts build',
      test: 'react-scripts test --env=jsdom',
      eject: 'react-scripts eject',
    },
    browserslist: ['>0.2%', 'not dead'],
  };

  const codesanboxPrefillConfig = {
    files: {
      'package.json': { content: codesandboxPackage },
      'index.css': { content: indexCssContent },
      [`index.${suffix}`]: { content: indexJsContent },
      [`demo.${suffix}`]: { content: demoJsContent },
      'index.html': {
        content: html,
      },
    },
  };

  const stackblitzPrefillConfig: Project = {
    title: `${localizedTitle} - antd@${dependencies.antd}`,
    template: 'create-react-app',
    dependencies,
    description: '',
    files: {
      'index.css': indexCssContent,
      [`index.${suffix}`]: indexJsContent,
      [`demo.${suffix}`]: demoJsContent,
      'index.html': html,
    },
  };

  if (suffix === 'tsx') {
    stackblitzPrefillConfig.files['tsconfig.json'] = JSON.stringify(tsconfig, null, 2);
  }

  const backgroundGrey = theme.includes('dark') ? '#303030' : '#f0f2f5';

  const codeBoxDemoStyle: React.CSSProperties = {
    padding: iframe || compact ? 0 : undefined,
    overflow: iframe || compact ? 'hidden' : undefined,
    backgroundColor: background === 'grey' ? backgroundGrey : undefined,
  };

  const codeBox: React.ReactNode = (
    <section className={codeBoxClass} id={asset.id}>
      <section className="code-box-demo" style={codeBoxDemoStyle}>
        <ErrorBoundary>
          <React.StrictMode>{liveDemo.current}</React.StrictMode>
        </ErrorBoundary>
      </section>
      <section className="code-box-meta markdown">
        <div className="code-box-title">
          <Tooltip title={originDebug ? <FormattedMessage id="app.demo.debug" /> : ''}>
            <a href={`#${asset.id}`} ref={anchorRef}>
              {localizedTitle}
            </a>
          </Tooltip>
          <EditButton title={<FormattedMessage id="app.content.edit-demo" />} filename={filename} />
        </div>
        <div className="code-box-description">{introChildren}</div>
        <Space wrap size="middle" className="code-box-actions">
          {showOnlineUrl && (
            <Tooltip title={<FormattedMessage id="app.demo.online" />}>
              <a
                className="code-box-code-action"
                target="_blank"
                rel="noreferrer"
                href={docsOnlineUrl}
              >
                <LinkOutlined aria-label="open in new tab" className="code-box-online" />
              </a>
            </Tooltip>
          )}
          {showRiddleButton ? (
            <form
              className="code-box-code-action"
              action="//riddle.alibaba-inc.com/riddles/define"
              method="POST"
              target="_blank"
              ref={riddleIconRef}
              onClick={() => {
                track({ type: 'riddle', demo: asset.id });
                riddleIconRef.current?.submit();
              }}
            >
              <input type="hidden" name="data" value={JSON.stringify(riddlePrefillConfig)} />
              <Tooltip title={<FormattedMessage id="app.demo.riddle" />}>
                <RiddleIcon className="code-box-riddle" />
              </Tooltip>
            </form>
          ) : null}
          <form
            className="code-box-code-action"
            action="https://codesandbox.io/api/v1/sandboxes/define"
            method="POST"
            target="_blank"
            ref={codeSandboxIconRef}
            onClick={() => {
              track({ type: 'codesandbox', demo: asset.id });
              codeSandboxIconRef.current?.submit();
            }}
          >
            <input
              type="hidden"
              name="parameters"
              value={compress(JSON.stringify(codesanboxPrefillConfig))}
            />
            <Tooltip title={<FormattedMessage id="app.demo.codesandbox" />}>
              <CodeSandboxIcon className="code-box-codesandbox" />
            </Tooltip>
          </form>
          <form
            className="code-box-code-action"
            action="https://codepen.io/pen/define"
            method="POST"
            target="_blank"
            ref={codepenIconRef}
            onClick={() => {
              track({ type: 'codepen', demo: asset.id });
              codepenIconRef.current?.submit();
            }}
          >
            <ClientOnly>
              <input type="hidden" name="data" value={JSON.stringify(codepenPrefillConfig)} />
            </ClientOnly>
            <Tooltip title={<FormattedMessage id="app.demo.codepen" />}>
              <CodePenIcon className="code-box-codepen" />
            </Tooltip>
          </form>
          <Tooltip title={<FormattedMessage id="app.demo.stackblitz" />}>
            <span
              className="code-box-code-action"
              onClick={() => {
                track({ type: 'stackblitz', demo: asset.id });
                stackblitzSdk.openProject(stackblitzPrefillConfig, {
                  openFile: [`demo.${suffix}`],
                });
              }}
            >
              <ThunderboltOutlined className="code-box-stackblitz" />
            </span>
          </Tooltip>
          <CopyToClipboard text={entryCode} onCopy={() => handleCodeCopied(asset.id)}>
            <Tooltip
              open={copyTooltipOpen as boolean}
              onOpenChange={onCopyTooltipOpenChange}
              title={<FormattedMessage id={`app.demo.${copied ? 'copied' : 'copy'}`} />}
            >
              {React.createElement(copied && copyTooltipOpen ? CheckOutlined : SnippetsOutlined, {
                className: 'code-box-code-copy code-box-code-action',
              })}
            </Tooltip>
          </CopyToClipboard>
          <Tooltip title={<FormattedMessage id="app.demo.separate" />}>
            <a
              className="code-box-code-action"
              aria-label="open in new tab"
              target="_blank"
              rel="noreferrer"
              href={demoUrl}
            >
              <ExternalLinkIcon className="code-box-separate" />
            </a>
          </Tooltip>
          <Tooltip
            title={<FormattedMessage id={`app.demo.code.${codeExpand ? 'hide' : 'show'}`} />}
          >
            <div className="code-expand-icon code-box-code-action">
              <img
                alt="expand code"
                src={
                  theme?.includes('dark')
                    ? 'https://gw.alipayobjects.com/zos/antfincdn/btT3qDZn1U/wSAkBuJFbdxsosKKpqyq.svg'
                    : 'https://gw.alipayobjects.com/zos/antfincdn/Z5c7kzvi30/expand.svg'
                }
                className={codeExpand ? 'code-expand-icon-hide' : 'code-expand-icon-show'}
                onClick={() => handleCodeExpand(asset.id)}
              />
              <img
                alt="expand code"
                src={
                  theme?.includes('dark')
                    ? 'https://gw.alipayobjects.com/zos/antfincdn/CjZPwcKUG3/OpROPHYqWmrMDBFMZtKF.svg'
                    : 'https://gw.alipayobjects.com/zos/antfincdn/4zAaozCvUH/unexpand.svg'
                }
                className={codeExpand ? 'code-expand-icon-show' : 'code-expand-icon-hide'}
                onClick={() => handleCodeExpand(asset.id)}
              />
            </div>
          </Tooltip>
        </Space>
      </section>
      {codeExpand && (
        <section className={highlightClass} key="code">
          <CodePreview
            sourceCode={entryCode}
            jsxCode={jsx}
            styleCode={style}
            onCodeTypeChange={setCodeType}
          />
          <div
            tabIndex={0}
            role="button"
            className={styles.codeHideBtn}
            onClick={() => setCodeExpand(false)}
          >
            <UpOutlined />
            <FormattedMessage id="app.demo.code.hide.simplify" />
          </div>
        </section>
      )}
    </section>
  );

  useEffect(() => {
    // In Safari, if style tag be inserted into non-head tag,
    // it will affect the rendering ability of the browser,
    // resulting in some response delays like following issue:
    // https://github.com/ant-design/ant-design/issues/39995
    // So we insert style tag into head tag.
    if (!style) {
      return;
    }
    const styleTag = document.createElement('style');
    styleTag.type = 'text/css';
    styleTag.innerHTML = style;
    styleTag['data-demo-url'] = demoUrl;
    document.head.appendChild(styleTag);
    return () => {
      document.head.removeChild(styleTag);
    };
  }, [style, demoUrl]);

  if (version) {
    return (
      <Badge.Ribbon text={version} color={version.includes('<') ? 'red' : null}>
        {codeBox}
      </Badge.Ribbon>
    );
  }

  return codeBox;
};

export default CodePreviewer;
