Add examples for disjoint? and subset?

Add examples for disjoint? and subset?

defmodule Set do
  # The ordered record contains a single bucket.
  @ordered_threshold 8

  defrecordp :ordered,
    size: 0,
    bucket: []

  # The bucketed record contains a series of buckets.
  @expand_load 5
  @contract_load 2
  @node_bitmap 0b111
  @node_shift 3
  @node_size 8
  @node_template :erlang.make_tuple(@node_size, [])

  defrecordp :trie,
    size: 0,
    depth: 0,
    expand_on: @node_size * @expand_load,
    contract_on: @contract_load,
    root: @node_template

  import Bitwise

  @compile { :inline, bucket_hash: 1, bucket_index: 1, bucket_nth_index: 2, bucket_next: 1 }

  @doc """
  Creates a new empty set.
  """
  def new() do
    ordered()
  end

  @doc """
  Creates a new set from the given enumerable.

  ## Examples

      iex> Set.new [1, 1, 2, 3, 3] |> Set.to_list
      [1,2,3]

  """
  def new(members) do
    Enum.reduce members, ordered(), fn member, set ->
      put(set, member)
    end
  end

  @doc """
  Returns a set combining the two input sets.

  ## Examples

      iex> Set.union(Set.new([1,2]), Set.new([2,3,4])) |> Set.to_list
      [1,2,3,4]

  """
  def union(set1, set2) when is_record(set1, Set) and is_record(set2, Set) and elem(set1, 1) < elem(set2, 1) do
    set_fold set1, set2, fn v1, acc ->
      put(acc, v1)
    end
  end

  def union(set1, set2) when is_record(set1, Set) and is_record(set2, Set) do
    set_fold set2, set1, fn v, acc ->
      put(acc, v)
    end
  end

  @doc """
  Returns a set containing only members in common between the two input sets.

  ## Examples

      iex> Set.intersection(Set.new([1,2]), Set.new([2,3,4])) |> Set.to_list
      [2]

  """
  def intersection(set1, set2) when is_record(set1, Set) and is_record(set2, Set) do
    set_intersect(set1, set2)
  end

  @doc """
  Returns a set that is the first set without members of the second set.

  ## Examples

      iex> Set.difference(Set.new([1,2]), Set.new([2,3,4])) |> Set.to_list
      [1]

  """
  def difference(set1, set2) when is_record(set1, Set) and is_record(set2, Set) do
    set_difference(set1, set2)
  end

  @doc """
  Checks if the set has the given value.
  """
  def member?(set, member) when is_record(set, Set) do
    case set_get(set, member) do
        ^member -> true
        _       -> false
    end
  end

  @doc """
  Returns an empty set.
  """
  def empty(_) do
    ordered()
  end

  @doc """
  Returns the set size.
  """
  def size(set) do
    elem(set, 1)
  end

  @doc """
  Converts a set to a list.
  """
  def to_list(ordered(bucket: bucket)) do
    bucket
  end

  def to_list(set) do
    set_fold(set, [], [&1|&2]) |> :lists.reverse
  end

  @doc """
  Puts the given value into the set if it does not already contain it.
  """
  def put(set, member) do
    { set, _ } = set_put(set, { :put, member })
    set
  end

  @doc """
  Deletes a value from the set.
  """
  def delete(set, member) do
    { set, _, _ } = set_delete(set, member)
    set
  end

  @doc """
  Checks if two sets are equal
  """
  def equal?(set1, set2) do
    size = elem(set1, 1)
    case elem(set2, 1) do
      ^size ->
        set_equal?(set1, set2)
      _ ->
        false
    end
  end

  @doc """
  Checks if set1's members are all contained in set2.

  ## Examples

      iex> Set.subset?(Set.new([1, 2]), Set.new([1, 2, 3]))
      true
      iex> Set.subset?(Set.new([1, 2, 3]), Set.new([1, 2]))
      false
  """
  def subset?(set1, set2) do
    set_equal?(set1, set2)
  end

  @doc """
  Checks if set1 and set2 have no members in common.

  ## Examples

      iex> Set.disjoint?(Set.new([1, 2]), Set.new([3, 4]))
      true
      iex> Set.disjoint?(Set.new([1, 2]), Set.new([2, 3]))
      false

  """
  def disjoint?(set1, set2) do
    set_disjoint?(set1, set2)
  end

  def reduce(ordered(bucket: bucket), acc, fun) do
    :lists.foldl(fun, acc, bucket)
  end

  def reduce(trie() = set, acc, fun) do
    set_fold(set, acc, fun)
  end

  ## Set-wide functions

  defp set_intersect(ordered(bucket: bucket1), ordered(bucket: bucket2)) do
    { new_bucket, size } = bucket_intersect(bucket1, bucket2)
    ordered(bucket: new_bucket, size: size)
  end

  defp set_intersect(trie(size: size1) = set1, trie(size: size2) = set2) when size1 > size2 do
    set_intersect(set2, set1)
  end

  defp set_intersect(trie(root: root, depth: depth, size: size) = set1, set2) do
    set_fold set2, ordered(), fn m, acc ->
      if member?(set1, m) do
        put acc, m
      else
        acc
      end
    end
  end

  defp set_difference(ordered(bucket: bucket1, size: size), ordered(bucket: bucket2)) do
    { new, count } = bucket_difference(bucket1, bucket2)
    ordered(bucket: new, size: size - count)
  end

  defp set_difference(trie(size: size1) = set1, set2) do
    set_fold set2, set1, fn m, acc ->
      delete(acc, m)
    end
  end

  defp set_put(ordered(size: @ordered_threshold, bucket: bucket), member) do
    root = node_relocate(bucket, 0)
    set_put(trie(size: @ordered_threshold, root: root), member)
  end

  defp set_put(ordered(size: size, bucket: bucket) = set, member) do
    { new, count } = bucket_put(bucket, member)
    { ordered(set, size: size + count, bucket: new), count }
  end

  defp set_put(trie(root: root, depth: depth, size: size, expand_on: size, contract_on: contract_on) = set, member) do
    root = node_expand(root, depth, depth + 1)
    set = trie(set, root: root, depth: depth + 1,
      expand_on: size * @node_size, contract_on: contract_on * @node_size)
    set_put(set, member)
  end

  defp set_put(trie(root: root, size: size, depth: depth) = set,  { :put, member }) do
    pos = bucket_hash(member)
    { root, count } = node_put(root, depth, pos, {:put, member})
    { trie(set, size: size + count, root: root), count }
  end

  defp set_get(ordered(bucket: bucket), member) do
    bucket_get(bucket, member)
  end

  defp set_get(trie(root: root, depth: depth), member) do
    bucket_get(node_bucket(root, depth, bucket_hash(member)), member)
  end

  defp set_delete(ordered(bucket: bucket, size: size) = set, member) do
    case bucket_delete(bucket, member) do
      { _, value, 0 } ->
        { set, value, 0 }
      { new_bucket, value, -1 } ->
        { ordered(set, size: size - 1, bucket: new_bucket), value, -1 }
    end
  end

  defp set_delete(trie(root: root, size: size, depth: depth) = set, member) do
    pos = bucket_hash(member)
    case node_delete(root, depth, pos, member) do
      { _, value, 0 } ->
        { set, value, 0 }
      { root, value, -1 } ->
        { if depth > 0 and trie(set, :contract_on) == size do
          root = node_contract(root, depth)
          trie(set,
            root: root,
            size: size - 1,
            depth: depth - 1,
            contract_on: div(size, @node_size),
            expand_on: div(trie(set, :expand_on), @node_size))
        else
          trie(set, size: size - 1, root: root)
        end, member, -1 }
    end
  end

  defp set_equal?(set1, set2) do
    try do
      reduce(set1, true, fn member, acc ->
        case member?(set2, member) do
          true -> acc
          _    -> throw(:error)
        end
      end)
    catch
      :error -> false
    end
  end

  defp set_disjoint?(set1, set2) do
    try do
      reduce(set1, true, fn member, acc ->
        case member?(set2, member) do
          false -> acc
          _     -> throw(:error)
        end
      end)
    catch
      :error -> false
    end
  end

  defp set_fold(ordered(bucket: bucket), acc, fun) do
    bucket_fold(bucket, acc, fun)
  end

  defp set_fold(trie(root: root, depth: depth), acc, fun) do
    node_fold(root, depth, acc, fun, @node_size)
  end

  ## Bucket helpers

  defp bucket_intersect([m | bucket1], [m | bucket2]) do
    { new, count } = bucket_intersect(bucket1, bucket2)
    { [m | new], count + 1 }
  end

  defp bucket_intersect([m1 | _] = bucket1, [m2 | bucket2]) when m1 > m2 do
    bucket_intersect(bucket1, bucket2)
  end

  defp bucket_intersect([_ | bucket1], bucket2) do
    bucket_intersect(bucket1, bucket2)
  end

  defp bucket_intersect([], _) do
    { [], 0 }
  end

  defp bucket_intersect(_, []) do
    { [], 0 }
  end

  defp bucket_difference([m | bucket1], [m | bucket2]) do
    { new, count } = bucket_difference(bucket1, bucket2)
    { new, count + 1 }
  end

  defp bucket_difference([m1 | bucket1], [m2 | _] = bucket2) when m1 < m2 do
    { new, count } = bucket_difference(bucket1, bucket2)
    { [m1 | new], count }
  end

  defp bucket_difference([m1 | _]= bucket1, [m2 | bucket2]) when m1 > m2 do
    bucket_difference(bucket1, bucket2)
  end

  defp bucket_difference([], bucket) do
    { [], 0 }
  end

  defp bucket_difference(bucket, []) do
    { bucket, 0 }
  end

  defp bucket_put([m|_]=bucket, { :put, member }) when m > member do
    { [member|bucket], 1 }
  end

  defp bucket_put([member|bucket], { :put, member }) do
    { [member|bucket], 0 }
  end

  defp bucket_put([e|bucket], member) do
    { rest, count } = bucket_put(bucket, member)
    { [e|rest], count }
  end

  defp bucket_put([], { :put, member }) do
    { [member], 1 }
  end

  defp bucket_put!([m|_]=bucket, member)     when m > member, do: [member|bucket]
  defp bucket_put!([member|bucket], member), do: [member|bucket]
  defp bucket_put!([e|bucket], member),      do: [e|bucket_put!(bucket, member)]
  defp bucket_put!([], member),              do: [member]

  # Deletes a key from the bucket
  defp bucket_delete([m,_|_]=bucket, member) when m > member do
    { bucket, nil, 0 }
  end

  defp bucket_delete([member|bucket], member) do
    { bucket, member, -1 }
  end

  defp bucket_delete([e|bucket], member) do
    { rest, value, count } = bucket_delete(bucket, member)
    { [e|rest], value, count }
  end

  defp bucket_delete([], _member) do
    { [], nil, 0 }
  end

  defp bucket_get([member|bucket], member) do
    member
  end

  defp bucket_get([member|bucket], candidate) when candidate > member do
    bucket_get(bucket, candidate)
  end

  defp bucket_get(_, member) do
    nil
  end

  defp bucket_fold(bucket, acc, fun) do
    :lists.foldl(fun, acc, bucket)
  end

  defp bucket_hash(key) do
    :erlang.phash2(key)
  end

  defp bucket_nth_index(hash, n) do
    (hash >>> (@node_shift * n)) &&& @node_bitmap
  end

  defp bucket_index(hash) do
    hash &&& @node_bitmap
  end

  defp bucket_next(hash) do
    hash >>> @node_shift
  end

  # Node helpers

  # Gets a bucket from the node
  defp node_bucket(node, 0, hash) do
    elem(node, bucket_index(hash))
  end

  defp node_bucket(node, depth, hash) do
    child = elem(node, bucket_index(hash))
    node_bucket(child, depth - 1, bucket_next(hash))
  end

  defp node_put(node, 0, hash, member) do
    pos = bucket_index(hash)
    { new, count } = bucket_put(elem(node, pos), member)
    { set_elem(node, pos, new), count }
  end

  defp node_put(node, depth, hash, member) do
    pos = bucket_index(hash)
    { new, count } = node_put(elem(node, pos), depth - 1, bucket_next(hash), member)
    { set_elem(node, pos, new), count }
  end

  # Deletes a key from the bucket
  defp node_delete(node, 0, hash, member) do
    pos = bucket_index(hash)
    case bucket_delete(elem(node, pos), member) do
      { _, value, 0 } -> { node, value, 0 }
      { new, value, -1 } -> { set_elem(node, pos, new), value, -1 }
    end
  end

  defp node_delete(node, depth, hash, member) do
    pos = bucket_index(hash)
    case node_delete(elem(node, pos), depth - 1, bucket_next(hash), member) do
      { _, value, 0 } -> { node, value, 0 }
      { new, value, -1 } -> { set_elem(node, pos, new), value, -1 }
    end
  end

  defp node_fold(bucket, -1, acc, fun, _) do
    bucket_fold(bucket, acc, fun)
  end

  defp node_fold(node, depth, acc, fun, count) when count >= 1 do
    acc = node_fold(:erlang.element(count, node), depth - 1, acc, fun, @node_size)
    node_fold(node, depth, acc, fun, count - 1)
  end

  defp node_fold(_node, _, acc, _fun, 0) do
    acc
  end

  defp node_relocate(node // @node_template, bucket, n) do
    :lists.foldl fn member, acc ->
      pos = member |> bucket_hash() |> bucket_nth_index(n)
      set_elem(acc, pos, bucket_put!(elem(acc, pos), member))
    end, node, bucket
  end

  # Node resizing
  defp node_expand({ b1, b2, b3, b4, b5, b6, b7, b8 }, 0, n) do
    { node_relocate(b1, n), node_relocate(b2, n), node_relocate(b3, n),
      node_relocate(b4, n), node_relocate(b5, n), node_relocate(b6, n),
      node_relocate(b7, n), node_relocate(b8, n) }
  end

  defp node_expand({ b1, b2, b3, b4, b5, b6, b7, b8 }, depth, n) do
    depth = depth - 1
    { node_expand(b1, depth, n), node_expand(b2, depth, n), node_expand(b3, depth, n),
      node_expand(b4, depth, n), node_expand(b5, depth, n), node_expand(b6, depth, n),
      node_expand(b7, depth, n), node_expand(b8, depth, n) }
  end

  defp node_contract({ b1, b2, b3, b4, b5, b6, b7, b8 }, depth) when depth > 0 do
    depth = depth - 1
    { node_contract(b1, depth), node_contract(b2, depth), node_contract(b3, depth),
      node_contract(b4, depth), node_contract(b5, depth), node_contract(b6, depth),
      node_contract(b7, depth), node_contract(b8, depth) }
  end

  defp node_contract({ b1, b2, b3, b4, b5, b6, b7, b8 }, 0) do
    b1 |> each_contract(b2) |> each_contract(b3) |> each_contract(b4)
       |> each_contract(b5) |> each_contract(b6) |> each_contract(b7)
       |> each_contract(b8)
  end

  defp each_contract([m1|acc], [m2|_]=bucket) when m1 < m2, do: [m1|each_contract(acc, bucket)]
  defp each_contract(acc, [m|bucket]), do: [m|each_contract(acc, bucket)]
  defp each_contract([], bucket), do: bucket
  defp each_contract(acc, []), do: acc

end

defimpl Enumerable, for: Set do
  def reduce(set, acc, fun), do: Set.reduce(set, acc, fun)
  def member?(set, v),       do: Set.member?(set, v)
  def member?(_set, _),      do: false
  def count(set),            do: Set.size(set)
end

defimpl Binary.Inspect, for: Set do
  import Kernel, except: [inspect: 2]

  def inspect(set, opts) do
    "#Set<" <> Kernel.inspect(Set.to_list(set), opts) <> ">"
  end
end

defmodule Set do
  # The ordered record contains a single bucket.
  @ordered_threshold 8

  defrecordp :ordered,
    size: 0,
    bucket: []

  # The bucketed record contains a series of buckets.
  @expand_load 5
  @contract_load 2
  @node_bitmap 0b111
  @node_shift 3
  @node_size 8
  @node_template :erlang.make_tuple(@node_size, [])

  defrecordp :trie,
    size: 0,
    depth: 0,
    expand_on: @node_size * @expand_load,
    contract_on: @contract_load,
    root: @node_template

  import Bitwise

  @compile { :inline, bucket_hash: 1, bucket_index: 1, bucket_nth_index: 2, bucket_next: 1 }

  @doc """
  Creates a new empty set.
  """
  def new() do
    ordered()
  end

  @doc """
  Creates a new set from the given enumerable.

  ## Examples

      iex> Set.new [1, 1, 2, 3, 3] |> Set.to_list
      [1,2,3]

  """
  def new(members) do
    Enum.reduce members, ordered(), fn member, set ->
      put(set, member)
    end
  end

  @doc """
  Returns a set combining the two input sets.

  ## Examples

      iex> Set.union(Set.new([1,2]), Set.new([2,3,4])) |> Set.to_list
      [1,2,3,4]

  """
  def union(set1, set2) when is_record(set1, Set) and is_record(set2, Set) and elem(set1, 1) < elem(set2, 1) do
    set_fold set1, set2, fn v1, acc ->
      put(acc, v1)
    end
  end

  def union(set1, set2) when is_record(set1, Set) and is_record(set2, Set) do
    set_fold set2, set1, fn v, acc ->
      put(acc, v)
    end
  end

  @doc """
  Returns a set containing only members in common between the two input sets.

  ## Examples

      iex> Set.intersection(Set.new([1,2]), Set.new([2,3,4])) |> Set.to_list
      [2]

  """
  def intersection(set1, set2) when is_record(set1, Set) and is_record(set2, Set) do
    set_intersect(set1, set2)
  end

  @doc """
  Returns a set that is the first set without members of the second set.

  ## Examples

      iex> Set.difference(Set.new([1,2]), Set.new([2,3,4])) |> Set.to_list
      [1]

  """
  def difference(set1, set2) when is_record(set1, Set) and is_record(set2, Set) do
    set_difference(set1, set2)
  end

  @doc """
  Checks if the set has the given value.
  """
  def member?(set, member) when is_record(set, Set) do
    case set_get(set, member) do
        ^member -> true
        _       -> false
    end
  end

  @doc """
  Returns an empty set.
  """
  def empty(_) do
    ordered()
  end

  @doc """
  Returns the set size.
  """
  def size(set) do
    elem(set, 1)
  end

  @doc """
  Converts a set to a list.
  """
  def to_list(ordered(bucket: bucket)) do
    bucket
  end

  def to_list(set) do
    set_fold(set, [], [&1|&2]) |> :lists.reverse
  end

  @doc """
  Puts the given value into the set if it does not already contain it.
  """
  def put(set, member) do
    { set, _ } = set_put(set, { :put, member })
    set
  end

  @doc """
  Deletes a value from the set.
  """
  def delete(set, member) do
    { set, _, _ } = set_delete(set, member)
    set
  end

  @doc """
  Checks if two sets are equal
  """
  def equal?(set1, set2) do
    size = elem(set1, 1)
    case elem(set2, 1) do
      ^size ->
        set_equal?(set1, set2)
      _ ->
        false
    end
  end

  @doc """
  Checks if set1's members are all contained in set2.
  """
  def subset?(set1, set2) do
    set_equal?(set1, set2)
  end

  @doc """
  Checks if set1 and set2 have no members in common.
  """
  def disjoint?(set1, set2) do
    set_disjoint?(set1, set2)
  end

  def reduce(ordered(bucket: bucket), acc, fun) do
    :lists.foldl(fun, acc, bucket)
  end

  def reduce(trie() = set, acc, fun) do
    set_fold(set, acc, fun)
  end

  ## Set-wide functions

  defp set_intersect(ordered(bucket: bucket1), ordered(bucket: bucket2)) do
    { new_bucket, size } = bucket_intersect(bucket1, bucket2)
    ordered(bucket: new_bucket, size: size)
  end

  defp set_intersect(trie(size: size1) = set1, trie(size: size2) = set2) when size1 > size2 do
    set_intersect(set2, set1)
  end

  defp set_intersect(trie(root: root, depth: depth, size: size) = set1, set2) do
    set_fold set2, ordered(), fn m, acc ->
      if member?(set1, m) do
        put acc, m
      else
        acc
      end
    end
  end

  defp set_difference(ordered(bucket: bucket1, size: size), ordered(bucket: bucket2)) do
    { new, count } = bucket_difference(bucket1, bucket2)
    ordered(bucket: new, size: size - count)
  end

  defp set_difference(trie(size: size1) = set1, set2) do
    set_fold set2, set1, fn m, acc ->
      delete(acc, m)
    end
  end

  defp set_put(ordered(size: @ordered_threshold, bucket: bucket), member) do
    root = node_relocate(bucket, 0)
    set_put(trie(size: @ordered_threshold, root: root), member)
  end

  defp set_put(ordered(size: size, bucket: bucket) = set, member) do
    { new, count } = bucket_put(bucket, member)
    { ordered(set, size: size + count, bucket: new), count }
  end

  defp set_put(trie(root: root, depth: depth, size: size, expand_on: size, contract_on: contract_on) = set, member) do
    root = node_expand(root, depth, depth + 1)
    set = trie(set, root: root, depth: depth + 1,
      expand_on: size * @node_size, contract_on: contract_on * @node_size)
    set_put(set, member)
  end

  defp set_put(trie(root: root, size: size, depth: depth) = set,  { :put, member }) do
    pos = bucket_hash(member)
    { root, count } = node_put(root, depth, pos, {:put, member})
    { trie(set, size: size + count, root: root), count }
  end

  defp set_get(ordered(bucket: bucket), member) do
    bucket_get(bucket, member)
  end

  defp set_get(trie(root: root, depth: depth), member) do
    bucket_get(node_bucket(root, depth, bucket_hash(member)), member)
  end

  defp set_delete(ordered(bucket: bucket, size: size) = set, member) do
    case bucket_delete(bucket, member) do
      { _, value, 0 } ->
        { set, value, 0 }
      { new_bucket, value, -1 } ->
        { ordered(set, size: size - 1, bucket: new_bucket), value, -1 }
    end
  end

  defp set_delete(trie(root: root, size: size, depth: depth) = set, member) do
    pos = bucket_hash(member)
    case node_delete(root, depth, pos, member) do
      { _, value, 0 } ->
        { set, value, 0 }
      { root, value, -1 } ->
        { if depth > 0 and trie(set, :contract_on) == size do
          root = node_contract(root, depth)
          trie(set,
            root: root,
            size: size - 1,
            depth: depth - 1,
            contract_on: div(size, @node_size),
            expand_on: div(trie(set, :expand_on), @node_size))
        else
          trie(set, size: size - 1, root: root)
        end, member, -1 }
    end
  end

  defp set_equal?(set1, set2) do
    try do
      reduce(set1, true, fn member, acc ->
        case member?(set2, member) do
          true -> acc
          _    -> throw(:error)
        end
      end)
    catch
      :error -> false
    end
  end

  defp set_disjoint?(set1, set2) do
    try do
      reduce(set1, true, fn member, acc ->
        case member?(set2, member) do
          false -> acc
          _     -> throw(:error)
        end
      end)
    catch
      :error -> false
    end
  end

  defp set_fold(ordered(bucket: bucket), acc, fun) do
    bucket_fold(bucket, acc, fun)
  end

  defp set_fold(trie(root: root, depth: depth), acc, fun) do
    node_fold(root, depth, acc, fun, @node_size)
  end

  ## Bucket helpers

  defp bucket_intersect([m | bucket1], [m | bucket2]) do
    { new, count } = bucket_intersect(bucket1, bucket2)
    { [m | new], count + 1 }
  end

  defp bucket_intersect([m1 | _] = bucket1, [m2 | bucket2]) when m1 > m2 do
    bucket_intersect(bucket1, bucket2)
  end

  defp bucket_intersect([_ | bucket1], bucket2) do
    bucket_intersect(bucket1, bucket2)
  end

  defp bucket_intersect([], _) do
    { [], 0 }
  end

  defp bucket_intersect(_, []) do
    { [], 0 }
  end

  defp bucket_difference([m | bucket1], [m | bucket2]) do
    { new, count } = bucket_difference(bucket1, bucket2)
    { new, count + 1 }
  end

  defp bucket_difference([m1 | bucket1], [m2 | _] = bucket2) when m1 < m2 do
    { new, count } = bucket_difference(bucket1, bucket2)
    { [m1 | new], count }
  end

  defp bucket_difference([m1 | _]= bucket1, [m2 | bucket2]) when m1 > m2 do
    bucket_difference(bucket1, bucket2)
  end

  defp bucket_difference([], bucket) do
    { [], 0 }
  end

  defp bucket_difference(bucket, []) do
    { bucket, 0 }
  end

  defp bucket_put([m|_]=bucket, { :put, member }) when m > member do
    { [member|bucket], 1 }
  end

  defp bucket_put([member|bucket], { :put, member }) do
    { [member|bucket], 0 }
  end

  defp bucket_put([e|bucket], member) do
    { rest, count } = bucket_put(bucket, member)
    { [e|rest], count }
  end

  defp bucket_put([], { :put, member }) do
    { [member], 1 }
  end

  defp bucket_put!([m|_]=bucket, member)     when m > member, do: [member|bucket]
  defp bucket_put!([member|bucket], member), do: [member|bucket]
  defp bucket_put!([e|bucket], member),      do: [e|bucket_put!(bucket, member)]
  defp bucket_put!([], member),              do: [member]

  # Deletes a key from the bucket
  defp bucket_delete([m,_|_]=bucket, member) when m > member do
    { bucket, nil, 0 }
  end

  defp bucket_delete([member|bucket], member) do
    { bucket, member, -1 }
  end

  defp bucket_delete([e|bucket], member) do
    { rest, value, count } = bucket_delete(bucket, member)
    { [e|rest], value, count }
  end

  defp bucket_delete([], _member) do
    { [], nil, 0 }
  end

  defp bucket_get([member|bucket], member) do
    member
  end

  defp bucket_get([member|bucket], candidate) when candidate > member do
    bucket_get(bucket, candidate)
  end

  defp bucket_get(_, member) do
    nil
  end

  defp bucket_fold(bucket, acc, fun) do
    :lists.foldl(fun, acc, bucket)
  end

  defp bucket_hash(key) do
    :erlang.phash2(key)
  end

  defp bucket_nth_index(hash, n) do
    (hash >>> (@node_shift * n)) &&& @node_bitmap
  end

  defp bucket_index(hash) do
    hash &&& @node_bitmap
  end

  defp bucket_next(hash) do
    hash >>> @node_shift
  end

  # Node helpers

  # Gets a bucket from the node
  defp node_bucket(node, 0, hash) do
    elem(node, bucket_index(hash))
  end

  defp node_bucket(node, depth, hash) do
    child = elem(node, bucket_index(hash))
    node_bucket(child, depth - 1, bucket_next(hash))
  end

  defp node_put(node, 0, hash, member) do
    pos = bucket_index(hash)
    { new, count } = bucket_put(elem(node, pos), member)
    { set_elem(node, pos, new), count }
  end

  defp node_put(node, depth, hash, member) do
    pos = bucket_index(hash)
    { new, count } = node_put(elem(node, pos), depth - 1, bucket_next(hash), member)
    { set_elem(node, pos, new), count }
  end

  # Deletes a key from the bucket
  defp node_delete(node, 0, hash, member) do
    pos = bucket_index(hash)
    case bucket_delete(elem(node, pos), member) do
      { _, value, 0 } -> { node, value, 0 }
      { new, value, -1 } -> { set_elem(node, pos, new), value, -1 }
    end
  end

  defp node_delete(node, depth, hash, member) do
    pos = bucket_index(hash)
    case node_delete(elem(node, pos), depth - 1, bucket_next(hash), member) do
      { _, value, 0 } -> { node, value, 0 }
      { new, value, -1 } -> { set_elem(node, pos, new), value, -1 }
    end
  end

  defp node_fold(bucket, -1, acc, fun, _) do
    bucket_fold(bucket, acc, fun)
  end

  defp node_fold(node, depth, acc, fun, count) when count >= 1 do
    acc = node_fold(:erlang.element(count, node), depth - 1, acc, fun, @node_size)
    node_fold(node, depth, acc, fun, count - 1)
  end

  defp node_fold(_node, _, acc, _fun, 0) do
    acc
  end

  defp node_relocate(node // @node_template, bucket, n) do
    :lists.foldl fn member, acc ->
      pos = member |> bucket_hash() |> bucket_nth_index(n)
      set_elem(acc, pos, bucket_put!(elem(acc, pos), member))
    end, node, bucket
  end

  # Node resizing
  defp node_expand({ b1, b2, b3, b4, b5, b6, b7, b8 }, 0, n) do
    { node_relocate(b1, n), node_relocate(b2, n), node_relocate(b3, n),
      node_relocate(b4, n), node_relocate(b5, n), node_relocate(b6, n),
      node_relocate(b7, n), node_relocate(b8, n) }
  end

  defp node_expand({ b1, b2, b3, b4, b5, b6, b7, b8 }, depth, n) do
    depth = depth - 1
    { node_expand(b1, depth, n), node_expand(b2, depth, n), node_expand(b3, depth, n),
      node_expand(b4, depth, n), node_expand(b5, depth, n), node_expand(b6, depth, n),
      node_expand(b7, depth, n), node_expand(b8, depth, n) }
  end

  defp node_contract({ b1, b2, b3, b4, b5, b6, b7, b8 }, depth) when depth > 0 do
    depth = depth - 1
    { node_contract(b1, depth), node_contract(b2, depth), node_contract(b3, depth),
      node_contract(b4, depth), node_contract(b5, depth), node_contract(b6, depth),
      node_contract(b7, depth), node_contract(b8, depth) }
  end

  defp node_contract({ b1, b2, b3, b4, b5, b6, b7, b8 }, 0) do
    b1 |> each_contract(b2) |> each_contract(b3) |> each_contract(b4)
       |> each_contract(b5) |> each_contract(b6) |> each_contract(b7)
       |> each_contract(b8)
  end

  defp each_contract([m1|acc], [m2|_]=bucket) when m1 < m2, do: [m1|each_contract(acc, bucket)]
  defp each_contract(acc, [m|bucket]), do: [m|each_contract(acc, bucket)]
  defp each_contract([], bucket), do: bucket
  defp each_contract(acc, []), do: acc

end

defimpl Enumerable, for: Set do
  def reduce(set, acc, fun), do: Set.reduce(set, acc, fun)
  def member?(set, v),       do: Set.member?(set, v)
  def member?(_set, _),      do: false
  def count(set),            do: Set.size(set)
end

defimpl Binary.Inspect, for: Set do
  import Kernel, except: [inspect: 2]

  def inspect(set, opts) do
    "#Set<" <> Kernel.inspect(Set.to_list(set), opts) <> ">"
  end
end


Add output for mix load task.

Add output for mix load task.

defmodule Mix.Tasks.Moongate.Load do
  @shortdoc "Load a world."
  use Mix.Task

  def run(args = []) do
    IO.puts(
    IO.chardata_to_string(
      IO.ANSI.format_fragment(
        [:red, "Please provide a world name." <> IO.ANSI.reset], true)))
    IO.puts ""
    IO.puts world_list
  end

  def run(args) do
    defaults = %{
      db: "localhost/moongate",
      db_user: {"moongate", "moongate"}
    }
    world = hd(args)
    {:ok, config} = Peon.from_file("priv/worlds/#{world}/moongate.peon")
    prepared = defaults |> Map.merge(config)
    {username, password} = prepared.db_user
    result = "#{world}\n#{username}:#{password}@#{prepared.db}"
    {:ok, file} = File.open("user", [:write])
    IO.binwrite(file, result)
    Mix.Tasks.Clean.run([])

    IO.puts(
      IO.chardata_to_string(
      ["Loaded world "] ++
      IO.ANSI.format_fragment(
        [:blue, "#{world}" <> IO.ANSI.reset <> "."], true)))
  end

  defp world_list do
    IO.chardata_to_string(["The worlds you can load are: "] ++
      IO.ANSI.format_fragment(
        [:cyan, "#{world_dirs}" <> IO.ANSI.reset], true))
  end

  defp world_dirs do
    {:ok, ls} = File.ls("priv/worlds")
    ls
    |> Enum.filter(&(File.dir?("priv/worlds/#{&1}")))
    |> Enum.join(" ")
  end
end

defmodule Mix.Tasks.Moongate.Load do
  @shortdoc "Load a world."
  use Mix.Task

  def run(args = []) do
    IO.puts(
    IO.chardata_to_string(
      IO.ANSI.format_fragment(
        [:red, "Please provide a world name." <> IO.ANSI.reset], true)))
    IO.puts ""
    IO.puts world_list
  end

  def run(args) do
    defaults = %{
      db: "localhost/moongate",
      db_user: {"moongate", "moongate"}
    }
    world = hd(args)
    {:ok, config} = Peon.from_file("priv/worlds/#{world}/moongate.peon")
    prepared = defaults |> Map.merge(config)
    {username, password} = prepared.db_user
    result = "#{world}\n#{username}:#{password}@#{prepared.db}"
    {:ok, file} = File.open("user", [:write])
    IO.binwrite(file, result)
    Mix.Tasks.Clean.run([])
  end

  defp world_list do
    IO.chardata_to_string(["The worlds you can load are: "] ++
      IO.ANSI.format_fragment(
        [:cyan, "#{world_dirs}" <> IO.ANSI.reset], true))
  end

  defp world_dirs do
    {:ok, ls} = File.ls("priv/worlds")
    ls
    |> Enum.filter(&(File.dir?("priv/worlds/#{&1}")))
    |> Enum.join(" ")
  end
end


Typo fix in live_reload patterns

Typo fix in live_reload patterns

use Mix.Config

# For development, we disable any cache and enable
# debugging and code reloading.
#
# The watchers configuration can be used to run external
# watchers to your application. For example, we use it
# with brunch.io to recompile .js and .css sources.
config :tilex, TilexWeb.Endpoint,
  http: [port: System.get_env("PORT") || 4000],
  debug_errors: true,
  code_reloader: true,
  check_origin: false,
  watchers: [
    node: [
      "node_modules/brunch/bin/brunch",
      "watch",
      "--stdin",
      cd: Path.expand("../assets", __DIR__)
    ]
  ]

# Watch static and templates for browser reloading.
config :tilex, TilexWeb.Endpoint,
  live_reload: [
    patterns: [
      ~r{priv/static/.*(js|css|png|jpeg|jpg|gif|svg)$},
      ~r{priv/gettext/.*(po)$},
      ~r{lib/tilex_web/views/.*(ex)$},
      ~r{lib/tilex_web/templates/.*(eex)$}
    ]
  ]

# Do not include metadata nor timestamps in development logs
config :logger, :console, format: "[$level] $message\n"

# Set a higher stacktrace during development. Avoid configuring such
# in production as building large stacktraces may be expensive.
config :phoenix, :stacktrace_depth, 20

# Configure your database
config :tilex, Tilex.Repo,
  adapter: Ecto.Adapters.Postgres,
  url: System.get_env("DATABASE_URL"),
  database: "tilex_dev",
  hostname: "localhost",
  pool_size: 10

config :tilex, :page_size, 50
config :tilex, :cors_origin, "http://localhost:3000"
config :tilex, :default_twitter_handle, "hashrocket"

config :tilex, :request_tracking, true

use Mix.Config

# For development, we disable any cache and enable
# debugging and code reloading.
#
# The watchers configuration can be used to run external
# watchers to your application. For example, we use it
# with brunch.io to recompile .js and .css sources.
config :tilex, TilexWeb.Endpoint,
  http: [port: System.get_env("PORT") || 4000],
  debug_errors: true,
  code_reloader: true,
  check_origin: false,
  watchers: [
    node: [
      "node_modules/brunch/bin/brunch",
      "watch",
      "--stdin",
      cd: Path.expand("../assets", __DIR__)
    ]
  ]

# Watch static and templates for browser reloading.
config :tilex, TilexWeb.Endpoint,
  live_reload: [
    patterns: [
      ~r{priv/static/.*(js|css|png|jpeg|jpg|gif|svg)$},
      ~r{priv/gettext/.*(po)$},
      ~r{lib/tilex/web/views/.*(ex)$},
      ~r{lib/tilex/web/templates/.*(eex)$}
    ]
  ]

# Do not include metadata nor timestamps in development logs
config :logger, :console, format: "[$level] $message\n"

# Set a higher stacktrace during development. Avoid configuring such
# in production as building large stacktraces may be expensive.
config :phoenix, :stacktrace_depth, 20

# Configure your database
config :tilex, Tilex.Repo,
  adapter: Ecto.Adapters.Postgres,
  url: System.get_env("DATABASE_URL"),
  database: "tilex_dev",
  hostname: "localhost",
  pool_size: 10

config :tilex, :page_size, 50
config :tilex, :cors_origin, "http://localhost:3000"
config :tilex, :default_twitter_handle, "hashrocket"

config :tilex, :request_tracking, true


Fix merge mistake

Fix merge mistake

require Logger

defmodule BorsNG.Worker.Batcher do
  @moduledoc """
  A "Batcher" manages the backlog of batches a project has.
  It implements this set of rules:

    * When a patch is reviewed ("r+'ed"),
      it gets added to the project's non-running batch.
      If no such batch exists, it creates it.
    * After a short delay, if there is no currently running batch,
      the project's non-running batch is started.
    * The project's CI is occasionally polled,
      if a batch is currently running.
      After polling, the completion logic is run.
    * If a notification related to the underlying CI is received,
      the completion logic is run.
    * When the completion logic is run, the batch is either
      bisected (if it failed and there are two or more patches in it),
      blocked (if it failed and there is only one patch in it),
      pushed to master (if it passed),
      or (if there are still CI jobs with no results) it is left alone.
  """

  use GenServer
  alias BorsNG.Worker.Batcher
  alias BorsNG.Database.Repo
  alias BorsNG.Database.Batch
  alias BorsNG.Database.BatchState
  alias BorsNG.Database.Patch
  alias BorsNG.Database.Project
  alias BorsNG.Database.Status
  alias BorsNG.Database.LinkPatchBatch
  alias BorsNG.GitHub
  alias BorsNG.Endpoint
  import BorsNG.Router.Helpers
  import Ecto.Query

  # Every half-hour
  @poll_period 30 * 60 * 1000

  # Public API

  def start_link(project_id) do
    GenServer.start_link(__MODULE__, project_id)
  end

  def reviewed(pid, patch_id, reviewer) when is_integer(patch_id) do
    GenServer.cast(pid, {:reviewed, patch_id, reviewer})
  end

  def set_priority(pid, patch_id, priority) when is_integer(patch_id) do
    GenServer.call(pid, {:set_priority, patch_id, priority})
  end

  def status(pid, stat) do
    GenServer.cast(pid, {:status, stat})
  end

  def poll(pid) do
    send(pid, {:poll, :once})
  end

  def cancel(pid, patch_id) when is_integer(patch_id) do
    GenServer.cast(pid, {:cancel, patch_id})
  end

  def cancel_all(pid) do
    GenServer.cast(pid, {:cancel_all})
  end

  # Server callbacks

  def init(project_id) do
    Process.send_after(
      self(),
      {:poll, :repeat},
      trunc(@poll_period * :rand.uniform(2) * 0.5))
    {:ok, project_id}
  end

  def handle_cast(args, project_id) do
    do_handle_cast(args, project_id)
    {:noreply, project_id}
  end

  def handle_call({:set_priority, patch_id, priority}, _from, project_id) do
    case Repo.get(Patch, patch_id) do
      nil -> nil
      %{priority: ^priority} -> nil
      patch ->
        patch.id
        |> Batch.all_for_patch(:incomplete)
        |> Repo.one()
        |> raise_batch_priority(priority)
        patch
        |> Patch.changeset(%{priority: priority})
        |> Repo.update!()
    end

    {:reply, :ok, project_id}
  end

  def do_handle_cast({:reviewed, patch_id, reviewer}, project_id) do
    case Repo.get(Patch.all(:awaiting_review), patch_id) do
      nil ->
        # Patch exists (otherwise, no ID), but is not awaiting review
        patch = Repo.get!(Patch, patch_id)
        project = Repo.get!(Project, patch.project_id)
        project
        |> get_repo_conn()
        |> send_message([patch], :already_running_review)
      patch ->
        # Patch exists and is awaiting review
        # This will cause the PR to start after the patch's scheduled delay
        project = Repo.get!(Project, patch.project_id)
        repo_conn = get_repo_conn(project)
        case patch_preflight(repo_conn, patch) do
          :ok ->
            {batch, is_new_batch} = get_new_batch(
              project_id,
              patch.into_branch,
              patch.priority
            )
            %LinkPatchBatch{}
            |> LinkPatchBatch.changeset(%{
              batch_id: batch.id,
              patch_id: patch.id,
              reviewer: reviewer})
            |> Repo.insert!()
            if is_new_batch do
              put_incomplete_on_hold(get_repo_conn(project), batch)
            end
            poll_after_delay(project)
            send_status(repo_conn, batch.id, [patch], :waiting)
          {:error, message} ->
            send_message(repo_conn, [patch], {:preflight, message})
        end
    end
  end

  def do_handle_cast({:status, {commit, identifier, state, url}}, project_id) do
    project_id
    |> Batch.get_assoc_by_commit(commit)
    |> Repo.all()
    |> case do
      [batch] ->
        batch.id
        |> Status.get_for_batch(identifier)
        |> Repo.update_all([set: [state: state, url: url, identifier: identifier]])
        if batch.state == :running do
          maybe_complete_batch(batch)
        end
      [] -> :ok
    end
  end

  def do_handle_cast({:cancel, patch_id}, _project_id) do
    patch_id
    |> Batch.all_for_patch(:incomplete)
    |> Repo.one()
    |> cancel_patch(patch_id)
  end

  def do_handle_cast({:cancel_all}, project_id) do
    waiting = project_id
    |> Batch.all_for_project(:waiting)
    |> Repo.all()
    Enum.each(waiting, &Repo.delete!/1)
    running = project_id
    |> Batch.all_for_project(:running)
    |> Repo.all()
    Enum.map(running, &Batch.changeset(&1, %{state: :canceled}))
    |> Enum.each(&Repo.update!/1)
    repo_conn = Project
    |> Repo.get!(project_id)
    |> get_repo_conn()
    Enum.each(running, &send_status(repo_conn, &1, :canceled))
    Enum.each(waiting, &send_status(repo_conn, &1, :canceled))
  end

  def handle_info({:poll, repetition}, project_id) do
    if repetition != :once do
      Process.send_after(self(), {:poll, repetition}, @poll_period)
    end
    case poll_(project_id) do
      :stop ->
        {:stop, :normal, project_id}
      :again ->
        {:noreply, project_id}
    end
  end

  # Private implementation details

  defp poll_(project_id) do
    project = Repo.get(Project, project_id)
    incomplete = project_id
    |> Batch.all_for_project(:incomplete)
    |> Repo.all()
    incomplete
    |> Enum.map(&%Batch{&1 | project: project})
    |> sort_batches()
    |> poll_batches()
    if Enum.empty?(incomplete) do
      :stop
    else
      :again
    end
  end

  def sort_batches(batches) do
    sorted_batches = Enum.sort_by(batches, &{
      -BatchState.numberize(&1.state),
      -&1.priority,
      &1.last_polled
    })
    new_batches = Enum.dedup_by(sorted_batches, &(&1.id))
    state = if new_batches != [] and hd(new_batches).state == :running do
      :running
    else
      Enum.each(new_batches, fn batch -> :waiting =  batch.state end)
      :waiting
    end
    {state, new_batches}
  end

  defp poll_batches({:waiting, batches}) do
    case Enum.filter(batches, &Batch.next_poll_is_past/1) do
      [] -> :ok
      [batch | _] -> start_waiting_batch(batch)
    end
  end

  defp poll_batches({:running, batches}) do
    batch = hd(batches)
    cond do
      Batch.timeout_is_past(batch) ->
        timeout_batch(batch)
      Batch.next_poll_is_past(batch) ->
        poll_running_batch(batch)
      true -> :ok
    end
  end

  defp start_waiting_batch(batch) do
    project = batch.project
    repo_conn = get_repo_conn(project)
    patch_links = Repo.all(LinkPatchBatch.from_batch(batch.id))
    |> Enum.sort_by(&(&1.patch.pr_xref))
    stmp = "#{project.staging_branch}.tmp"

    base = GitHub.get_branch!(
      repo_conn,
      batch.into_branch)
    tbase = %{
      tree: base.tree,
      commit: GitHub.synthesize_commit!(
        repo_conn,
        %{
          branch: stmp,
          tree: base.tree,
          parents: [base.commit],
          commit_message: "[ci skip][skip ci][skip netlify]",
          committer: nil})}
    do_merge_patch = fn %{patch: patch}, branch ->
      case branch do
        :conflict -> :conflict
        :canceled -> :canceled
        _ -> GitHub.merge_branch!(
          repo_conn,
          %{
            from: patch.commit,
            to: stmp,
            commit_message: "[ci skip][skip ci][skip netlify] -bors-staging-tmp-#{patch.pr_xref}"})
      end
    end

    merge = Enum.reduce(patch_links, tbase, do_merge_patch)
    {status, commit} = start_waiting_merged_batch(
      batch,
      patch_links,
      base,
      merge)
    now = DateTime.to_unix(DateTime.utc_now(), :second)

    GitHub.delete_branch!(repo_conn, stmp)
    send_status(repo_conn, batch, status)
    batch
    |> Batch.changeset(%{state: status, commit: commit, last_polled: now})
    |> Repo.update!()

    Project.ping!(batch.project_id)
    status
  end

  defp start_waiting_merged_batch(_batch, [], _, _) do
    {:canceled, nil}
  end

  defp start_waiting_merged_batch(batch, patch_links, base, %{tree: tree}) do
    repo_conn = get_repo_conn(batch.project)
    patches = Enum.map(patch_links, &(&1.patch))

    repo_conn
    |> Batcher.GetBorsToml.get("#{batch.project.staging_branch}.tmp")
    |> case do
      {:ok, toml} ->
        parents = if toml.use_squash_merge do
          stmp = "#{batch.project.staging_branch}.tmp2"
          GitHub.force_push!(repo_conn, base.commit, stmp)

          new_head = Enum.reduce(patch_links, base.commit, fn patch_link, prev_head ->

            Logger.debug("Patch Link #{inspect(patch_link)}")
            Logger.debug("Patch #{inspect(patch_link.patch)}")

            {:ok, commits} = GitHub.get_pr_commits(repo_conn, patch_link.patch.pr_xref)
            {:ok, pr} = GitHub.get_pr(repo_conn, patch_link.patch.pr_xref)

            {token, _} = repo_conn
             user = GitHub.get_user_by_login!(token, pr.user.login)

            Logger.debug("PR #{inspect(pr)}")
            Logger.debug("User #{inspect(user)}")

            # If a user doesn't have a public email address in their GH profile
            # then get the email from the first commit to the PR
            user_email = if user.email != nil do
              user.email
            else
              Enum.at(commits, 0).author_email
            end

            # The head sha is the final commit in the PR.
            source_sha =  pr.head_sha
            Logger.info("Staging branch #{stmp}")
            Logger.info("Commit sha #{source_sha}")

            # Create a merge commit for each PR
            # because each PR is merged on top of each other in stmp, we can verify against any merge conflicts
            merge_commit = GitHub.merge_branch!(repo_conn,
               %{
               from: source_sha,
               to: stmp,
                 commit_message: "[ci skip][skip ci][skip netlify] -bors-staging-tmp-#{source_sha}"}
            )

            Logger.info("Merge Commit #{inspect(merge_commit)}")

            Logger.info("Previous Head #{inspect(prev_head)}")
            # Then compress the merge commit into tree into a single commit
            # appent it to the previous commit
            # Because the merges are iterative the contain *only* the changes from the PR vs the previous PR(or head)
            cpt = GitHub.create_commit!(
              repo_conn,
              %{
                tree: merge_commit.tree,
                parents: [prev_head],
                commit_message: "#{pr.title} (##{pr.number})\n#{pr.body}",
                committer: %{name: user.login, email: user_email}})

            Logger.info("Commit Sha #{inspect(cpt)}")
              cpt

          end)
          GitHub.delete_branch!(repo_conn, stmp)
          [new_head]
        else
          parents = [base.commit | Enum.map(patch_links, &(&1.patch.commit))]
          parents
        end

        head = if toml.use_squash_merge do
          # This will avoid creating a merge commit, which is important since it will prevent
          # bors from polluting th git blame history with it's own name
            head = Enum.at(parents, 0)
            GitHub.force_push!(repo_conn, head, batch.project.staging_branch)
            head
        else
          commit_message = Batcher.Message.generate_commit_message(
            patch_links,
            toml.cut_body_after,
            gather_co_authors(batch, patch_links))
          GitHub.synthesize_commit!(
            repo_conn,
            %{
              branch: batch.project.staging_branch,
              tree: tree,
              parents: parents,
              commit_message: commit_message,
              committer: toml.committer})
        end

        setup_statuses(batch, toml)
        {:running, head}
      {:error, message} ->
        message = Batcher.Message.generate_bors_toml_error(message)
        send_message(repo_conn, patches, {:config, message})
        {:error, nil}
    end
  end

  defp start_waiting_merged_batch(batch, patch_links, _base, :conflict) do
    repo_conn = get_repo_conn(batch.project)
    patches = Enum.map(patch_links, &(&1.patch))
    state = bisect(patch_links, batch)
    send_message(repo_conn, patches, {:conflict, state})
    {:conflict, nil}
  end

  def gather_co_authors(batch, patch_links) do
    repo_conn = get_repo_conn(batch.project)
    patch_links
    |> Enum.map(&(&1.patch.pr_xref))
    |> Enum.flat_map(&GitHub.get_pr_commits!(repo_conn, &1))
    |> Enum.map(&("#{&1.author_name} <#{&1.author_email}>"))
    |> Enum.uniq
  end

  defp setup_statuses(batch, toml) do
    toml.status
    |> Enum.map(&%Status{
        batch_id: batch.id,
        identifier: &1,
        url: nil,
        state: :running})
    |> Enum.each(&Repo.insert!/1)
    now = DateTime.to_unix(DateTime.utc_now(), :second)
    batch
    |> Batch.changeset(%{timeout_at: now + toml.timeout_sec})
    |> Repo.update!()
  end

  defp poll_running_batch(batch) do
    batch.project
    |> get_repo_conn()
    |> GitHub.get_commit_status!(batch.commit)
    |> Enum.each(fn {identifier, state} ->
      batch.id
      |> Status.get_for_batch(identifier)
      |> Repo.update_all([set: [state: state, identifier: identifier]])
    end)
    maybe_complete_batch(batch)
  end

  defp maybe_complete_batch(batch) do
    statuses = Repo.all(Status.all_for_batch(batch.id))
    status = Batcher.State.summary_database_statuses(statuses)
    now = DateTime.to_unix(DateTime.utc_now(), :second)
    if status != :running do
      batch.project
      |> get_repo_conn()
      |> send_status(batch, status)
      Project.ping!(batch.project_id)
      complete_batch(status, batch, statuses)
    end
    batch
    |> Batch.changeset(%{state: status, last_polled: now})
    |> Repo.update!()
    if status != :running do
      poll_(batch.project_id)
    end
  end

  @spec complete_batch(Status.state, Batch.t, [Status.t]) :: :ok
  defp complete_batch(:ok, batch, statuses) do
    project = batch.project
    repo_conn = get_repo_conn(project)
    {res,toml} = case Batcher.GetBorsToml.get(repo_conn, "#{batch.project.staging_branch}") do
      {:error, :fetch_failed} -> Batcher.GetBorsToml.get(repo_conn, "#{batch.project.staging_branch}.tmp")
      {:ok, x} -> {:ok, x}
    end

    {:ok, _} = push_with_retry(
      repo_conn,
      batch.commit,
      batch.into_branch)

    patches = batch.id
    |> Patch.all_for_batch()
    |> Repo.all()

    if toml.use_squash_merge do
      Enum.each(patches, fn patch ->
        pr = GitHub.get_pr!(repo_conn, patch.pr_xref)
        pr = %BorsNG.GitHub.Pr{pr | state: :closed, title: "[Merged by Bors] - #{pr.title}"}
        pr = GitHub.update_pr!(repo_conn, pr)
        GitHub.post_comment!(repo_conn, patch.pr_xref, "# Pull request successfully merged into master.")
      end)
    end

    send_message(repo_conn, patches, {:succeeded, statuses})
  end

  defp complete_batch(:error, batch, statuses) do
    project = batch.project
    repo_conn = get_repo_conn(project)
    erred = Enum.filter(statuses, &(&1.state == :error))
    patch_links = batch.id
    |> LinkPatchBatch.from_batch()
    |> Repo.all()
    patches = Enum.map(patch_links, &(&1.patch))
    state = bisect(patch_links, batch)
    send_message(repo_conn, patches, {state, erred})
  end

  # A delay has been observed between Bors sending the Status change
  # and GitHub allowing a Status-bearing commit to be pushed to master.
  # As a workaround, retry with exponential backoff.
  # This should retry *nine times*, by the way.
  defp push_with_retry(repo_conn, commit, into_branch, timeout \\ 1) do
    Process.sleep(timeout)
    result = GitHub.push(
      repo_conn,
      commit,
      into_branch)
    case result do
      {:ok, _} -> result
      _ when timeout >= 512 -> result
      _ -> push_with_retry(repo_conn, commit, into_branch, timeout * 2)
    end
  end

  defp timeout_batch(batch) do
    project = batch.project
    patch_links = batch.id
    |> LinkPatchBatch.from_batch()
    |> Repo.all()
    patches = Enum.map(patch_links, &(&1.patch))
    state = bisect(patch_links, batch)
    project
    |> get_repo_conn()
    |> send_message(patches, {:timeout, state})
    batch
    |> Batch.changeset(%{state: :error})
    |> Repo.update!()
    Project.ping!(project.id)
    project
    |> get_repo_conn()
    |> send_status(batch, :timeout)
  end

  defp cancel_patch(nil, _), do: :ok

  defp cancel_patch(batch, patch_id) do
    cancel_patch(batch, patch_id, batch.state)
    Project.ping!(batch.project_id)
  end

  defp cancel_patch(batch, patch_id, :running) do
    project = batch.project
    patch_links = batch.id
    |> LinkPatchBatch.from_batch()
    |> Repo.all()
    patches = Enum.map(patch_links, &(&1.patch))
    state = case tl(patch_links) do
      [] -> :failed
      _ -> :retrying
    end
    batch
    |> Batch.changeset(%{state: :canceled})
    |> Repo.update!()
    repo_conn = get_repo_conn(project)
    if state == :retrying do
      uncanceled_patch_links = Enum.filter(
        patch_links,
        &(&1.patch_id != patch_id))
      clone_batch(uncanceled_patch_links, project.id, batch.into_branch)
      canceled_patches = Enum.filter(
        patches,
        &(&1.id == patch_id))
      uncanceled_patches = Enum.filter(
        patches,
        &(&1.id != patch_id))
      send_message(repo_conn, canceled_patches, {:canceled, :failed})
      send_message(repo_conn, uncanceled_patches, {:canceled, :retrying})
    else
      send_message(repo_conn, patches, {:canceled, :failed})
    end
    send_status(repo_conn, batch, :canceled)
  end

  defp cancel_patch(batch, patch_id, _state) do
    project = batch.project
    LinkPatchBatch
    |> Repo.get_by!(batch_id: batch.id, patch_id: patch_id)
    |> Repo.delete!()
    if Batch.is_empty(batch.id, Repo) do
      Repo.delete!(batch)
    end
    patch = Repo.get!(Patch, patch_id)
    repo_conn = get_repo_conn(project)
    send_status(repo_conn, batch.id, [patch], :canceled)
    send_message(repo_conn, [patch], {:canceled, :failed})
  end

  defp bisect(patch_links, %Batch{project: project, into_branch: into}) do
    count = Enum.count(patch_links)
    if count > 1 do
      {lo, hi} = Enum.split(patch_links, div(count, 2))
      clone_batch(lo, project.id, into)
      clone_batch(hi, project.id, into)
      poll_after_delay(project)
      :retrying
    else
      :failed
    end
  end

  defp patch_preflight(repo_conn, patch) do
    if Patch.ci_skip?(patch) do
      {:error, :ci_skip}
    else
      toml = Batcher.GetBorsToml.get(
        repo_conn,
        patch.commit)
      patch_preflight(repo_conn, patch, toml)
    end
  end

  defp patch_preflight(_repo_conn, _patch, {:error, _}) do
    :ok
  end

  defp patch_preflight(repo_conn, patch, {:ok, toml}) do
    passed_label = repo_conn
    |> GitHub.get_labels!(patch.pr_xref)
    |> MapSet.new()
    |> MapSet.disjoint?(MapSet.new(toml.block_labels))
    passed_status = repo_conn
    |> GitHub.get_commit_status!(patch.commit)
    |> Enum.filter(fn {_, status} -> status != :ok end)
    |> Enum.map(fn {context, _} -> context end)
    |> MapSet.new()
    |> MapSet.disjoint?(MapSet.new(toml.pr_status))
    passed_review = repo_conn
    |> GitHub.get_reviews!(patch.pr_xref)
    |> reviews_status(toml)
    case {passed_label, passed_status, passed_review} do
      {true, true, :sufficient} -> :ok
      {false, _, _}             -> {:error, :blocked_labels}
      {_, false, _}             -> {:error, :pr_status}
      {_, _, :insufficient}     -> {:error, :insufficient_approvals}
      {_, _, :failed}           -> {:error, :blocked_review}
    end
  end

  @spec reviews_status(map, Batcher.BorsToml.t) :: :sufficient | :failed | :insufficient
  defp reviews_status(reviews, toml) do
    %{"CHANGES_REQUESTED" => failed, "APPROVED" => approvals} = reviews
    review_required? = is_integer(toml.required_approvals)
    approvals_needed = review_required? && toml.required_approvals || 0
    approved? = approvals >= approvals_needed
    failed? = failed > 0
    cond do
      # NOTE: A way to disable the code reviewing behaviour was requested on #587.
      #   As such, we only apply the reviewing rules if, on bors, the config
      #   `required_approvals` is present and an integer.
      not review_required? ->
        :sufficient
      failed? ->
        :failed
      approved? ->
        :sufficient
      review_required? ->
        :insufficient
    end
  end

  defp clone_batch(patch_links, project_id, into_branch) do
    batch = Repo.insert!(Batch.new(project_id, into_branch))
    patch_links
    |> Enum.map(&%{
      batch_id: batch.id,
      patch_id: &1.patch_id,
      reviewer: &1.reviewer})
    |> Enum.map(&LinkPatchBatch.changeset(%LinkPatchBatch{}, &1))
    |> Enum.each(&Repo.insert!/1)
    batch
  end

  def get_new_batch(project_id, into_branch, priority) do
    Batch
    |> where([b], b.project_id == ^project_id)
    |> where([b], b.state == ^(:waiting))
    |> where([b], b.into_branch == ^into_branch)
    |> where([b], b.priority == ^priority)
    |> order_by([b], [desc: b.updated_at])
    |> limit(1)
    |> Repo.all()
    |> case do
      [batch] -> {batch, false}
      _ -> {Repo.insert!(Batch.new(project_id, into_branch, priority)), true}
    end
  end

  defp raise_batch_priority(%Batch{priority: old_priority} = batch, priority) when old_priority < priority do
    project = Repo.get!(Project, batch.project_id)
    batch = batch
    |> Batch.changeset_raise_priority(%{priority: priority})
    |> Repo.update!()
    put_incomplete_on_hold(get_repo_conn(project), batch)
  end
  defp raise_batch_priority(_, _) do
    :ok
  end

  defp send_message(repo_conn, patches, message) do
    body = Batcher.Message.generate_message(message)
    Enum.each(patches, &GitHub.post_comment!(
      repo_conn,
      &1.pr_xref,
      body))
  end

  defp send_status(
         repo_conn,
         %Batch{id: id, commit: commit, project_id: project_id},
         message
       ) do
    patches = id
    |> Patch.all_for_batch()
    |> Repo.all()
    send_status(repo_conn, id, patches, message)
    unless is_nil commit do
      {msg, status} = Batcher.Message.generate_status(message)
      repo_conn
      |> GitHub.post_commit_status!({
        commit,
        status,
        msg,
        project_url(Endpoint, :log, project_id) <> "#batch-#{id}"})
    end
  end
  defp send_status(repo_conn, batch_id,  patches, message) do
    {msg, status} = Batcher.Message.generate_status(message)
    Enum.each(patches, &GitHub.post_commit_status!(
      repo_conn,
      {
        &1.commit,
        status,
        msg,
        project_url(Endpoint, :log, &1.project_id) <> "#batch-#{batch_id}"}))
  end

  @spec get_repo_conn(%Project{}) :: {{:installation, number}, number}
  defp get_repo_conn(project) do
    Project.installation_connection(project.repo_xref, Repo)
  end

  defp put_incomplete_on_hold(repo_conn, batch) do
    batches = batch.project_id
    |> Batch.all_for_project(:running)
    |> where([b], b.id != ^batch.id and b.priority < ^batch.priority)
    |> Repo.all()

    ids = Enum.map(batches, &(&1.id))

    Status
    |> where([s], s.batch_id in ^ids)
    |> Repo.delete_all()

    Enum.each(batches, &send_status(repo_conn, &1, :delayed))
    Batch
    |> where([b], b.id in ^ids)
    |> Repo.update_all(set: [state: :waiting])
  end

  defp poll_after_delay(project) do
    poll_at = (project.batch_delay_sec + 1) * 1000
    Process.send_after(self(), {:poll, :once}, poll_at)
  end
end

require Logger

defmodule BorsNG.Worker.Batcher do
  @moduledoc """
  A "Batcher" manages the backlog of batches a project has.
  It implements this set of rules:

    * When a patch is reviewed ("r+'ed"),
      it gets added to the project's non-running batch.
      If no such batch exists, it creates it.
    * After a short delay, if there is no currently running batch,
      the project's non-running batch is started.
    * The project's CI is occasionally polled,
      if a batch is currently running.
      After polling, the completion logic is run.
    * If a notification related to the underlying CI is received,
      the completion logic is run.
    * When the completion logic is run, the batch is either
      bisected (if it failed and there are two or more patches in it),
      blocked (if it failed and there is only one patch in it),
      pushed to master (if it passed),
      or (if there are still CI jobs with no results) it is left alone.
  """

  use GenServer
  alias BorsNG.Worker.Batcher
  alias BorsNG.Database.Repo
  alias BorsNG.Database.Batch
  alias BorsNG.Database.BatchState
  alias BorsNG.Database.Patch
  alias BorsNG.Database.Project
  alias BorsNG.Database.Status
  alias BorsNG.Database.LinkPatchBatch
  alias BorsNG.GitHub
  alias BorsNG.Endpoint
  import BorsNG.Router.Helpers
  import Ecto.Query

  # Every half-hour
  @poll_period 30 * 60 * 1000

  # Public API

  def start_link(project_id) do
    GenServer.start_link(__MODULE__, project_id)
  end

  def reviewed(pid, patch_id, reviewer) when is_integer(patch_id) do
    GenServer.cast(pid, {:reviewed, patch_id, reviewer})
  end

  def set_priority(pid, patch_id, priority) when is_integer(patch_id) do
    GenServer.call(pid, {:set_priority, patch_id, priority})
  end

  def status(pid, stat) do
    GenServer.cast(pid, {:status, stat})
  end

  def poll(pid) do
    send(pid, {:poll, :once})
  end

  def cancel(pid, patch_id) when is_integer(patch_id) do
    GenServer.cast(pid, {:cancel, patch_id})
  end

  def cancel_all(pid) do
    GenServer.cast(pid, {:cancel_all})
  end

  # Server callbacks

  def init(project_id) do
    Process.send_after(
      self(),
      {:poll, :repeat},
      trunc(@poll_period * :rand.uniform(2) * 0.5))
    {:ok, project_id}
  end

  def handle_cast(args, project_id) do
    do_handle_cast(args, project_id)
    {:noreply, project_id}
  end

  def handle_call({:set_priority, patch_id, priority}, _from, project_id) do
    case Repo.get(Patch, patch_id) do
      nil -> nil
      %{priority: ^priority} -> nil
      patch ->
        patch.id
        |> Batch.all_for_patch(:incomplete)
        |> Repo.one()
        |> raise_batch_priority(priority)
        patch
        |> Patch.changeset(%{priority: priority})
        |> Repo.update!()
    end

    {:reply, :ok, project_id}
  end

  def do_handle_cast({:reviewed, patch_id, reviewer}, project_id) do
    case Repo.get(Patch.all(:awaiting_review), patch_id) do
      nil ->
        # Patch exists (otherwise, no ID), but is not awaiting review
        patch = Repo.get!(Patch, patch_id)
        project = Repo.get!(Project, patch.project_id)
        project
        |> get_repo_conn()
        |> send_message([patch], :already_running_review)
      patch ->
        # Patch exists and is awaiting review
        # This will cause the PR to start after the patch's scheduled delay
        project = Repo.get!(Project, patch.project_id)
        repo_conn = get_repo_conn(project)
        case patch_preflight(repo_conn, patch) do
          :ok ->
            {batch, is_new_batch} = get_new_batch(
              project_id,
              patch.into_branch,
              patch.priority
            )
            %LinkPatchBatch{}
            |> LinkPatchBatch.changeset(%{
              batch_id: batch.id,
              patch_id: patch.id,
              reviewer: reviewer})
            |> Repo.insert!()
            if is_new_batch do
              put_incomplete_on_hold(get_repo_conn(project), batch)
            end
            poll_after_delay(project)
            send_status(repo_conn, batch.id, [patch], :waiting)
          {:error, message} ->
            send_message(repo_conn, [patch], {:preflight, message})
        end
    end
  end

  def do_handle_cast({:status, {commit, identifier, state, url}}, project_id) do
    project_id
    |> Batch.get_assoc_by_commit(commit)
    |> Repo.all()
    |> case do
      [batch] ->
        batch.id
        |> Status.get_for_batch(identifier)
        |> Repo.update_all([set: [state: state, url: url, identifier: identifier]])
        if batch.state == :running do
          maybe_complete_batch(batch)
        end
      [] -> :ok
    end
  end

  def do_handle_cast({:cancel, patch_id}, _project_id) do
    patch_id
    |> Batch.all_for_patch(:incomplete)
    |> Repo.one()
    |> cancel_patch(patch_id)
  end

  def do_handle_cast({:cancel_all}, project_id) do
    waiting = project_id
    |> Batch.all_for_project(:waiting)
    |> Repo.all()
    Enum.each(waiting, &Repo.delete!/1)
    running = project_id
    |> Batch.all_for_project(:running)
    |> Repo.all()
    Enum.map(running, &Batch.changeset(&1, %{state: :canceled}))
    |> Enum.each(&Repo.update!/1)
    repo_conn = Project
    |> Repo.get!(project_id)
    |> get_repo_conn()
    Enum.each(running, &send_status(repo_conn, &1, :canceled))
    Enum.each(waiting, &send_status(repo_conn, &1, :canceled))
  end

  def handle_info({:poll, repetition}, project_id) do
    if repetition != :once do
      Process.send_after(self(), {:poll, repetition}, @poll_period)
    end
    case poll_(project_id) do
      :stop ->
        {:stop, :normal, project_id}
      :again ->
        {:noreply, project_id}
    end
  end

  # Private implementation details

  defp poll_(project_id) do
    project = Repo.get(Project, project_id)
    incomplete = project_id
    |> Batch.all_for_project(:incomplete)
    |> Repo.all()
    incomplete
    |> Enum.map(&%Batch{&1 | project: project})
    |> sort_batches()
    |> poll_batches()
    if Enum.empty?(incomplete) do
      :stop
    else
      :again
    end
  end

  def sort_batches(batches) do
    sorted_batches = Enum.sort_by(batches, &{
      -BatchState.numberize(&1.state),
      -&1.priority,
      &1.last_polled
    })
    new_batches = Enum.dedup_by(sorted_batches, &(&1.id))
    state = if new_batches != [] and hd(new_batches).state == :running do
      :running
    else
      Enum.each(new_batches, fn batch -> :waiting =  batch.state end)
      :waiting
    end
    {state, new_batches}
  end

  defp poll_batches({:waiting, batches}) do
    case Enum.filter(batches, &Batch.next_poll_is_past/1) do
      [] -> :ok
      [batch | _] -> start_waiting_batch(batch)
    end
  end

  defp poll_batches({:running, batches}) do
    batch = hd(batches)
    cond do
      Batch.timeout_is_past(batch) ->
        timeout_batch(batch)
      Batch.next_poll_is_past(batch) ->
        poll_running_batch(batch)
      true -> :ok
    end
  end

  defp start_waiting_batch(batch) do
    project = batch.project
    repo_conn = get_repo_conn(project)
    patch_links = Repo.all(LinkPatchBatch.from_batch(batch.id))
    |> Enum.sort_by(&(&1.patch.pr_xref))
    stmp = "#{project.staging_branch}.tmp"

    base = GitHub.get_branch!(
      repo_conn,
      batch.into_branch)
    tbase = %{
      tree: base.tree,
      commit: GitHub.synthesize_commit!(
        repo_conn,
        %{
          branch: stmp,
          tree: base.tree,
          parents: [base.commit],
          commit_message: "[ci skip][skip ci][skip netlify]",
          committer: nil})}
    do_merge_patch = fn %{patch: patch}, branch ->
      case branch do
        :conflict -> :conflict
        :canceled -> :canceled
        _ -> GitHub.merge_branch!(
          repo_conn,
          %{
            from: patch.commit,
            to: stmp,
            commit_message: "[ci skip][skip ci][skip netlify] -bors-staging-tmp-#{patch.pr_xref}"})
      end
    end

    merge = Enum.reduce(patch_links, tbase, do_merge_patch)
    {status, commit} = start_waiting_merged_batch(
      batch,
      patch_links,
      base,
      merge)
    now = DateTime.to_unix(DateTime.utc_now(), :second)

    GitHub.delete_branch!(repo_conn, stmp)
    send_status(repo_conn, batch, status)
    batch
    |> Batch.changeset(%{state: status, commit: commit, last_polled: now})
    |> Repo.update!()

          stmp = "#{batch.project.staging_branch}.tmp2"
          GitHub.force_push!(repo_conn, base.commit, stmp)

          new_head = Enum.reduce(patch_links, base.commit, fn patch_link, prev_head ->

            Logger.debug("Patch Link #{inspect(patch_link)}")
            Logger.debug("Patch #{inspect(patch_link.patch)}")

            {:ok, commits} = GitHub.get_pr_commits(repo_conn, patch_link.patch.pr_xref)
            {:ok, pr} = GitHub.get_pr(repo_conn, patch_link.patch.pr_xref)

            {token, _} = repo_conn
             user = GitHub.get_user_by_login!(token, pr.user.login)

            Logger.debug("PR #{inspect(pr)}")
            Logger.debug("User #{inspect(user)}")

            # If a user doesn't have a public email address in their GH profile
            # then get the email from the first commit to the PR
            user_email = if user.email != nil do
              user.email
            else
              Enum.at(commits, 0).author_email
            end

            # The head sha is the final commit in the PR.
            source_sha =  pr.head_sha
            Logger.info("Staging branch #{stmp}")
            Logger.info("Commit sha #{source_sha}")

            # Create a merge commit for each PR
            # because each PR is merged on top of each other in stmp, we can verify against any merge conflicts
            merge_commit = GitHub.merge_branch!(repo_conn,
               %{
               from: source_sha,
               to: stmp,
                 commit_message: "[ci skip][skip ci][skip netlify] -bors-staging-tmp-#{source_sha}"}
            )

            Logger.info("Merge Commit #{inspect(merge_commit)}")

            Logger.info("Previous Head #{inspect(prev_head)}")
            # Then compress the merge commit into tree into a single commit
            # appent it to the previous commit
            # Because the merges are iterative the contain *only* the changes from the PR vs the previous PR(or head)
            cpt = GitHub.create_commit!(
              repo_conn,
              %{
                tree: merge_commit.tree,
                parents: [prev_head],
                commit_message: "#{pr.title} (##{pr.number})\n#{pr.body}",
                committer: %{name: user.login, email: user_email}})

            Logger.info("Commit Sha #{inspect(cpt)}")
              cpt

          end)
          GitHub.delete_branch!(repo_conn, stmp)
          [new_head]
        else
          parents = [base.commit | Enum.map(patch_links, &(&1.patch.commit))]
          parents
        end
        commit_message = Batcher.Message.generate_commit_message(
          patch_links,
          toml.cut_body_after,
          gather_co_authors(batch, patch_links))

        head = if toml.use_squash_merge do
            head = Enum.at(parents, 0)
            GitHub.force_push!(repo_conn, head, batch.project.staging_branch)
            head
        else
          GitHub.synthesize_commit!(
            repo_conn,
            %{
              branch: batch.project.staging_branch,
              tree: tree,
              parents: parents,
              commit_message: commit_message,
              committer: toml.committer})
        end

        setup_statuses(batch, toml)
        {:running, head}
      {:error, message} ->
        message = Batcher.Message.generate_bors_toml_error(message)
        send_message(repo_conn, patches, {:config, message})
        {:error, nil}
    end
  end

  defp start_waiting_merged_batch(batch, patch_links, _base, :conflict) do
    repo_conn = get_repo_conn(batch.project)
    patches = Enum.map(patch_links, &(&1.patch))
    state = bisect(patch_links, batch)
    send_message(repo_conn, patches, {:conflict, state})
    {:conflict, nil}
  end

  def gather_co_authors(batch, patch_links) do
    repo_conn = get_repo_conn(batch.project)
    patch_links
    |> Enum.map(&(&1.patch.pr_xref))
    |> Enum.flat_map(&GitHub.get_pr_commits!(repo_conn, &1))
    |> Enum.map(&("#{&1.author_name} <#{&1.author_email}>"))
    |> Enum.uniq
  end

  defp setup_statuses(batch, toml) do
    toml.status
    |> Enum.map(&%Status{
        batch_id: batch.id,
        identifier: &1,
        url: nil,
        state: :running})
    |> Enum.each(&Repo.insert!/1)
    now = DateTime.to_unix(DateTime.utc_now(), :second)
    batch
    |> Batch.changeset(%{timeout_at: now + toml.timeout_sec})
    |> Repo.update!()
  end

  defp poll_running_batch(batch) do
    batch.project
    |> get_repo_conn()
    |> GitHub.get_commit_status!(batch.commit)
    |> Enum.each(fn {identifier, state} ->
      batch.id
      |> Status.get_for_batch(identifier)
      |> Repo.update_all([set: [state: state, identifier: identifier]])
    end)
    maybe_complete_batch(batch)
  end

  defp maybe_complete_batch(batch) do
    statuses = Repo.all(Status.all_for_batch(batch.id))
    status = Batcher.State.summary_database_statuses(statuses)
    now = DateTime.to_unix(DateTime.utc_now(), :second)
    if status != :running do
      batch.project
      |> get_repo_conn()
      |> send_status(batch, status)
      Project.ping!(batch.project_id)
      complete_batch(status, batch, statuses)
    end
    batch
    |> Batch.changeset(%{state: status, last_polled: now})
    |> Repo.update!()
    if status != :running do
      poll_(batch.project_id)
    end
  end

  @spec complete_batch(Status.state, Batch.t, [Status.t]) :: :ok
  defp complete_batch(:ok, batch, statuses) do
    project = batch.project
    repo_conn = get_repo_conn(project)
    {res,toml} = case Batcher.GetBorsToml.get(repo_conn, "#{batch.project.staging_branch}") do
      {:error, :fetch_failed} -> Batcher.GetBorsToml.get(repo_conn, "#{batch.project.staging_branch}.tmp")
      {:ok, x} -> {:ok, x}
    end

    {:ok, _} = push_with_retry(
      repo_conn,
      batch.commit,
      batch.into_branch)

    patches = batch.id
    |> Patch.all_for_batch()
    |> Repo.all()

    if toml.use_squash_merge do
      Enum.each(patches, fn patch ->
        pr = GitHub.get_pr!(repo_conn, patch.pr_xref)
        pr = %BorsNG.GitHub.Pr{pr | state: :closed, title: "[Merged by Bors] - #{pr.title}"}
        pr = GitHub.update_pr!(repo_conn, pr)
        GitHub.post_comment!(repo_conn, patch.pr_xref, "# Pull request successfully merged into master.")
      end)
    end

    send_message(repo_conn, patches, {:succeeded, statuses})
  end

  defp complete_batch(:error, batch, statuses) do
    project = batch.project
    repo_conn = get_repo_conn(project)
    erred = Enum.filter(statuses, &(&1.state == :error))
    patch_links = batch.id
    |> LinkPatchBatch.from_batch()
    |> Repo.all()
    patches = Enum.map(patch_links, &(&1.patch))
    state = bisect(patch_links, batch)
    send_message(repo_conn, patches, {state, erred})
  end

  # A delay has been observed between Bors sending the Status change
  # and GitHub allowing a Status-bearing commit to be pushed to master.
  # As a workaround, retry with exponential backoff.
  # This should retry *nine times*, by the way.
  defp push_with_retry(repo_conn, commit, into_branch, timeout \\ 1) do
    Process.sleep(timeout)
    result = GitHub.push(
      repo_conn,
      commit,
      into_branch)
    case result do
      {:ok, _} -> result
      _ when timeout >= 512 -> result
      _ -> push_with_retry(repo_conn, commit, into_branch, timeout * 2)
    end
  end

  defp timeout_batch(batch) do
    project = batch.project
    patch_links = batch.id
    |> LinkPatchBatch.from_batch()
    |> Repo.all()
    patches = Enum.map(patch_links, &(&1.patch))
    state = bisect(patch_links, batch)
    project
    |> get_repo_conn()
    |> send_message(patches, {:timeout, state})
    batch
    |> Batch.changeset(%{state: :error})
    |> Repo.update!()
    Project.ping!(project.id)
    project
    |> get_repo_conn()
    |> send_status(batch, :timeout)
  end

  defp cancel_patch(nil, _), do: :ok

  defp cancel_patch(batch, patch_id) do
    cancel_patch(batch, patch_id, batch.state)
    Project.ping!(batch.project_id)
  end

  defp cancel_patch(batch, patch_id, :running) do
    project = batch.project
    patch_links = batch.id
    |> LinkPatchBatch.from_batch()
    |> Repo.all()
    patches = Enum.map(patch_links, &(&1.patch))
    state = case tl(patch_links) do
      [] -> :failed
      _ -> :retrying
    end
    batch
    |> Batch.changeset(%{state: :canceled})
    |> Repo.update!()
    repo_conn = get_repo_conn(project)
    if state == :retrying do
      uncanceled_patch_links = Enum.filter(
        patch_links,
        &(&1.patch_id != patch_id))
      clone_batch(uncanceled_patch_links, project.id, batch.into_branch)
      canceled_patches = Enum.filter(
        patches,
        &(&1.id == patch_id))
      uncanceled_patches = Enum.filter(
        patches,
        &(&1.id != patch_id))
      send_message(repo_conn, canceled_patches, {:canceled, :failed})
      send_message(repo_conn, uncanceled_patches, {:canceled, :retrying})
    else
      send_message(repo_conn, patches, {:canceled, :failed})
    end
    send_status(repo_conn, batch, :canceled)
  end

  defp cancel_patch(batch, patch_id, _state) do
    project = batch.project
    LinkPatchBatch
    |> Repo.get_by!(batch_id: batch.id, patch_id: patch_id)
    |> Repo.delete!()
    if Batch.is_empty(batch.id, Repo) do
      Repo.delete!(batch)
    end
    patch = Repo.get!(Patch, patch_id)
    repo_conn = get_repo_conn(project)
    send_status(repo_conn, batch.id, [patch], :canceled)
    send_message(repo_conn, [patch], {:canceled, :failed})
  end

  defp bisect(patch_links, %Batch{project: project, into_branch: into}) do
    count = Enum.count(patch_links)
    if count > 1 do
      {lo, hi} = Enum.split(patch_links, div(count, 2))
      clone_batch(lo, project.id, into)
      clone_batch(hi, project.id, into)
      poll_after_delay(project)
      :retrying
    else
      :failed
    end
  end

  defp patch_preflight(repo_conn, patch) do
    if Patch.ci_skip?(patch) do
      {:error, :ci_skip}
    else
      toml = Batcher.GetBorsToml.get(
        repo_conn,
        patch.commit)
      patch_preflight(repo_conn, patch, toml)
    end
  end

  defp patch_preflight(_repo_conn, _patch, {:error, _}) do
    :ok
  end

  defp patch_preflight(repo_conn, patch, {:ok, toml}) do
    passed_label = repo_conn
    |> GitHub.get_labels!(patch.pr_xref)
    |> MapSet.new()
    |> MapSet.disjoint?(MapSet.new(toml.block_labels))
    passed_status = repo_conn
    |> GitHub.get_commit_status!(patch.commit)
    |> Enum.filter(fn {_, status} -> status != :ok end)
    |> Enum.map(fn {context, _} -> context end)
    |> MapSet.new()
    |> MapSet.disjoint?(MapSet.new(toml.pr_status))
    passed_review = repo_conn
    |> GitHub.get_reviews!(patch.pr_xref)
    |> reviews_status(toml)
    case {passed_label, passed_status, passed_review} do
      {true, true, :sufficient} -> :ok
      {false, _, _}             -> {:error, :blocked_labels}
      {_, false, _}             -> {:error, :pr_status}
      {_, _, :insufficient}     -> {:error, :insufficient_approvals}
      {_, _, :failed}           -> {:error, :blocked_review}
    end
  end

  @spec reviews_status(map, Batcher.BorsToml.t) :: :sufficient | :failed | :insufficient
  defp reviews_status(reviews, toml) do
    %{"CHANGES_REQUESTED" => failed, "APPROVED" => approvals} = reviews
    review_required? = is_integer(toml.required_approvals)
    approvals_needed = review_required? && toml.required_approvals || 0
    approved? = approvals >= approvals_needed
    failed? = failed > 0
    cond do
      # NOTE: A way to disable the code reviewing behaviour was requested on #587.
      #   As such, we only apply the reviewing rules if, on bors, the config
      #   `required_approvals` is present and an integer.
      not review_required? ->
        :sufficient
      failed? ->
        :failed
      approved? ->
        :sufficient
      review_required? ->
        :insufficient
    end
  end

  defp clone_batch(patch_links, project_id, into_branch) do
    batch = Repo.insert!(Batch.new(project_id, into_branch))
    patch_links
    |> Enum.map(&%{
      batch_id: batch.id,
      patch_id: &1.patch_id,
      reviewer: &1.reviewer})
    |> Enum.map(&LinkPatchBatch.changeset(%LinkPatchBatch{}, &1))
    |> Enum.each(&Repo.insert!/1)
    batch
  end

  def get_new_batch(project_id, into_branch, priority) do
    Batch
    |> where([b], b.project_id == ^project_id)
    |> where([b], b.state == ^(:waiting))
    |> where([b], b.into_branch == ^into_branch)
    |> where([b], b.priority == ^priority)
    |> order_by([b], [desc: b.updated_at])
    |> limit(1)
    |> Repo.all()
    |> case do
      [batch] -> {batch, false}
      _ -> {Repo.insert!(Batch.new(project_id, into_branch, priority)), true}
    end
  end

  defp raise_batch_priority(%Batch{priority: old_priority} = batch, priority) when old_priority < priority do
    project = Repo.get!(Project, batch.project_id)
    batch = batch
    |> Batch.changeset_raise_priority(%{priority: priority})
    |> Repo.update!()
    put_incomplete_on_hold(get_repo_conn(project), batch)
  end
  defp raise_batch_priority(_, _) do
    :ok
  end

  defp send_message(repo_conn, patches, message) do
    body = Batcher.Message.generate_message(message)
    Enum.each(patches, &GitHub.post_comment!(
      repo_conn,
      &1.pr_xref,
      body))
  end

  defp send_status(
         repo_conn,
         %Batch{id: id, commit: commit, project_id: project_id},
         message
       ) do
    patches = id
    |> Patch.all_for_batch()
    |> Repo.all()
    send_status(repo_conn, id, patches, message)
    unless is_nil commit do
      {msg, status} = Batcher.Message.generate_status(message)
      repo_conn
      |> GitHub.post_commit_status!({
        commit,
        status,
        msg,
        project_url(Endpoint, :log, project_id) <> "#batch-#{id}"})
    end
  end
  defp send_status(repo_conn, batch_id,  patches, message) do
    {msg, status} = Batcher.Message.generate_status(message)
    Enum.each(patches, &GitHub.post_commit_status!(
      repo_conn,
      {
        &1.commit,
        status,
        msg,
        project_url(Endpoint, :log, &1.project_id) <> "#batch-#{batch_id}"}))
  end

  @spec get_repo_conn(%Project{}) :: {{:installation, number}, number}
  defp get_repo_conn(project) do
    Project.installation_connection(project.repo_xref, Repo)
  end

  defp put_incomplete_on_hold(repo_conn, batch) do
    batches = batch.project_id
    |> Batch.all_for_project(:running)
    |> where([b], b.id != ^batch.id and b.priority < ^batch.priority)
    |> Repo.all()

    ids = Enum.map(batches, &(&1.id))

    Status
    |> where([s], s.batch_id in ^ids)
    |> Repo.delete_all()

    Enum.each(batches, &send_status(repo_conn, &1, :delayed))
    Batch
    |> where([b], b.id in ^ids)
    |> Repo.update_all(set: [state: :waiting])
  end

  defp poll_after_delay(project) do
    poll_at = (project.batch_delay_sec + 1) * 1000
    Process.send_after(self(), {:poll, :once}, poll_at)
  end
end