import type { DisplayObject, DisplayObjectConfig, Group, LineStyleProps, PathStyleProps } from '@antv/g';
import { Image, Path } from '@antv/g';
import type { PathArray } from '@antv/util';
import { isFunction, pick } from '@antv/util';
import type {
  Edge,
  EdgeArrowStyleProps,
  EdgeBadgeStyleProps,
  EdgeKey,
  EdgeLabelStyleProps,
  ID,
  Keyframe,
  LoopStyleProps,
  Node,
  Point,
  Prefix,
} from '../../types';
import { getBBoxHeight, getBBoxWidth, getNodeBBox } from '../../utils/bbox';
import { getArrowSize, getBadgePositionStyle, getCubicLoopPath, getLabelPositionStyle } from '../../utils/edge';
import { findPorts, getConnectionPoint, getPortPosition, isSameNode } from '../../utils/element';
import { subStyleProps } from '../../utils/prefix';
import { parseSize } from '../../utils/size';
import { mergeOptions } from '../../utils/style';
import * as Symbol from '../../utils/symbol';
import { getWordWrapWidthByEnds } from '../../utils/text';
import { BaseElement } from '../base-element';
import type { BadgeStyleProps, BaseShapeStyleProps, LabelStyleProps } from '../shapes';
import { Badge, Label } from '../shapes';

/**
 * <zh/> 边的通用样式属性
 *
 * <en/> Base style properties of the edge
 */
export interface BaseEdgeStyleProps
  extends BaseShapeStyleProps,
    Prefix<'label', EdgeLabelStyleProps>,
    Prefix<'halo', PathStyleProps>,
    Prefix<'badge', EdgeBadgeStyleProps>,
    Prefix<'startArrow', EdgeArrowStyleProps>,
    Prefix<'endArrow', EdgeArrowStyleProps>,
    Prefix<'loop', LoopStyleProps> {
  /**
   * <zh/> 是否显示边的标签
   *
   * <en/> Whether to display the label of the edge
   * @defaultValue true
   */
  label?: boolean;
  /**
   * <zh/> 是否启用自环边
   *
   * <en/> Whether to enable self-loop edge
   * @defaultValue true
   */
  loop?: boolean;
  /**
   * <zh/> 是否显示边的光晕
   *
   * <en/> Whether to display the halo of the edge
   * @defaultValue false
   */
  halo?: boolean;
  /**
   * <zh/> 是否显示边的徽标
   *
   * <en/> Whether to display the badge of the edge
   * @defaultValue true
   */
  badge?: boolean;
  /**
   * <zh/> 是否显示边的起始箭头
   *
   * <en/> Whether to display the start arrow of the edge
   * @defaultValue false
   */
  startArrow?: boolean;
  /**
   * <zh/> 是否显示边的结束箭头
   *
   * <en/> Whether to display the end arrow of the edge
   * @defaultValue false
   */
  endArrow?: boolean;
  /**
   * <zh/> 起始箭头的偏移量
   *
   * <en/> Offset of the start arrow
   */
  startArrowOffset?: number;
  /**
   * <zh/> 结束箭头的偏移量
   *
   * <en/> Offset of the end arrow
   */
  endArrowOffset?: number;
  /**
   * <zh/> 边的起点 ID
   *
   * <en/> The ID of the source node
   * @remarks
   * <zh/> 该属性指向物理意义上的起点，由 G6 内部维护，用户无需过多关注。通常情况下，`sourceNode` 与上一级的 `source` 属性一致。但在某些情况下，`sourceNode` 可能会被 G6 内部转换，例如在 Combo 收起时内部节点上的边会自动连接到父 Combo，此时 `sourceNode` 会变更为父 Combo 的 ID。
   *
   * <en/> This property concerning the physical origin, maintained internally by G6. In general, `sourceNode` corresponds to the `source` attribute of the parent level. However, in certain cases, such as when a Combo is collapsed and internal nodes are destroyed, corresponding edges will automatically connect to the parent Combo. At this point, `sourceNode` will be changed to the ID of the parent Combo
   */
  sourceNode: ID;
  /**
   * <zh/> 边的终点 shape
   *
   * <en/> The source shape. Represents the start of the edge
   */
  targetNode: ID;
  /**
   * <zh/> 边起始连接的 port
   *
   * <en/> The Port of the source node
   */
  sourcePort?: string;
  /**
   * <zh/> 边终点连接的 port
   *
   * <en/> The Port of the target node
   */
  targetPort?: string;
  /**
   * <zh/> 在 “起点” 处添加一个标记图形，其中 “起始点” 为边与起始节点的交点
   *
   * <en/> Add a marker at the "start point", where the "start point" is the intersection of the edge and the source node
   */
  markerStart?: DisplayObject | null;
  /**
   * <zh/> 调整 “起点” 处标记图形的位置，正偏移量向内，负偏移量向外
   *
   * <en/> Adjust the position of the marker at the "start point", positive offset inward, negative offset outward
   * @defaultValue 0
   */
  markerStartOffset?: number;
  /**
   * <zh/> 在 “终点” 处添加一个标记图形，其中 “终点” 为边与终止节点的交点
   *
   * <en/> Add a marker at the "end point", where the "end point" is the intersection of the edge and the target node
   */
  markerEnd?: DisplayObject | null;
  /**
   * <zh/> 调整 “终点” 处标记图形的位置，正偏移量向内，负偏移量向外
   *
   * <en/> Adjust the position of the marker at the "end point", positive offset inward, negative offset outward
   * @defaultValue 0
   */
  markerEndOffset?: number;
  /**
   * <zh/> 在路径除了 “起点” 和 “终点” 之外的每一个顶点上放置标记图形。在内部实现中，由于我们会把路径中部分命令转换成 C 命令，因此这些顶点实际是三阶贝塞尔曲线的控制点
   *
   * <en/> Place a marker on each vertex of the path except for the "start point" and "end point". In the internal implementation, because we will convert some commands in the path to C commands, these controlPoints are actually the control points of the cubic Bezier curve
   */
  markerMid?: DisplayObject | null;
  /**
   * <zh/> 3D 场景中生效，始终朝向屏幕，因此线宽不受透视投影影像
   *
   * <en/> Effective in 3D scenes, always facing the screen, so the line width is not affected by the perspective projection image
   * @defaultValue true
   */
  isBillboard?: boolean;
}

