import { CustomType as $CustomType, isEqual } from "../gleam.mjs";
import * as $dict from "../gleam/dict.mjs";
import * as $list from "../gleam/list.mjs";
import * as $result from "../gleam/result.mjs";

class Set extends $CustomType {
  constructor(dict) {
    super();
    this.dict = dict;
  }
}

/**
 * Creates a new empty set.
 */
export function new$() {
  return new Set($dict.new$());
}

/**
 * Gets the number of members in a set.
 *
 * This function runs in constant time.
 *
 * ## Examples
 *
 * ```gleam
 * new()
 * |> insert(1)
 * |> insert(2)
 * |> size
 * // -> 2
 * ```
 */
export function size(set) {
  return $dict.size(set.dict);
}

/**
 * Determines whether or not the set is empty.
 *
 * ## Examples
 *
 * ```gleam
 * new() |> is_empty
 * // -> True
 * ```
 *
 * ```gleam
 * new() |> insert(1) |> is_empty
 * // -> False
 * ```
 */
export function is_empty(set) {
  return isEqual(set, new$());
}

/**
 * Checks whether a set contains a given member.
 *
 * This function runs in logarithmic time.
 *
 * ## Examples
 *
 * ```gleam
 * new()
 * |> insert(2)
 * |> contains(2)
 * // -> True
 * ```
 *
 * ```gleam
 * new()
 * |> insert(2)
 * |> contains(1)
 * // -> False
 * ```
 */
export function contains(set, member) {
  let _pipe = set.dict;
  let _pipe$1 = $dict.get(_pipe, member);
  return $result.is_ok(_pipe$1);
}

/**
 * Removes a member from a set. If the set does not contain the member then
 * the set is returned unchanged.
 *
 * This function runs in logarithmic time.
 *
 * ## Examples
 *
 * ```gleam
 * new()
 * |> insert(2)
 * |> delete(2)
 * |> contains(1)
 * // -> False
 * ```
 */
export function delete$(set, member) {
  return new Set($dict.delete$(set.dict, member));
}

/**
 * Converts the set into a list of the contained members.
 *
 * The list has no specific ordering, any unintentional ordering may change in
 * future versions of Gleam or Erlang.
 *
 * This function runs in linear time.
 *
 * ## Examples
 *
 * ```gleam
 * new() |> insert(2) |> to_list
 * // -> [2]
 * ```
 */
export function to_list(set) {
  return $dict.keys(set.dict);
}

/**
 * Combines all entries into a single value by calling a given function on each
 * one.
 *
 * Sets are not ordered so the values are not returned in any specific order.
 * Do not write code that relies on the order entries are used by this
 * function as it may change in later versions of Gleam or Erlang.
 *
 * # Examples
 *
 * ```gleam
 * from_list([1, 3, 9])
 * |> fold(0, fn(accumulator, member) { accumulator + member })
 * // -> 13
 * ```
 */
export function fold(set, initial, reducer) {
  return $dict.fold(set.dict, initial, (a, k, _) => { return reducer(a, k); });
}

/**
 * Creates a new set from an existing set, minus any members that a given
 * function returns `False` for.
 *
 * This function runs in loglinear time.
 *
 * ## Examples
 *
 * ```gleam
 * import gleam/int
 *
 * from_list([1, 4, 6, 3, 675, 44, 67])
 * |> filter(keeping: int.is_even)
 * |> to_list
 * // -> [4, 6, 44]
 * ```
 */
export function filter(set, predicate) {
  return new Set($dict.filter(set.dict, (m, _) => { return predicate(m); }));
}

/**
 * Creates a new set from a given set with all the same entries except any
 * entry found on the given list.
 *
 * ## Examples
 *
 * ```gleam
 * from_list([1, 2, 3, 4])
 * |> drop([1, 3])
 * |> to_list
 * // -> [2, 4]
 * ```
 */
export function drop(set, disallowed) {
  return $list.fold(disallowed, set, delete$);
}

/**
 * Creates a new set from a given set, only including any members which are in
 * a given list.
 *
 * This function runs in loglinear time.
 *
 * ## Examples
 *
 * ```gleam
 * from_list([1, 2, 3])
 * |> take([1, 3, 5])
 * |> to_list
 * // -> [1, 3]
 * ```
 */
export function take(set, desired) {
  return new Set($dict.take(set.dict, desired));
}

function order(first, second) {
  let $ = $dict.size(first.dict) > $dict.size(second.dict);
  if ($) {
    return [first, second];
  } else {
    return [second, first];
  }
}

