/**
 * Intended as a typescript-friendly replacement for {[k: string]: boolean} that allows us to specify what the key type should be (
 * rather than allowing any keyType.toString() to be a valid key, and without going through the trouble of declaring distinguishable
 * types for each key type we want to use). Also serves as a slightly different version of ES6 native Set(), which is hardcoded
 * to use === for object referential equality.
 * 
 * NOTE: this assume hash() is a strong test for equality, i.e. 2 objects are considered equal if and only if their hashes are the same!!!
 * TODO: write StrictHashSet<K extends {hash(): string, equals(k: K): boolean}> to handle custom equality checks
 */
export class HashSet<K extends { hash(): string }> {
  private _values: HashMap<K, K>;

  constructor(initialValues: K[] = []) {
    this._values = new HashMap<K, K>();

    for (const value of initialValues) {
      this.put(value);
    }
  }

  remove(key: K): void {
    this._values.remove(key);
  }

  put(key: K): void {
    this._values.put(key, key);
  }

  get(key: K): boolean {
    return this._values.get(key) !== undefined;
  }

  contains(key: K): boolean {
    return this._values.contains(key);
  }

  values(): K[] {
    return this._values.values();
  }

  // hash(): string {
  //   return this._values.hashKeyset();
  // }

  clone(): HashSet<K> {
    let n = new HashSet<K>();
    n._values = this._values.clone()
    return n;
  }

  size(): number {
    return this._values.size();
  }

  equals(other: HashSet<K> | undefined | null) {
    if (other === undefined || other === null) {
      return false;
    }

    if (this.size() !== other.size()) {
      return false;
    }

    for (let k of this.values()) {
      if (!other.contains(k)) {
        return false;
      }
    }

    return true;
  }

  // *[Symbol.iterator]() {
  //   // construct a new iterator. note that as usual
  //   for (let key of Object.keys(this._values)) {
  //     yield key;
  //   }
  // }
}

/**
 * Intended as a typescript-friendly replacement for {[k: string]: V} that allows us to specify what the key type should be (
 * rather than allowing any keyType.toString() to be a valid key, and without going through the trouble of declaring distinguishable
 * types for each key type we want to use). Also serves as a slightly different version of ES6 native Map(), which is hardcoded
 * to use === for object referential equality.
 * 
 * NOTE: this assume hash() is a strong test for equality, i.e. 2 objects are considered equal if and only if their hashes are the same!!!
 * TODO: write StrictHashMap<K extends {hash(): string, equals(K): boolean}> to handle custom equality checks
 */
export class HashMap<K extends { hash(): string }, V> {
  protected _values: { [key: string]: V } = {};

  put(key: K, value: V) {
    this._values[key.hash()] = value;
  }

  remove(key: K): void {
    delete this._values[key.hash()];
  }

  get(key: K): V | undefined {
    return this._values[key.hash()];
  }

  contains(key: K): boolean {
    // V may be an undefined type
    return this.get(key) !== undefined && key.hash() in this._values;
  }

  values(): V[] {
    return Object.values(this._values);
    // return Object.keys(this._values).map(key => this._values[key]); // why grant???
  }

  // *[Symbol.iterator]() {
  //   // construct a new iterator. note that as usual editing the object during iteration is not supported
  //   for (let key of Object.keys(this._values)) {
  //     yield key;
  //   }
  // }

  // hashes only the keys - use HashableHashMap if you know that the value type here is also hashable
  // hashKeyset(): string {
  //   const hashes: number[] = Object.keys(this._values).map(s => hashCode(s));
  //   let code: number = hashes.reduce((pv, cv) => pv + cv);
  //   return code.toString();
  // }

  size(): number {
    return Object.keys(this._values).length;
  }

  clone(): HashMap<K, V> {
    let n = new HashMap<K, V>();
    n._values = { ...this._values };
    return n;
  }
}

export class HashableHashMap<K extends { hash(): string }, V extends { hash(): string }> extends HashMap<K, V> {
  hash(): string {
    const hashes: number[] = Object.entries(this._values).map(([s, v]) => hashCode(s) + hashCode(v.hash()));
    let code: number = hashes.reduce((pv, cv) => pv + cv);
    return code.toString();
  }
}

/**
 * Same as HashMap, but actually stores the keys used to key the hashmap, instead of just their hashes.
 * Allows iteration over the full key-value pair set.
 */
export class KeyedHashMap<K extends { hash(): string }, V>{
  private _kvalues: { [key: string]: [K, V] } = {};

  put(key: K, value: V) {
    this._kvalues[key.hash()] = [key, value];
  }

  remove(key: K): void {
    delete this._kvalues[key.hash()];
  }

  get(key: K): V | undefined {
    return this._kvalues[key.hash()]?.[1];
  }

  contains(key: K): boolean {
    // V may be an undefined type
    return this.get(key) !== undefined && key.hash() in this._kvalues;
  }

  keys(): K[] {
    return Object.keys(this._kvalues).map(key => this._kvalues[key][0]);
  }

  entries(): ([K, V])[] {
    return Object.keys(this._kvalues).map(key => this._kvalues[key]);
  }

  values(): V[] {
    return Object.keys(this._kvalues).map(key => this._kvalues[key][1]);
  }

  hashKeyset(): string {
    const hashes: number[] = Object.keys(this._kvalues).map(s => hashCode(s));
    let code: number = hashes.reduce((pv, cv) => pv + cv);
    return code.toString();
  }

  clone(): KeyedHashMap<K, V> {
    let n = new KeyedHashMap<K, V>();
    n._kvalues = { ...this._kvalues };
    return n;
  }

  size(): number {
    return Object.keys(this._kvalues).length;
  }
}

export class DefaultHashMap<K extends { hash(): string }, V> {
  private _values: { [key: string]: V } = {};
  private _makeDefault: () => V;

  constructor(makeDefaultValue: () => V) {
    this._makeDefault = makeDefaultValue;
  }

  put(key: K, value: V) {
    this._values[key.hash()] = value;
  }

  get(key: K): V {
    if (this._values[key.hash()] === undefined) {
      this._values[key.hash()] = this._makeDefault();
    } 

    return this._values[key.hash()];
  }
}

// Hash a string to a number. source: https://gist.github.com/hyamamoto/fd435505d29ebfa3d9716fd2be8d42f0
export function hashCode(s: string): number {
  let h = 0;
  for (let i = 0; i < s.length; i++) {
    h = Math.imul(31, h) + s.charCodeAt(i) | 0;
  }
  return h;
}

// declare global {
//   interface Array<T extends { hash(): string }> {
//     hash(): string
//   }
// 
//   interface Number {
//     hash(): string
//   }
// 
//   interface String {
//     hash(): String
//   }
// }
// 
// Array.prototype.hash = function () {
//   return hashArray(this);
// }
// 
// Number.prototype.hash = function () {
//   return this.toString();
// }
// 
// String.prototype.hash = function () {
//   return this;
// }
// 
// function hashArray<T extends { hash(): string }>(arr: T[]): string {
//   return arr.map(elt => hashCode(elt.hash())).reduce((pv, cv) => 31 * pv + cv).hash();
// }
//