import { css } from '@emotion/css';
import cx from 'classnames';
import React, { MouseEvent, memo } from 'react';
import tinycolor from 'tinycolor2';

import { Field, getFieldColorModeForField, GrafanaTheme2 } from '@grafana/data';
import { Icon, useTheme2 } from '@grafana/ui';

import { HoverState } from './NodeGraph';
import { NodeDatum } from './types';
import { statToString } from './utils';

export const nodeR = 40;
export const highlightedNodeColor = '#a00';

const getStyles = (theme: GrafanaTheme2, hovering: HoverState) => ({
  mainGroup: css`
    cursor: pointer;
    font-size: 10px;
    transition: opacity 300ms;
    opacity: ${hovering === 'inactive' ? 0.5 : 1};
  `,

  mainCircle: css`
    fill: ${theme.components.panel.background};
  `,

  filledCircle: css`
    fill: ${highlightedNodeColor};
  `,

  hoverCircle: css`
    opacity: 0.5;
    fill: transparent;
    stroke: ${theme.colors.primary.text};
  `,

  text: css`
    fill: ${theme.colors.text.primary};
    pointer-events: none;
  `,

  titleText: css`
    text-align: center;
    text-overflow: ellipsis;
    overflow: hidden;
    white-space: nowrap;
    background-color: ${tinycolor(theme.colors.background.primary).setAlpha(0.6).toHex8String()};
    width: 140px;
  `,

  statsText: css`
    text-align: center;
    text-overflow: ellipsis;
    overflow: hidden;
    white-space: nowrap;
    width: 70px;
  `,

  textHovering: css`
    width: 200px;
    & span {
      background-color: ${tinycolor(theme.colors.background.primary).setAlpha(0.8).toHex8String()};
    }
  `,

  clickTarget: css`
    fill: none;
    stroke: none;
    pointer-events: fill;
  `,
});

export const computeNodeCircumferenceStrokeWidth = (nodeRadius: number) => Math.ceil(nodeRadius * 0.075);

export const Node = memo(function Node(props: {
  node: NodeDatum;
  hovering: HoverState;
  onMouseEnter: (id: string) => void;
  onMouseLeave: (id: string) => void;
  onClick: (event: MouseEvent<SVGElement>, node: NodeDatum) => void;
}) {
  const { node, onMouseEnter, onMouseLeave, onClick, hovering } = props;
  const theme = useTheme2();
  const styles = getStyles(theme, hovering);
  const isHovered = hovering === 'active';
  const nodeRadius = node.nodeRadius?.values[node.dataFrameRowIndex] || nodeR;
  const strokeWidth = computeNodeCircumferenceStrokeWidth(nodeRadius);

  if (!(node.x !== undefined && node.y !== undefined)) {
    return null;
  }

  return (
    <g data-node-id={node.id} className={styles.mainGroup} aria-label={`Node: ${node.title}`}>
      <circle
        data-testid={`node-circle-${node.id}`}
        className={node.highlighted ? styles.filledCircle : styles.mainCircle}
        r={nodeRadius}
        cx={node.x}
        cy={node.y}
      />
      {isHovered && (
        <circle className={styles.hoverCircle} r={nodeRadius - 3} cx={node.x} cy={node.y} strokeWidth={strokeWidth} />
      )}
      <ColorCircle node={node} />
      <g className={styles.text} style={{ pointerEvents: 'none' }}>
        <NodeContents node={node} hovering={hovering} />
        <foreignObject
          x={node.x - (isHovered ? 100 : 70)}
          y={node.y + nodeRadius + 5}
          width={isHovered ? '200' : '140'}
          height="40"
        >
          <div className={cx(styles.titleText, isHovered && styles.textHovering)}>
            <span>{node.title}</span>
            <br />
            <span>{node.subTitle}</span>
          </div>
        </foreignObject>
      </g>
      <rect
        data-testid={`node-click-rect-${node.id}`}
        onMouseEnter={() => {
          onMouseEnter(node.id);
        }}
        onMouseLeave={() => {
          onMouseLeave(node.id);
        }}
        onClick={(event) => {
          onClick(event, node);
        }}
        className={styles.clickTarget}
        x={node.x - nodeRadius - 5}
        y={node.y - nodeRadius - 5}
        width={nodeRadius * 2 + 10}
        height={nodeRadius * 2 + 50}
      />
    </g>
  );
});

/**
 * Shows contents of the node which can be either an Icon or a main and secondary stat values.
 */
