'use client';

import { TinyColor, readability } from '@ctrl/tinycolor';
import clsx from 'clsx';
import React, { useEffect, useState } from 'react';
import { twMerge } from 'tailwind-merge';

import {
  ImpressionLoggerConfig, // MultiImpressionLogger,
} from '@/components/ImpressionLogger';

export type DiscoverCardProps = {
  impressionLoggerConfig?: ImpressionLoggerConfig;
  backgroundClassName?: string;
  backgroundImageClassName?: string;
  backgroundOverlayClassName?: string;
  contentClassName?: string;
  /**
   * Element displayed in the background
   *
   * Supply a string to use an image as the `DiscoverCardBackground` with the
   * default overlay gradient. Otherwise, use literal JSX or a component that
   * will render with `backgroundClassName` as its `className` prop.
   */
  backgroundContent?:
    | [url: string, tint?: string, id?: string]
    | string
    | React.ReactNode
    | React.ComponentType<{ className?: string }>;
};

export type Props = Omit<
  React.HTMLAttributes<HTMLDivElement>,
  keyof DiscoverCardProps
> &
  DiscoverCardProps;

let canvasCtx: CanvasRenderingContext2D | null;
function getCanvasCtx() {
  if (!canvasCtx) {
    // Create 1x1 canvas
    const canvas = document.createElement('canvas');
    canvas.width = 1;
    canvas.height = 1;
    canvasCtx = canvas.getContext('2d');
  }
  return canvasCtx;
}

type ColorTuple = [r: number, g: number, b: number];

function getMedoid<C extends ColorTuple = ColorTuple>(
  sourceColors: Array<C>,
  comparisonColors: Array<C> = sourceColors,
  distanceFunction = null
) {
  // Handle edge cases
  if (!sourceColors || sourceColors.length === 0) {
    return null;
  }

  if (sourceColors.length === 1) {
    return sourceColors[0];
  }

  // Default distance function (Euclidean distance in RGB space)
  const calculateDistance =
    distanceFunction ||
    ((color1: C, color2: C) => {
      return Math.sqrt(
        Math.pow(color1[0] - color2[0], 2) +
          Math.pow(color1[1] - color2[1], 2) +
          Math.pow(color1[2] - color2[2], 2)
      );
    });

  let minSumDistances = Infinity;
  let medoidColor = null;

  // For each color, calculate the sum of distances to all other colors
  for (let i = 0; i < sourceColors.length; i++) {
    const currentColor = sourceColors[i];
    let sumDistances = 0;

    for (let j = 0; j < comparisonColors.length; j++) {
      if (i !== j) {
        sumDistances += calculateDistance(currentColor, comparisonColors[j]);
      }
    }

    // Update if this color has a smaller sum of distances
    if (sumDistances < minSumDistances) {
      minSumDistances = sumDistances;
      medoidColor = currentColor;
    }
  }

  return medoidColor;
}

function getBestContrastSamples<C extends ColorTuple = ColorTuple>(
  colorTuples: Array<C>,
  contrastColor: string | TinyColor = '#fff',
  minColors = 3
) {
  const colors = colorTuples.map((color) => {
    const [r, g, b] = color;
    return [color, new TinyColor({ r, g, b })] as const;
  });
  const readableColors = colors.filter(
    ([, color]) => readability(color, contrastColor) >= 4.5
  );
  // If we don't have enough colors, take the minimum number of most contrasty colors
  if (readableColors.length < minColors) {
    colors.sort(
      ([, a], [, b]) =>
        readability(b, contrastColor) - readability(a, contrastColor)
    );
    return colors.slice(0, minColors).map(([color]) => color);
  }
  // Otherwise, use any colors with adequate contrast
  return readableColors.map(([color]) => color);
}

