import type { ComponentProps, CSSProperties, ReactNode } from "react";
import type { EllipsisConfig } from "antd/lib/typography/Base";
import type {
  ContentEntity,
  LinkEntity,
  MentionEntity,
  TagEntity,
} from "../../core";

import React, { useCallback, useEffect, useMemo, useRef, useState } from "react";

import { TachTypography } from "@hublib-web/tach-typography/react";
import { findAllEntities } from "../../core";

type CustomEllipsisConfig =
  | ({
      count: number;
      rows?: never;
      expandable?: boolean;
    } & Partial<EllipsisConfig>)
  | ({
      rows: number;
      count?: never;
      expandable?: boolean;
    } & Partial<EllipsisConfig>)
  | false;

type ParagraphBodyProps = ComponentProps<typeof TachTypography.Paragraph.Body>;
type TextBodyProps = ComponentProps<typeof TachTypography.Text.Body>;
type TitleBodyProps = ComponentProps<typeof TachTypography.Title.Body>;

const joinClassName = (...values: Array<string | false | undefined>) =>
  values.filter(Boolean).join(" ");

const baseSelectableStyle: CSSProperties = {
  whiteSpace: "pre-wrap",
  WebkitTouchCallout: "default",
  WebkitUserSelect: "text",
  KhtmlUserSelect: "text",
  MozUserSelect: "text",
  msUserSelect: "text",
  userSelect: "text",
};

const blurStyle: CSSProperties = {
  filter: "blur(3px)",
  WebkitUserSelect: "none",
  KhtmlUserSelect: "none",
  MozUserSelect: "none",
  msUserSelect: "none",
  userSelect: "none",
  pointerEvents: "none",
};

const ReadMoreButton: React.FC<{
  onClick: (event: React.MouseEvent<HTMLElement, MouseEvent>) => void;
  symbol?: EllipsisConfig["symbol"];
  expanded?: boolean;
  TitleComponent: React.ComponentType<TitleBodyProps>;
}> = ({ onClick, symbol = "Читать полностью", expanded = false, TitleComponent }) => (
  <TitleComponent
    onClick={event => {
      event.preventDefault();
      event.stopPropagation();
      onClick(event);
    }}
    weight="bold"
    level={5}
  >
    {typeof symbol === "function" ? symbol(expanded) : symbol}
  </TitleComponent>
);

export interface ContentTextProps {
  className?: string;
  weight?: TextBodyProps["weight"];
  text?: string | null;
  ellipsis?: CustomEllipsisConfig;
  blur?: boolean;
  style?: TextBodyProps["style"];
  onView?: () => void;
  renderMention?: (entity: MentionEntity, index: number) => ReactNode;
  renderTag?: (entity: TagEntity, index: number) => ReactNode;
  renderLink?: (entity: LinkEntity, index: number) => ReactNode;
  ParagraphComponent?: React.ComponentType<ParagraphBodyProps>;
  TextComponent?: React.ComponentType<TextBodyProps>;
  TitleComponent?: React.ComponentType<TitleBodyProps>;
}