type ParsedBaseEdgeStyleProps = Required<BaseEdgeStyleProps>;

/**
 * <zh/> 边元素基类
 *
 * <en/> Base class of the edge
 */
export abstract class BaseEdge extends BaseElement<BaseEdgeStyleProps> implements Edge {
  public type = 'edge';

  static defaultStyleProps: Partial<BaseEdgeStyleProps> = {
    badge: true,
    badgeOffsetX: 0,
    badgeOffsetY: 0,
    badgePlacement: 'suffix',
    isBillboard: true,
    label: true,
    labelAutoRotate: true,
    labelIsBillboard: true,
    labelMaxWidth: '80%',
    labelOffsetX: 4,
    labelOffsetY: 0,
    labelPlacement: 'center',
    labelTextBaseline: 'middle',
    labelWordWrap: false,
    halo: false,
    haloDroppable: false,
    haloLineDash: 0,
    haloLineWidth: 12,
    haloPointerEvents: 'none',
    haloStrokeOpacity: 0.25,
    haloZIndex: -1,
    loop: true,
    startArrow: false,
    startArrowLineDash: 0,
    startArrowLineJoin: 'round',
    startArrowLineWidth: 1,
    startArrowTransformOrigin: 'center',
    startArrowType: 'vee',
    endArrow: false,
    endArrowLineDash: 0,
    endArrowLineJoin: 'round',
    endArrowLineWidth: 1,
    endArrowTransformOrigin: 'center',
    endArrowType: 'vee',
    loopPlacement: 'top',
    loopClockwise: true,
  };

  constructor(options: DisplayObjectConfig<BaseEdgeStyleProps>) {
    super(mergeOptions({ style: BaseEdge.defaultStyleProps }, options));
  }

  protected get sourceNode() {
    const { sourceNode: source } = this.parsedAttributes;
    return this.context.element!.getElement<Node>(source)!;
  }

  protected get targetNode() {
    const { targetNode: target } = this.parsedAttributes;
    return this.context.element!.getElement<Node>(target)!;
  }

