import { cosmosRuntimeConfig } from "./cosmos_config";
import type { SelectionTriple } from "./selection_queries";

export type TripleGraphTerm = SelectionTriple["s"];

export type TripleGraphNode = {
  key: string;
  index: number;
  term: TripleGraphTerm;
  text: string;
  backendId?: number;
  isSelectedSource: boolean;
};

export type TripleGraphLink = {
  index: number;
  sourceIndex: number;
  targetIndex: number;
  sourceText: string;
  targetText: string;
  predicate: SelectionTriple["p"];
  predicateText: string;
  predicateId?: number;
  triple: SelectionTriple;
};

export type TripleGraphModel = {
  nodes: TripleGraphNode[];
  linksMeta: TripleGraphLink[];
  pointPositions: Float32Array;
  seedMetrics: GraphLayoutMetrics;
  pointColors: Float32Array;
  pointSizes: Float32Array;
  links: Float32Array;
  linkColors: Float32Array;
  linkWidths: Float32Array;
  nodeCount: number;
  edgeCount: number;
};

export type GraphLayoutMetrics = {
  centroidX: number;
  centroidY: number;
  minX: number;
  maxX: number;
  minY: number;
  maxY: number;
  width: number;
  height: number;
  maxRadius: number;
};

type MutableNode = {
  term: TripleGraphTerm;
  text: string;
  backendId?: number;
  isSelectedSource: boolean;
};

export function buildTripleGraphModel(triples: SelectionTriple[], selectedIds: number[]): TripleGraphModel {
  const selectedSet = new Set<number>(selectedIds);
  const nodeMap = new Map<string, MutableNode>();

  for (const triple of triples) {
    addNode(nodeMap, triple.s, triple.subject_id, selectedSet);
    addNode(nodeMap, triple.o, triple.object_id, selectedSet);
  }

  const nodes = Array.from(nodeMap.entries())
    .sort(([leftKey], [rightKey]) => leftKey.localeCompare(rightKey))
    .map(([key, node], index) => ({
      key,
      index,
      term: node.term,
      text: node.text,
      backendId: node.backendId,
      isSelectedSource: node.isSelectedSource,
    }));

  const nodeIndexByKey = new Map<string, number>();
  for (const node of nodes) {
    nodeIndexByKey.set(node.key, node.index);
  }

  const linksMeta: TripleGraphLink[] = [];
  for (const triple of triples) {
    const sourceIndex = nodeIndexByKey.get(termKey(triple.s));
    const targetIndex = nodeIndexByKey.get(termKey(triple.o));
    if (sourceIndex === undefined || targetIndex === undefined) continue;
    linksMeta.push({
      index: linksMeta.length,
      sourceIndex,
      targetIndex,
      sourceText: formatTermText(triple.s),
      targetText: formatTermText(triple.o),
      predicate: triple.p,
      predicateText: formatTermText(triple.p),
      predicateId: triple.predicate_id,
      triple,
    });
  }

  const pointPositions = buildPointPositions(nodes);
  const seedMetrics = computeLayoutMetrics(pointPositions);
  const pointColors = buildPointColors(nodes);
  const pointSizes = buildPointSizes(nodes);
  const links = buildLinks(linksMeta);
  const linkColors = buildLinkColors(linksMeta);
  const linkWidths = buildLinkWidths(linksMeta);

  return {
    nodes,
    linksMeta,
    pointPositions,
    seedMetrics,
    pointColors,
    pointSizes,
    links,
    linkColors,
    linkWidths,
    nodeCount: nodes.length,
    edgeCount: linksMeta.length,
  };
}

