import * as React from 'react';
import { useCallback, useEffect, useLayoutEffect, useMemo, useReducer, useRef, useState } from 'react';
import { createPortal } from 'react-dom';
import tinycolor from 'tinycolor2';
import uPlot from 'uplot';

import { arrayToDataFrame, colorManipulator, type DataFrame, DataTopic, type InterpolateFunction } from '@grafana/data';
import { type TimeZone } from '@grafana/schema';
import {
  DEFAULT_ANNOTATION_COLOR,
  getPortalContainer,
  type UPlotConfigBuilder,
  usePanelContext,
  useTheme2,
} from '@grafana/ui';

import { AnnotationMarker2 } from './annotations2/AnnotationMarker2';
import { ANNOTATION_LANE_SIZE, getXAnnotationFrames, getXYAnnotationFrames } from './utils';

// (copied from TooltipPlugin2)
interface TimeRange2 {
  from: number;
  to: number;
}

interface AnnotationsPluginProps {
  config: UPlotConfigBuilder;
  annotations?: DataFrame[];
  timeZone: TimeZone;
  newRange: TimeRange2 | null;
  setNewRange: (newRange: TimeRange2 | null) => void;
  canvasRegionRendering?: boolean;
  replaceVariables: InterpolateFunction;
  multiLane?: boolean;
}

// TODO: batch by color, use Path2D objects
const renderLine = (ctx: CanvasRenderingContext2D, y0: number, y1: number, x: number, color: string) => {
  ctx.beginPath();
  ctx.moveTo(x, y0);
  ctx.lineTo(x, y1);
  ctx.strokeStyle = color;
  ctx.stroke();
};

// const renderUpTriangle = (ctx: CanvasRenderingContext2D, x: number, y: number, w: number, h: number, color: string) => {
//   ctx.beginPath();
//   ctx.moveTo(x - w/2, y + h/2);
//   ctx.lineTo(x + w/2, y + h/2);
//   ctx.lineTo(x, y);
//   ctx.closePath();
//   ctx.fillStyle = color;
//   ctx.fill();
// }

const DEFAULT_ANNOTATION_COLOR_HEX8 = tinycolor(DEFAULT_ANNOTATION_COLOR).toHex8String();

function getVals(frame: DataFrame) {
  let vals: Record<string, any[]> = {};
  frame.fields.forEach((f) => {
    vals[f.name] = f.values;
  });

  return vals;
}

/**
 * @deprecated - remove and replace with AnnotationsPlugin2Cluster
 * @param annotations
 * @param timeZone
 * @param config
 * @param newRange
 * @param setNewRange
 * @param replaceVariables
 * @param canvasRegionRendering
 * @param multiLane
 * @constructor
 */