export const ContentText: React.NamedExoticComponent<ContentTextProps> = React.memo(
  ({
    text,
    className,
    ellipsis = false,
    blur = false,
    weight = "normal",
    style,
    onView,
    renderMention,
    renderTag,
    renderLink,
    ParagraphComponent = TachTypography.Paragraph.Body,
    TextComponent = TachTypography.Text.Body,
    TitleComponent = TachTypography.Title.Body,
    ...props
  }: ContentTextProps) => {
    const containerRef = useRef<HTMLDivElement | null>(null);
    const [expanded, setExpanded] = useState(false);
    const [containerInsideView, setContainerInsideView] = useState(false);
    const [viewed, setViewed] = useState(false);
    const content = text || "";

    const entities = useMemo<ContentEntity[]>(() => findAllEntities(content), [content]);

    const wrapWithKey = (node: ReactNode, key: React.Key) => {
      if (React.isValidElement(node)) {
        return React.cloneElement(node, { key });
      }

      return <React.Fragment key={key}>{node}</React.Fragment>;
    };

    const buildMentionNode = (entity: MentionEntity, index: number) => {
      const defaultNode = (
        <TextComponent color="link" weight={weight}>
          {entity.displayText}
        </TextComponent>
      );
      const customNode = renderMention?.(entity, index) ?? defaultNode;
      return wrapWithKey(customNode, `mention-${entity.start}-${index}`);
    };

    const buildTagNode = (entity: TagEntity, index: number) => {
      const defaultNode = (
        <TextComponent color="link" weight={weight}>
          {entity.text}
        </TextComponent>
      );
      const customNode = renderTag?.(entity, index) ?? defaultNode;
      return wrapWithKey(customNode, `tag-${entity.start}-${index}`);
    };

    const buildLinkNode = (entity: LinkEntity, index: number) => {
      const defaultNode = (
        <TachTypography.Link.Body
          target="_blank"
          referrerPolicy="no-referrer"
          color="link"
          weight={weight}
          href={entity.url}
        >
          {entity.text}
        </TachTypography.Link.Body>
      );
      const customNode = renderLink?.(entity, index) ?? defaultNode;
      return wrapWithKey(customNode, `link-${entity.start}-${index}`);
    };

    const buildParts = (upto: number | null = null): ReactNode[] => {
      let lastIndex = 0;
      const nodes: ReactNode[] = [];

      for (const [i, entity] of entities.entries()) {

        if (upto !== null && entity.start >= upto) {
          break;
        }

        const textEnd = upto !== null ? Math.min(entity.start, upto) : entity.start;
        if (entity.start > lastIndex && lastIndex < textEnd) {
          nodes.push(content.slice(lastIndex, textEnd));
        }

        if (upto === null || entity.end <= upto) {
          if (entity.type === "mention") {
            nodes.push(buildMentionNode(entity, i));
          } else if (entity.type === "tag") {
            nodes.push(buildTagNode(entity, i));
          } else if (entity.type === "link") {
            nodes.push(buildLinkNode(entity, i));
          }
        }

        lastIndex = entity.end;
      }

      if (upto === null) {
        if (lastIndex < content.length) {
          nodes.push(content.slice(lastIndex));
        }
      } else if (lastIndex < upto) {
        nodes.push(content.slice(lastIndex, upto));
      }

      return nodes;
    };

    const ellipsisConfig = ellipsis && typeof ellipsis === "object" ? ellipsis : null;
    const expandedFromProps =
      ellipsisConfig && typeof ellipsisConfig.expanded === "boolean"
        ? ellipsisConfig.expanded
        : undefined;
    const isExpandedControlled = expandedFromProps !== undefined;
    const mergedExpanded = isExpandedControlled ? expandedFromProps : expanded;

    const handleExpand = useCallback(
      (event: React.MouseEvent<HTMLElement, MouseEvent>) => {
        if (!isExpandedControlled) {
          setExpanded(true);
        }

        ellipsisConfig?.onExpand?.(event, { expanded: true });
      },
      [ellipsisConfig, isExpandedControlled],
    );

    useEffect(() => {
      if (isExpandedControlled) {
        return;
      }

      if (ellipsisConfig && "count" in ellipsisConfig) {
        const count = ellipsisConfig.count ?? 0;
        if (content.length <= count && !expanded) {
          setExpanded(true);
        }
      }
    }, [content.length, ellipsisConfig, expanded, isExpandedControlled]);

    useEffect(() => {
      if (mergedExpanded && !viewed && containerInsideView) {
        setViewed(true);
        onView?.();
      }
    }, [mergedExpanded, viewed, containerInsideView, onView]);

    useEffect(() => {
      const ref = containerRef.current;
      if (!ref) {
        return;
      }

      const observer = new IntersectionObserver((entries) => {
        const entry = entries[0];
        if (!entry) {
          return;
        }

        if (entry.isIntersecting && !containerInsideView) {
          setContainerInsideView(true);
        }
      }, { threshold: 0.5 });

      observer.observe(ref);

      return () => {
        observer.unobserve(ref);
      };
    }, [containerInsideView]);

    const mergedStyle: CSSProperties = {
      ...baseSelectableStyle,
      ...(blur ? blurStyle : undefined),
      ...(style as CSSProperties | undefined),
    };

    if (ellipsisConfig && "count" in ellipsisConfig) {
      const { count, expandable } = ellipsisConfig;

      if (!mergedExpanded && count && content.length > count) {
        let cutoff = count;
        let extended = true;

        while (extended) {
          extended = false;
          for (const entity of entities) {
            if (entity.start < cutoff && entity.end > cutoff) {
              cutoff = entity.end;
              extended = true;
            }
          }
        }

        const truncatedNodes = buildParts(cutoff);

        return (
          <ParagraphComponent
            ref={containerRef}
            weight={weight}
            className={joinClassName(className)}
            style={mergedStyle}
            {...(props as ParagraphBodyProps)}
          >
            {truncatedNodes}…
            {expandable && (
              <ReadMoreButton
                symbol={ellipsisConfig.symbol}
                onClick={handleExpand}
                expanded={mergedExpanded}
                TitleComponent={TitleComponent}
              />
            )}
          </ParagraphComponent>
        );
      }
    }

    if (ellipsisConfig && "rows" in ellipsisConfig) {
      const paragraphEllipsis = mergedExpanded
        ? false
        : {
            ...ellipsisConfig,
            symbol: ellipsisConfig.expandable ? (
              <ReadMoreButton
                symbol={ellipsisConfig.symbol}
                onClick={handleExpand}
                expanded={mergedExpanded}
                TitleComponent={TitleComponent}
              />
            ) : (
              ellipsisConfig.symbol
            ),
          };

      return (
        <ParagraphComponent
          ref={containerRef}
          weight={weight}
          className={joinClassName(className)}
          style={mergedStyle}
          ellipsis={paragraphEllipsis}
          {...(props as ParagraphBodyProps)}
        >
          {buildParts()}
        </ParagraphComponent>
      );
    }

    return (
      <ParagraphComponent
        ref={containerRef}
        weight={weight}
        className={joinClassName(className)}
        style={mergedStyle}
        {...(props as ParagraphBodyProps)}
      >
        {buildParts()}
      </ParagraphComponent>
    );
  },
);

ContentText.displayName = "ContentText";
