class MinimalPerfectHash {
  constructor({ seed1, seed2, bucketCount, seeds, table, keys, values, keySource = null, keyIndices = null }) {
    this.seed1 = seed1;
    this.seed2 = seed2;
    this.bucketCount = bucketCount;
    this.seeds = seeds;
    this.table = table;
    this.keys = keys;
    this.values = values;
    this.keySource = keySource;
    this.keyIndices = keyIndices;
  }

  static build(keys, values = null, options = {}, keyIndices = null) {
    const count = keys.length;
    if (!count) return null;

    const seed1 = options.seed1 !== undefined ? options.seed1 : 0x811c9dc5;
    const seed2 = options.seed2 !== undefined ? options.seed2 : 0x9e3779b9;
    const maxSeedAttempts = options.maxSeedAttempts !== undefined ? options.maxSeedAttempts : 10000;
    const bucketCount = count;
    const seeds = new Int32Array(bucketCount);
    const table = new Int32Array(count);
    table.fill(-1);

    const buckets = Array.from({ length: bucketCount }, () => []);
    for (let i = 0; i < count; i++) {
      const key = keys[i];
      const bucket = MinimalPerfectHash._hashKey(key, seed1) % bucketCount;
      buckets[bucket].push(i);
    }

    const order = buckets
      .map((bucket, index) => ({ index, size: bucket.length }))
      .sort((a, b) => b.size - a.size);

    for (const { index: bucketIndex, size } of order) {
      if (size === 0) continue;
      const bucket = buckets[bucketIndex];
      let seed = 1;
      let placed = false;

      while (!placed && seed <= maxSeedAttempts) {
        let collision = false;
        const slots = [];

        for (let i = 0; i < bucket.length; i++) {
          const keyIndex = bucket[i];
          const key = keys[keyIndex];
          const slot = MinimalPerfectHash._hashKey(key, seed2 ^ seed) % count;

          if (table[slot] !== -1 || slots.includes(slot)) {
            collision = true;
            break;
          }
          slots.push(slot);
        }

        if (!collision) {
          for (let i = 0; i < bucket.length; i++) {
            const keyIndex = bucket[i];
            const key = keys[keyIndex];
            const slot = MinimalPerfectHash._hashKey(key, seed2 ^ seed) % count;
            table[slot] = keyIndex;
          }
          seeds[bucketIndex] = seed;
          placed = true;
        } else {
          seed += 1;
        }
      }
      if (!placed) return null;
    }

    const finalValues = values || keys.map((_, index) => index);
    return new MinimalPerfectHash({ seed1, seed2, bucketCount, seeds, table, keys, values: finalValues, keyIndices });
  }

  static fromSerialized(serialized, keySource) {
    if (!serialized) return null;
    return new MinimalPerfectHash({
      seed1: serialized.seed1,
      seed2: serialized.seed2,
      bucketCount: serialized.bucketCount,
      seeds: serialized.seeds,
      table: serialized.table,
      keys: null,
      values: serialized.values,
      keySource,
      keyIndices: serialized.keyIndices
    });
  }

  lookup(key) {
    const bucket = MinimalPerfectHash._hashKey(key, this.seed1) % this.bucketCount;
    const seed = this.seeds[bucket];
    const slot = MinimalPerfectHash._hashKey(key, this.seed2 ^ seed) % this.table.length;
    const index = this.table[slot];
    if (index === -1) return -1;
    const keyIndex = this.keyIndices ? this.keyIndices[index] : index;
    let storedKey = null;
    if (this.keySource) {
      storedKey = typeof this.keySource.get === 'function'
        ? this.keySource.get(keyIndex)
        : this.keySource[keyIndex];
    } else {
      storedKey = this.keys[index];
    }
    if (storedKey !== key) return -1;
    if (this.values) return this.values[index];
    return keyIndex;
  }

  static _hashKey(key, seed) {
    const str = typeof key === 'string' ? key : String(key);
    let hash = seed >>> 0;
    for (let i = 0; i < str.length; i++) {
      hash ^= str.charCodeAt(i);
      hash = Math.imul(hash, 0x01000193);
    }
    return hash >>> 0;
  }
}

export { MinimalPerfectHash };
