import React, { useMemo } from "react";
import { SVGProps } from "react";
import { BBox } from "../vendor/bezier-js";
import { FILL_REPLACEMENT_COLOR, STROKE_REPLACEMENT_COLOR } from "./colors";
import { AttachmentPointType, PointWithNormal, Specs } from "./specs";
import { SvgSymbolMetadata } from "./svg-symbol-metadata";
import {
  UniqueIdMap,
  URL_FUNC_TO_ANCHOR_RE,
  useUniqueIdMap,
} from "./unique-id";
import { VisibleSpecs } from "./visible-specs";

const DEFAULT_UNIFORM_STROKE_WIDTH = 1;

export type SvgSymbolData = {
  name: string;
  bbox: BBox;
  layers: SvgSymbolElement[];
  defs?: SvgSymbolDef[];
  meta?: SvgSymbolMetadata;
  specs?: Specs;
};

export type SvgSymbolGradientStop = {
  offset: string;
  color: string;
};

/**
 * This represents a definition that will be referenced
 * from elsewhere in an SVG, such as a radial gradient.
 */
export type SvgSymbolDef =
  | {
      type: "radialGradient";
      id: string;
      cx: string;
      cy: string;
      r: string;
      stops: SvgSymbolGradientStop[];
    }
  | {
      type: "linearGradient";
      id: string;
      x1: string;
      y1: string;
      x2: string;
      y2: string;
      stops: SvgSymbolGradientStop[];
    };

export const EMPTY_SVG_SYMBOL_DATA: SvgSymbolData = {
  name: "",
  bbox: {
    x: { min: 0, max: 0 },
    y: { min: 0, max: 0 },
  },
  layers: [],
};

export type SvgSymbolElement = (
  | {
      tagName: "g";
      props: SVGProps<SVGGElement>;
    }
  | {
      tagName: "path";
      props: SVGProps<SVGPathElement>;
    }
) & {
  children: SvgSymbolElement[];
};

export type SvgSymbolContext = {
  /** The stroke color of the symbol, as a hex hash (e.g. '#ff0000'). */
  stroke: string;

  /** The fill color of the symbol, as a hex hash (e.g. '#ff0000'). */
  fill: string;

  /**
   * Whether or not to visibly show the specifications for the symbol,
   * e.g. its attachment points, nesting boxes, and so on.
   */
  showSpecs: boolean;

  /**
   * Whether or not to forcibly apply a uniform stroke width to all
   * the shapes in the symbol.  If defined, the stroke width will
   * *not* vary as the symbol is scaled.
   */
  uniformStrokeWidth?: number;

  /**
   * Whether or not to disable any gradients in the symbol. Defaults
   * to `false`.
   */
  disableGradients: boolean;
};

const DEFAULT_CONTEXT: SvgSymbolContext = {
  stroke: "#000000",
  fill: "#ffffff",
  showSpecs: false,
  uniformStrokeWidth: DEFAULT_UNIFORM_STROKE_WIDTH,
  disableGradients: false,
};

/**
 * If the given symbol context is visibly showing its specifications,
 * return one with its fill color set to "none" so that the specs can
 * be seen more easily.
 */
export function noFillIfShowingSpecs<T extends SvgSymbolContext>(ctx: T): T {
  return {
    ...ctx,
    fill: ctx.showSpecs ? "none" : ctx.fill,
  };
}

/**
 * Return a symbol context with the stroke and fill colors swapped.
 */
export function swapColors<T extends SvgSymbolContext>(ctx: T): T {
  return {
    ...ctx,
    fill: ctx.stroke,
    stroke: ctx.fill,
  };
}

export function createSvgSymbolContext(
  ctx: Partial<SvgSymbolContext> = {}
): SvgSymbolContext {
  return {
    ...DEFAULT_CONTEXT,
    ...ctx,
  };
}

function getColor(
  ctx: SvgSymbolContext,
  color: string | undefined
): string | undefined {
  switch (color) {
    case STROKE_REPLACEMENT_COLOR:
      return ctx.stroke;
    case FILL_REPLACEMENT_COLOR:
      return ctx.fill;
  }
  return color;
}

