export const N = 1;
export const E = 2;
export const S = 4;
export const W = 8;

export const DIRS = {
  N: { bit: N, dx: 0, dy: -1, opp: 'S', name: 'N', ang: -Math.PI / 2 },
  E: { bit: E, dx: 1, dy: 0, opp: 'W', name: 'E', ang: 0 },
  S: { bit: S, dx: 0, dy: 1, opp: 'N', name: 'S', ang: Math.PI / 2 },
  W: { bit: W, dx: -1, dy: 0, opp: 'E', name: 'W', ang: Math.PI },
};

export const DIR_ORDER = ['N', 'E', 'S', 'W'];

export function opp(dir) {
  return DIRS[dir].opp;
}

export function hasPort(mask, dir) {
  return !!(mask & DIRS[dir].bit);
}

export function portsOf(mask) {
  return DIR_ORDER.filter((d) => mask & DIRS[d].bit);
}

export function rotateMask(mask, steps = 1) {
  let m = mask & 15;
  const s = ((steps % 4) + 4) % 4;
  for (let i = 0; i < s; i++) {
    const n = m & N;
    const e = m & E;
    const sBit = m & S;
    const w = m & W;
    m = (n ? E : 0) | (e ? S : 0) | (sBit ? W : 0) | (w ? N : 0);
  }
  return m;
}

function def(partial) {
  return {
    delay: 1,
    independent: false,
    oneWay: null,
    rotatable: true,
    ...partial,
  };
}

export const PIECES = {
  h: def({ id: 'h', mask: E | W, name: 'Straight', glyph: '━' }),
  v: def({ id: 'v', mask: N | S, name: 'Straight', glyph: '┃' }),
  ne: def({ id: 'ne', mask: N | E, name: 'Elbow', glyph: '┗' }),
  nw: def({ id: 'nw', mask: N | W, name: 'Elbow', glyph: '┛' }),
  se: def({ id: 'se', mask: S | E, name: 'Elbow', glyph: '┏' }),
  sw: def({ id: 'sw', mask: S | W, name: 'Elbow', glyph: '┓' }),
  cross: def({
    id: 'cross',
    mask: N | E | S | W,
    name: 'Junction',
    glyph: '╋',
    independent: true,
  }),
  tank_h: def({ id: 'tank_h', mask: E | W, name: 'Reservoir', glyph: '▭', delay: 2.6 }),
  tank_v: def({ id: 'tank_v', mask: N | S, name: 'Reservoir', glyph: '▯', delay: 2.6 }),
  ow_e: def({ id: 'ow_e', mask: E | W, name: 'One-way', glyph: '→', oneWay: 'E' }),
  ow_w: def({ id: 'ow_w', mask: E | W, name: 'One-way', glyph: '←', oneWay: 'W' }),
  ow_n: def({ id: 'ow_n', mask: N | S, name: 'One-way', glyph: '↑', oneWay: 'N' }),
  ow_s: def({ id: 'ow_s', mask: N | S, name: 'One-way', glyph: '↓', oneWay: 'S' }),
};

export const BASIC_IDS = ['h', 'v', 'ne', 'nw', 'se', 'sw', 'cross'];
export const TANK_IDS = ['tank_h', 'tank_v'];
export const ONEWAY_IDS = ['ow_e', 'ow_w', 'ow_n', 'ow_s'];

const ROT_MAP = {
  h: 'v',
  v: 'h',
  ne: 'se',
  se: 'sw',
  sw: 'nw',
  nw: 'ne',
  cross: 'cross',
  tank_h: 'tank_v',
  tank_v: 'tank_h',
  ow_e: 'ow_s',
  ow_s: 'ow_w',
  ow_w: 'ow_n',
  ow_n: 'ow_e',
};

export function rotatePiece(id, steps = 1) {
  let cur = id;
  const n = ((steps % 4) + 4) % 4;
  for (let i = 0; i < n; i++) cur = ROT_MAP[cur] || cur;
  return cur;
}

export function pieceById(id) {
  return PIECES[id] || PIECES.h;
}

export function channelKey(enteredFrom) {
  if (enteredFrom === 'N' || enteredFrom === 'S') return 'ns';
  return 'ew';
}

export function exitDir(piece, enteredFrom) {
  if (piece.oneWay) {
    const needEnter = opp(piece.oneWay);
    if (enteredFrom !== needEnter) return null;
    return piece.oneWay;
  }
  if (piece.independent) {
    return opp(enteredFrom);
  }
  const ports = portsOf(piece.mask);
  if (!hasPort(piece.mask, enteredFrom)) return null;
  const others = ports.filter((d) => d !== enteredFrom);
  if (others.length === 1) return others[0];
  return others[0] || null;
}

export function accepts(piece, enteredFrom) {
  if (!piece) return false;
  if (!hasPort(piece.mask, enteredFrom)) return false;
  if (piece.oneWay && enteredFrom !== opp(piece.oneWay)) return false;
  return true;
}

export function pathSpec(piece, enteredFrom, size) {
  const half = size / 2;
  const edge = {
    N: [0, -half],
    E: [half, 0],
    S: [0, half],
    W: [-half, 0],
  };
  const exit = exitDir(piece, enteredFrom);
  const isTank = piece.id.startsWith('tank');
  const isCorner =
    piece.id === 'ne' || piece.id === 'nw' || piece.id === 'se' || piece.id === 'sw';

  if (!exit) {
    return { kind: 'line', from: edge[enteredFrom] || [0, 0], to: [0, 0], length: half };
  }

  if (isTank) {
    return {
      kind: 'tank',
      from: edge[enteredFrom],
      to: edge[exit],
      length: size * 2.2,
    };
  }

  if (isCorner) {
    return {
      kind: 'corner',
      from: edge[enteredFrom],
      to: edge[exit],
      length: size,
    };
  }

  return {
    kind: 'line',
    from: edge[enteredFrom],
    to: edge[exit],
    length: size,
  };
}

export function strokePipePorts(ctx, mask, cx, cy, size) {
  const half = size / 2;
  const pts = {
    N: [cx, cy - half],
    E: [cx + half, cy],
    S: [cx, cy + half],
    W: [cx - half, cy],
  };
  ctx.beginPath();
  for (const d of portsOf(mask)) {
    ctx.moveTo(cx, cy);
    ctx.lineTo(pts[d][0], pts[d][1]);
  }
}

export function makeBag(levelCfg, rng) {
  const bag = [];
  const push = (id, n) => {
    for (let i = 0; i < n; i++) bag.push(id);
  };
  push('h', 3);
  push('v', 3);
  push('ne', 2);
  push('nw', 2);
  push('se', 2);
  push('sw', 2);
  push('cross', levelCfg.crosses ? 2 : 1);
  if (levelCfg.tanks) {
    push('tank_h', 1);
    push('tank_v', 1);
  }
  if (levelCfg.oneWays) {
    for (const id of ONEWAY_IDS) push(id, 1);
  }
  for (let i = bag.length - 1; i > 0; i--) {
    const j = Math.floor(rng() * (i + 1));
    [bag[i], bag[j]] = [bag[j], bag[i]];
  }
  return bag;
}

export function mulberry32(seed) {
  let a = seed >>> 0;
  return function rng() {
    a |= 0;
    a = (a + 0x6d2b79f5) | 0;
    let t = Math.imul(a ^ (a >>> 15), 1 | a);
    t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
    return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
  };
}