function darkenUntilReadable(
  color: string | TinyColor,
  textColor: string | TinyColor = '#fff',
  targetContrast = 4.5,
  maxIterations = 10
) {
  const inputColor = typeof color === 'string' ? new TinyColor(color) : color;

  let lower = 1;
  let upper = 100;
  let currentAmount = 50;
  let currentColor = inputColor;
  let iterations = 0;

  // Sanity check that darkening actually increases contrast
  if (
    readability(currentColor, textColor) <
    readability(inputColor.darken(50), textColor)
  ) {
    return inputColor;
  }

  // Binary search to find minimum darkening
  while (iterations < maxIterations) {
    const currentContrast = readability(currentColor, textColor);

    // Close enough to the contrast target
    if (
      currentContrast >= targetContrast &&
      currentContrast - targetContrast < 0.05
    ) {
      break;
    }

    // Adjust based on whether we undershot or overshot
    if (currentContrast < targetContrast) {
      lower = currentAmount;
    } else {
      upper = currentAmount;
    }

    // Calculate new midpoint
    currentAmount = (lower + upper) / 2;
    currentColor = inputColor.darken(currentAmount);

    iterations++;
  }

  return currentColor;
}

function rgb2hex(r: number, g: number, b: number) {
  return `#${[r, g, b].map((v) => v.toString(16).padStart(2, '0')).join('')}`;
}

function sampleAverageColor(
  img: HTMLImageElement,
  x: number,
  y: number,
  w: number,
  h: number,
  returnRgb: false
): string;
function sampleAverageColor(
  img: HTMLImageElement,
  x: number,
  y: number,
  w: number,
  h: number,
  returnRgb: true
): [r: number, g: number, b: number];
function sampleAverageColor(
  img: HTMLImageElement,
  x: number = 0.5,
  y: number = 0.5,
  w: number = 1,
  h: number = 1,
  returnRgb = false
) {
  const ctx = getCanvasCtx();
  if (!ctx || !img) return;

  const { naturalWidth, naturalHeight } = img;

  // Draw scaled image to canvas
  if (naturalWidth && naturalHeight) {
    // Sample based on input size
    const scx = x * naturalWidth;
    const scy = y * naturalHeight;
    const scw = w * naturalWidth;
    const sch = h * naturalHeight;
    const sx = Math.max(0, Math.floor(scx - 0.5 * scw));
    const sy = Math.max(0, Math.floor(scy - 0.5 * sch));
    const sxr = Math.min(Math.ceil(scx + 0.5 * scw), naturalWidth);
    const syr = Math.min(Math.ceil(scy + 0.5 * sch), naturalHeight);
    const sw = Math.min(1, sxr - sx);
    const sh = Math.min(1, syr - sy);
    ctx.drawImage(img, sx, sy, sw, sh, 0, 0, 1, 1);
  } else {
    // Otherwise just use the whole image
    ctx.drawImage(img, 0, 0, 1, 1);
  }

  // Get pixel data from the entire canvas
  const imageData = ctx.getImageData(0, 0, 1, 1);
  const [r, g, b] = imageData.data;

  // Return the resulting color
  return returnRgb ? [r, g, b] : rgb2hex(r, g, b);
}

export const DiscoverCardBackground: React.FC<
  React.HTMLAttributes<HTMLDivElement> & {
    imageClassName?: string;
    overlayClassName?: string;
    backgroundImage?: string;
    backgroundColor?: string;
    overlayColor?: string | null;
  }