function addNode(
  nodeMap: Map<string, MutableNode>,
  term: TripleGraphTerm,
  backendId: number | undefined,
  selectedSet: Set<number>
): void {
  const key = termKey(term);
  const existing = nodeMap.get(key);
  const isSelectedSource = typeof backendId === "number" && selectedSet.has(backendId);
  if (existing) {
    if (existing.backendId === undefined && typeof backendId === "number") {
      existing.backendId = backendId;
    }
    if (isSelectedSource) existing.isSelectedSource = true;
    return;
  }
  nodeMap.set(key, {
    term,
    text: formatTermText(term),
    backendId,
    isSelectedSource,
  });
}

function termKey(term: TripleGraphTerm): string {
  return `${term.type}\x00${term.value}`;
}

function formatTermText(term: TripleGraphTerm): string {
  if (term.type === "literal") {
    if (term.lang) return `"${term.value}"@${term.lang}`;
    return `"${term.value}"`;
  }
  return term.value;
}

function buildPointPositions(nodes: TripleGraphNode[]): Float32Array {
  const out = new Float32Array(nodes.length * 2);
  const simulationSpaceCenter = cosmosRuntimeConfig.spaceSize / 2;
  if (nodes.length === 0) return out;
  if (nodes.length === 1) {
    out[0] = simulationSpaceCenter;
    out[1] = simulationSpaceCenter;
    return out;
  }

  for (const node of nodes) {
    const primaryHash = hashString(node.key);
    const secondaryHash = hashString(`${node.key}\x01`);
    const angle = ((primaryHash % 3600) / 3600) * Math.PI * 2;
    const radius = 80 + (((primaryHash >>> 12) % 1000) / 1000) * 70;
    const jitterX = ((((secondaryHash >>> 4) % 200) / 200) - 0.5) * 18;
    const jitterY = ((((secondaryHash >>> 12) % 200) / 200) - 0.5) * 18;
    out[node.index * 2] = Math.cos(angle) * radius + jitterX;
    out[node.index * 2 + 1] = Math.sin(angle) * radius + jitterY;
  }

  recenterPointPositions(out);
  offsetPointPositionsToSimulationCenter(out, simulationSpaceCenter);
  return out;
}

export function computeLayoutMetrics(pointPositions: ArrayLike<number>): GraphLayoutMetrics {
  const pairCount = Math.floor(pointPositions.length / 2);
  if (pairCount === 0) {
    return {
      centroidX: 0,
      centroidY: 0,
      minX: 0,
      maxX: 0,
      minY: 0,
      maxY: 0,
      width: 0,
      height: 0,
      maxRadius: 0,
    };
  }

  let sumX = 0;
  let sumY = 0;
  let minX = Number.POSITIVE_INFINITY;
  let maxX = Number.NEGATIVE_INFINITY;
  let minY = Number.POSITIVE_INFINITY;
  let maxY = Number.NEGATIVE_INFINITY;

  for (let i = 0; i < pairCount; i++) {
    const x = pointPositions[i * 2];
    const y = pointPositions[i * 2 + 1];
    sumX += x;
    sumY += y;
    if (x < minX) minX = x;
    if (x > maxX) maxX = x;
    if (y < minY) minY = y;
    if (y > maxY) maxY = y;
  }

  const centroidX = sumX / pairCount;
  const centroidY = sumY / pairCount;
  let maxRadius = 0;
  for (let i = 0; i < pairCount; i++) {
    const dx = pointPositions[i * 2] - centroidX;
    const dy = pointPositions[i * 2 + 1] - centroidY;
    const radius = Math.hypot(dx, dy);
    if (radius > maxRadius) maxRadius = radius;
  }

  return {
    centroidX,
    centroidY,
    minX,
    maxX,
    minY,
    maxY,
    width: maxX - minX,
    height: maxY - minY,
    maxRadius,
  };
}

function recenterPointPositions(pointPositions: Float32Array): void {
  const metrics = computeLayoutMetrics(pointPositions);
  if (metrics.centroidX === 0 && metrics.centroidY === 0) return;
  const pairCount = Math.floor(pointPositions.length / 2);
  for (let i = 0; i < pairCount; i++) {
    pointPositions[i * 2] -= metrics.centroidX;
    pointPositions[i * 2 + 1] -= metrics.centroidY;
  }
}

