/**
 * Return a copy of the set, with the item excluded.
 */
export function excluding<T>(set: ReadonlySet<T>, item: T): ReadonlySet<T> {
  if (set.has(item)) {
    const newSet = new Set(set);
    newSet.delete(item);
    return newSet;
  } else {
    return set;
  }
}

export const empty: ReadonlySet<never> = new Set<never>();

/**
 * Return a copy of the set, with the item included.
 */
export function including<T>(set: ReadonlySet<T>, item: T): ReadonlySet<T> {
  return set.has(item) ? set : new Set(set).add(item);
}

/**
 * Return a copy of the set, with the item removed it is was present, else with
 * it added.
 */
export function toggled<T>(set: ReadonlySet<T>, item: T): ReadonlySet<T> {
  return set.has(item) ? excluding(set, item) : including(set, item);
}

/**
 * Return the items in the set as an array.
 */
export function toArray<T>(set: ReadonlySet<T>): T[] {
  return Array.from(set.values());
}

/**
 * Combine the items from two sets.
 */
export function union<T>(a: ReadonlySet<T>, b: ReadonlySet<T>): ReadonlySet<T> {
  let result = a;
  for (const item of b) {
    result = including(result, item);
  }
  return result;
}

export function subtraction<T>(a: ReadonlySet<T>, b: ReadonlySet<T>): ReadonlySet<T> {
  let result = a;
  for (const item of b) {
    result = excluding(result, item);
  }
  return result;
}

export function intersection<T>(a: ReadonlySet<T>, b: ReadonlySet<T>): ReadonlySet<T> {
  const result = new Set<T>();
  for (const item of a) {
    if (b.has(item)) {
      result.add(item);
    }
  }
  return result;
}

export function some<T>(a: ReadonlySet<T>, predicate: (item: T) => boolean): boolean {
  for (const item of a) {
    if (predicate(item)) {
      return true;
    }
  }
  return false;
}

export class CountedSet<T> {
  constructor(private readonly counts = new Map<T, number>()) {}

  public add(item: T): number {
    const count = this.counts.get(item);
    const newCount = count === undefined ? 1 : count + 1;
    this.counts.set(item, newCount);
    return newCount;
  }

  public remove(item: T): number {
    const count = this.counts.get(item);
    const newCount = count !== undefined ? count - 1 : 0;
    this.counts.set(item, newCount);
    return newCount;
  }

  public count(item: T): number {
    const count = this.counts.get(item);
    return count === undefined ? 0 : count;
  }

  public *filter(predicate: (item: T, count: number) => boolean): IterableIterator<T> {
    for (const [item, count] of this.counts) {
      if (predicate(item, count)) {
        yield item;
      }
    }
  }

  public entries(): IterableIterator<[T, number]> {
    return this.counts.entries();
  }

  public copy() {
    return new CountedSet(new Map(this.counts.entries()));
  }
}