> = (props) => {
  const {
    children,
    className,
    imageClassName,
    overlayClassName,
    backgroundImage,
    backgroundColor,
    overlayColor: explicitOverlayColor,
    ...restProps
  } = props;

  const [overlayColor, setOverlayColor] = useState(explicitOverlayColor);
  const sampleBackgroundImage =
    explicitOverlayColor !== null ? backgroundImage : '';

  useEffect(() => {
    if (sampleBackgroundImage) {
      const img = new Image();
      img.crossOrigin = 'Anonymous';
      img.onload = () => {
        try {
          // Get the representative color from a few samples
          const colorSamples = [
            [0.1, 0.1],
            [0.3, 0.3],
            [0.5, 0.1],
            [0.7, 0.3],
            [0.9, 0.1],
            [0.1, 0.5],
            [0.3, 0.5],
            [0.5, 0.5],
            [0.7, 0.5],
            [0.9, 0.5],
            [0.1, 0.9],
            [0.3, 0.7],
            [0.5, 0.9],
            [0.7, 0.7],
            [0.9, 0.9],
          ].map(([x, y]) => sampleAverageColor(img, x, y, 0.05, 0.05, true));
          const bestContrastSamples = getBestContrastSamples(colorSamples);
          const [r, g, b] = getMedoid(bestContrastSamples, colorSamples)!;
          // Get a sufficiently dark color
          const color = darkenUntilReadable(new TinyColor({ r, g, b }));
          // Set the hex value
          setOverlayColor(color.toHexString());
          // setOverlayColor(rgb2hex(r, g, b));
        } catch (e) {
          // If anything goes wrong, default to black
          setOverlayColor('#000');
        }
      };
      // If the image fails to load, default to black
      img.onerror = () => {
        setOverlayColor('#000');
      };
      img.src = sampleBackgroundImage;
    }
  }, [sampleBackgroundImage]);

  const style = {
    ...props.style,
    backgroundColor,
    '--color-overlay': explicitOverlayColor ?? overlayColor,
    '--color-overlay-angle': '45deg',
    '--color-overlay-start': '40%',
    '--color-overlay-end': '70%',
    '--color-overlay-size': '200%',
  };

  return (
    <div
      {...restProps}
      className={twMerge(
        clsx('relative transition-opacity duration-300', {
          'opacity-0': !!sampleBackgroundImage && overlayColor == null,
        }),
        className
      )}
      style={style}
    >
      {/* Background image */}
      <div
        className={clsx(
          'absolute inset-0 bg-cover bg-center',
          {
            'transition-color duration-300': !sampleBackgroundImage,
          },
          imageClassName
        )}
        style={{
          backgroundImage: `url(${backgroundImage})`,
        }}
      />
      {/* Color overlay */}
      <div
        className={clsx(
          'absolute inset-0 bg-(--color-overlay)',
          'mask-[linear-gradient(var(--color-overlay-angle),#000,#000000f6_var(--color-overlay-start,0%),transparent_var(--color-overlay-end,100%),transparent)]',
          'mask-size-(--color-overlay-size) mask-[center_center]',
          {
            'transition-color duration-300': !sampleBackgroundImage,
          },
          overlayClassName
        )}
      />
      {children}
    </div>
  );
};

const DiscoverCard: React.FC<Props> = (props) => {
  const {
    className: explicitClassName,
    backgroundClassName: explicitBackgroundClassName,
    backgroundImageClassName,
    backgroundOverlayClassName,
    contentClassName: explicitContentClassName,
    backgroundContent,
    children,
    style,
    impressionLoggerConfig,
    ...restProps
  } = props;

  const className = twMerge(
    'relative overflow-clip clip rounded-2xl p-3.5 text-foreground-primary-glass',
    // Inner border (on top of background, no content shift)
    // 'after:absolute after:inset-0 after:rounded-[inherit] after:border after:border-border-secondary-glass after:pointer-events-none',
    // Outer border (shifts card content inward)
    'border border-border-secondary',
    explicitClassName
  );
  const backgroundClassName = twMerge(
    'absolute inset-0',
    explicitBackgroundClassName
  );
  const contentClassName = twMerge(
    'relative w-full min-h-full',
    explicitContentClassName
  );

  return (
    <article {...restProps} className={className}>
      {typeof backgroundContent === 'string' ? (
        <DiscoverCardBackground
          className={twMerge('text-black', backgroundClassName)}
          imageClassName={backgroundImageClassName}
          overlayClassName={backgroundOverlayClassName}
          {...{
            [backgroundContent.startsWith('url(')
              ? 'backgroundImage'
              : 'backgroundColor']: backgroundContent,
          }}
        />
      ) : Array.isArray(backgroundContent) &&
        typeof backgroundContent[0] === 'string' ? (
        <DiscoverCardBackground
          className={twMerge('text-black', backgroundClassName)}
          imageClassName={backgroundImageClassName}
          overlayClassName={backgroundOverlayClassName}
          backgroundImage={backgroundContent[0]}
          overlayColor={backgroundContent[1]}
        />
      ) : typeof backgroundContent === 'function' ? (
        React.createElement(backgroundContent, {
          className: backgroundClassName,
        })
      ) : (
        backgroundContent
      )}
      {children && <section className={contentClassName}>{children}</section>}
    </article>
  );
};

export default DiscoverCard;
