import omit from 'omit.js';
import LocaleReceiver from '../locale-provider/LocaleReceiver';
import warning from '../_util/warning';
import TransButton from '../_util/transButton';
import raf from '../_util/raf';
import isStyleSupport from '../_util/styleChecker';
import Editable from './Editable';
import measure from './util';
import PropTypes from '../_util/vue-types';
import Typography, { TypographyProps } from './Typography';
import ResizeObserver from '../vc-resize-observer';
import Tooltip from '../tooltip';
import copy from '../_util/copy-to-clipboard';
import CheckOutlined from '@ant-design/icons-vue/CheckOutlined';
import CopyOutlined from '@ant-design/icons-vue/CopyOutlined';
import EditOutlined from '@ant-design/icons-vue/EditOutlined';
import {
  defineComponent,
  VNodeTypes,
  VNode,
  reactive,
  ref,
  onMounted,
  onBeforeUnmount,
  watch,
  watchEffect,
  nextTick,
  CSSProperties,
  computed,
  toRaw,
} from 'vue';
import { AutoSizeType } from '../input/ResizableTextArea';
import useConfigInject from '../_util/hooks/useConfigInject';

export type BaseType = 'secondary' | 'success' | 'warning' | 'danger';

const isLineClampSupport = isStyleSupport('webkitLineClamp');
const isTextOverflowSupport = isStyleSupport('textOverflow');

export interface CopyConfig {
  text?: string;
  onCopy?: () => void;
  tooltip?: boolean;
}

export interface EditConfig {
  editing?: boolean;
  tooltip?: boolean;
  onStart?: () => void;
  onChange?: (value: string) => void;
  onCancel?: () => void;
  onEnd?: () => void;
  maxlength?: number;
  autoSize?: boolean | AutoSizeType;
}

export interface EllipsisConfig {
  rows?: number;
  expandable?: boolean;
  suffix?: string;
  symbol?: string;
  onExpand?: EventHandlerNonNull;
  onEllipsis?: (ellipsis: boolean) => void;
  tooltip?: boolean;
}

export interface BlockProps extends TypographyProps {
  title?: string;
  editable?: boolean | EditConfig;
  copyable?: boolean | CopyConfig;
  type?: BaseType;
  disabled?: boolean;
  ellipsis?: boolean | EllipsisConfig;
  // decorations
  code?: boolean;
  mark?: boolean;
  underline?: boolean;
  delete?: boolean;
  strong?: boolean;
  keyboard?: boolean;
  content?: string;
}

interface Locale {
  edit?: string;
  copy?: string;
  copied?: string;
  expand?: string;
}

interface InternalBlockProps extends BlockProps {
  component: string;
}

const ELLIPSIS_STR = '...';