/**
 * Creates a new set that contains members that are present in both given sets.
 *
 * This function runs in loglinear time.
 *
 * ## Examples
 *
 * ```gleam
 * intersection(from_list([1, 2]), from_list([2, 3])) |> to_list
 * // -> [2]
 * ```
 */
export function intersection(first, second) {
  let $ = order(first, second);
  let larger;
  let smaller;
  larger = $[0];
  smaller = $[1];
  return take(larger, to_list(smaller));
}

/**
 * Creates a new set that contains members that are present in the first set
 * but not the second.
 *
 * ## Examples
 *
 * ```gleam
 * difference(from_list([1, 2]), from_list([2, 3, 4])) |> to_list
 * // -> [1]
 * ```
 */
export function difference(first, second) {
  return drop(first, to_list(second));
}

/**
 * Determines if a set is fully contained by another.
 *
 * ## Examples
 *
 * ```gleam
 * is_subset(from_list([1]), from_list([1, 2]))
 * // -> True
 * ```
 *
 * ```gleam
 * is_subset(from_list([1, 2, 3]), from_list([3, 4, 5]))
 * // -> False
 * ```
 */
export function is_subset(first, second) {
  return isEqual(intersection(first, second), first);
}

/**
 * Determines if two sets contain no common members
 *
 * ## Examples
 *
 * ```gleam
 * is_disjoint(from_list([1, 2, 3]), from_list([4, 5, 6]))
 * // -> True
 * ```
 *
 * ```gleam
 * is_disjoint(from_list([1, 2, 3]), from_list([3, 4, 5]))
 * // -> False
 * ```
 */
export function is_disjoint(first, second) {
  return isEqual(intersection(first, second), new$());
}

/**
 * Calls a function for each member in a set, discarding the return
 * value.
 *
 * Useful for producing a side effect for every item of a set.
 *
 * ```gleam
 * let set = from_list(["apple", "banana", "cherry"])
 *
 * each(set, io.println)
 * // -> Nil
 * // apple
 * // banana
 * // cherry
 * ```
 *
 * The order of elements in the iteration is an implementation detail that
 * should not be relied upon.
 */
export function each(set, fun) {
  return fold(
    set,
    undefined,
    (nil, member) => {
      fun(member);
      return nil;
    },
  );
}

const token = undefined;

/**
 * Inserts an member into the set.
 *
 * This function runs in logarithmic time.
 *
 * ## Examples
 *
 * ```gleam
 * new()
 * |> insert(1)
 * |> insert(2)
 * |> size
 * // -> 2
 * ```
 */
export function insert(set, member) {
  return new Set($dict.insert(set.dict, member, token));
}

/**
 * Creates a new set of the members in a given list.
 *
 * This function runs in loglinear time.
 *
 * ## Examples
 *
 * ```gleam
 * import gleam/int
 * import gleam/list
 *
 * [1, 1, 2, 4, 3, 2] |> from_list |> to_list |> list.sort(by: int.compare)
 * // -> [1, 2, 3, 4]
 * ```
 */
export function from_list(members) {
  let dict = $list.fold(
    members,
    $dict.new$(),
    (m, k) => { return $dict.insert(m, k, token); },
  );
  return new Set(dict);
}

/**
 * Creates a new set from a given set with the result of applying the given
 * function to each member.
 *
 * ## Examples
 *
 * ```gleam
 * from_list([1, 2, 3, 4])
 * |> map(with: fn(x) { x * 2 })
 * |> to_list
 * // -> [2, 4, 6, 8]
 * ```
 */
export function map(set, fun) {
  return fold(
    set,
    new$(),
    (acc, member) => { return insert(acc, fun(member)); },
  );
}

/**
 * Creates a new set that contains all members of both given sets.
 *
 * This function runs in loglinear time.
 *
 * ## Examples
 *
 * ```gleam
 * union(from_list([1, 2]), from_list([2, 3])) |> to_list
 * // -> [1, 2, 3]
 * ```
 */
export function union(first, second) {
  let $ = order(first, second);
  let larger;
  let smaller;
  larger = $[0];
  smaller = $[1];
  return fold(smaller, larger, insert);
}

/**
 * Creates a new set that contains members that are present in either set, but
 * not both.
 *
 * ```gleam
 * symmetric_difference(from_list([1, 2, 3]), from_list([3, 4])) |> to_list
 * // -> [1, 2, 4]
 * ```
 */
export function symmetric_difference(first, second) {
  return difference(union(first, second), intersection(first, second));
}