  protected getKeyStyle(attributes: ParsedBaseEdgeStyleProps): PathStyleProps {
    const { loop, ...style } = this.getGraphicStyle(attributes);
    const { sourceNode, targetNode } = this;

    const d = loop && isSameNode(sourceNode, targetNode) ? this.getLoopPath(attributes) : this.getKeyPath(attributes);

    const keyStyle: PathStyleProps = { d };

    Path.PARSED_STYLE_LIST.forEach((key) => {
      // @ts-expect-error skip type error
      if (key in style) keyStyle[key] = style[key];
    });

    return keyStyle;
  }

  protected abstract getKeyPath(attributes: ParsedBaseEdgeStyleProps): PathArray;

  protected getLoopPath(attributes: ParsedBaseEdgeStyleProps): PathArray {
    const { sourcePort, targetPort } = attributes;
    const node = this.sourceNode;

    const bbox = getNodeBBox(node);
    const defaultDist = Math.max(getBBoxWidth(bbox), getBBoxHeight(bbox));

    const {
      placement,
      clockwise,
      dist = defaultDist,
    } = subStyleProps<Required<LoopStyleProps>>(this.getGraphicStyle(attributes), 'loop');

    return getCubicLoopPath(node, placement, clockwise, dist, sourcePort, targetPort);
  }

  protected getEndpoints(
    attributes: ParsedBaseEdgeStyleProps,
    optimize = true,
    controlPoints: Point[] | (() => Point[]) = [],
  ): [Point, Point] {
    const { sourcePort: sourcePortKey, targetPort: targetPortKey } = attributes;
    const { sourceNode, targetNode } = this;

    const [sourcePort, targetPort] = findPorts(sourceNode, targetNode, sourcePortKey, targetPortKey);

    if (!optimize) {
      const sourcePoint = sourcePort ? getPortPosition(sourcePort) : sourceNode.getCenter();
      const targetPoint = targetPort ? getPortPosition(targetPort) : targetNode.getCenter();
      return [sourcePoint, targetPoint];
    }

    const _controlPoints = typeof controlPoints === 'function' ? controlPoints() : controlPoints;
    const sourcePoint = getConnectionPoint(sourcePort || sourceNode, _controlPoints[0] || targetPort || targetNode);
    const targetPoint = getConnectionPoint(
      targetPort || targetNode,
      _controlPoints[_controlPoints.length - 1] || sourcePort || sourceNode,
    );

    return [sourcePoint, targetPoint];
  }

  protected getHaloStyle(attributes: ParsedBaseEdgeStyleProps): false | PathStyleProps {
    if (attributes.halo === false) return false;

    const keyStyle = this.getKeyStyle(attributes);
    const haloStyle = subStyleProps<LineStyleProps>(this.getGraphicStyle(attributes), 'halo');

    return { ...keyStyle, ...haloStyle };
  }

  protected getLabelStyle(attributes: ParsedBaseEdgeStyleProps): false | LabelStyleProps {
    if (attributes.label === false || !attributes.labelText) return false;

    const labelStyle = subStyleProps<Required<EdgeLabelStyleProps>>(this.getGraphicStyle(attributes), 'label');
    const { placement, offsetX, offsetY, autoRotate, maxWidth, ...restStyle } = labelStyle;
    const labelPositionStyle = getLabelPositionStyle(
      this.shapeMap.key as EdgeKey,
      placement,
      autoRotate,
      offsetX,
      offsetY,
    );

    const bbox = this.shapeMap.key.getLocalBounds();
    const wordWrapWidth = getWordWrapWidthByEnds([bbox.min, bbox.max], maxWidth);

    return Object.assign({ wordWrapWidth }, labelPositionStyle, restStyle);
  }

  protected getBadgeStyle(attributes: ParsedBaseEdgeStyleProps): false | BadgeStyleProps {
    if (attributes.badge === false || !attributes.badgeText) return false;

    const { offsetX, offsetY, placement, ...badgeStyle } = subStyleProps<Required<EdgeBadgeStyleProps>>(
      attributes,
      'badge',
    );

    return Object.assign(
      badgeStyle,
      getBadgePositionStyle(this.shapeMap, placement, attributes.labelPlacement, offsetX, offsetY),
    );
  }