function getFill(
  ctx: SvgSymbolContext,
  fill: string | undefined,
  uidMap: UniqueIdMap
): string | undefined {
  fill = getColor(ctx, fill);
  if (fill) {
    if (URL_FUNC_TO_ANCHOR_RE.test(fill)) {
      if (ctx.disableGradients) {
        // Note that we could actually interpret the gradient here
        // and use whichever color (fill or stroke) is most dominant
        // in it, but at the time of this writing, it's always the
        // fill color, so we're just using that. For more details,
        // see:
        //
        //   https://github.com/mysticsymbolic/mysticsymbolic.github.io/issues/140
        fill = ctx.fill;
      } else {
        fill = uidMap.rewriteUrl(fill);
      }
    }
  }
  return fill;
}

function reactifySvgSymbolElement(
  ctx: SvgSymbolContext,
  uidMap: UniqueIdMap,
  el: SvgSymbolElement,
  key: number
): JSX.Element {
  let { fill, stroke, strokeWidth } = el.props;
  let vectorEffect;
  fill = getFill(ctx, fill, uidMap);
  stroke = getColor(ctx, stroke);
  if (strokeWidth !== undefined && typeof ctx.uniformStrokeWidth === "number") {
    strokeWidth = ctx.uniformStrokeWidth;
    vectorEffect = "non-scaling-stroke";
  }
  const props: typeof el.props = {
    ...el.props,
    id: undefined,
    vectorEffect,
    strokeWidth,
    fill,
    stroke,
    key,
  };
  return React.createElement(
    el.tagName,
    props,
    el.children.map(reactifySvgSymbolElement.bind(null, ctx, uidMap))
  );
}

const SvgSymbolDef: React.FC<
  { def: SvgSymbolDef; uidMap: UniqueIdMap } & SvgSymbolContext
> = ({ def, uidMap, ...ctx }) => {
  const id = uidMap.getStrict(def.id);
  const stops = def.stops.map((stop, i) => (
    <stop key={i} offset={stop.offset} stopColor={getColor(ctx, stop.color)} />
  ));
  switch (def.type) {
    case "radialGradient":
      return ctx.disableGradients ? null : (
        <radialGradient id={id} cx={def.cx} cy={def.cy} r={def.r}>
          {stops}
        </radialGradient>
      );
    case "linearGradient":
      return ctx.disableGradients ? null : (
        <linearGradient id={id} x1={def.x1} y1={def.y1} x2={def.x2} y2={def.y2}>
          {stops}
        </linearGradient>
      );
  }
};

export const SvgSymbolContent: React.FC<
  { data: SvgSymbolData } & SvgSymbolContext
> = (props) => {
  const d = props.data;
  const origIds = useMemo(() => d.defs?.map((def) => def.id) ?? [], [d.defs]);
  const uidMap = useUniqueIdMap(origIds);

  return (
    <g data-symbol-name={d.name}>
      {d.defs &&
        d.defs.map((def, i) => (
          <SvgSymbolDef key={i} {...props} def={def} uidMap={uidMap} />
        ))}
      {props.data.layers.map(
        reactifySvgSymbolElement.bind(null, props, uidMap)
      )}
      {props.showSpecs && d.specs && <VisibleSpecs specs={d.specs} />}
    </g>
  );
};

export class AttachmentPointError extends Error {}

export function getAttachmentPoint(
  s: SvgSymbolData,
  type: AttachmentPointType,
  idx: number = 0
): PointWithNormal {
  const { specs } = s;
  if (!specs) {
    throw new AttachmentPointError(`Symbol ${s.name} has no specs.`);
  }
  const points = specs[type];
  if (!(points && points.length > idx)) {
    throw new AttachmentPointError(
      `Expected symbol ${s.name} to have at least ${
        idx + 1
      } ${type} attachment point(s).`
    );
  }

  return points[idx];
}

export function safeGetAttachmentPoint(
  s: SvgSymbolData,
  type: AttachmentPointType,
  idx: number = 0
): PointWithNormal | null {
  try {
    return getAttachmentPoint(s, type, idx);
  } catch (e) {
    if (e instanceof AttachmentPointError) {
      console.log(e.message);
    } else {
      throw e;
    }
  }

  return null;
}