export const AnnotationsPlugin2 = ({
  annotations,
  timeZone,
  config,
  newRange,
  setNewRange,
  replaceVariables,
  canvasRegionRendering = true,
  multiLane = false,
}: AnnotationsPluginProps) => {
  const plotRef = useRef<uPlot | null>(null);

  const [portalRoot] = useState(() => getPortalContainer());
  const [pinnedAnnotationId, setPinnedAnnotationId] = useState<string | undefined>();
  const getColorByName = useTheme2().visualization.getColorByName;

  const [_, forceUpdate] = useReducer((x) => x + 1, 0);

  const { canExecuteActions } = usePanelContext();
  const userCanExecuteActions = canExecuteActions?.() ?? false;

  const { xAnnos, xyAnnos } = useMemo(() => {
    let xAnnos = getXAnnotationFrames(annotations);
    let xyAnnos = getXYAnnotationFrames(annotations);

    if (newRange) {
      let isRegion = newRange.to > newRange.from;

      const wipAnnoFrame = arrayToDataFrame([
        {
          time: newRange.from,
          timeEnd: isRegion ? newRange.to : null,
          isRegion: isRegion,
          color: DEFAULT_ANNOTATION_COLOR_HEX8,
        },
      ]);

      wipAnnoFrame.meta = {
        dataTopic: DataTopic.Annotations,
        custom: {
          isWip: true,
        },
      };

      xAnnos.push(wipAnnoFrame);
    }

    return {
      xAnnos,
      xyAnnos,
    };
  }, [annotations, newRange]);

  const exitWipEdit = useCallback(() => {
    setNewRange(null);
  }, [setNewRange]);

  const xAnnoRef = useRef(xAnnos);
  xAnnoRef.current = xAnnos;

  const xyAnnoRef = useRef(xyAnnos);
  xyAnnoRef.current = xyAnnos;

  const newRangeRef = useRef(newRange);
  newRangeRef.current = newRange;

  const xAxisRef = useRef<HTMLDivElement | undefined>(undefined);

  useLayoutEffect(() => {
    config.addHook('ready', (u) => {
      let xAxisEl = u.root.querySelector<HTMLDivElement>('.u-axis')!;
      xAxisRef.current = xAxisEl;
      plotRef.current = u;
      // If annos were defined before uPlot ready is called, we need to force the component to re-render annos now that uplot is available
      if (annotations?.length) {
        forceUpdate();
      }
    });

    config.addHook('draw', (u) => {
      let xAnnos = xAnnoRef.current;
      let xyAnnos = xyAnnoRef.current;

      const ctx = u.ctx;

      ctx.save();

      ctx.beginPath();
      ctx.rect(u.bbox.left, u.bbox.top, u.bbox.width, u.bbox.height);
      ctx.clip();

      // Multi-lane annotations do not support vertical lines or shaded regions
      xAnnos.forEach((frame) => {
        let vals = getVals(frame);
        if (!multiLane) {
          let y0 = u.bbox.top;
          let y1 = y0 + u.bbox.height;

          ctx.lineWidth = 2;
          ctx.setLineDash([5, 5]);

          for (let i = 0; i < vals.time.length; i++) {
            let color = getColorByName(vals.color?.[i] ?? DEFAULT_ANNOTATION_COLOR_HEX8);

            let x0 = u.valToPos(vals.time[i], 'x', true);
            renderLine(ctx, y0, y1, x0, color);

            // If dataframe does not have end times, let's omit rendering the region for now to prevent runtime error in valToPos
            // @todo do we want to fix isRegion to render a point (or use "to" as timeEnd) when we're missing timeEnd?
            if (vals.isRegion?.[i] && vals.timeEnd?.[i]) {
              let x1 = u.valToPos(vals.timeEnd[i], 'x', true);
              renderLine(ctx, y0, y1, x1, color);

              if (canvasRegionRendering) {
                ctx.fillStyle = colorManipulator.alpha(color, 0.1);
                ctx.fillRect(x0, y0, x1 - x0, u.bbox.height);
              }
            }
          }
        }
      });

      // xMin, xMax, yMin, yMax, color, lineWidth, lineStyle, fillOpacity, text
      xyAnnos.forEach((frame) => {
        let vals = getVals(frame);

        let xKey = config.scales[0].props.scaleKey;
        let yKey = config.scales[1].props.scaleKey;

        for (let i = 0; i < frame.length; i++) {
          let color = getColorByName(vals.color?.[i] || DEFAULT_ANNOTATION_COLOR_HEX8);

          let x0 = u.valToPos(vals.xMin[i], xKey, true);
          let x1 = u.valToPos(vals.xMax[i], xKey, true);
          let y0 = u.valToPos(vals.yMax[i], yKey, true);
          let y1 = u.valToPos(vals.yMin[i], yKey, true);

          ctx.fillStyle = colorManipulator.alpha(color, vals.fillOpacity[i]);
          ctx.fillRect(x0, y0, x1 - x0, y1 - y0);

          ctx.lineWidth = Math.round(vals.lineWidth[i] * uPlot.pxRatio);

          if (vals.lineStyle[i] === 'dash') {
            // maybe extract this to vals.lineDash[i] in future?
            ctx.setLineDash([5, 5]);
          } else {
            // solid
            ctx.setLineDash([]);
          }

          ctx.strokeStyle = color;
          ctx.strokeRect(x0, y0, x1 - x0, y1 - y0);
        }
      });

      ctx.restore();
    });
  }, [config, canvasRegionRendering, getColorByName, multiLane, annotations?.length]);

  // ensure xAnnos are re-drawn whenever they change
  useEffect(() => {
    if (plotRef.current) {
      plotRef.current.redraw();

      // this forces a second redraw after uPlot is updated (in the Plot.tsx didUpdate) with new data/scales
      // and ensures the anno marker positions in the dom are re-rendered in correct places
      // (this is temp fix until uPlot integrtion is refactored)
      setTimeout(() => {
        forceUpdate();
      }, 0);
    }
  }, [xAnnos]);

  if (plotRef.current) {
    const plot = plotRef.current;
    const wipFrame = xAnnos.filter((fr) => fr.meta?.custom?.isWip)?.[0];
    const wipVals = wipFrame ? getVals(wipFrame) : null;
    const isWipVisible = wipFrame?.meta?.custom?.isWip && wipVals?.time[0] > 0;

    let markers = xAnnos.flatMap((frame, frameIdx) => {
      const isWipFrame = frame?.meta?.custom?.isWip;
      const vals = getVals(frame);

      let markers: React.ReactNode[] = [];

      // Top offset for multi-lane annotations
      const top = multiLane ? frameIdx * ANNOTATION_LANE_SIZE : undefined;

      for (let i = 0; i < vals.time.length; i++) {
        let color = getColorByName(vals.color?.[i] || DEFAULT_ANNOTATION_COLOR);
        let left = Math.round(plot.valToPos(vals.time[i], 'x')) || 0; // handles -0
        let style: React.CSSProperties | null = null;
        let isVisible = true;

        if (vals.isRegion?.[i]) {
          let right = Math.round(plot.valToPos(vals.timeEnd?.[i], 'x')) || 0; // handles -0

          isVisible = left < plot.rect.width && right > 0;

          if (isVisible) {
            let clampedLeft = Math.max(0, left);
            let clampedRight = Math.min(plot.rect.width, right);

            style = { left: clampedLeft, background: color, width: clampedRight - clampedLeft, top };
          }
        } else {
          isVisible = left >= 0 && left <= plot.rect.width;

          if (isVisible) {
            style = { left, borderBottomColor: color, top };
          }
        }

        // @TODO: Reset newRange after annotation is saved
        if (isVisible) {
          const annotationKey = getAnnotationKey(frameIdx, i);
          const setPinned = (active: boolean) => {
            if (active) {
              setPinnedAnnotationId(annotationKey);
            } else {
              setPinnedAnnotationId(undefined);
            }
          };

          // Do not let other tooltips render if one is already pinned, or the wip is being edited
          const showTooltipOnHover = !pinnedAnnotationId && !isWipVisible;

          // The tooltip should render as pinned if the pinned state index matches this annotation
          const isPinned = pinnedAnnotationId === annotationKey;

          markers.push(
            <AnnotationMarker2
              key={annotationKey}
              setPinned={setPinned}
              isPinned={isPinned}
              showTooltipOnHover={showTooltipOnHover}
              frame={frame}
              annoIdx={i}
              annoVals={vals}
              style={style}
              timeZone={timeZone}
              exitWipEdit={isWipFrame ? exitWipEdit : null}
              portalRoot={portalRoot}
              canExecuteActions={userCanExecuteActions}
              replaceVariables={replaceVariables}
            />
          );
        }
      }

      return markers;
    });

    return createPortal(markers, xAxisRef.current!);
  }

  return null;
};

/**
 * helper method to return a unique identifier for an annotation
 * @param frameIdx
 * @param i
 */
const getAnnotationKey = (frameIdx: number, i: number) => {
  return `${frameIdx}:${i}`;
};