function NodeContents({ node, hovering }: { node: NodeDatum; hovering: HoverState }) {
  const theme = useTheme2();
  const styles = getStyles(theme, hovering);
  const isHovered = hovering === 'active';

  if (!(node.x !== undefined && node.y !== undefined)) {
    return null;
  }

  return node.icon ? (
    <foreignObject x={node.x - 35} y={node.y - 20} width="70" height="40">
      <div style={{ width: 70, overflow: 'hidden', display: 'flex', justifyContent: 'center', marginTop: -4 }}>
        <Icon data-testid={`node-icon-${node.icon}`} name={node.icon} size={'xxxl'} />
      </div>
    </foreignObject>
  ) : (
    <foreignObject x={node.x - (isHovered ? 100 : 35)} y={node.y - 15} width={isHovered ? '200' : '70'} height="40">
      <div className={cx(styles.statsText, isHovered && styles.textHovering)}>
        <span>{node.mainStat && statToString(node.mainStat.config, node.mainStat.values[node.dataFrameRowIndex])}</span>
        <br />
        <span>
          {node.secondaryStat &&
            statToString(node.secondaryStat.config, node.secondaryStat.values[node.dataFrameRowIndex])}
        </span>
      </div>
    </foreignObject>
  );
}

/**
 * Shows the outer segmented circle with different colors based on the supplied data.
 */
function ColorCircle(props: { node: NodeDatum }) {
  const { node } = props;
  const fullStat = node.arcSections.find((s) => s.values[node.dataFrameRowIndex] >= 1);
  const theme = useTheme2();
  const nodeRadius = node.nodeRadius?.values[node.dataFrameRowIndex] || nodeR;
  const strokeWidth = computeNodeCircumferenceStrokeWidth(nodeRadius);

  if (fullStat) {
    // Drawing a full circle with a `path` tag does not work well, it's better to use a `circle` tag in that case
    return (
      <circle
        fill="none"
        stroke={theme.visualization.getColorByName(fullStat.config.color?.fixedColor || '')}
        strokeWidth={strokeWidth}
        r={nodeRadius}
        cx={node.x}
        cy={node.y}
      />
    );
  }

  const nonZero = node.arcSections.filter((s) => s.values[node.dataFrameRowIndex] !== 0);
  if (nonZero.length === 0) {
    // Fallback if no arc is defined
    return (
      <circle
        fill="none"
        stroke={node.color ? getColor(node.color, node.dataFrameRowIndex, theme) : 'gray'}
        strokeWidth={strokeWidth}
        r={nodeRadius}
        cx={node.x}
        cy={node.y}
      />
    );
  }

  const { elements } = nonZero.reduce<{
    elements: React.ReactNode[];
    percent: number;
  }>(
    (acc, section, index) => {
      const color = section.config.color?.fixedColor || '';
      const value = section.values[node.dataFrameRowIndex];

      const el = (
        <ArcSection
          key={index}
          r={nodeRadius}
          x={node.x!}
          y={node.y!}
          startPercent={acc.percent}
          percent={
            value + acc.percent > 1
              ? // If the values aren't correct and add up to more than 100% lets still render correctly the amounts we
                // already have and cap it at 100%
                1 - acc.percent
              : value
          }
          color={theme.visualization.getColorByName(color)}
          strokeWidth={strokeWidth}
        />
      );
      acc.elements.push(el);
      acc.percent = acc.percent + value;
      return acc;
    },
    { elements: [], percent: 0 }
  );

  return <>{elements}</>;
}

function ArcSection({
  r,
  x,
  y,
  startPercent,
  percent,
  color,
  strokeWidth = 2,
}: {
  r: number;
  x: number;
  y: number;
  startPercent: number;
  percent: number;
  color: string;
  strokeWidth?: number;
}) {
  const endPercent = startPercent + percent;
  const startXPos = x + Math.sin(2 * Math.PI * startPercent) * r;
  const startYPos = y - Math.cos(2 * Math.PI * startPercent) * r;
  const endXPos = x + Math.sin(2 * Math.PI * endPercent) * r;
  const endYPos = y - Math.cos(2 * Math.PI * endPercent) * r;
  const largeArc = percent > 0.5 ? '1' : '0';
  return (
    <path
      fill="none"
      d={`M ${startXPos} ${startYPos} A ${r} ${r} 0 ${largeArc} 1 ${endXPos} ${endYPos}`}
      stroke={color}
      strokeWidth={strokeWidth}
    />
  );
}

function getColor(field: Field, index: number, theme: GrafanaTheme2): string {
  if (!field.config.color) {
    return field.values[index];
  }

  return getFieldColorModeForField(field).getCalculator(field, theme)(0, field.values[index]);
}
