import type { LiquidityEdge, LiquidityNode, TokenCategory } from "./liquidity";

export type Point = { x: number; y: number };
const layouts = new Map<string, Map<string, Point>>();
export function tickerLabel(node: LiquidityNode, nodes: LiquidityNode[]) {
  return nodes.some(n => n.id !== node.id && n.symbol === node.symbol) ? `${node.symbol}·${node.id.slice(-4)}` : node.symbol;
}
export const palette: Record<TokenCategory, string> = { stable: "#119b79", eth: "#3679ed", btc: "#c88124", equity: "#ed2189", other: "#87919f" };
export function sameClass(a: LiquidityNode, b: LiquidityNode) { return a.category === b.category && a.category !== "other"; }
export const diagramSize = (count: number, mobile: boolean) => ({ width: mobile ? 390 : 1000, height: mobile ? Math.max(570, 570 + (count - 40) * 8) : 600 });
export function nodeRadius(node: LiquidityNode, max: number, mobile: boolean, metric: "volume24h" | "liquidity") {
  return Math.sqrt(5 ** 2 + (mobile ? 25 ** 2 : 35 ** 2) * node[metric] / Math.max(1, max));
}
function hash(value: string) { let h = 2166136261; for (const c of value) h = Math.imul(h ^ c.charCodeAt(0), 16777619); return h >>> 0; }

/** Stable layout with label-aware collision bounds. Metric changes never shuffle tickers. */
export function layoutNetwork(nodes: LiquidityNode[], edges: LiquidityEdge[], mobile: boolean): Map<string, Point> {
  const key = JSON.stringify([mobile, nodes.map(n => [n.id, n.symbol, n.category]).sort(), edges.map(e => e.id).sort()]);
  const saved = layouts.get(key); if (saved) return saved;
  const { width: w, height: h } = diagramSize(nodes.length, mobile);
  const centers: Record<TokenCategory, Point> = {
    stable: { x: w * .50, y: h * .25 }, eth: { x: w * .20, y: h * .45 },
    btc: { x: w * .18, y: h * .22 }, equity: { x: w * .78, y: h * .45 }, other: { x: w * .48, y: h * .73 },
  };
  const positions = new Map(nodes.map((node, i) => {
    const a = (hash(node.id) % 628) / 100;
    return [node.id, { x: w / 2 + Math.cos(a) * w * (.15 + (i % 5) * .045), y: h / 2 + Math.sin(a) * h * .36 }];
  }));
  const max = Math.max(...nodes.map(n => Math.max(n.volume24h, n.liquidity)), 1);
  const bounds = new Map(nodes.map(n => {
    const r = nodeRadius(n, max, mobile, "volume24h");
    return [n.id, { width: Math.max(r * 2 + 8, Array.from(tickerLabel(n, nodes)).reduce((width, char) => width + (char.charCodeAt(0) > 255 ? 14 : mobile ? 7 : 8), 0) + 12), height: r * 2 + (r > 20 ? 8 : 29) }];
  }));
  for (let step = 0; step < 300; step++) {
    const force = new Map(nodes.map(n => [n.id, { x: 0, y: 0 }]));
    for (let i = 0; i < nodes.length; i++) for (let j = i + 1; j < nodes.length; j++) {
      const a = positions.get(nodes[i].id)!, b = positions.get(nodes[j].id)!;
      const ab = bounds.get(nodes[i].id)!, bb = bounds.get(nodes[j].id)!;
      const dx = b.x - a.x || .1, dy = b.y - a.y || .1;
      const d2 = Math.max(1, dx * dx + dy * dy), d = Math.sqrt(d2);
      let fx = dx / d * Math.min(4, (mobile ? 250 : 1600) / d2), fy = dy / d * Math.min(4, (mobile ? 250 : 1600) / d2);
      const ox = (ab.width + bb.width) / 2 + 5 - Math.abs(dx), oy = (ab.height + bb.height) / 2 + 4 - Math.abs(dy);
      if (ox > 0 && oy > 0) {
        if (ox < oy) fx += Math.sign(dx) * ox * .20;
        else fy += Math.sign(dy) * oy * .20;
      }
      force.get(nodes[i].id)!.x -= fx; force.get(nodes[i].id)!.y -= fy;
      force.get(nodes[j].id)!.x += fx; force.get(nodes[j].id)!.y += fy;
    }
    for (const e of edges) {
      const a = positions.get(e.source), b = positions.get(e.target);
      if (!a || !b) continue;
      const dx = b.x - a.x, dy = b.y - a.y, d = Math.max(1, Math.hypot(dx, dy));
      const pull = (d - (mobile ? 125 : 205)) * .002;
      force.get(e.source)!.x += dx / d * pull; force.get(e.source)!.y += dy / d * pull;
      force.get(e.target)!.x -= dx / d * pull; force.get(e.target)!.y -= dy / d * pull;
    }
    for (const n of nodes) {
      const p = positions.get(n.id)!, f = force.get(n.id)!, c = centers[n.category], b = bounds.get(n.id)!;
      p.x = Math.max(b.width / 2 + 9, Math.min(w - b.width / 2 - 9, p.x + f.x + (c.x - p.x) * .003));
      p.y = Math.max(b.height / 2 + 22, Math.min(h - b.height / 2 - 20, p.y + f.y + (c.y - p.y) * .003));
    }
  }
  // Use the canvas rather than leaving the cluster compressed in its lower half.
  const values = [...positions.values()];
  const minX = Math.min(...values.map(p => p.x)), maxX = Math.max(...values.map(p => p.x));
  const minY = Math.min(...values.map(p => p.y)), maxY = Math.max(...values.map(p => p.y));
  const padX = mobile ? 53 : 90, padTop = mobile ? 42 : 50, padBottom = mobile ? 70 : 65;
  for (const p of values) {
    p.x = padX + (p.x - minX) / Math.max(1, maxX - minX) * (w - 2 * padX);
    p.y = padTop + (p.y - minY) / Math.max(1, maxY - minY) * (h - padTop - padBottom);
  }
  if (layouts.size >= 12) layouts.delete(layouts.keys().next().value!);
  layouts.set(key, positions);
  return positions;
}
