import {pathRound as path} from "d3";
import {create} from "../context.js";
import {Mark} from "../mark.js";
import {maybeFrameAnchor, maybeNumberChannel, maybeTuple, keyword, identity} from "../options.js";
import {
  applyChannelStyles,
  applyDirectStyles,
  applyFrameAnchor,
  applyIndirectStyles,
  applyTransform
} from "../style.js";
import {template} from "../template.js";

const defaults = {
  ariaLabel: "vector",
  fill: "none",
  stroke: "currentColor",
  strokeWidth: 1.5,
  strokeLinejoin: "round",
  strokeLinecap: "round"
};

const defaultRadius = 3.5;

// The size of the arrowhead is proportional to its length, but we still allow
// the relative size of the head to be controlled via the mark’s width option;
// doubling the default radius will produce an arrowhead that is twice as big.
// That said, we’ll probably want a arrow with a fixed head size, too.
const wingRatio = defaultRadius * 5;

const shapeArrow = {
  draw(context, l, r) {
    const wing = (l * r) / wingRatio;
    context.moveTo(0, 0);
    context.lineTo(0, -l);
    context.moveTo(-wing, wing - l);
    context.lineTo(0, -l);
    context.lineTo(wing, wing - l);
  }
};

const shapeSpike = {
  draw(context, l, r) {
    context.moveTo(-r, 0);
    context.lineTo(0, -l);
    context.lineTo(r, 0);
  }
};

const shapes = new Map([
  ["arrow", shapeArrow],
  ["spike", shapeSpike]
]);

function isShapeObject(value) {
  return value && typeof value.draw === "function";
}

function maybeShape(shape) {
  if (isShapeObject(shape)) return shape;
  const value = shapes.get(`${shape}`.toLowerCase());
  if (value) return value;
  throw new Error(`invalid shape: ${shape}`);
}

export class Vector extends Mark {
  constructor(data, options = {}) {
    const {x, y, r = defaultRadius, length, rotate, shape = shapeArrow, anchor = "middle", frameAnchor} = options;
    const [vl, cl] = maybeNumberChannel(length, 12);
    const [vr, cr] = maybeNumberChannel(rotate, 0);
    super(
      data,
      {
        x: {value: x, scale: "x", optional: true},
        y: {value: y, scale: "y", optional: true},
        length: {value: vl, scale: "length", optional: true},
        rotate: {value: vr, optional: true}
      },
      options,
      defaults
    );
    this.r = +r;
    this.length = cl;
    this.rotate = cr;
    this.shape = maybeShape(shape);
    this.anchor = keyword(anchor, "anchor", ["start", "middle", "end"]);
    this.frameAnchor = maybeFrameAnchor(frameAnchor);
  }
  render(index, scales, channels, dimensions, context) {
    const {x, y} = scales;
    const {x: X, y: Y, length: L, rotate: A} = channels;
    const {length, rotate, anchor, shape, r} = this;
    const [cx, cy] = applyFrameAnchor(this, dimensions);
    return create("svg:g", context)
      .call(applyIndirectStyles, this, dimensions, context)
      .call(applyTransform, this, {x: X && x, y: Y && y})
      .call((g) =>
        g
          .selectAll()
          .data(index)
          .enter()
          .append("path")
          .call(applyDirectStyles, this)
          .attr(
            "transform",
            template`translate(${X ? (i) => X[i] : cx},${Y ? (i) => Y[i] : cy})${
              A ? (i) => ` rotate(${A[i]})` : rotate ? ` rotate(${rotate})` : ``
            }${
              anchor === "start"
                ? ``
                : anchor === "end"
                ? L
                  ? (i) => ` translate(0,${L[i]})`
                  : ` translate(0,${length})`
                : L
                ? (i) => ` translate(0,${L[i] / 2})`
                : ` translate(0,${length / 2})`
            }`
          )
          .attr(
            "d",
            L
              ? (i) => {
                  const p = path();
                  shape.draw(p, L[i], r);
                  return p;
                }
              : (() => {
                  const p = path();
                  shape.draw(p, length, r);
                  return p;
                })()
          )
          .call(applyChannelStyles, this, channels)
      )
      .node();
  }
}

export function vector(data, options = {}) {
  let {x, y, ...rest} = options;
  if (options.frameAnchor === undefined) [x, y] = maybeTuple(x, y);
  return new Vector(data, {...rest, x, y});
}

export function vectorX(data, options = {}) {
  const {x = identity, ...rest} = options;
  return new Vector(data, {...rest, x});
}

export function vectorY(data, options = {}) {
  const {y = identity, ...rest} = options;
  return new Vector(data, {...rest, y});
}

export function spike(data, options = {}) {
  const {
    shape = shapeSpike,
    stroke = defaults.stroke,
    strokeWidth = 1,
    fill = stroke,
    fillOpacity = 0.3,
    anchor = "start",
    ...rest
  } = options;
  return vector(data, {...rest, shape, stroke, strokeWidth, fill, fillOpacity, anchor});
}
