import {InternMap, cross, rollup, sum} from "d3";
import {keyof, map, range} from "./options.js";
import {createScales} from "./scales.js";

// Returns an array of {x?, y?, i} objects representing the facet domain.
export function createFacets(channelsByScale, options) {
  const {fx, fy} = createScales(channelsByScale, options);
  const fxDomain = fx?.scale.domain();
  const fyDomain = fy?.scale.domain();
  return fxDomain && fyDomain
    ? cross(fxDomain, fyDomain).map(([x, y], i) => ({x, y, i}))
    : fxDomain
    ? fxDomain.map((x, i) => ({x, i}))
    : fyDomain
    ? fyDomain.map((y, i) => ({y, i}))
    : undefined;
}

export function recreateFacets(facets, {x: X, y: Y}) {
  X &&= facetIndex(X);
  Y &&= facetIndex(Y);
  return facets
    .filter(
      X && Y // remove any facets no longer present in the domain
        ? (f) => X.has(f.x) && Y.has(f.y)
        : X
        ? (f) => X.has(f.x)
        : (f) => Y.has(f.y)
    )
    .sort(
      X && Y // reorder facets to match the new scale domains
        ? (a, b) => X.get(a.x) - X.get(b.x) || Y.get(a.y) - Y.get(b.y)
        : X
        ? (a, b) => X.get(a.x) - X.get(b.x)
        : (a, b) => Y.get(a.y) - Y.get(b.y)
    );
}

// Returns a (possibly nested) Map of [[key1, index1], [key2, index2], …]
// representing the data indexes associated with each facet.
export function facetGroups(data, {fx, fy}) {
  const I = range(data);
  const FX = fx?.value;
  const FY = fy?.value;
  return fx && fy
    ? rollup(
        I,
        (G) => ((G.fx = FX[G[0]]), (G.fy = FY[G[0]]), G),
        (i) => FX[i],
        (i) => FY[i]
      )
    : fx
    ? rollup(
        I,
        (G) => ((G.fx = FX[G[0]]), G),
        (i) => FX[i]
      )
    : rollup(
        I,
        (G) => ((G.fy = FY[G[0]]), G),
        (i) => FY[i]
      );
}

export function facetTranslator(fx, fy, {marginTop, marginLeft}) {
  const x = fx ? ({x}) => fx(x) - marginLeft : () => 0;
  const y = fy ? ({y}) => fy(y) - marginTop : () => 0;
  return function (d) {
    if (this.tagName === "svg") {
      this.setAttribute("x", x(d));
      this.setAttribute("y", y(d));
    } else {
      this.setAttribute("transform", `translate(${x(d)},${y(d)})`);
    }
  };
}

// Returns an index that for each facet lists all the elements present in other
// facets in the original index. TODO Memoize to avoid repeated work?
export function facetExclude(index) {
  const ex = [];
  const e = new Uint32Array(sum(index, (d) => d.length));
  for (const i of index) {
    let n = 0;
    for (const j of index) {
      if (i === j) continue;
      e.set(j, n);
      n += j.length;
    }
    ex.push(e.slice(0, n));
  }
  return ex;
}

const facetAnchors = new Map([
  ["top", facetAnchorTop],
  ["right", facetAnchorRight],
  ["bottom", facetAnchorBottom],
  ["left", facetAnchorLeft],
  ["top-left", and(facetAnchorTop, facetAnchorLeft)],
  ["top-right", and(facetAnchorTop, facetAnchorRight)],
  ["bottom-left", and(facetAnchorBottom, facetAnchorLeft)],
  ["bottom-right", and(facetAnchorBottom, facetAnchorRight)],
  ["top-empty", facetAnchorTopEmpty],
  ["right-empty", facetAnchorRightEmpty],
  ["bottom-empty", facetAnchorBottomEmpty],
  ["left-empty", facetAnchorLeftEmpty],
  ["empty", facetAnchorEmpty]
]);

export function maybeFacetAnchor(facetAnchor) {
  if (facetAnchor == null) return null;
  const anchor = facetAnchors.get(`${facetAnchor}`.toLowerCase());
  if (anchor) return anchor;
  throw new Error(`invalid facet anchor: ${facetAnchor}`);
}

const indexCache = new WeakMap();

function facetIndex(V) {
  let I = indexCache.get(V);
  if (!I) indexCache.set(V, (I = new InternMap(map(V, (v, i) => [v, i]))));
  return I;
}

// Like V.indexOf(v), but with the same semantics as InternMap.
function facetIndexOf(V, v) {
  return facetIndex(V).get(v);
}

// Like facets.find, but with the same semantics as InternMap.
function facetFind(facets, x, y) {
  x = keyof(x);
  y = keyof(y);
  return facets.find((f) => Object.is(keyof(f.x), x) && Object.is(keyof(f.y), y));
}

function facetEmpty(facets, x, y) {
  return facetFind(facets, x, y)?.empty;
}

function facetAnchorTop(facets, {y: Y}, {y}) {
  return Y ? facetIndexOf(Y, y) === 0 : true;
}

function facetAnchorBottom(facets, {y: Y}, {y}) {
  return Y ? facetIndexOf(Y, y) === Y.length - 1 : true;
}

function facetAnchorLeft(facets, {x: X}, {x}) {
  return X ? facetIndexOf(X, x) === 0 : true;
}

function facetAnchorRight(facets, {x: X}, {x}) {
  return X ? facetIndexOf(X, x) === X.length - 1 : true;
}

function facetAnchorTopEmpty(facets, {y: Y}, {x, y, empty}) {
  if (empty) return false;
  if (!Y) return;
  const i = facetIndexOf(Y, y);
  if (i > 0) return facetEmpty(facets, x, Y[i - 1]);
}

function facetAnchorBottomEmpty(facets, {y: Y}, {x, y, empty}) {
  if (empty) return false;
  if (!Y) return;
  const i = facetIndexOf(Y, y);
  if (i < Y.length - 1) return facetEmpty(facets, x, Y[i + 1]);
}

function facetAnchorLeftEmpty(facets, {x: X}, {x, y, empty}) {
  if (empty) return false;
  if (!X) return;
  const i = facetIndexOf(X, x);
  if (i > 0) return facetEmpty(facets, X[i - 1], y);
}

function facetAnchorRightEmpty(facets, {x: X}, {x, y, empty}) {
  if (empty) return false;
  if (!X) return;
  const i = facetIndexOf(X, x);
  if (i < X.length - 1) return facetEmpty(facets, X[i + 1], y);
}

function facetAnchorEmpty(facets, channels, {empty}) {
  return empty;
}

function and(a, b) {
  return function () {
    return a.apply(null, arguments) && b.apply(null, arguments);
  };
}

// Facet filter, by mark; for now only the "eq" filter is provided.
export function facetFilter(facets, {channels: {fx, fy}, groups}) {
  return fx && fy
    ? facets.map(({x, y}) => groups.get(x)?.get(y) ?? [])
    : fx
    ? facets.map(({x}) => groups.get(x) ?? [])
    : facets.map(({y}) => groups.get(y) ?? []);
}
