/* eslint-disable react-hooks/rules-of-hooks */
import React, { useRef, useEffect, useState } from 'react';
import clsx from 'clsx';
import { Buffer } from 'buffer';
import get from 'lodash/get';
import pako from 'pako';
import base64 from 'base64-url';
import isSvg from 'is-svg';
import noop from 'lodash/noop';
import { Box, type BoxProps } from '@mui/material';

import AspectRatioContainer from './aspect-ratio-container';
import ImgSvgEmbedder from './svg-embedder/img';
import InlineSvgEmbedder from './svg-embedder/inline-svg';
import DefaultLoading from './loading';
import DefaultBrokenImage from './broken';
import { styled } from '../Theme';
import displayApi from './displayApi';
import RenderSvg from './render-svg';

/**
 * 从 assetState 中获取 nft data, 兼容新旧两种类型的数据结构, 建议将该方法的返回值传入 NFTDisplay 组件的 data prop
 * - 旧: assetState.data.value (.credentialSubject.display)
 * - 新: assetState.display
 */
export function getNFTData(assetState: Record<string, any>) {
  return assetState?.display || assetState?.data?.value;
}

function fromBase64(v: string) {
  if (typeof v !== 'string') {
    throw new Error('fromBase64 requires input to be a string');
  }
  return Buffer.from(base64.unescape(v), 'base64');
}

// const isVC = type => {
//   return String(type).includes('VerifiableCredential');
// };

// 仅针对非 url type 的情况
const getSvgEmbedder = (preferredSvgEmbedder: 'img' | 'svg') => {
  const embedders = {
    img: ImgSvgEmbedder,
    svg: InlineSvgEmbedder,
  };
  return embedders[preferredSvgEmbedder];
};

export interface NFTDisplayProps extends BoxProps {
  /** asset data 可以是 raw data 和 parsed data */
  data: string | Record<string, any>;
  address: string;
  inset?: false | true;
  aspect?: number;
  component?: React.ElementType<any>;
  className?: string;
  renderError?: () => React.ReactNode;
  renderLoading?: () => React.ReactNode;
  /** 对于非 url type 的情况, 支持优先选用的 svg 嵌入方式, 默认是 img */
  preferredSvgEmbedder?: 'img' | 'svg';
  /** 针对非 url type 的情况, 检测 svg 有效性, 默认禁用 */
  checkSvg?: false | true;
  /** loading 最小显示时间 (避免闪烁) */
  minimumLoadingTime?: number;
  /** 完成回调, 无论加载成功|失败 */
  onCompleted?: () => void;
  /**
   * 图片处理，参考：https://team.arcblock.io/comment/docs/c158aee4-accd-42f4-9ced-6a23f28c00e0/en/blocklet-image-service-guide
   * 配置参数会全部转发给 Image Service
   */
  imageFilter?: object | null;
}

/**
 * TODO:
 * 考虑把 asset data 解析部分和 nft display 分离, android 端有相关使用场景 - 只传入 svg 或 url, 也可以传入 asset data,
 * 目前如果想直接传入 svg 或 url, 需要构造一个 asset data 的数据才能使用 NFTDisplay 组件
 */