function offsetPointPositionsToSimulationCenter(pointPositions: Float32Array, center: number): void {
  if (center === 0) return;
  const pairCount = Math.floor(pointPositions.length / 2);
  for (let i = 0; i < pairCount; i++) {
    pointPositions[i * 2] += center;
    pointPositions[i * 2 + 1] += center;
  }
}

function buildPointColors(nodes: TripleGraphNode[]): Float32Array {
  const out = new Float32Array(nodes.length * 4);
  for (const node of nodes) {
    const offset = node.index * 4;
    const color = node.isSelectedSource ? [53, 214, 255, 1] : colorFromHash(node.key, 210, 35, 58, 18, 8);
    out[offset] = color[0];
    out[offset + 1] = color[1];
    out[offset + 2] = color[2];
    out[offset + 3] = color[3];
  }
  return out;
}

function buildPointSizes(nodes: TripleGraphNode[]): Float32Array {
  const out = new Float32Array(nodes.length);
  for (const node of nodes) {
    out[node.index] = node.isSelectedSource ? 11 : 7.5;
  }
  return out;
}

function buildLinks(linksMeta: TripleGraphLink[]): Float32Array {
  const out = new Float32Array(linksMeta.length * 2);
  for (const link of linksMeta) {
    const offset = link.index * 2;
    out[offset] = link.sourceIndex;
    out[offset + 1] = link.targetIndex;
  }
  return out;
}

function buildLinkColors(linksMeta: TripleGraphLink[]): Float32Array {
  const out = new Float32Array(linksMeta.length * 4);
  for (const link of linksMeta) {
    const offset = link.index * 4;
    const color = colorFromHash(link.predicateText, 28, 65, 58, 32, 10);
    out[offset] = color[0];
    out[offset + 1] = color[1];
    out[offset + 2] = color[2];
    out[offset + 3] = color[3];
  }
  return out;
}

function buildLinkWidths(linksMeta: TripleGraphLink[]): Float32Array {
  const out = new Float32Array(linksMeta.length);
  for (const link of linksMeta) {
    out[link.index] = 1.8;
  }
  return out;
}

function colorFromHash(
  value: string,
  baseHue: number,
  hueRange: number,
  lightness: number,
  saturation: number,
  lightnessRange: number
): [number, number, number, number] {
  const hash = hashString(value);
  const hue = (baseHue + (hash % hueRange) + 360) % 360;
  const sat = saturation + ((hash >>> 10) % 10);
  const light = lightness + ((hash >>> 20) % lightnessRange) - lightnessRange / 2;
  const [r, g, b] = hslToRgb(hue / 360, sat / 100, light / 100);
  return [r, g, b, 1];
}

function hslToRgb(h: number, s: number, l: number): [number, number, number] {
  if (s === 0) {
    const value = Math.round(l * 255);
    return [value, value, value];
  }

  const q = l < 0.5 ? l * (1 + s) : l + s - l * s;
  const p = 2 * l - q;
  const r = hueToRgb(p, q, h + 1 / 3);
  const g = hueToRgb(p, q, h);
  const b = hueToRgb(p, q, h - 1 / 3);
  return [Math.round(r * 255), Math.round(g * 255), Math.round(b * 255)];
}

function hueToRgb(p: number, q: number, t: number): number {
  let value = t;
  if (value < 0) value += 1;
  if (value > 1) value -= 1;
  if (value < 1 / 6) return p + (q - p) * 6 * value;
  if (value < 1 / 2) return q;
  if (value < 2 / 3) return p + (q - p) * (2 / 3 - value) * 6;
  return p;
}

function hashString(value: string): number {
  let hash = 2166136261;
  for (let i = 0; i < value.length; i++) {
    hash ^= value.charCodeAt(i);
    hash = Math.imul(hash, 16777619);
  }
  return hash >>> 0;
}