  protected drawArrow(attributes: ParsedBaseEdgeStyleProps, type: 'start' | 'end') {
    const isStart = type === 'start';
    const arrowType = type === 'start' ? 'startArrow' : 'endArrow';
    const enable = attributes[arrowType];

    const keyShape = this.shapeMap.key as Path;

    if (enable) {
      const arrowStyle = this.getArrowStyle(attributes, isStart);

      const [marker, markerOffset, arrowOffset] = isStart
        ? (['markerStart', 'markerStartOffset', 'startArrowOffset'] as const)
        : (['markerEnd', 'markerEndOffset', 'endArrowOffset'] as const);

      const arrow = keyShape.parsedStyle[marker];
      // update
      if (arrow) arrow.attr(arrowStyle);
      // create
      else {
        const Ctor = arrowStyle.src ? Image : Path;
        const arrowShape = new Ctor({ style: arrowStyle });
        keyShape.style[marker] = arrowShape;
      }
      keyShape.style[markerOffset] = attributes[arrowOffset] || arrowStyle.width / 2 + +arrowStyle.lineWidth;
    } else {
      // destroy
      const marker = isStart ? 'markerStart' : 'markerEnd';
      keyShape.style[marker]?.destroy();
      keyShape.style[marker] = null;
    }
  }

  private getArrowStyle(attributes: ParsedBaseEdgeStyleProps, isStart: boolean) {
    const keyStyle = this.getShape('key')!.attributes;
    const arrowType = isStart ? 'startArrow' : 'endArrow';
    const { size, type, ...arrowStyle } = subStyleProps<Required<EdgeArrowStyleProps>>(
      this.getGraphicStyle(attributes),
      arrowType,
    );
    const [width, height] = parseSize(getArrowSize(keyStyle.lineWidth, size));
    const arrowFn = isFunction(type) ? type : Symbol[type] || Symbol.triangle;
    const d = arrowFn(width, height);

    return Object.assign(
      pick(keyStyle, ['stroke', 'strokeOpacity', 'fillOpacity']),
      { width, height },
      { ...(d && { d, fill: type === 'simple' ? '' : keyStyle.stroke }) },
      arrowStyle,
    );
  }

  protected drawLabelShape(attributes: ParsedBaseEdgeStyleProps, container: Group) {
    const style = this.getLabelStyle(attributes);
    this.upsert('label', Label, style, container);
  }

  protected drawHaloShape(attributes: ParsedBaseEdgeStyleProps, container: Group) {
    const style = this.getHaloStyle(attributes);
    this.upsert('halo', Path, style, container);
  }

  protected drawBadgeShape(attributes: ParsedBaseEdgeStyleProps, container: Group) {
    const style = this.getBadgeStyle(attributes);
    this.upsert('badge', Badge, style, container);
  }

  protected drawSourceArrow(attributes: ParsedBaseEdgeStyleProps) {
    this.drawArrow(attributes, 'start');
  }

  protected drawTargetArrow(attributes: ParsedBaseEdgeStyleProps) {
    this.drawArrow(attributes, 'end');
  }

  protected drawKeyShape(attributes: ParsedBaseEdgeStyleProps, container: Group): Path | undefined {
    const style = this.getKeyStyle(attributes);
    return this.upsert('key', Path, style, container);
  }

  public render(attributes = this.parsedAttributes, container: Group = this): void {
    // 1. key shape
    this.drawKeyShape(attributes, container);
    if (!this.getShape('key')) return;

    // 2. arrows
    this.drawSourceArrow(attributes);
    this.drawTargetArrow(attributes);

    // 3. label
    this.drawLabelShape(attributes, container);

    // 4. halo
    this.drawHaloShape(attributes, container);

    // 5. badges
    this.drawBadgeShape(attributes, container);
  }

  protected onframe() {
    this.drawKeyShape(this.parsedAttributes, this);
    this.drawSourceArrow(this.parsedAttributes);
    this.drawTargetArrow(this.parsedAttributes);
    this.drawHaloShape(this.parsedAttributes, this);
    this.drawLabelShape(this.parsedAttributes, this);
    this.drawBadgeShape(this.parsedAttributes, this);
  }

  public animate(keyframes: Keyframe[], options?: number | KeyframeAnimationOptions) {
    const animation = super.animate(keyframes, options);

    if (!animation) return animation;

    // 设置 currentTime 时触发更新
    // Trigger update when setting currentTime
    return new Proxy(animation, {
      set: (target, propKey, value) => {
        // 需要推迟 onframe 调用时机，等待节点位置更新完成
        // Need to delay the timing of the onframe call, wait for the node position update to complete
        if (propKey === 'currentTime') Promise.resolve().then(() => this.onframe());
        return Reflect.set(target, propKey, value);
      },
    });
  }
}