function NFTDisplay({
  data,
  address,
  inset,
  aspect,
  component = 'span',
  className = '',
  renderError,
  renderLoading,
  preferredSvgEmbedder = 'img',
  checkSvg = false,
  minimumLoadingTime = 0,
  onCompleted = noop,
  imageFilter = null,
  ...rest
}: NFTDisplayProps) {
  const wrapRoot = (children: React.ReactNode) => (
    // @ts-expect-error
    <Root component={component} {...rest} className={clsx(className, { 'nft-display--inset': inset })}>
      {children}
    </Root>
  );

  const parsed = useRef<Record<string, any>>({});
  try {
    // 如果是 raw data 先解析
    if (typeof data === 'string') {
      parsed.current = JSON.parse(data);
    } else {
      parsed.current = data;
    }

    const { vcId } = parsed.current;
    // 需要兼容新旧两种类型的数据结构, nft data 有 credentialSubject 属性, 说明是旧 nft data, 否则是新 nft data
    const display = parsed.current.credentialSubject
      ? get(parsed.current, 'credentialSubject.display')
      : parsed.current;
    const { content, type } = display as { content: string; type: string };
    const isUrlType = type === 'url';

    // 首次加载, 对于 url type 的情况, loading 为 true
    const [state, setState] = useState<{
      loading: boolean;
      error: boolean;
      loadingUrlType: boolean;
      urlType: string | null;
    }>({
      loading: isUrlType,
      error: false,
      loadingUrlType: true,
      urlType: null,
    });
    const [minimumLoadingReady, setMinimumLoadingReady] = useState(minimumLoadingTime <= 0);
    // console.log('[debug] render', {type, minimumLoadingTime}, JSON.stringify(state))

    // assemble the complete url
    const getFullContentUrl = ({ useImageFilter = false, t }: { useImageFilter?: boolean; t?: string } = {}) => {
      const urlObj = new URL(content);

      // check protocol
      if (window.location.protocol === 'https:' && urlObj.protocol === 'http:') {
        urlObj.protocol = 'https:';
      }

      if (!urlObj.searchParams.has('assetId')) {
        urlObj.searchParams.append('assetId', address);
      }

      if (!urlObj.searchParams.has('vcId') && vcId) {
        urlObj.searchParams.append('vcId', vcId);
      }

      // image filter
      if (useImageFilter && imageFilter) {
        Object.entries(imageFilter).forEach(([key, value]) => {
          urlObj.searchParams.append(key, value);
        });
      }

      // other params
      if (t) {
        urlObj.searchParams.append('t', t);
      }

      return urlObj.href;
    };

    // get url content type
    const getUrlContentType = async () => {
      try {
        const response = await displayApi({
          url: getFullContentUrl({ useImageFilter: false, t: 'nftdisplay' }),
          method: 'HEAD',
        });
        // @ts-expect-error
        const contentType = response?.headers?.get('Content-Type');
        setState({ ...state, loadingUrlType: false, urlType: contentType });
      } catch (error) {
        console.error('Failed to fetch url content type', error);
        // display an error message when timeout occurs to avoid repeated waiting.
        if ((error as Error)?.message?.includes('timeout')) {
          setState({ ...state, loadingUrlType: false, loading: false, error: true });
          return;
        }

        setState({ ...state, loadingUrlType: false, urlType: null });
      }
    };

    useEffect(() => {
      let timer: NodeJS.Timeout;
      if (minimumLoadingTime > 0) {
        timer = setTimeout(() => setMinimumLoadingReady(true), minimumLoadingTime);
      }

      if (type === 'url') {
        getUrlContentType();
      }

      return () => clearTimeout(timer);
      // eslint-disable-next-line react-hooks/exhaustive-deps
    }, []);

    // onCompleted
    useEffect(() => {
      if ((!state.loading && minimumLoadingReady) || state.error) {
        onCompleted();
      }
      // eslint-disable-next-line react-hooks/exhaustive-deps
    }, [state, minimumLoadingReady]);

    if (state.error) {
      throw new Error('Failed to render NFT Display.');
    }

    const onLoad = () => {
      if (state.loading) {
        setState({ ...state, loading: false });
      }
    };

    const onError = () => {
      setState({ ...state, error: true, loading: false });
    };

    // render image
    const renderImg = () => {
      const url = getFullContentUrl({ useImageFilter: true });
      return (
        <img src={url} onError={onError} onLoad={onLoad} alt="NFT Display" style={{ width: 'auto', height: 'auto' }} />
      );
    };

    // render object
    const renderObject = () => {
      const objectType = state.urlType || 'image/svg+xml';
      const url = getFullContentUrl();

      // do naive image render
      const pathname = new URL(url).pathname.toLowerCase();
      const imageExtensions = [
        '.jpg',
        '.jpeg',
        '.png',
        '.gif',
        '.bmp',
        '.webp',
        '.svg',
        '.tiff',
        '.tif',
        '.ico',
        '.jfif',
        '.pjpeg',
        '.pjp',
        '.avif',
      ];
      if (imageExtensions.some((ext) => pathname.endsWith(ext))) {
        return renderImg();
      }

      // related: https://github.com/ArcBlock/did-spaces/issues/1367
      if (objectType.includes('text/html;')) {
        return <DefaultBrokenImage />;
      }

      return (
        // eslint-disable-next-line jsx-a11y/alt-text
        <object
          key={url}
          type={objectType}
          data={url}
          onErrorCapture={onError}
          onLoad={onLoad}
          style={{ width: '100%', height: '100%', pointerEvents: 'none' }}
        />
      );
    };

    const renderNFT = () => {
      if (content) {
        switch (type) {
          case 'url': {
            if (state.loadingUrlType) {
              return null;
            }

            if (state.urlType?.startsWith('image/svg+xml')) {
              return <RenderSvg src={getFullContentUrl()} onLoad={onLoad} />;
            }

            // render image
            if (state.urlType?.startsWith('image')) {
              return renderImg();
            }

            return renderObject();
          }
          case 'uri': {
            return (
              <img
                src={content}
                onError={() => setState({ ...state, error: true })}
                onLoad={() => setState({ ...state, loading: false })}
                alt="NFT Display"
              />
            );
          }
          case 'svg_gzipped': {
            const buffer = pako.ungzip(fromBase64(content), {});
            const svg = Buffer.from(buffer).toString('utf8');
            if (checkSvg && !isSvg(svg)) {
              throw new Error('Invalid SVG of type svg_gzipped');
            }
            const Embedder = getSvgEmbedder(preferredSvgEmbedder);
            return <Embedder svg={svg} />;
          }
          case 'svg': {
            if (checkSvg && !isSvg(content)) {
              throw new Error('Invalid SVG of type svg');
            }
            const Embedder = getSvgEmbedder(preferredSvgEmbedder);
            return <Embedder svg={content} />;
          }
          // TODO: 准备测试数据
          case 'html': {
            break;
          }
          default:
        }
      }
      throw new Error(`unsupported display protocol: ${display.type}`);
    };

    return wrapRoot(
      <>
        {(state.loading || !minimumLoadingReady) && (renderLoading ? renderLoading() : <DefaultLoading />)}
        {renderNFT()}
      </>
    );
  } catch (e) {
    console.error((e as Error)?.message, {
      nftId: address,
      vcId: parsed?.current?.vcId,
    });
    return wrapRoot(renderError ? renderError() : <DefaultBrokenImage />);
  }
}

const Root = styled(Box)`
  display: flex;
  justify-content: center;
  align-items: center;
  position: relative;
  /* 默认尺寸 */
  width: 150px;
  height: 150px;
  overflow: hidden;

  &,
  img,
  object {
    max-width: 100%;
    max-height: 100%;
  }

  img,
  object {
    width: 100%;
    height: 100%;
  }

  &.nft-display--inset {
    width: 100%;
    height: 100%;
  }
`;

function withAspectRatio(Component: React.ComponentType<NFTDisplayProps>) {
  // eslint-disable-next-line func-names, react/prop-types
  return function ({ aspect = undefined, inset = false, ...rest }: NFTDisplayProps) {
    // inset 比 aspect ratio 优先级高, 如果同时设置了 inset 和 aspect, 则后者不生效
    const applyAspectRatio = aspect && aspect > 0 && !inset;
    if (applyAspectRatio) {
      return (
        <AspectRatioContainer aspect={aspect}>
          <Component inset {...rest} />
        </AspectRatioContainer>
      );
    }
    return <Component inset={inset} {...rest} />;
  };
}

export default withAspectRatio(NFTDisplay);