const Base = defineComponent<InternalBlockProps>({
  name: 'Base',
  inheritAttrs: false,
  emits: ['update:content'],
  setup(props, { slots, attrs, emit }) {
    const { prefixCls } = useConfigInject('typography', props);

    const state = reactive({
      edit: false,
      copied: false,
      ellipsisText: '',
      ellipsisContent: null,
      isEllipsis: false,
      expanded: false,
      clientRendered: false,
      //locale
      expandStr: '',
      copyStr: '',
      copiedStr: '',
      editStr: '',

      copyId: undefined,
      rafId: undefined,
      prevProps: undefined,

      originContent: '',
    });

    const contentRef = ref();
    const editIcon = ref();
    const ellipsis = computed(
      (): EllipsisConfig => {
        const ellipsis = props.ellipsis;
        if (!ellipsis) return {};

        return {
          rows: 1,
          expandable: false,
          ...(typeof ellipsis === 'object' ? ellipsis : null),
        };
      },
    );
    onMounted(() => {
      state.clientRendered = true;
    });

    onBeforeUnmount(() => {
      window.clearTimeout(state.copyId);
      raf.cancel(state.rafId);
    });

    watch(
      [() => ellipsis.value.rows, () => props.content],
      () => {
        nextTick(() => {
          resizeOnNextFrame();
        });
      },
      { flush: 'post', deep: true, immediate: true },
    );

    watchEffect(() => {
      if (!('content' in props)) {
        warning(
          !props.editable,
          'Typography',
          'When `editable` is enabled, please use `content` instead of children',
        );
        warning(
          !props.ellipsis,
          'Typography',
          'When `ellipsis` is enabled, please use `content` instead of children',
        );
      }
    });

    function saveTypographyRef(node: VNode) {
      contentRef.value = node;
    }

    function saveEditIconRef(node: VNode) {
      editIcon.value = node;
    }

    function getChildrenText(): string {
      return props.ellipsis || props.editable ? props.content : contentRef.value?.$el?.innerText;
    }

    // =============== Expand ===============
    function onExpandClick(e: MouseEvent) {
      const { onExpand } = ellipsis.value;
      state.expanded = true;
      onExpand?.(e);
    }
    // ================ Edit ================
    function onEditClick() {
      state.originContent = props.content;
      triggerEdit(true);
    }

    function onEditChange(value: string) {
      onContentChange(value);
      triggerEdit(false);
    }
    function onContentChange(value: string) {
      const { onChange } = editable.value;
      if (value !== props.content) {
        emit('update:content', value);
        onChange?.(value);
      }
    }

    function onEditCancel() {
      triggerEdit(false);
    }

    // ================ Copy ================
    function onCopyClick(e: MouseEvent) {
      e.preventDefault();
      const { copyable } = props;

      const copyConfig = {
        ...(typeof copyable === 'object' ? copyable : null),
      };

      if (copyConfig.text === undefined) {
        copyConfig.text = getChildrenText();
      }

      copy(copyConfig.text || '');

      state.copied = true;
      nextTick(() => {
        if (copyConfig.onCopy) {
          copyConfig.onCopy();
        }

        state.copyId = window.setTimeout(() => {
          state.copied = false;
        }, 3000);
      });
    }
    const editable = computed(() => {
      const editable = props.editable;
      if (!editable) return { editing: state.edit };

      return {
        editing: state.edit,
        ...(typeof editable === 'object' ? editable : null),
      };
    });

    function triggerEdit(edit: boolean) {
      const { onStart } = editable.value;
      if (edit && onStart) {
        onStart();
      }

      state.edit = edit;
      nextTick(() => {
        if (!edit) {
          editIcon.value?.focus();
        }
      });
    }

    // ============== Ellipsis ==============
    function resizeOnNextFrame() {
      raf.cancel(state.rafId);
      state.rafId = raf(() => {
        // Do not bind `syncEllipsis`. It need for test usage on prototype
        syncEllipsis();
      });
    }

    const canUseCSSEllipsis = computed(() => {
      const { rows, expandable, suffix, onEllipsis, tooltip } = ellipsis.value;

      if (suffix || tooltip) return false;

      // Can't use css ellipsis since we need to provide the place for button
      if (props.editable || props.copyable || expandable || onEllipsis) {
        return false;
      }

      if (rows === 1) {
        return isTextOverflowSupport;
      }

      return isLineClampSupport;
    });

    const syncEllipsis = () => {
      const { ellipsisText, isEllipsis } = state;
      const { rows, suffix, onEllipsis } = ellipsis.value;
      if (
        !rows ||
        rows < 0 ||
        !contentRef.value?.$el ||
        state.expanded ||
        props.content === undefined
      )
        return;

      // Do not measure if css already support ellipsis
      if (canUseCSSEllipsis.value) return;

      const { content, text, ellipsis: ell } = measure(
        contentRef.value?.$el,
        { rows, suffix },
        props.content,
        renderOperations(true),
        ELLIPSIS_STR,
      );

      if (ellipsisText !== text || state.isEllipsis !== ell) {
        state.ellipsisText = text;
        state.ellipsisContent = content;
        state.isEllipsis = ell;
        if (isEllipsis !== ell && onEllipsis) {
          onEllipsis(ell);
        }
      }
    };

    function wrapperDecorations(
      { mark, code, underline, delete: del, strong, keyboard }: BlockProps,
      content,
    ) {
      let currentContent = content;

      function wrap(needed: boolean, Tag: string) {
        if (!needed) return;

        currentContent = <Tag>{currentContent}</Tag>;
      }

      wrap(strong, 'strong');
      wrap(underline, 'u');
      wrap(del, 'del');
      wrap(code, 'code');
      wrap(mark, 'mark');
      wrap(keyboard, 'kbd');

      return currentContent;
    }

    function renderExpand(forceRender?: boolean) {
      const { expandable, symbol } = ellipsis.value;

      if (!expandable) return null;

      // force render expand icon for measure usage or it will cause dead loop
      if (!forceRender && (state.expanded || !state.isEllipsis)) return null;
      const expandContent =
        (slots.ellipsisSymbol ? slots.ellipsisSymbol() : symbol) || state.expandStr;

      return (
        <a
          key="expand"
          class={`${prefixCls.value}-expand`}
          onClick={onExpandClick}
          aria-label={state.expandStr}
        >
          {expandContent}
        </a>
      );
    }

    function renderEdit() {
      if (!props.editable) return;

      const { tooltip } = props.editable as EditConfig;
      const icon = slots.editableIcon ? slots.editableIcon() : <EditOutlined role="button" />;
      const title = slots.editableTooltip ? slots.editableTooltip() : state.editStr;
      const ariaLabel = typeof title === 'string' ? title : '';

      return (
        <Tooltip key="edit" title={tooltip === false ? '' : title}>
          <TransButton
            ref={saveEditIconRef}
            class={`${prefixCls.value}-edit`}
            onClick={onEditClick}
            aria-label={ariaLabel}
          >
            {icon}
          </TransButton>
        </Tooltip>
      );
    }

    function renderCopy() {
      if (!props.copyable) return;

      const { tooltip } = props.copyable as CopyConfig;
      const defaultTitle = state.copied ? state.copiedStr : state.copyStr;
      const title = slots.copyableTooltip
        ? slots.copyableTooltip({ copied: state.copied })
        : defaultTitle;
      const ariaLabel = typeof title === 'string' ? title : '';
      const defaultIcon = state.copied ? <CheckOutlined /> : <CopyOutlined />;
      const icon = slots.copyableIcon
        ? slots.copyableIcon({ copied: !!state.copied })
        : defaultIcon;

      return (
        <Tooltip key="copy" title={tooltip === false ? '' : title}>
          <TransButton
            class={[
              `${prefixCls.value}-copy`,
              { [`${prefixCls.value}-copy-success`]: state.copied },
            ]}
            onClick={onCopyClick}
            aria-label={ariaLabel}
          >
            {icon}
          </TransButton>
        </Tooltip>
      );
    }

    function renderEditInput() {
      const { class: className, style } = attrs;
      const { maxlength, autoSize } = editable.value;

      return (
        <Editable
          class={className}
          style={style}
          prefixCls={prefixCls.value}
          value={props.content}
          originContent={state.originContent}
          maxlength={maxlength}
          autoSize={autoSize}
          onSave={onEditChange}
          onChange={onContentChange}
          onCancel={onEditCancel}
        />
      );
    }

    function renderOperations(forceRenderExpanded?: boolean) {
      return [renderExpand(forceRenderExpanded), renderEdit(), renderCopy()].filter(node => node);
    }

    return () => {
      const { editing } = editable.value;
      const children =
        props.ellipsis || props.editable
          ? 'content' in props
            ? props.content
            : slots.default?.()
          : slots.default
          ? slots.default()
          : props.content;

      if (editing) {
        return renderEditInput();
      }
      return (
        <LocaleReceiver
          componentName="Text"
          children={(locale: Locale) => {
            const { type, disabled, content, class: className, style, ...restProps } = {
              ...props,
              ...attrs,
            };
            const { rows, suffix, tooltip } = ellipsis.value;

            const { edit, copy: copyStr, copied, expand } = locale;

            state.editStr = edit;
            state.copyStr = copyStr;
            state.copiedStr = copied;
            state.expandStr = expand;

            const textProps = omit(restProps, [
              'prefixCls',
              'editable',
              'copyable',
              'ellipsis',
              'mark',
              'code',
              'delete',
              'underline',
              'strong',
              'keyboard',
            ]);

            const cssEllipsis = canUseCSSEllipsis.value;
            const cssTextOverflow = rows === 1 && cssEllipsis;
            const cssLineClamp = rows && rows > 1 && cssEllipsis;

            let textNode = children as VNodeTypes;
            let ariaLabel: string | undefined;

            // Only use js ellipsis when css ellipsis not support
            if (rows && state.isEllipsis && !state.expanded && !cssEllipsis) {
              const { title } = restProps;
              let restContent = title || '';

              if (!title && (typeof children === 'string' || typeof children === 'number')) {
                restContent = String(children);
              }

              // show rest content as title on symbol
              restContent = restContent?.slice(String(state.ellipsisContent || '').length);
              // We move full content to outer element to avoid repeat read the content by accessibility
              textNode = (
                <>
                  {toRaw(state.ellipsisContent)}
                  <span title={restContent} aria-hidden="true">
                    {ELLIPSIS_STR}
                  </span>
                  {suffix}
                </>
              );
            } else {
              textNode = (
                <>
                  {children}
                  {suffix}
                </>
              );
            }

            textNode = wrapperDecorations(props, textNode);

            const showTooltip =
              tooltip && rows && state.isEllipsis && !state.expanded && !cssEllipsis;
            const title = slots.ellipsisTooltip ? slots.ellipsisTooltip() : tooltip;
            return (
              <ResizeObserver onResize={resizeOnNextFrame} disabled={!rows}>
                <Typography
                  ref={saveTypographyRef}
                  class={[
                    { [`${prefixCls.value}-${type}`]: type },
                    { [`${prefixCls.value}-disabled`]: disabled },
                    { [`${prefixCls.value}-ellipsis`]: rows },
                    { [`${prefixCls.value}-ellipsis-single-line`]: cssTextOverflow },
                    { [`${prefixCls.value}-ellipsis-multiple-line`]: cssLineClamp },
                    className,
                  ]}
                  style={{
                    ...(style as CSSProperties),
                    WebkitLineClamp: cssLineClamp ? rows : undefined,
                  }}
                  aria-label={ariaLabel}
                  {...textProps}
                >
                  {showTooltip ? (
                    <Tooltip title={tooltip === true ? children : title}>
                      <span>{textNode}</span>
                    </Tooltip>
                  ) : (
                    textNode
                  )}
                  {renderOperations()}
                </Typography>
              </ResizeObserver>
            );
          }}
        />
      );
    };
  },
});

export const baseProps = () => ({
  editable: PropTypes.oneOfType([PropTypes.looseBool, PropTypes.object]),
  copyable: PropTypes.oneOfType([PropTypes.looseBool, PropTypes.object]),
  prefixCls: PropTypes.string,
  component: PropTypes.string,
  type: PropTypes.oneOf(['secondary', 'success', 'danger', 'warning']),
  disabled: PropTypes.looseBool,
  ellipsis: PropTypes.oneOfType([PropTypes.looseBool, PropTypes.object]),
  code: PropTypes.looseBool,
  mark: PropTypes.looseBool,
  underline: PropTypes.looseBool,
  delete: PropTypes.looseBool,
  strong: PropTypes.looseBool,
  keyboard: PropTypes.looseBool,
  content: PropTypes.string,
});

Base.props = baseProps();
export default Base;
