import { h64 } from '@pezkuwi/bizinikiwi-bindings';
import { buildLookupGraph, getSubgraph, getStronglyConnectedComponents, mergeSCCsWithCommonNodes } from './lookup-graph.mjs';

const textEncoder = new TextEncoder();
const encodeText = textEncoder.encode.bind(textEncoder);
const getChecksum = (values) => {
  const res = new Uint8Array(values.length * 8);
  const dv = new DataView(res.buffer);
  for (let i = 0; i < values.length; i++) dv.setBigUint64(i * 8, values[i]);
  return h64(res);
};
const getStringChecksum = (values) => getChecksum(values.map((v) => h64(encodeText(v))));
const shapeIds = {
  primitive: 0n,
  vector: 1n,
  tuple: 2n,
  struct: 3n,
  option: 4n,
  result: 5n,
  enum: 6n,
  void: 7n
};
const runtimePrimitiveIds = {
  undefined: 0n,
  number: 1n,
  string: 2n,
  bigint: 3n,
  boolean: 4n,
  bitSequence: 5n,
  // {bitsLen: number, bytes: Uint8Array}
  byteSequence: 6n,
  // Binary
  accountId32: 7n,
  // SS58String
  accountId20: 8n
  // EthAccount
};
const metadataPrimitiveIds = {
  bool: runtimePrimitiveIds.boolean,
  char: runtimePrimitiveIds.string,
  str: runtimePrimitiveIds.string,
  u8: runtimePrimitiveIds.number,
  u16: runtimePrimitiveIds.number,
  u32: runtimePrimitiveIds.number,
  u64: runtimePrimitiveIds.bigint,
  u128: runtimePrimitiveIds.bigint,
  u256: runtimePrimitiveIds.bigint,
  i8: runtimePrimitiveIds.number,
  i16: runtimePrimitiveIds.number,
  i32: runtimePrimitiveIds.number,
  i64: runtimePrimitiveIds.bigint,
  i128: runtimePrimitiveIds.bigint,
  i256: runtimePrimitiveIds.bigint
};
const structLikeBuilder = (shapeId, input, innerChecksum) => {
  const sortedEntries = Object.entries(input).sort(
    ([a], [b]) => a.localeCompare(b)
  );
  const keysChecksum = getStringChecksum(sortedEntries.map(([key]) => key));
  const valuesChecksum = getChecksum(
    sortedEntries.map(([, entry]) => innerChecksum(entry))
  );
  return getChecksum([shapeId, keysChecksum, valuesChecksum]);
};
const _buildChecksum = (input, buildNextChecksum) => {
  if (input.type === "primitive")
    return getChecksum([shapeIds.primitive, metadataPrimitiveIds[input.value]]);
  if (input.type === "void") return getChecksum([shapeIds.void]);
  if (input.type === "compact")
    return getChecksum([
      shapeIds.primitive,
      runtimePrimitiveIds[input.isBig ? "bigint" : "number"]
    ]);
  if (input.type === "bitSequence")
    return getChecksum([shapeIds.primitive, runtimePrimitiveIds.bitSequence]);
  if (input.type === "AccountId32") {
    return getChecksum([shapeIds.primitive, runtimePrimitiveIds.accountId32]);
  }
  if (input.type === "AccountId20") {
    return getChecksum([shapeIds.primitive, runtimePrimitiveIds.accountId20]);
  }
  const buildVector = (entry, length) => {
    const innerChecksum = buildNextChecksum(entry);
    return getChecksum(
      length !== void 0 ? [shapeIds.vector, innerChecksum, BigInt(length)] : [shapeIds.vector, innerChecksum]
    );
  };
  if (input.type === "array") {
    const innerValue = input.value;
    if (innerValue.type === "primitive" && innerValue.value === "u8") {
      return getChecksum([
        shapeIds.primitive,
        runtimePrimitiveIds.byteSequence,
        BigInt(input.len)
      ]);
    }
    return buildVector(innerValue, input.len);
  }
  if (input.type === "sequence") {
    const innerValue = input.value;
    if (innerValue.type === "primitive" && innerValue.value === "u8") {
      return getChecksum([shapeIds.primitive, runtimePrimitiveIds.byteSequence]);
    }
    return buildVector(innerValue);
  }
  const buildTuple = (entries) => getChecksum([shapeIds.tuple, ...entries.map(buildNextChecksum)]);
  const buildStruct = (entries) => structLikeBuilder(shapeIds.struct, entries, buildNextChecksum);
  if (input.type === "tuple") return buildTuple(input.value);
  if (input.type === "struct") return buildStruct(input.value);
  if (input.type === "option")
    return getChecksum([shapeIds.option, buildNextChecksum(input.value)]);
  if (input.type === "result")
    return getChecksum([
      shapeIds.result,
      buildNextChecksum(input.value.ok),
      buildNextChecksum(input.value.ko)
    ]);
  return structLikeBuilder(shapeIds.enum, input.value, (entry) => {
    if (entry.type === "lookupEntry") return buildNextChecksum(entry.value);
    switch (entry.type) {
      case "void":
        return getChecksum([shapeIds.void]);
      case "tuple":
        return buildTuple(entry.value);
      case "struct":
        return buildStruct(entry.value);
      case "array":
        return buildVector(entry.value, entry.len);
    }
  });
};
const sortCyclicGroups = (groups, graph) => {
  const getReachableNodes = (group) => {
    const result2 = /* @__PURE__ */ new Set();
    const toVisit = Array.from(group);
    while (toVisit.length) {
      const id = toVisit.pop();
      if (result2.has(id)) continue;
      result2.add(id);
      graph.get(id)?.refs.forEach((id2) => toVisit.push(id2));
    }
    return Array.from(result2);
  };
  const result = new Array();
  function dependentsFirst(group) {
    if (result.includes(group)) return;
    const dependents = groups.filter(
      (candidate) => candidate !== group && getReachableNodes(group).some((node) => candidate.has(node))
    );
    dependents.forEach((group2) => dependentsFirst(group2));
    if (result.includes(group)) return;
    result.push(group);
  }
  groups.forEach((group) => dependentsFirst(group));
  return result;
};
function iterateChecksums(group, iterations, cache, graph) {
  const groupReadCache = new Map([...group].map((id) => [id, 0n]));
  const groupWriteCache = /* @__PURE__ */ new Map();
  const recursiveBuildChecksum = (entry, skipCache = true) => {
    if (!skipCache && (groupReadCache.has(entry.id) || cache.has(entry.id))) {
      return groupReadCache.get(entry.id) ?? cache.get(entry.id);
    }
    const result = _buildChecksum(
      entry,
      (nextEntry) => recursiveBuildChecksum(nextEntry, false)
    );
    if (group.has(entry.id)) {
      groupWriteCache.set(entry.id, result);
    } else {
      cache.set(entry.id, result);
    }
    return result;
  };
  for (let i = 0; i < iterations; i++) {
    group.forEach((id) => recursiveBuildChecksum(graph.get(id).entry));
    group.forEach((id) => groupReadCache.set(id, groupWriteCache.get(id)));
  }
  return groupReadCache;
}
function getMirroredNodes(cyclicGroups, graph) {
  const maxSize = cyclicGroups.reduce(
    (acc, group) => Math.max(acc, group.size),
    0
  );
  const allEntries = new Set([...graph.values()].map((v) => v.entry.id));
  const resultingChecksums = iterateChecksums(
    allEntries,
    maxSize,
    // Cache won't be used, since it's using the internal one for every node.
    /* @__PURE__ */ new Map(),
    graph
  );
  const checksumToNodes = /* @__PURE__ */ new Map();
  for (const id of allEntries) {
    const checksum = resultingChecksums.get(id);
    if (checksum == void 0) throw new Error("Unreachable");
    if (!checksumToNodes.has(checksum)) {
      checksumToNodes.set(checksum, []);
    }
    checksumToNodes.get(checksum).push(id);
  }
  const checksumsWithDuplicates = [...checksumToNodes.entries()].filter(
    ([, nodes]) => nodes.length > 1
  );
  const duplicatesMap = {};
  checksumsWithDuplicates.forEach(([, nodes]) => {
    nodes.forEach((n) => duplicatesMap[n] = nodes);
  });
  return duplicatesMap;
}
const buildChecksum = (entry, cache, graph) => {
  if (cache.has(entry.id)) return cache.get(entry.id);
  const subGraph = getSubgraph(entry.id, graph);
  const cycles = getStronglyConnectedComponents(subGraph).filter(
    // SCCs can be of length=1, but for those we're only interested with those that are circular with themselves
    (group) => group.size > 1 || isSelfCircular(group, subGraph)
  );
  const cyclicGroups = mergeSCCsWithCommonNodes(cycles).filter((group) => {
    return !cache.has(group.values().next().value);
  });
  const mirrored = getMirroredNodes(cyclicGroups, subGraph);
  const sortedCyclicGroups = sortCyclicGroups(cyclicGroups, subGraph);
  sortedCyclicGroups.forEach((group) => {
    if (cache.has(group.values().next().value)) {
      return;
    }
    const result = iterateChecksums(group, group.size, cache, graph);
    group.forEach((id) => {
      const checksum = result.get(id);
      if (id in mirrored) {
        mirrored[id].forEach((id2) => cache.set(id2, checksum));
      } else {
        cache.set(id, checksum);
      }
    });
  });
  const getChecksum2 = (entry2) => {
    if (cache.has(entry2.id)) return cache.get(entry2.id);
    return _buildChecksum(entry2, getChecksum2);
  };
  return getChecksum2(entry);
};
const isSelfCircular = (group, graph) => {
  if (group.size !== 1) return false;
  const [id] = group;
  return graph.get(id).refs.has(id);
};
const getChecksumBuilder = (getLookupEntryDef) => {
  const { metadata } = getLookupEntryDef;
  const graph = buildLookupGraph(getLookupEntryDef, metadata.lookup.length);
  const cache = /* @__PURE__ */ new Map();
  const buildDefinition = (id) => buildChecksum(getLookupEntryDef(id), cache, graph);
  const buildStorage = (pallet, entry) => {
    try {
      const storageEntry = metadata.pallets.find((x) => x.name === pallet).storage.items.find((s) => s.name === entry);
      if (storageEntry.type.tag === "plain")
        return buildDefinition(storageEntry.type.value);
      const { key, value } = storageEntry.type.value;
      const val = buildDefinition(value);
      const returnKey = buildDefinition(key);
      return getChecksum([val, returnKey]);
    } catch (_) {
      return null;
    }
  };
  const buildViewFns = (pallet, entry) => {
    try {
      const viewFn = metadata.pallets.find((x) => x.name === pallet)?.viewFns.find((x) => x.name === entry);
      if (!viewFn) throw null;
      const argNamesChecksum = getStringChecksum(
        viewFn.inputs.map((x) => x.name)
      );
      const argValuesChecksum = getChecksum(
        viewFn.inputs.map((x) => buildDefinition(x.type))
      );
      const outputChecksum = buildDefinition(viewFn.output);
      return getChecksum([argNamesChecksum, argValuesChecksum, outputChecksum]);
    } catch (_) {
      return null;
    }
  };
  const buildRuntimeCall = (api, method) => {
    try {
      const entry = metadata.apis.find((x) => x.name === api)?.methods.find((x) => x.name === method);
      if (!entry) throw null;
      const argNamesChecksum = getStringChecksum(
        entry.inputs.map((x) => x.name)
      );
      const argValuesChecksum = getChecksum(
        entry.inputs.map((x) => buildDefinition(x.type))
      );
      const outputChecksum = buildDefinition(entry.output);
      return getChecksum([argNamesChecksum, argValuesChecksum, outputChecksum]);
    } catch (_) {
      return null;
    }
  };
  const buildComposite = (input) => {
    if (input.type === "void") return getChecksum([0n]);
    if (input.type === "tuple") {
      const values = Object.values(input.value).map(
        (entry) => buildDefinition(entry.id)
      );
      return getChecksum([shapeIds.tuple, ...values]);
    }
    if (input.type === "array") {
      return getChecksum([
        shapeIds.vector,
        buildDefinition(input.value.id),
        BigInt(input.len)
      ]);
    }
    return structLikeBuilder(
      shapeIds.struct,
      input.value,
      (entry) => buildDefinition(entry.id)
    );
  };
  const buildNamedTuple = (input) => {
    return structLikeBuilder(
      shapeIds.tuple,
      input.value,
      (entry) => buildDefinition(entry.id)
    );
  };
  const variantShapeId = {
    errors: 1n,
    events: 2n,
    calls: 3n
  };
  const buildVariant = (variantType) => (pallet, name) => {
    try {
      const palletEntry = metadata.pallets.find((x) => x.name === pallet);
      const enumLookup = getLookupEntryDef(palletEntry[variantType].type);
      buildDefinition(enumLookup.id);
      if (enumLookup.type !== "enum") throw null;
      const entry = enumLookup.value[name];
      const valueChecksum = entry.type === "lookupEntry" ? buildDefinition(entry.value.id) : buildComposite(entry);
      return getChecksum([variantShapeId[variantType], valueChecksum]);
    } catch (_) {
      return null;
    }
  };
  const buildConstant = (pallet, constantName) => {
    try {
      const storageEntry = metadata.pallets.find((x) => x.name === pallet).constants.find((s) => s.name === constantName);
      return buildDefinition(storageEntry.type);
    } catch (_) {
      return null;
    }
  };
  const toStringEnhancer = (fn) => (...args) => fn(...args)?.toString(32) ?? null;
  return {
    buildDefinition: toStringEnhancer(buildDefinition),
    buildRuntimeCall: toStringEnhancer(buildRuntimeCall),
    buildStorage: toStringEnhancer(buildStorage),
    buildViewFns: toStringEnhancer(buildViewFns),
    buildCall: toStringEnhancer(buildVariant("calls")),
    buildEvent: toStringEnhancer(buildVariant("events")),
    buildError: toStringEnhancer(buildVariant("errors")),
    buildConstant: toStringEnhancer(buildConstant),
    buildComposite: toStringEnhancer(buildComposite),
    buildNamedTuple: toStringEnhancer(buildNamedTuple),
    getAllGeneratedChecksums: () => Array.from(cache.values()).map((v) => v.toString(32))
  };
};

export { getChecksumBuilder };
//# sourceMappingURL=checksum-builder.mjs.map
