temp: bad voting logic

temp: bad voting logic

defmodule Api.Suffrages do
  import Ecto.Query, warn: false

  alias Api.Repo
  alias Api.Accounts.User
  alias Api.Teams.Team
  alias Api.Competitions
  alias Api.Competitions.Attendance
  alias Api.Suffrages.{Suffrage, Vote, PaperVote, Candidate}
  alias Ecto.{Multi, Changeset}

  def all_suffrages do
    Repo.all(Suffrage)
  end

  def create_suffrage(params) do
    changeset = Suffrage.changeset(
      %Suffrage{competition_id: Competitions.default_competition().id},
      params
    )

    Repo.insert(changeset)
  end

  def get_suffrage(id) do
    Repo.get(Suffrage, id)
  end

  def by_slug(slug) do
    Repo.get_by(Suffrage, slug: slug)
  end

  def update_suffrage(id, params) do
    suffrage = get_suffrage(id)

    Suffrage.changeset(suffrage, params)
    |> Repo.update
  end

  def delete_suffrage(id) do
    suffrage = get_suffrage(id)

    suffrage
    |> Repo.delete
  end

  def suffrage_status(id) do
    s = get_suffrage(id)
    now = DateTime.utc_now

    cond do
      s.voting_ended_at && DateTime.compare(s.voting_ended_at, now) == :lt -> :ended
      s.voting_started_at && DateTime.compare(s.voting_started_at, now) == :lt -> :started
      true -> :not_started
    end
  end

  def suffrage_voting_started_at(id) do
    get_suffrage(id).voting_started_at
  end

  def suffrage_voting_ended_at(id) do
    get_suffrage(id).voting_ended_at
  end

  def suffrage_summary(id) do
    %{
      status: suffrage_status(id),
      unredeemed_paper_votes: unredeemed_paper_votes(id),
      missing_voters: missing_voters(),
    }
  end

  def start_suffrage(suffrage_id) do
    suffrage = get_suffrage(suffrage_id)
    case suffrage_status(suffrage_id) do
      :not_started ->
        create_candidates(suffrage_id)
        assign_tie_breakers(suffrage_id)
        assign_missing_preferences(suffrage.competition_id)
        update_suffrage(suffrage_id, %{voting_started_at: DateTime.utc_now})
      :started -> :already_started
      :ended -> :already_ended
    end
  end

  def candidates(suffrage_id, at \\ nil) do
    at = at || DateTime.utc_now

    from(
      c in Candidate,
      left_join: t in assoc(c, :team),
      where: c.suffrage_id == ^suffrage_id,
      where: is_nil(c.disqualified_at) or c.disqualified_at > ^at,
      select: c
    )
  end

  def create_candidate(params) do
    changeset = Candidate.changeset(%Candidate{}, params)

    Repo.insert(changeset)
  end

  def update_candidate(candidate, params) do
    Candidate.changeset(candidate, params) |> Repo.update()
  end

  def create_candidates(suffrage_id) do
    suffrage = get_suffrage(suffrage_id)
    from(t in Team, where: t.competition_id == ^suffrage.competition_id and t.eligible == ^true)
    |> Repo.all()
    |> Enum.each(fn team ->
      create_candidate(%{team_id: team.id, suffrage_id: suffrage_id})
    end)
  end

  def end_suffrage(suffrage_id) do
    case suffrage_status(suffrage_id) do
      :not_started -> :not_started
      :started ->
        at = DateTime.utc_now
        resolve_suffrage!(suffrage_id, at)
        update_suffrage(suffrage_id, %{voting_ended_at: at})
      :ended -> :already_ended
    end
  end

  # votes:
  #
  # [
  #   {suffrage_id, ["uuid", "uuid"]},
  #   {suffrage_id, ["uuid", "uuid"]},
  # ]
  def upsert_votes(user, votes) do
    multi = Multi.new()

    attendance = Competitions.get_attendance(Competitions.default_competition.id, user.id)

    # votes_length = Enum.reduce(votes, 0, fn {_, ballot}, acc -> acc + length(ballot) end)

    # valid_votes_length = Enum.reduce(votes, 0, fn {suffrage_id, ballot}, acc ->
    #   if validate_ballot(suffrage_id, ballot, user), do: acc + length(ballot), else: acc
    # end)

    # if votes_length != valid_votes_length do
    #   IO.puts "INVALID VOTES"
    #   IO.puts votes_length
    #   IO.puts valid_votes_length
    #   throw {:error, "Invalid vote"}
    # end

    multi = Enum.reduce(votes, multi, fn {suffrage_id, ballot}, acc ->

      case suffrage_status(suffrage_id) do
        :not_started -> throw {:error, :not_started}
        :ended -> throw {:error, :already_ended}
        _ -> nil
      end

      IO.inspect multi

      Multi.insert_or_update(acc,
        suffrage_id,
        Vote.changeset(
          get_struct(user, suffrage_id),
          %{
            voter_identity: attendance.voter_identity,
            suffrage_id: suffrage_id,
            ballot: ballot,
          }
        )
      )
    end)

    Repo.transaction(multi)
  catch
    e -> e
  end

  def get_votes(user) do
    votes = from(
      v in Vote,
      join: a in Attendance,
      where: a.attendee == ^user.id,
      where: a.competition_id == ^Competitions.default_competition.id,
      where: a.voter_identity == v.voter_identity,
    )
    |> Repo.all

    {:ok, votes}
  end

  def build_info_start(suffrage_id) do
    at = suffrage_voting_started_at(suffrage_id)

    %{
      participants: %{
        initial_count: Repo.aggregate(voters(suffrage_id, at), :count, :id),
      },
      paper_votes: %{
        initial_count: Repo.aggregate(valid_paper_votes(suffrage_id, at), :count, :id),
      },
      teams: candidates(suffrage_id, at) |> Repo.all(),
    }
  end

  def voters(suffrage_id, at \\ nil) do
    at = at || DateTime.utc_now

    from(
      c in Candidate,
      left_join: t in assoc(c, :team),
      left_join: m in assoc(t, :members),
      where: t.suffrage_id == ^suffrage_id,
      where: is_nil(c.disqualified_at) or c.disqualified_at > ^at,
      select: m
    )
  end

  def build_info_end(suffrage_id) do
    begun_at = suffrage_voting_started_at(suffrage_id)
    ended_at = suffrage_voting_ended_at(suffrage_id)
    suffrage = get_suffrage(suffrage_id)

    %{
      participants: %{
        initial_count:
          Repo.aggregate(voters(suffrage_id, begun_at), :count, :id),
        final_count:
          Repo.aggregate(voters(suffrage_id, ended_at), :count, :id),
      },
      paper_votes: %{
        initial_count:
          Repo.aggregate(valid_paper_votes(suffrage_id, begun_at), :count, :id),
        final_count:
          Repo.aggregate(redeemed_paper_votes(suffrage_id, ended_at), :count, :id),
      },
      teams: candidates(suffrage_id, begun_at) |> Repo.all,
      name: suffrage.name,
      all_teams: Repo.all(from t in Team, where: t.competition_id == ^suffrage.competition_id),
      suffrages_to_votes: all_suffrages() |> Map.new(fn s ->
        {
          s.id,
          ballots(suffrage.id, ended_at),
        }
      end),
    }
  end

  defp validate_ballot(suffrage_id, votes, user), do: validate_ballot(suffrage_id, votes, user, [])
  defp validate_ballot(suffrage_id, [], _, acc), do: Enum.all?(acc)
  defp validate_ballot(suffrage_id, [vote|rest], user, acc), do: validate_ballot(
    suffrage_id,
    rest,
    user,
    acc ++ [validate_vote(suffrage_id, vote, user)]
  )

  defp validate_vote(suffrage_id, vote, user) do
    vote
    |> on_valid_team()
    |> on_votable_team(suffrage_id)
    |> not_on_own_team(user)
  end

  defp on_valid_team(vote) do
    Repo.get(Team, vote)
  end

  defp on_votable_team(nil, _), do: nil
  defp on_votable_team(team, suffrage_id) do
    teams = candidates(suffrage_id) |> Repo.all()

    case team in teams do
      true -> team
      false -> nil
    end
  end

  defp not_on_own_team(nil, _) do
    false
  end

  defp not_on_own_team(team, user) do
    user = Repo.preload(user, :teams)
    user_team = Repo.one(from t in user.teams, where: t.competition_id == ^Competitions.default_competition().id)
    user_team.id != team.id
  end

  defp get_struct(user, suffrage_id) do
    suffrage = get_suffrage(suffrage_id)
    query = from v in Vote,
      join: a in Attendance,
      where: v.suffrage_id == ^suffrage_id,
      where: a.attendee == ^user.id,
      where: a.competition_id == ^suffrage.competition_id,
      where: a.voter_identity == v.voter_identity

    case Repo.one(query) do
      nil -> %Vote{}
      vote -> vote
    end
  end

  def ballots(suffrage_id, at \\ nil) do
    at = at || DateTime.utc_now

    votes =
      Repo.all(from(
        v in Vote,
        where: v.suffrage_id == ^suffrage_id,
      ))
      |> Enum.map(&({&1.voter_identity, &1.ballot}))

    paper_votes =
      Repo.all(from(
        pv in redeemed_paper_votes(suffrage_id, at),
        where: pv.suffrage_id == ^(suffrage_id)
      ))
      |> Enum.map(&({&1.id, [&1.team_id]}))

    (paper_votes ++ votes)
  end

  def resolve_suffrage!(suffrage_id, at \\ nil) do
    get_suffrage(suffrage_id)
    |> Changeset.change(podium: calculate_podium(suffrage_id, at))
    |> Repo.update!
  end

  def calculate_podium(suffrage_id, at \\ nil) do
    candidates = candidates(suffrage_id, at)
    |> Repo.all()

    valid_team_ids =
      candidates
      |> Enum.map(&(&1.team_id))

    votes =
      ballots(suffrage_id, at)
      |> clean_votes_into_ballots(valid_team_ids)

    tie_breakers =
      candidates
      |> Map.new(fn t ->
        {
          t.team_id,
          t.tie_breaker,
        }
      end)

    calculate_podium(votes, valid_team_ids, tie_breakers)
  end

  def clean_votes_into_ballots(votes, valid_team_ids) do
    votes
    |> Enum.map(fn {_id, ballot} ->
      ballot
      |> Enum.filter(&Enum.member?(valid_team_ids, &1))
    end)
    |> Enum.reject(&Enum.empty?/1)
  end

  def calculate_podium(votes, team_ids, tie_breakers) do
    votes
    |> Enum.flat_map(&Markus.ballot_to_pairs(&1, team_ids))
    |> Markus.pairs_to_preferences(team_ids)
    |> Markus.normalize_margins(team_ids)
    |> Markus.widen_paths(team_ids)
    |> Markus.sort_candidates_with_tie_breakers(team_ids, tie_breakers)
    |> Enum.take(3)
  end

  def missing_voters do
    voters =
      from(
        v in Vote,
        join: u in assoc(v, :voter),
        select: u.id
      )
      |> Repo.all()

    missing_voters = from(
      u in User,
      join: tm in assoc(u, :teams),
      join: t in assoc(tm, :team),
      where: not (u.id in ^voters),
      order_by: [asc: u.id],
      select: {t, u},
    )

    Repo.all(missing_voters)
    |> Enum.group_by(fn {team, _} -> team end, fn {_, user} -> user end)
    |> Enum.map(fn {k, v} -> %{
      team: k,
      users: v,
    } end)
  end

  def valid_paper_votes(suffrage_id, at \\ nil) do
    at = at || DateTime.utc_now

    from(
      pv in PaperVote,
      where: pv.suffrage_id == ^suffrage_id,
      where: is_nil(pv.annulled_at) or pv.annulled_at > ^at
    )
  end

  def redeemed_paper_votes(suffrage_id, at \\ nil) do
    at = at || DateTime.utc_now

    from pv in valid_paper_votes(suffrage_id, at),
      where: not is_nil(pv.team_id)
  end

  def unredeemed_paper_votes(suffrage_id, at \\ nil) do
    at = at || DateTime.utc_now

    from pv in valid_paper_votes(suffrage_id, at),
      where: is_nil(pv.redeemed_at)
  end

  def get_paper_vote(id) do
    Repo.get!(PaperVote, id)
    |> Repo.preload(:suffrage)
  end

  def create_paper_vote(suffrage_id, admin) do
    case suffrage_status(suffrage_id) do
      :ended -> :already_ended
      _ ->
        {
          :ok,
          %PaperVote{}
          |> PaperVote.changeset(%{
            created_by_id: admin.id,
            suffrage_id: suffrage_id,
          })
          |> Repo.insert!
        }
    end
  end

  def redeem_paper_vote(paper_vote, team, member, suffrage, admin, at \\ nil) do
    at = at || DateTime.utc_now

    candidate = from(
      c in Candidate,
      where: c.team_id == ^team.id,
      where: c.suffrage_id == ^suffrage.id,
      where: is_nil(c.disqualified_at)
    ) |> Repo.one()

    cond do
      suffrage_status(paper_vote.suffrage_id) == :not_started -> {:error, :not_started}
      suffrage_status(paper_vote.suffrage_id) == :ended -> {:error, :already_ended}
      paper_vote.redeemed_at -> {:error, :already_redeemed}
      paper_vote.annulled_at -> {:error, :annulled}
      is_nil(candidate) -> {:error, :team_not_candidate}
      !is_nil(candidate.disqualified_at) -> {:error, :team_disqualified}
      true ->
        {
          :ok,
          paper_vote
          |> PaperVote.changeset(%{
            redeemed_at: at,
            redeeming_admin_id: admin.id,
            redeeming_member_id: member.id,
            team_id: team.id,
          })
          |> Repo.update!
        }
    end
  end

  def annul_paper_vote(paper_vote, admin, suffrage, at \\ nil) do
    at = at || DateTime.utc_now

    case suffrage_status(suffrage.id) do
      :ended -> {:error, :already_ended}
      _ ->
        {
          :ok,
          paper_vote
          |> PaperVote.changeset(%{
            annulled_at: at,
            annulled_by_id: admin.id,
          })
          |> Repo.update!
          |> Repo.preload(:suffrage)
        }
    end
  end

  def disqualify_team(team_id, suffrage_id, admin) do
    from(
      c in Candidate,
      where: c.team_id == ^team_id,
      where: c.suffrage_id == ^suffrage_id,
      where: is_nil(c.disqualified_at),
      update: [set: [
        disqualified_at: ^(DateTime.utc_now),
        disqualified_by_id: ^(admin.id),
      ]]
    )
    |> Repo.update_all([])
  end

  def assign_tie_breakers(suffrage_id) do
    candidates = candidates(suffrage_id) |> Repo.all()

    tie_breakers = 1..Enum.count(candidates) |> Enum.shuffle()

    Enum.reduce(candidates, tie_breakers, fn candidate, acc ->
      {number, remaining} = List.pop_at(acc, 0)
      update_candidate(candidate, %{tie_breaker: number})
      remaining
    end)
  end

  def assign_missing_preferences(competition_id) do
    suffrages = Repo.all(Suffrage) |> Enum.map(&(&1.id))

    Repo.all(from(t in Team,
      where: t.competition_id == ^competition_id and is_nil(t.prize_preference))
    )
    |> Enum.map(fn t ->
      t
      |> Changeset.change(prize_preference: suffrages |> Enum.shuffle)
      |> Repo.update!
    end)
  end
end

defmodule Api.Suffrages do
  import Ecto.Query, warn: false

  alias Api.Repo
  alias Api.Accounts.User
  alias Api.Teams.Team
  alias Api.Competitions
  alias Api.Competitions.Attendance
  alias Api.Suffrages.{Suffrage, Vote, PaperVote, Candidate}
  alias Ecto.{Multi, Changeset}

  def all_suffrages do
    Repo.all(Suffrage)
  end

  def create_suffrage(params) do
    changeset = Suffrage.changeset(
      %Suffrage{competition_id: Competitions.default_competition().id},
      params
    )

    Repo.insert(changeset)
  end

  def get_suffrage(id) do
    Repo.get(Suffrage, id)
  end

  def by_slug(slug) do
    Repo.get_by(Suffrage, slug: slug)
  end

  def update_suffrage(id, params) do
    suffrage = get_suffrage(id)

    Suffrage.changeset(suffrage, params)
    |> Repo.update
  end

  def delete_suffrage(id) do
    suffrage = get_suffrage(id)

    suffrage
    |> Repo.delete
  end

  def suffrage_status(id) do
    s = get_suffrage(id)
    now = DateTime.utc_now

    cond do
      s.voting_ended_at && DateTime.compare(s.voting_ended_at, now) == :lt -> :ended
      s.voting_started_at && DateTime.compare(s.voting_started_at, now) == :lt -> :started
      true -> :not_started
    end
  end

  def suffrage_voting_started_at(id) do
    get_suffrage(id).voting_started_at
  end

  def suffrage_voting_ended_at(id) do
    get_suffrage(id).voting_ended_at
  end

  def suffrage_summary(id) do
    %{
      status: suffrage_status(id),
      unredeemed_paper_votes: unredeemed_paper_votes(id),
      missing_voters: missing_voters(),
    }
  end

  def start_suffrage(suffrage_id) do
    suffrage = get_suffrage(suffrage_id)
    case suffrage_status(suffrage_id) do
      :not_started ->
        create_candidates(suffrage_id)
        assign_tie_breakers(suffrage_id)
        assign_missing_preferences(suffrage.competition_id)
        update_suffrage(suffrage_id, %{voting_started_at: DateTime.utc_now})
      :started -> :already_started
      :ended -> :already_ended
    end
  end

  def candidates(suffrage_id, at \\ nil) do
    at = at || DateTime.utc_now

    from(
      c in Candidate,
      left_join: t in assoc(c, :team),
      where: c.suffrage_id == ^suffrage_id,
      where: is_nil(c.disqualified_at) or c.disqualified_at > ^at,
      select: c
    )
  end

  def create_candidate(params) do
    changeset = Candidate.changeset(%Candidate{}, params)

    Repo.insert(changeset)
  end

  def update_candidate(candidate, params) do
    Candidate.changeset(candidate, params) |> Repo.update()
  end

  def create_candidates(suffrage_id) do
    suffrage = get_suffrage(suffrage_id)
    from(t in Team, where: t.competition_id == ^suffrage.competition_id and t.eligible == ^true)
    |> Repo.all()
    |> Enum.each(fn team ->
      create_candidate(%{team_id: team.id, suffrage_id: suffrage_id})
    end)
  end

  def end_suffrage(suffrage_id) do
    case suffrage_status(suffrage_id) do
      :not_started -> :not_started
      :started ->
        at = DateTime.utc_now
        resolve_suffrage!(suffrage_id, at)
        update_suffrage(suffrage_id, %{voting_ended_at: at})
      :ended -> :already_ended
    end
  end

  # votes:
  #
  # [
  #   {suffrage_id, ["uuid", "uuid"]},
  #   {suffrage_id, ["uuid", "uuid"]},
  # ]
  def upsert_votes(user, votes) do
    multi = Multi.new()

    attendance = Competitions.get_attendance(Competitions.default_competition.id, user.id)

    votes_length = Enum.reduce(votes, 0, fn {_, ballot}, acc -> acc + length(ballot) end)

    valid_votes_length = Enum.reduce(votes, 0, fn {_, ballot}, acc ->
      if validate_ballot(ballot, user), do: acc + length(ballot), else: acc
    end)

    if votes_length != valid_votes_length do
      throw {:error, "Invalid vote"}
    end

    multi = Enum.reduce(votes, multi, fn {suffrage_id, ballot}, multi ->

      case suffrage_status(suffrage_id) do
        :not_started -> throw {:error, :not_started}
        :ended -> throw {:error, :already_ended}
      end

      Multi.insert_or_update(multi,
        suffrage_id,
        Vote.changeset(
          get_struct(user, suffrage_id),
          %{
            voter_identity: attendance.voter_identity,
            suffrage_id: suffrage_id,
            ballot: ballot,
          }
        )
      )
    end)

    Repo.transaction(multi)
  catch
    e -> e
  end

  def get_votes(user) do
    votes = from(
      v in Vote,
      join: a in Attendance,
      where: a.attendee == ^user.id,
      where: a.competition_id == ^Competitions.default_competition.id,
      where: a.voter_identity == v.voter_identity,
    )
    |> Repo.all

    {:ok, votes}
  end

  def build_info_start(suffrage_id) do
    at = suffrage_voting_started_at(suffrage_id)

    %{
      participants: %{
        initial_count: Repo.aggregate(voters(suffrage_id, at), :count, :id),
      },
      paper_votes: %{
        initial_count: Repo.aggregate(valid_paper_votes(suffrage_id, at), :count, :id),
      },
      teams: candidates(suffrage_id, at) |> Repo.all(),
    }
  end

  def voters(suffrage_id, at \\ nil) do
    at = at || DateTime.utc_now

    from(
      c in Candidate,
      left_join: t in assoc(c, :team),
      left_join: m in assoc(t, :members),
      where: t.suffrage_id == ^suffrage_id,
      where: is_nil(c.disqualified_at) or c.disqualified_at > ^at,
      select: m
    )
  end

  def build_info_end(suffrage_id) do
    begun_at = suffrage_voting_started_at(suffrage_id)
    ended_at = suffrage_voting_ended_at(suffrage_id)
    suffrage = get_suffrage(suffrage_id)

    %{
      participants: %{
        initial_count:
          Repo.aggregate(voters(suffrage_id, begun_at), :count, :id),
        final_count:
          Repo.aggregate(voters(suffrage_id, ended_at), :count, :id),
      },
      paper_votes: %{
        initial_count:
          Repo.aggregate(valid_paper_votes(suffrage_id, begun_at), :count, :id),
        final_count:
          Repo.aggregate(redeemed_paper_votes(suffrage_id, ended_at), :count, :id),
      },
      teams: candidates(suffrage_id, begun_at) |> Repo.all,
      name: suffrage.name,
      all_teams: Repo.all(from t in Team, where: t.competition_id == ^suffrage.competition_id),
      suffrages_to_votes: all_suffrages() |> Map.new(fn s ->
        {
          s.id,
          ballots(suffrage.id, ended_at),
        }
      end),
    }
  end

  defp validate_ballot(votes, user), do: validate_ballot(votes, user, [])
  defp validate_ballot([], _, acc), do: Enum.all?(acc)
  defp validate_ballot([vote|rest], user, acc), do: validate_ballot(
    rest,
    user,
    acc ++ [validate_vote(vote, user)]
  )

  defp validate_vote(vote, user) do
    vote
    |> on_valid_team()
    |> on_votable_team(vote.suffrage_id)
    |> not_on_own_team(user)
  end

  defp on_valid_team(vote) do
    Repo.get(Team, vote)
  end

  defp on_votable_team(nil, _), do: nil
  defp on_votable_team(team, suffrage_id) do
    teams = candidates(suffrage_id) |> Repo.all()

    case team in teams do
      true -> team
      false -> nil
    end
  end

  defp not_on_own_team(nil, _), do: false
  defp not_on_own_team(team, user), do: user.team.team_id != team.id

  defp get_struct(user, suffrage_id) do
    suffrage = get_suffrage(suffrage_id)
    query = from v in Vote,
      join: a in Attendance,
      where: v.suffrage_id == ^suffrage_id,
      where: a.attendee == ^user.id,
      where: a.competition_id == ^suffrage.competition_id,
      where: a.voter_identity == v.voter_identity

    case Repo.one(query) do
      nil -> %Vote{}
      vote -> vote
    end
  end

  def ballots(suffrage_id, at \\ nil) do
    at = at || DateTime.utc_now

    votes =
      Repo.all(from(
        v in Vote,
        where: v.suffrage_id == ^suffrage_id,
      ))
      |> Enum.map(&({&1.voter_identity, &1.ballot}))

    paper_votes =
      Repo.all(from(
        pv in redeemed_paper_votes(suffrage_id, at),
        where: pv.suffrage_id == ^(suffrage_id)
      ))
      |> Enum.map(&({&1.id, [&1.team_id]}))

    (paper_votes ++ votes)
  end

  def resolve_suffrage!(suffrage_id, at \\ nil) do
    get_suffrage(suffrage_id)
    |> Changeset.change(podium: calculate_podium(suffrage_id, at))
    |> Repo.update!
  end

  def calculate_podium(suffrage_id, at \\ nil) do
    candidates = candidates(suffrage_id, at)
    |> Repo.all()

    valid_team_ids =
      candidates
      |> Enum.map(&(&1.team_id))

    votes =
      ballots(suffrage_id, at)
      |> clean_votes_into_ballots(valid_team_ids)

    tie_breakers =
      candidates
      |> Map.new(fn t ->
        {
          t.team_id,
          t.tie_breaker,
        }
      end)

    calculate_podium(votes, valid_team_ids, tie_breakers)
  end

  def clean_votes_into_ballots(votes, valid_team_ids) do
    votes
    |> Enum.map(fn {_id, ballot} ->
      ballot
      |> Enum.filter(&Enum.member?(valid_team_ids, &1))
    end)
    |> Enum.reject(&Enum.empty?/1)
  end

  def calculate_podium(votes, team_ids, tie_breakers) do
    votes
    |> Enum.flat_map(&Markus.ballot_to_pairs(&1, team_ids))
    |> Markus.pairs_to_preferences(team_ids)
    |> Markus.normalize_margins(team_ids)
    |> Markus.widen_paths(team_ids)
    |> Markus.sort_candidates_with_tie_breakers(team_ids, tie_breakers)
    |> Enum.take(3)
  end

  def missing_voters do
    voters =
      from(
        v in Vote,
        join: u in assoc(v, :voter),
        select: u.id
      )
      |> Repo.all()

    missing_voters = from(
      u in User,
      join: tm in assoc(u, :teams),
      join: t in assoc(tm, :team),
      where: not (u.id in ^voters),
      order_by: [asc: u.id],
      select: {t, u},
    )

    Repo.all(missing_voters)
    |> Enum.group_by(fn {team, _} -> team end, fn {_, user} -> user end)
    |> Enum.map(fn {k, v} -> %{
      team: k,
      users: v,
    } end)
  end

  def valid_paper_votes(suffrage_id, at \\ nil) do
    at = at || DateTime.utc_now

    from(
      pv in PaperVote,
      where: pv.suffrage_id == ^suffrage_id,
      where: is_nil(pv.annulled_at) or pv.annulled_at > ^at
    )
  end

  def redeemed_paper_votes(suffrage_id, at \\ nil) do
    at = at || DateTime.utc_now

    from pv in valid_paper_votes(suffrage_id, at),
      where: not is_nil(pv.team_id)
  end

  def unredeemed_paper_votes(suffrage_id, at \\ nil) do
    at = at || DateTime.utc_now

    from pv in valid_paper_votes(suffrage_id, at),
      where: is_nil(pv.redeemed_at)
  end

  def get_paper_vote(id) do
    Repo.get!(PaperVote, id)
    |> Repo.preload(:suffrage)
  end

  def create_paper_vote(suffrage_id, admin) do
    case suffrage_status(suffrage_id) do
      :ended -> :already_ended
      _ ->
        {
          :ok,
          %PaperVote{}
          |> PaperVote.changeset(%{
            created_by_id: admin.id,
            suffrage_id: suffrage_id,
          })
          |> Repo.insert!
        }
    end
  end

  def redeem_paper_vote(paper_vote, team, member, suffrage, admin, at \\ nil) do
    at = at || DateTime.utc_now

    candidate = from(
      c in Candidate,
      where: c.team_id == ^team.id,
      where: c.suffrage_id == ^suffrage.id,
      where: is_nil(c.disqualified_at)
    ) |> Repo.one()

    cond do
      suffrage_status(paper_vote.suffrage_id) == :not_started -> {:error, :not_started}
      suffrage_status(paper_vote.suffrage_id) == :ended -> {:error, :already_ended}
      paper_vote.redeemed_at -> {:error, :already_redeemed}
      paper_vote.annulled_at -> {:error, :annulled}
      is_nil(candidate) -> {:error, :team_not_candidate}
      !is_nil(candidate.disqualified_at) -> {:error, :team_disqualified}
      true ->
        {
          :ok,
          paper_vote
          |> PaperVote.changeset(%{
            redeemed_at: at,
            redeeming_admin_id: admin.id,
            redeeming_member_id: member.id,
            team_id: team.id,
          })
          |> Repo.update!
        }
    end
  end

  def annul_paper_vote(paper_vote, admin, suffrage, at \\ nil) do
    at = at || DateTime.utc_now

    case suffrage_status(suffrage.id) do
      :ended -> {:error, :already_ended}
      _ ->
        {
          :ok,
          paper_vote
          |> PaperVote.changeset(%{
            annulled_at: at,
            annulled_by_id: admin.id,
          })
          |> Repo.update!
          |> Repo.preload(:suffrage)
        }
    end
  end

  def disqualify_team(team_id, suffrage_id, admin) do
    from(
      c in Candidate,
      where: c.team_id == ^team_id,
      where: c.suffrage_id == ^suffrage_id,
      where: is_nil(c.disqualified_at),
      update: [set: [
        disqualified_at: ^(DateTime.utc_now),
        disqualified_by_id: ^(admin.id),
      ]]
    )
    |> Repo.update_all([])
  end

  def assign_tie_breakers(suffrage_id) do
    candidates = candidates(suffrage_id) |> Repo.all()

    tie_breakers = 1..Enum.count(candidates) |> Enum.shuffle()

    Enum.reduce(candidates, tie_breakers, fn candidate, acc ->
      {number, remaining} = List.pop_at(acc, 0)
      update_candidate(candidate, %{tie_breaker: number})
      remaining
    end)
  end

  def assign_missing_preferences(competition_id) do
    suffrages = Repo.all(Suffrage) |> Enum.map(&(&1.id))

    Repo.all(from(t in Team,
      where: t.competition_id == ^competition_id and is_nil(t.prize_preference))
    )
    |> Enum.map(fn t ->
      t
      |> Changeset.change(prize_preference: suffrages |> Enum.shuffle)
      |> Repo.update!
    end)
  end
end


Add more logs to factory reset

Add more logs to factory reset

defmodule Farmbot.CeleryScript.AST.Node.FactoryReset do
  @moduledoc false
  use Farmbot.CeleryScript.AST.Node
  allow_args [:package]
  use Farmbot.Logger

  def execute(%{package: :farmbot_os}, _, env) do
    env = mutate_env(env)
    Farmbot.BotState.set_sync_status(:maintenance)
    Farmbot.BotState.force_state_push()
    Logger.warn 1, "Going down for factory reset Farmbot OS!"
    Farmbot.System.factory_reset "CeleryScript request."
    {:ok, env}
  end

  def execute(%{package: :arduino_firmware}, _, env) do
    env = mutate_env(env)
    Farmbot.BotState.set_sync_status(:maintenance)
    Logger.warn 1, "Going down for factory reset Arduino Firmware!"
    params = Farmbot.System.ConfigStorage.get_config_as_map["hardware_params"]
    for {param, _val} <- params do
      Farmbot.Firmware.update_param(:"#{param}", -1)
    end

    Farmbot.BotState.force_state_push()
    Farmbot.Firmware.read_all_params()
    Farmbot.System.reboot("Arduino factory reset.")
    {:ok, env}
  end
end

defmodule Farmbot.CeleryScript.AST.Node.FactoryReset do
  @moduledoc false
  use Farmbot.CeleryScript.AST.Node
  allow_args [:package]
  use Farmbot.Logger

  def execute(%{package: :farmbot_os}, _, env) do
    env = mutate_env(env)
    Farmbot.BotState.set_sync_status(:maintenance)
    Farmbot.BotState.force_state_push()
    Logger.warn 1, "Going down for factory reset!"
    Farmbot.System.factory_reset "CeleryScript request."
    {:ok, env}
  end

  def execute(%{package: :arduino_firmware}, _, env) do
    env = mutate_env(env)
    Farmbot.BotState.set_sync_status(:maintenance)
    Logger.warn 1, "Going down for factory reset!"
    params = Farmbot.System.ConfigStorage.get_config_as_map["hardware_params"]
    for {param, _val} <- params do
      Farmbot.Firmware.update_param(:"#{param}", -1)
    end

    Farmbot.BotState.force_state_push()
    Farmbot.Firmware.read_all_params()
    Farmbot.System.reboot("Arduino factory reset.")
    {:ok, env}
  end
end


[Elixir Quiz] Virtual pet assignment.

[Elixir Quiz] Virtual pet assignment.


Add Execution.get_issues_grouped_by_filename/1

defmodule Credo.Execution do
  @moduledoc """
  Every run of Credo is configured via an `Credo.Execution` struct, which is created and
  manipulated via the `Credo.Execution` module.
  """

  @doc """
  The `Credo.Execution` struct is created and manipulated via the `Credo.Execution` module.
  """
  defstruct argv: [],
            cli_options: nil,
            # TODO: these initial switches should also be %Credo.CLI.Switch{} struct
            cli_switches: [
              debug: :boolean,
              color: :boolean,
              config_name: :string,
              config_file: :string,
              working_dir: :string
            ],
            cli_aliases: [C: :config_name, D: :debug],
            cli_switch_plugin_param_converters: [],

            # config
            files: nil,
            color: true,
            debug: false,
            checks: nil,
            requires: [],
            plugins: [],
            parse_timeout: 5000,
            strict: false,

            # options, set by the command line
            format: nil,
            help: false,
            verbose: false,
            version: false,

            # options, that are kept here for legacy reasons
            all: false,
            crash_on_error: true,
            enable_disabled_checks: nil,
            ignore_checks_tags: [],
            ignore_checks: nil,
            max_concurrent_check_runs: nil,
            min_priority: 0,
            mute_exit_status: false,
            only_checks_tags: [],
            only_checks: nil,
            read_from_stdin: false,

            # state, which is accessed and changed over the course of Credo's execution
            pipeline_map: %{},
            commands: %{},
            config_files: [],
            current_task: nil,
            parent_task: nil,
            initializing_plugin: nil,
            halted: false,
            config_files_pid: nil,
            source_files_pid: nil,
            issues_pid: nil,
            timing_pid: nil,
            skipped_checks: nil,
            assigns: %{},
            results: %{},
            config_comment_map: %{}

  @typedoc false
  @type t :: %__MODULE__{}

  @execution_pipeline_key __MODULE__
  @execution_pipeline_key_backwards_compatibility_map %{
    Credo.CLI.Command.Diff.DiffCommand => "diff",
    Credo.CLI.Command.List.ListCommand => "list",
    Credo.CLI.Command.Suggest.SuggestCommand => "suggest",
    Credo.CLI.Command.Info.InfoCommand => "info"
  }
  @execution_pipeline [
    __pre__: [
      Credo.Execution.Task.AppendDefaultConfig,
      Credo.Execution.Task.AppendExtraConfig,
      {Credo.Execution.Task.ParseOptions, parser_mode: :preliminary},
      Credo.Execution.Task.ConvertCLIOptionsToConfig,
      Credo.Execution.Task.InitializePlugins
    ],
    parse_cli_options: [{Credo.Execution.Task.ParseOptions, parser_mode: :preliminary}],
    initialize_plugins: [
      # This is where plugins can "put" their hooks using `Credo.Plugin.append_task/3`
      # to initialize themselves based on the params given in the config as well as
      # in their own command line switches.
      #
      # Example:
      #
      #     defmodule CredoDemoPlugin do
      #       import Credo.Plugin
      #
      #       def init(exec) do
      #         append_task(exec, :initialize_plugins, CredoDemoPlugin.SetDiffAsDefaultCommand)
      #       end
      #     end
    ],
    determine_command: [Credo.Execution.Task.DetermineCommand],
    set_default_command: [Credo.Execution.Task.SetDefaultCommand],
    initialize_command: [Credo.Execution.Task.InitializeCommand],
    parse_cli_options_final: [{Credo.Execution.Task.ParseOptions, parser_mode: :strict}],
    validate_cli_options: [Credo.Execution.Task.ValidateOptions],
    convert_cli_options_to_config: [Credo.Execution.Task.ConvertCLIOptionsToConfig],
    resolve_config: [Credo.Execution.Task.UseColors, Credo.Execution.Task.RequireRequires],
    validate_config: [Credo.Execution.Task.ValidateConfig],
    run_command: [Credo.Execution.Task.RunCommand],
    halt_execution: [Credo.Execution.Task.AssignExitStatusForIssues]
  ]

  alias Credo.Execution.ExecutionConfigFiles
  alias Credo.Execution.ExecutionIssues
  alias Credo.Execution.ExecutionSourceFiles
  alias Credo.Execution.ExecutionTiming

  @doc "Builds an Execution struct for the the given `argv`."
  def build(argv \\ []) when is_list(argv) do
    max_concurrent_check_runs = System.schedulers_online()

    %__MODULE__{argv: argv, max_concurrent_check_runs: max_concurrent_check_runs}
    |> put_pipeline(@execution_pipeline_key, @execution_pipeline)
    |> put_builtin_command("categories", Credo.CLI.Command.Categories.CategoriesCommand)
    |> put_builtin_command("diff", Credo.CLI.Command.Diff.DiffCommand)
    |> put_builtin_command("explain", Credo.CLI.Command.Explain.ExplainCommand)
    |> put_builtin_command("gen.check", Credo.CLI.Command.GenCheck)
    |> put_builtin_command("gen.config", Credo.CLI.Command.GenConfig)
    |> put_builtin_command("help", Credo.CLI.Command.Help)
    |> put_builtin_command("info", Credo.CLI.Command.Info.InfoCommand)
    |> put_builtin_command("list", Credo.CLI.Command.List.ListCommand)
    |> put_builtin_command("suggest", Credo.CLI.Command.Suggest.SuggestCommand)
    |> put_builtin_command("version", Credo.CLI.Command.Version)
    |> start_servers()
  end

  @doc false
  def build(%__MODULE__{} = previous_exec, files_that_changed) when is_list(files_that_changed) do
    previous_exec.argv
    |> build()
    |> put_rerun(previous_exec, files_that_changed)
  end

  def build(argv, files_that_changed) when is_list(files_that_changed) do
    build(argv)
  end

  @doc false
  defp start_servers(%__MODULE__{} = exec) do
    exec
    |> ExecutionConfigFiles.start_server()
    |> ExecutionIssues.start_server()
    |> ExecutionSourceFiles.start_server()
    |> ExecutionTiming.start_server()
  end

  @doc """
  Returns the checks that should be run for a given `exec` struct.

  Takes all checks from the `checks:` field of the exec, matches those against
  any patterns to include or exclude certain checks given via the command line.
  """
  def checks(exec)

  def checks(%__MODULE__{checks: nil}) do
    {[], [], []}
  end

  def checks(%__MODULE__{
        checks: %{enabled: checks},
        only_checks: only_checks,
        only_checks_tags: only_checks_tags,
        ignore_checks: ignore_checks,
        ignore_checks_tags: ignore_checks_tags
      }) do
    only_matching =
      checks |> filter_only_checks_by_tags(only_checks_tags) |> filter_only_checks(only_checks)

    ignore_matching_by_name = filter_ignore_checks(checks, ignore_checks)
    ignore_matching_by_tags = filter_ignore_checks_by_tags(checks, ignore_checks_tags)
    ignore_matching = ignore_matching_by_name ++ ignore_matching_by_tags

    result = only_matching -- ignore_matching

    {result, only_matching, ignore_matching}
  end

  defp filter_only_checks(checks, nil), do: checks
  defp filter_only_checks(checks, []), do: checks
  defp filter_only_checks(checks, patterns), do: filter_checks(checks, patterns)

  defp filter_ignore_checks(_checks, nil), do: []
  defp filter_ignore_checks(_checks, []), do: []
  defp filter_ignore_checks(checks, patterns), do: filter_checks(checks, patterns)

  defp filter_checks(checks, patterns) do
    regexes =
      patterns
      |> List.wrap()
      |> to_match_regexes

    Enum.filter(checks, &match_regex(&1, regexes, true))
  end

  defp match_regex(_tuple, [], default_for_empty), do: default_for_empty

  defp match_regex(tuple, regexes, _default_for_empty) do
    check_name =
      tuple
      |> Tuple.to_list()
      |> List.first()
      |> to_string

    Enum.any?(regexes, &Regex.run(&1, check_name))
  end

  defp to_match_regexes(list) do
    Enum.map(list, fn match_check ->
      {:ok, match_pattern} = Regex.compile(match_check, "i")
      match_pattern
    end)
  end

  defp filter_only_checks_by_tags(checks, nil), do: checks
  defp filter_only_checks_by_tags(checks, []), do: checks
  defp filter_only_checks_by_tags(checks, tags), do: filter_checks_by_tags(checks, tags)

  defp filter_ignore_checks_by_tags(_checks, nil), do: []
  defp filter_ignore_checks_by_tags(_checks, []), do: []
  defp filter_ignore_checks_by_tags(checks, tags), do: filter_checks_by_tags(checks, tags)

  defp filter_checks_by_tags(_checks, nil), do: []
  defp filter_checks_by_tags(_checks, []), do: []

  defp filter_checks_by_tags(checks, tags) do
    tags = Enum.map(tags, &String.to_atom/1)

    Enum.filter(checks, &match_tags(&1, tags, true))
  end

  defp match_tags(_tuple, [], default_for_empty), do: default_for_empty

  defp match_tags({check, params}, tags, _default_for_empty) do
    tags_for_check = tags_for_check(check, params)

    Enum.any?(tags, &Enum.member?(tags_for_check, &1))
  end

  @doc """
  Returns the tags for a given `check` and its `params`.
  """
  def tags_for_check(check, params)

  def tags_for_check(check, nil), do: check.tags
  def tags_for_check(check, []), do: check.tags

  def tags_for_check(check, params) when is_list(params) do
    params
    |> Credo.Check.Params.tags(check)
    |> Enum.flat_map(fn
      :__initial__ -> check.tags
      tag -> [tag]
    end)
  end

  @doc """
  Sets the exec values which `strict` implies (if applicable).
  """
  def set_strict(exec)

  def set_strict(%__MODULE__{strict: true} = exec) do
    %__MODULE__{exec | all: true, min_priority: -99}
  end

  def set_strict(%__MODULE__{strict: false} = exec) do
    %__MODULE__{exec | min_priority: 0}
  end

  def set_strict(exec), do: exec

  @doc false
  @deprecated "Use `Execution.working_dir/1` instead"
  def get_path(exec) do
    exec.cli_options.path
  end

  @doc false
  def working_dir(exec) do
    Path.expand(exec.cli_options.path)
  end

  # Commands

  @doc """
  Returns the name of the command, which should be run by the given execution.

      Credo.Execution.get_command_name(exec)
      # => "suggest"
  """
  def get_command_name(exec) do
    exec.cli_options.command
  end

  @doc """
  Returns all valid command names.

      Credo.Execution.get_valid_command_names(exec)
      # => ["categories", "diff", "explain", "gen.check", "gen.config", "help", "info",
      #     "list", "suggest", "version"]
  """
  def get_valid_command_names(exec) do
    Map.keys(exec.commands)
  end

  @doc """
  Returns the `Credo.CLI.Command` module for the given `name`.

      Credo.Execution.get_command(exec, "explain")
      # => Credo.CLI.Command.Explain.ExplainCommand
  """
  def get_command(exec, name) do
    Map.get(exec.commands, name) ||
      raise ~s'Command not found: "#{inspect(name)}"\n\nRegistered commands: #{inspect(exec.commands, pretty: true)}'
  end

  @doc false
  def put_command(exec, _plugin_mod, name, command_mod) do
    commands = Map.put(exec.commands, name, command_mod)

    %__MODULE__{exec | commands: commands}
    |> command_mod.init()
  end

  @doc false
  def set_initializing_plugin(%__MODULE__{initializing_plugin: nil} = exec, plugin_mod) do
    %__MODULE__{exec | initializing_plugin: plugin_mod}
  end

  def set_initializing_plugin(exec, nil) do
    %__MODULE__{exec | initializing_plugin: nil}
  end

  def set_initializing_plugin(%__MODULE__{initializing_plugin: mod1}, mod2) do
    raise "Attempting to initialize plugin #{inspect(mod2)}, " <>
            "while already initializing plugin #{mod1}"
  end

  # Plugin params

  @doc """
  Returns the `Credo.Plugin` module's param value.

      Credo.Execution.get_command(exec, CredoDemoPlugin, "foo")
      # => nil

      Credo.Execution.get_command(exec, CredoDemoPlugin, "foo", 42)
      # => 42
  """
  def get_plugin_param(exec, plugin_mod, param_name) do
    exec.plugins[plugin_mod][param_name]
  end

  @doc false
  def put_plugin_param(exec, plugin_mod, param_name, param_value) do
    plugins =
      Keyword.update(exec.plugins, plugin_mod, [], fn list ->
        Keyword.update(list, param_name, param_value, fn _ -> param_value end)
      end)

    %__MODULE__{exec | plugins: plugins}
  end

  # CLI switches

  @doc """
  Returns the value for the given `switch_name`.

      Credo.Execution.get_given_cli_switch(exec, "foo")
      # => "bar"
  """
  def get_given_cli_switch(exec, switch_name) do
    if Map.has_key?(exec.cli_options.switches, switch_name) do
      {:ok, exec.cli_options.switches[switch_name]}
    else
      :error
    end
  end

  @doc false
  def put_cli_switch(exec, _plugin_mod, name, type) do
    %__MODULE__{exec | cli_switches: exec.cli_switches ++ [{name, type}]}
  end

  @doc false
  def put_cli_switch_alias(exec, _plugin_mod, _name, nil), do: exec

  def put_cli_switch_alias(exec, _plugin_mod, name, alias_name) do
    %__MODULE__{exec | cli_aliases: exec.cli_aliases ++ [{alias_name, name}]}
  end

  @doc false
  def put_cli_switch_plugin_param_converter(exec, plugin_mod, cli_switch_name, plugin_param_name) do
    converter_tuple = {cli_switch_name, plugin_mod, plugin_param_name}

    %__MODULE__{
      exec
      | cli_switch_plugin_param_converters:
          exec.cli_switch_plugin_param_converters ++ [converter_tuple]
    }
  end

  # Assigns

  @doc """
  Returns the assign with the given `name` for the given `exec` struct (or return the given `default` value).

      Credo.Execution.get_assign(exec, "foo")
      # => nil

      Credo.Execution.get_assign(exec, "foo", 42)
      # => 42
  """
  def get_assign(exec, name_or_list, default \\ nil)

  def get_assign(exec, path, default) when is_list(path) do
    case get_in(exec.assigns, path) do
      nil -> default
      value -> value
    end
  end

  def get_assign(exec, name, default) do
    Map.get(exec.assigns, name, default)
  end

  @doc """
  Puts the given `value` with the given `name` as assign into the given `exec` struct and returns the struct.

      Credo.Execution.put_assign(exec, "foo", 42)
      # => %Credo.Execution{...}
  """
  def put_assign(exec, name_or_list, value)

  def put_assign(exec, path, value) when is_list(path) do
    %__MODULE__{exec | assigns: do_put_nested_assign(exec.assigns, path, value)}
  end

  def put_assign(exec, name, value) do
    %__MODULE__{exec | assigns: Map.put(exec.assigns, name, value)}
  end

  defp do_put_nested_assign(map, [last_key], value) do
    Map.put(map, last_key, value)
  end

  defp do_put_nested_assign(map, [next_key | rest], value) do
    new_map =
      map
      |> Map.get(next_key, %{})
      |> do_put_nested_assign(rest, value)

    Map.put(map, next_key, new_map)
  end

  # Config Files

  @doc false
  def get_config_files(exec) do
    Credo.Execution.ExecutionConfigFiles.get(exec)
  end

  @doc false
  def append_config_file(exec, {_, _, _} = config_file) do
    config_files = get_config_files(exec) ++ [config_file]

    ExecutionConfigFiles.put(exec, config_files)

    exec
  end

  # Source Files

  @doc """
  Returns all source files for the given `exec` struct.

      Credo.Execution.get_source_files(exec)
      # => [%SourceFile<lib/my_project.ex>,
      #     %SourceFile<lib/credo/my_project/foo.ex>]
  """
  def get_source_files(exec) do
    Credo.Execution.ExecutionSourceFiles.get(exec)
  end

  @doc false
  def put_source_files(exec, source_files) do
    ExecutionSourceFiles.put(exec, source_files)

    exec
  end

  # Issues

  @doc """
  Returns all issues for the given `exec` struct.
  """
  def get_issues(exec) do
    exec
    |> ExecutionIssues.to_map()
    |> Map.values()
    |> List.flatten()
  end

  @doc """
  Returns all issues grouped by filename for the given `exec` struct.
  """
  def get_issues_grouped_by_filename(exec) do
    ExecutionIssues.to_map(exec)
  end

  @doc """
  Returns all issues for the given `exec` struct that relate to the given `filename`.
  """
  def get_issues(exec, filename) do
    exec
    |> ExecutionIssues.to_map()
    |> Map.get(filename, [])
  end

  @doc """
  Sets the issues for the given `exec` struct, overwriting any existing issues.
  """
  def put_issues(exec, issues) do
    ExecutionIssues.set(exec, issues)

    exec
  end

  @doc false
  @deprecated "Use put_issues/2 instead"
  def set_issues(exec, issues) do
    put_issues(exec, issues)
  end

  # Results

  @doc """
  Returns the result with the given `name` for the given `exec` struct (or return the given `default` value).

      Credo.Execution.get_result(exec, "foo")
      # => nil

      Credo.Execution.get_result(exec, "foo", 42)
      # => 42
  """
  def get_result(exec, name, default \\ nil) do
    Map.get(exec.results, name, default)
  end

  @doc """
  Puts the given `value` with the given `name` as result into the given `exec` struct.

      Credo.Execution.put_result(exec, "foo", 42)
      # => %Credo.Execution{...}
  """
  def put_result(exec, name, value) do
    %__MODULE__{exec | results: Map.put(exec.results, name, value)}
  end

  @doc false
  def put_exit_status(exec, exit_status) do
    put_assign(exec, "credo.exit_status", exit_status)
  end

  @doc false
  def get_exit_status(exec) do
    get_assign(exec, "credo.exit_status", 0)
  end

  # Halt

  @doc """
  Halts further execution of the pipeline meaning all subsequent steps are skipped.

  The `error` callback is called for the current Task.

      defmodule FooTask do
        use Credo.Execution.Task

        def call(exec) do
          Execution.halt(exec)
        end

        def error(exec) do
          IO.puts("Execution has been halted!")

          exec
        end
      end
  """
  def halt(exec) do
    %__MODULE__{exec | halted: true}
  end

  @doc """
  Halts further execution of the pipeline using the given `halt_message`.

  The `error` callback is called for the current Task.
  If the callback is not implemented, Credo outputs the given `halt_message`.

      defmodule FooTask do
        use Credo.Execution.Task

        def call(exec) do
          Execution.halt(exec, "Execution has been halted!")
        end
      end
  """
  def halt(exec, halt_message) do
    %__MODULE__{exec | halted: true}
    |> put_halt_message(halt_message)
  end

  @doc false
  def get_halt_message(exec) do
    get_assign(exec, "credo.halt_message")
  end

  @doc false
  def put_halt_message(exec, halt_message) do
    put_assign(exec, "credo.halt_message", halt_message)
  end

  # Task tracking

  @doc false
  def set_parent_and_current_task(exec, parent_task, current_task) do
    %__MODULE__{exec | parent_task: parent_task, current_task: current_task}
  end

  # Running tasks

  @doc false
  def run(exec) do
    run_pipeline(exec, __MODULE__)
  end

  # Pipelines

  @doc false
  defp get_pipeline(exec, pipeline_key) do
    case exec.pipeline_map[get_pipeline_key(exec, pipeline_key)] do
      nil -> raise "Could not find execution pipeline for '#{pipeline_key}'"
      pipeline -> pipeline
    end
  end

  @doc false
  defp get_pipeline_key(exec, pipeline_key) do
    case exec.pipeline_map[pipeline_key] do
      nil -> @execution_pipeline_key_backwards_compatibility_map[pipeline_key]
      _ -> pipeline_key
    end
  end

  @doc """
  Puts a given `pipeline` in `exec` under `pipeline_key`.

  A pipeline is a keyword list of named groups. Each named group is a list of `Credo.Execution.Task` modules:

      Execution.put_pipeline(exec, :my_pipeline_key,
        load_things: [ MyProject.LoadThings ],
        run_analysis: [ MyProject.Run ],
        print_results: [ MyProject.PrintResults ]
      )

  A named group can also be a list of two-element tuples, consisting of a `Credo.Execution.Task` module and a
  keyword list of options, which are passed to the Task module's `call/2` function:

      Execution.put_pipeline(exec, :my_pipeline_key,
        load_things: [ {MyProject.LoadThings, []} ],
        run_analysis: [ {MyProject.Run, [foo: "bar"]} ],
        print_results: [ {MyProject.PrintResults, []} ]
      )
  """
  def put_pipeline(exec, pipeline_key, pipeline) do
    new_pipelines = Map.put(exec.pipeline_map, pipeline_key, pipeline)

    %__MODULE__{exec | pipeline_map: new_pipelines}
  end

  @doc """
  Runs the pipeline with the given `pipeline_key` and returns the result `Credo.Execution` struct.

      Execution.run_pipeline(exec, :my_pipeline_key)
      # => %Credo.Execution{...}
  """
  def run_pipeline(%__MODULE__{} = initial_exec, pipeline_key)
      when is_atom(pipeline_key) and not is_nil(pipeline_key) do
    initial_pipeline = get_pipeline(initial_exec, pipeline_key)

    Enum.reduce(initial_pipeline, initial_exec, fn {group_name, _list}, exec_inside_pipeline ->
      outer_pipeline = get_pipeline(exec_inside_pipeline, pipeline_key)

      task_group = outer_pipeline[group_name]

      Enum.reduce(task_group, exec_inside_pipeline, fn
        {task_mod, opts}, exec_inside_task_group ->
          Credo.Execution.Task.run(task_mod, exec_inside_task_group, opts)

        task_mod, exec_inside_task_group when is_atom(task_mod) ->
          Credo.Execution.Task.run(task_mod, exec_inside_task_group, [])
      end)
    end)
  end

  @doc false
  def prepend_task(exec, plugin_mod, pipeline_key, group_name, task_tuple)

  def prepend_task(exec, plugin_mod, nil, group_name, task_tuple) do
    prepend_task(exec, plugin_mod, @execution_pipeline_key, group_name, task_tuple)
  end

  def prepend_task(exec, plugin_mod, pipeline_key, group_name, task_mod) when is_atom(task_mod) do
    prepend_task(exec, plugin_mod, pipeline_key, group_name, {task_mod, []})
  end

  def prepend_task(exec, _plugin_mod, pipeline_key, group_name, task_tuple) do
    pipeline =
      exec
      |> get_pipeline(pipeline_key)
      |> Enum.map(fn
        {^group_name, list} -> {group_name, [task_tuple] ++ list}
        value -> value
      end)

    put_pipeline(exec, get_pipeline_key(exec, pipeline_key), pipeline)
  end

  @doc false
  def append_task(exec, plugin_mod, pipeline_key, group_name, task_tuple)

  def append_task(exec, plugin_mod, nil, group_name, task_tuple) do
    append_task(exec, plugin_mod, __MODULE__, group_name, task_tuple)
  end

  def append_task(exec, plugin_mod, pipeline_key, group_name, task_mod) when is_atom(task_mod) do
    append_task(exec, plugin_mod, pipeline_key, group_name, {task_mod, []})
  end

  def append_task(exec, _plugin_mod, pipeline_key, group_name, task_tuple) do
    pipeline =
      exec
      |> get_pipeline(pipeline_key)
      |> Enum.map(fn
        {^group_name, list} -> {group_name, list ++ [task_tuple]}
        value -> value
      end)

    put_pipeline(exec, get_pipeline_key(exec, pipeline_key), pipeline)
  end

  @doc false
  defp put_builtin_command(exec, name, command_mod) do
    exec
    |> command_mod.init()
    |> put_command(Credo, name, command_mod)
  end

  @doc ~S"""
  Ensures that the given `value` is a `%Credo.Execution{}` struct, raises an error otherwise.

  Example:

      exec
      |> mod.init()
      |> Credo.Execution.ensure_execution_struct("#{mod}.init/1")
  """
  def ensure_execution_struct(value, fun_name)

  def ensure_execution_struct(%__MODULE__{} = exec, _fun_name), do: exec

  def ensure_execution_struct(value, fun_name) do
    raise("Expected #{fun_name} to return %Credo.Execution{}, got: #{inspect(value)}")
  end

  @doc false
  def get_rerun(exec) do
    case get_assign(exec, "credo.rerun.previous_execution") do
      nil -> :notfound
      previous_exec -> {previous_exec, get_assign(exec, "credo.rerun.files_that_changed")}
    end
  end

  defp put_rerun(exec, previous_exec, files_that_changed) do
    exec
    |> put_assign("credo.rerun.previous_execution", previous_exec)
    |> put_assign(
      "credo.rerun.files_that_changed",
      Enum.map(files_that_changed, fn filename ->
        filename
        |> Path.expand()
        |> Path.relative_to_cwd()
      end)
    )
  end
end

defmodule Credo.Execution do
  @moduledoc """
  Every run of Credo is configured via an `Credo.Execution` struct, which is created and
  manipulated via the `Credo.Execution` module.
  """

  @doc """
  The `Credo.Execution` struct is created and manipulated via the `Credo.Execution` module.
  """
  defstruct argv: [],
            cli_options: nil,
            # TODO: these initial switches should also be %Credo.CLI.Switch{} struct
            cli_switches: [
              debug: :boolean,
              color: :boolean,
              config_name: :string,
              config_file: :string,
              working_dir: :string
            ],
            cli_aliases: [C: :config_name, D: :debug],
            cli_switch_plugin_param_converters: [],

            # config
            files: nil,
            color: true,
            debug: false,
            checks: nil,
            requires: [],
            plugins: [],
            parse_timeout: 5000,
            strict: false,

            # options, set by the command line
            format: nil,
            help: false,
            verbose: false,
            version: false,

            # options, that are kept here for legacy reasons
            all: false,
            crash_on_error: true,
            enable_disabled_checks: nil,
            ignore_checks_tags: [],
            ignore_checks: nil,
            max_concurrent_check_runs: nil,
            min_priority: 0,
            mute_exit_status: false,
            only_checks_tags: [],
            only_checks: nil,
            read_from_stdin: false,

            # state, which is accessed and changed over the course of Credo's execution
            pipeline_map: %{},
            commands: %{},
            config_files: [],
            current_task: nil,
            parent_task: nil,
            initializing_plugin: nil,
            halted: false,
            config_files_pid: nil,
            source_files_pid: nil,
            issues_pid: nil,
            timing_pid: nil,
            skipped_checks: nil,
            assigns: %{},
            results: %{},
            config_comment_map: %{}

  @typedoc false
  @type t :: %__MODULE__{}

  @execution_pipeline_key __MODULE__
  @execution_pipeline_key_backwards_compatibility_map %{
    Credo.CLI.Command.Diff.DiffCommand => "diff",
    Credo.CLI.Command.List.ListCommand => "list",
    Credo.CLI.Command.Suggest.SuggestCommand => "suggest",
    Credo.CLI.Command.Info.InfoCommand => "info"
  }
  @execution_pipeline [
    __pre__: [
      Credo.Execution.Task.AppendDefaultConfig,
      Credo.Execution.Task.AppendExtraConfig,
      {Credo.Execution.Task.ParseOptions, parser_mode: :preliminary},
      Credo.Execution.Task.ConvertCLIOptionsToConfig,
      Credo.Execution.Task.InitializePlugins
    ],
    parse_cli_options: [{Credo.Execution.Task.ParseOptions, parser_mode: :preliminary}],
    initialize_plugins: [
      # This is where plugins can "put" their hooks using `Credo.Plugin.append_task/3`
      # to initialize themselves based on the params given in the config as well as
      # in their own command line switches.
      #
      # Example:
      #
      #     defmodule CredoDemoPlugin do
      #       import Credo.Plugin
      #
      #       def init(exec) do
      #         append_task(exec, :initialize_plugins, CredoDemoPlugin.SetDiffAsDefaultCommand)
      #       end
      #     end
    ],
    determine_command: [Credo.Execution.Task.DetermineCommand],
    set_default_command: [Credo.Execution.Task.SetDefaultCommand],
    initialize_command: [Credo.Execution.Task.InitializeCommand],
    parse_cli_options_final: [{Credo.Execution.Task.ParseOptions, parser_mode: :strict}],
    validate_cli_options: [Credo.Execution.Task.ValidateOptions],
    convert_cli_options_to_config: [Credo.Execution.Task.ConvertCLIOptionsToConfig],
    resolve_config: [Credo.Execution.Task.UseColors, Credo.Execution.Task.RequireRequires],
    validate_config: [Credo.Execution.Task.ValidateConfig],
    run_command: [Credo.Execution.Task.RunCommand],
    halt_execution: [Credo.Execution.Task.AssignExitStatusForIssues]
  ]

  alias Credo.Execution.ExecutionConfigFiles
  alias Credo.Execution.ExecutionIssues
  alias Credo.Execution.ExecutionSourceFiles
  alias Credo.Execution.ExecutionTiming

  @doc "Builds an Execution struct for the the given `argv`."
  def build(argv \\ []) when is_list(argv) do
    max_concurrent_check_runs = System.schedulers_online()

    %__MODULE__{argv: argv, max_concurrent_check_runs: max_concurrent_check_runs}
    |> put_pipeline(@execution_pipeline_key, @execution_pipeline)
    |> put_builtin_command("categories", Credo.CLI.Command.Categories.CategoriesCommand)
    |> put_builtin_command("diff", Credo.CLI.Command.Diff.DiffCommand)
    |> put_builtin_command("explain", Credo.CLI.Command.Explain.ExplainCommand)
    |> put_builtin_command("gen.check", Credo.CLI.Command.GenCheck)
    |> put_builtin_command("gen.config", Credo.CLI.Command.GenConfig)
    |> put_builtin_command("help", Credo.CLI.Command.Help)
    |> put_builtin_command("info", Credo.CLI.Command.Info.InfoCommand)
    |> put_builtin_command("list", Credo.CLI.Command.List.ListCommand)
    |> put_builtin_command("suggest", Credo.CLI.Command.Suggest.SuggestCommand)
    |> put_builtin_command("version", Credo.CLI.Command.Version)
    |> start_servers()
  end

  @doc false
  def build(%__MODULE__{} = previous_exec, files_that_changed) when is_list(files_that_changed) do
    previous_exec.argv
    |> build()
    |> put_rerun(previous_exec, files_that_changed)
  end

  def build(argv, files_that_changed) when is_list(files_that_changed) do
    build(argv)
  end

  @doc false
  defp start_servers(%__MODULE__{} = exec) do
    exec
    |> ExecutionConfigFiles.start_server()
    |> ExecutionIssues.start_server()
    |> ExecutionSourceFiles.start_server()
    |> ExecutionTiming.start_server()
  end

  @doc """
  Returns the checks that should be run for a given `exec` struct.

  Takes all checks from the `checks:` field of the exec, matches those against
  any patterns to include or exclude certain checks given via the command line.
  """
  def checks(exec)

  def checks(%__MODULE__{checks: nil}) do
    {[], [], []}
  end

  def checks(%__MODULE__{
        checks: %{enabled: checks},
        only_checks: only_checks,
        only_checks_tags: only_checks_tags,
        ignore_checks: ignore_checks,
        ignore_checks_tags: ignore_checks_tags
      }) do
    only_matching =
      checks |> filter_only_checks_by_tags(only_checks_tags) |> filter_only_checks(only_checks)

    ignore_matching_by_name = filter_ignore_checks(checks, ignore_checks)
    ignore_matching_by_tags = filter_ignore_checks_by_tags(checks, ignore_checks_tags)
    ignore_matching = ignore_matching_by_name ++ ignore_matching_by_tags

    result = only_matching -- ignore_matching

    {result, only_matching, ignore_matching}
  end

  defp filter_only_checks(checks, nil), do: checks
  defp filter_only_checks(checks, []), do: checks
  defp filter_only_checks(checks, patterns), do: filter_checks(checks, patterns)

  defp filter_ignore_checks(_checks, nil), do: []
  defp filter_ignore_checks(_checks, []), do: []
  defp filter_ignore_checks(checks, patterns), do: filter_checks(checks, patterns)

  defp filter_checks(checks, patterns) do
    regexes =
      patterns
      |> List.wrap()
      |> to_match_regexes

    Enum.filter(checks, &match_regex(&1, regexes, true))
  end

  defp match_regex(_tuple, [], default_for_empty), do: default_for_empty

  defp match_regex(tuple, regexes, _default_for_empty) do
    check_name =
      tuple
      |> Tuple.to_list()
      |> List.first()
      |> to_string

    Enum.any?(regexes, &Regex.run(&1, check_name))
  end

  defp to_match_regexes(list) do
    Enum.map(list, fn match_check ->
      {:ok, match_pattern} = Regex.compile(match_check, "i")
      match_pattern
    end)
  end

  defp filter_only_checks_by_tags(checks, nil), do: checks
  defp filter_only_checks_by_tags(checks, []), do: checks
  defp filter_only_checks_by_tags(checks, tags), do: filter_checks_by_tags(checks, tags)

  defp filter_ignore_checks_by_tags(_checks, nil), do: []
  defp filter_ignore_checks_by_tags(_checks, []), do: []
  defp filter_ignore_checks_by_tags(checks, tags), do: filter_checks_by_tags(checks, tags)

  defp filter_checks_by_tags(_checks, nil), do: []
  defp filter_checks_by_tags(_checks, []), do: []

  defp filter_checks_by_tags(checks, tags) do
    tags = Enum.map(tags, &String.to_atom/1)

    Enum.filter(checks, &match_tags(&1, tags, true))
  end

  defp match_tags(_tuple, [], default_for_empty), do: default_for_empty

  defp match_tags({check, params}, tags, _default_for_empty) do
    tags_for_check = tags_for_check(check, params)

    Enum.any?(tags, &Enum.member?(tags_for_check, &1))
  end

  @doc """
  Returns the tags for a given `check` and its `params`.
  """
  def tags_for_check(check, params)

  def tags_for_check(check, nil), do: check.tags
  def tags_for_check(check, []), do: check.tags

  def tags_for_check(check, params) when is_list(params) do
    params
    |> Credo.Check.Params.tags(check)
    |> Enum.flat_map(fn
      :__initial__ -> check.tags
      tag -> [tag]
    end)
  end

  @doc """
  Sets the exec values which `strict` implies (if applicable).
  """
  def set_strict(exec)

  def set_strict(%__MODULE__{strict: true} = exec) do
    %__MODULE__{exec | all: true, min_priority: -99}
  end

  def set_strict(%__MODULE__{strict: false} = exec) do
    %__MODULE__{exec | min_priority: 0}
  end

  def set_strict(exec), do: exec

  @doc false
  @deprecated "Use `Execution.working_dir/1` instead"
  def get_path(exec) do
    exec.cli_options.path
  end

  @doc false
  def working_dir(exec) do
    Path.expand(exec.cli_options.path)
  end

  # Commands

  @doc """
  Returns the name of the command, which should be run by the given execution.

      Credo.Execution.get_command_name(exec)
      # => "suggest"
  """
  def get_command_name(exec) do
    exec.cli_options.command
  end

  @doc """
  Returns all valid command names.

      Credo.Execution.get_valid_command_names(exec)
      # => ["categories", "diff", "explain", "gen.check", "gen.config", "help", "info",
      #     "list", "suggest", "version"]
  """
  def get_valid_command_names(exec) do
    Map.keys(exec.commands)
  end

  @doc """
  Returns the `Credo.CLI.Command` module for the given `name`.

      Credo.Execution.get_command(exec, "explain")
      # => Credo.CLI.Command.Explain.ExplainCommand
  """
  def get_command(exec, name) do
    Map.get(exec.commands, name) ||
      raise ~s'Command not found: "#{inspect(name)}"\n\nRegistered commands: #{inspect(exec.commands, pretty: true)}'
  end

  @doc false
  def put_command(exec, _plugin_mod, name, command_mod) do
    commands = Map.put(exec.commands, name, command_mod)

    %__MODULE__{exec | commands: commands}
    |> command_mod.init()
  end

  @doc false
  def set_initializing_plugin(%__MODULE__{initializing_plugin: nil} = exec, plugin_mod) do
    %__MODULE__{exec | initializing_plugin: plugin_mod}
  end

  def set_initializing_plugin(exec, nil) do
    %__MODULE__{exec | initializing_plugin: nil}
  end

  def set_initializing_plugin(%__MODULE__{initializing_plugin: mod1}, mod2) do
    raise "Attempting to initialize plugin #{inspect(mod2)}, " <>
            "while already initializing plugin #{mod1}"
  end

  # Plugin params

  @doc """
  Returns the `Credo.Plugin` module's param value.

      Credo.Execution.get_command(exec, CredoDemoPlugin, "foo")
      # => nil

      Credo.Execution.get_command(exec, CredoDemoPlugin, "foo", 42)
      # => 42
  """
  def get_plugin_param(exec, plugin_mod, param_name) do
    exec.plugins[plugin_mod][param_name]
  end

  @doc false
  def put_plugin_param(exec, plugin_mod, param_name, param_value) do
    plugins =
      Keyword.update(exec.plugins, plugin_mod, [], fn list ->
        Keyword.update(list, param_name, param_value, fn _ -> param_value end)
      end)

    %__MODULE__{exec | plugins: plugins}
  end

  # CLI switches

  @doc """
  Returns the value for the given `switch_name`.

      Credo.Execution.get_given_cli_switch(exec, "foo")
      # => "bar"
  """
  def get_given_cli_switch(exec, switch_name) do
    if Map.has_key?(exec.cli_options.switches, switch_name) do
      {:ok, exec.cli_options.switches[switch_name]}
    else
      :error
    end
  end

  @doc false
  def put_cli_switch(exec, _plugin_mod, name, type) do
    %__MODULE__{exec | cli_switches: exec.cli_switches ++ [{name, type}]}
  end

  @doc false
  def put_cli_switch_alias(exec, _plugin_mod, _name, nil), do: exec

  def put_cli_switch_alias(exec, _plugin_mod, name, alias_name) do
    %__MODULE__{exec | cli_aliases: exec.cli_aliases ++ [{alias_name, name}]}
  end

  @doc false
  def put_cli_switch_plugin_param_converter(exec, plugin_mod, cli_switch_name, plugin_param_name) do
    converter_tuple = {cli_switch_name, plugin_mod, plugin_param_name}

    %__MODULE__{
      exec
      | cli_switch_plugin_param_converters:
          exec.cli_switch_plugin_param_converters ++ [converter_tuple]
    }
  end

  # Assigns

  @doc """
  Returns the assign with the given `name` for the given `exec` struct (or return the given `default` value).

      Credo.Execution.get_assign(exec, "foo")
      # => nil

      Credo.Execution.get_assign(exec, "foo", 42)
      # => 42
  """
  def get_assign(exec, name_or_list, default \\ nil)

  def get_assign(exec, path, default) when is_list(path) do
    case get_in(exec.assigns, path) do
      nil -> default
      value -> value
    end
  end

  def get_assign(exec, name, default) do
    Map.get(exec.assigns, name, default)
  end

  @doc """
  Puts the given `value` with the given `name` as assign into the given `exec` struct and returns the struct.

      Credo.Execution.put_assign(exec, "foo", 42)
      # => %Credo.Execution{...}
  """
  def put_assign(exec, name_or_list, value)

  def put_assign(exec, path, value) when is_list(path) do
    %__MODULE__{exec | assigns: do_put_nested_assign(exec.assigns, path, value)}
  end

  def put_assign(exec, name, value) do
    %__MODULE__{exec | assigns: Map.put(exec.assigns, name, value)}
  end

  defp do_put_nested_assign(map, [last_key], value) do
    Map.put(map, last_key, value)
  end

  defp do_put_nested_assign(map, [next_key | rest], value) do
    new_map =
      map
      |> Map.get(next_key, %{})
      |> do_put_nested_assign(rest, value)

    Map.put(map, next_key, new_map)
  end

  # Config Files

  @doc false
  def get_config_files(exec) do
    Credo.Execution.ExecutionConfigFiles.get(exec)
  end

  @doc false
  def append_config_file(exec, {_, _, _} = config_file) do
    config_files = get_config_files(exec) ++ [config_file]

    ExecutionConfigFiles.put(exec, config_files)

    exec
  end

  # Source Files

  @doc """
  Returns all source files for the given `exec` struct.

      Credo.Execution.get_source_files(exec)
      # => [%SourceFile<lib/my_project.ex>,
      #     %SourceFile<lib/credo/my_project/foo.ex>]
  """
  def get_source_files(exec) do
    Credo.Execution.ExecutionSourceFiles.get(exec)
  end

  @doc false
  def put_source_files(exec, source_files) do
    ExecutionSourceFiles.put(exec, source_files)

    exec
  end

  # Issues

  @doc """
  Returns all issues for the given `exec` struct.
  """
  def get_issues(exec) do
    exec
    |> ExecutionIssues.to_map()
    |> Map.values()
    |> List.flatten()
  end

  @doc """
  Returns all issues for the given `exec` struct that relate to the given `filename`.
  """
  def get_issues(exec, filename) do
    exec
    |> ExecutionIssues.to_map()
    |> Map.get(filename, [])
  end

  @doc """
  Sets the issues for the given `exec` struct, overwriting any existing issues.
  """
  def put_issues(exec, issues) do
    ExecutionIssues.set(exec, issues)

    exec
  end

  @doc false
  @deprecated "Use put_issues/2 instead"
  def set_issues(exec, issues) do
    put_issues(exec, issues)
  end

  # Results

  @doc """
  Returns the result with the given `name` for the given `exec` struct (or return the given `default` value).

      Credo.Execution.get_result(exec, "foo")
      # => nil

      Credo.Execution.get_result(exec, "foo", 42)
      # => 42
  """
  def get_result(exec, name, default \\ nil) do
    Map.get(exec.results, name, default)
  end

  @doc """
  Puts the given `value` with the given `name` as result into the given `exec` struct.

      Credo.Execution.put_result(exec, "foo", 42)
      # => %Credo.Execution{...}
  """
  def put_result(exec, name, value) do
    %__MODULE__{exec | results: Map.put(exec.results, name, value)}
  end

  @doc false
  def put_exit_status(exec, exit_status) do
    put_assign(exec, "credo.exit_status", exit_status)
  end

  @doc false
  def get_exit_status(exec) do
    get_assign(exec, "credo.exit_status", 0)
  end

  # Halt

  @doc """
  Halts further execution of the pipeline meaning all subsequent steps are skipped.

  The `error` callback is called for the current Task.

      defmodule FooTask do
        use Credo.Execution.Task

        def call(exec) do
          Execution.halt(exec)
        end

        def error(exec) do
          IO.puts("Execution has been halted!")

          exec
        end
      end
  """
  def halt(exec) do
    %__MODULE__{exec | halted: true}
  end

  @doc """
  Halts further execution of the pipeline using the given `halt_message`.

  The `error` callback is called for the current Task.
  If the callback is not implemented, Credo outputs the given `halt_message`.

      defmodule FooTask do
        use Credo.Execution.Task

        def call(exec) do
          Execution.halt(exec, "Execution has been halted!")
        end
      end
  """
  def halt(exec, halt_message) do
    %__MODULE__{exec | halted: true}
    |> put_halt_message(halt_message)
  end

  @doc false
  def get_halt_message(exec) do
    get_assign(exec, "credo.halt_message")
  end

  @doc false
  def put_halt_message(exec, halt_message) do
    put_assign(exec, "credo.halt_message", halt_message)
  end

  # Task tracking

  @doc false
  def set_parent_and_current_task(exec, parent_task, current_task) do
    %__MODULE__{exec | parent_task: parent_task, current_task: current_task}
  end

  # Running tasks

  @doc false
  def run(exec) do
    run_pipeline(exec, __MODULE__)
  end

  # Pipelines

  @doc false
  defp get_pipeline(exec, pipeline_key) do
    case exec.pipeline_map[get_pipeline_key(exec, pipeline_key)] do
      nil -> raise "Could not find execution pipeline for '#{pipeline_key}'"
      pipeline -> pipeline
    end
  end

  @doc false
  defp get_pipeline_key(exec, pipeline_key) do
    case exec.pipeline_map[pipeline_key] do
      nil -> @execution_pipeline_key_backwards_compatibility_map[pipeline_key]
      _ -> pipeline_key
    end
  end

  @doc """
  Puts a given `pipeline` in `exec` under `pipeline_key`.

  A pipeline is a keyword list of named groups. Each named group is a list of `Credo.Execution.Task` modules:

      Execution.put_pipeline(exec, :my_pipeline_key,
        load_things: [ MyProject.LoadThings ],
        run_analysis: [ MyProject.Run ],
        print_results: [ MyProject.PrintResults ]
      )

  A named group can also be a list of two-element tuples, consisting of a `Credo.Execution.Task` module and a
  keyword list of options, which are passed to the Task module's `call/2` function:

      Execution.put_pipeline(exec, :my_pipeline_key,
        load_things: [ {MyProject.LoadThings, []} ],
        run_analysis: [ {MyProject.Run, [foo: "bar"]} ],
        print_results: [ {MyProject.PrintResults, []} ]
      )
  """
  def put_pipeline(exec, pipeline_key, pipeline) do
    new_pipelines = Map.put(exec.pipeline_map, pipeline_key, pipeline)

    %__MODULE__{exec | pipeline_map: new_pipelines}
  end

  @doc """
  Runs the pipeline with the given `pipeline_key` and returns the result `Credo.Execution` struct.

      Execution.run_pipeline(exec, :my_pipeline_key)
      # => %Credo.Execution{...}
  """
  def run_pipeline(%__MODULE__{} = initial_exec, pipeline_key)
      when is_atom(pipeline_key) and not is_nil(pipeline_key) do
    initial_pipeline = get_pipeline(initial_exec, pipeline_key)

    Enum.reduce(initial_pipeline, initial_exec, fn {group_name, _list}, exec_inside_pipeline ->
      outer_pipeline = get_pipeline(exec_inside_pipeline, pipeline_key)

      task_group = outer_pipeline[group_name]

      Enum.reduce(task_group, exec_inside_pipeline, fn
        {task_mod, opts}, exec_inside_task_group ->
          Credo.Execution.Task.run(task_mod, exec_inside_task_group, opts)

        task_mod, exec_inside_task_group when is_atom(task_mod) ->
          Credo.Execution.Task.run(task_mod, exec_inside_task_group, [])
      end)
    end)
  end

  @doc false
  def prepend_task(exec, plugin_mod, pipeline_key, group_name, task_tuple)

  def prepend_task(exec, plugin_mod, nil, group_name, task_tuple) do
    prepend_task(exec, plugin_mod, @execution_pipeline_key, group_name, task_tuple)
  end

  def prepend_task(exec, plugin_mod, pipeline_key, group_name, task_mod) when is_atom(task_mod) do
    prepend_task(exec, plugin_mod, pipeline_key, group_name, {task_mod, []})
  end

  def prepend_task(exec, _plugin_mod, pipeline_key, group_name, task_tuple) do
    pipeline =
      exec
      |> get_pipeline(pipeline_key)
      |> Enum.map(fn
        {^group_name, list} -> {group_name, [task_tuple] ++ list}
        value -> value
      end)

    put_pipeline(exec, get_pipeline_key(exec, pipeline_key), pipeline)
  end

  @doc false
  def append_task(exec, plugin_mod, pipeline_key, group_name, task_tuple)

  def append_task(exec, plugin_mod, nil, group_name, task_tuple) do
    append_task(exec, plugin_mod, __MODULE__, group_name, task_tuple)
  end

  def append_task(exec, plugin_mod, pipeline_key, group_name, task_mod) when is_atom(task_mod) do
    append_task(exec, plugin_mod, pipeline_key, group_name, {task_mod, []})
  end

  def append_task(exec, _plugin_mod, pipeline_key, group_name, task_tuple) do
    pipeline =
      exec
      |> get_pipeline(pipeline_key)
      |> Enum.map(fn
        {^group_name, list} -> {group_name, list ++ [task_tuple]}
        value -> value
      end)

    put_pipeline(exec, get_pipeline_key(exec, pipeline_key), pipeline)
  end

  @doc false
  defp put_builtin_command(exec, name, command_mod) do
    exec
    |> command_mod.init()
    |> put_command(Credo, name, command_mod)
  end

  @doc ~S"""
  Ensures that the given `value` is a `%Credo.Execution{}` struct, raises an error otherwise.

  Example:

      exec
      |> mod.init()
      |> Credo.Execution.ensure_execution_struct("#{mod}.init/1")
  """
  def ensure_execution_struct(value, fun_name)

  def ensure_execution_struct(%__MODULE__{} = exec, _fun_name), do: exec

  def ensure_execution_struct(value, fun_name) do
    raise("Expected #{fun_name} to return %Credo.Execution{}, got: #{inspect(value)}")
  end

  @doc false
  def get_rerun(exec) do
    case get_assign(exec, "credo.rerun.previous_execution") do
      nil -> :notfound
      previous_exec -> {previous_exec, get_assign(exec, "credo.rerun.files_that_changed")}
    end
  end

  defp put_rerun(exec, previous_exec, files_that_changed) do
    exec
    |> put_assign("credo.rerun.previous_execution", previous_exec)
    |> put_assign(
      "credo.rerun.files_that_changed",
      Enum.map(files_that_changed, fn filename ->
        filename
        |> Path.expand()
        |> Path.relative_to_cwd()
      end)
    )
  end
end


Run the code formatter on Dict (#6691)

Run the code formatter on Dict (#6691)

defmodule Dict do
  @moduledoc ~S"""
  WARNING: this module is deprecated.

  If you need a general dictionary, use the `Map` module.
  If you need to manipulate keyword lists, use `Keyword`.

  To convert maps into keywords and vice-versa, use the
  `new` function in the respective modules.
  """

  @type key :: any
  @type value :: any
  @type t :: list | map

  # TODO: Remove by 2.0
  # (hard-deprecated in elixir_dispatch)

  defmacro __using__(_) do
    # Use this import to guarantee proper code expansion
    import Kernel, except: [size: 1]

    quote do
      def get(dict, key, default \\ nil) do
        case fetch(dict, key) do
          {:ok, value} -> value
          :error -> default
        end
      end

      def get_lazy(dict, key, fun) when is_function(fun, 0) do
        case fetch(dict, key) do
          {:ok, value} -> value
          :error -> fun.()
        end
      end

      def get_and_update(dict, key, fun) do
        current_value = get(dict, key)
        {get, new_value} = fun.(current_value)
        {get, put(dict, key, new_value)}
      end

      def fetch!(dict, key) do
        case fetch(dict, key) do
          {:ok, value} -> value
          :error -> raise KeyError, key: key, term: dict
        end
      end

      def has_key?(dict, key) do
        match?({:ok, _}, fetch(dict, key))
      end

      def put_new(dict, key, value) do
        case has_key?(dict, key) do
          true -> dict
          false -> put(dict, key, value)
        end
      end

      def put_new_lazy(dict, key, fun) when is_function(fun, 0) do
        case has_key?(dict, key) do
          true -> dict
          false -> put(dict, key, fun.())
        end
      end

      def drop(dict, keys) do
        Enum.reduce(keys, dict, &delete(&2, &1))
      end

      def take(dict, keys) do
        Enum.reduce(keys, new(), fn key, acc ->
          case fetch(dict, key) do
            {:ok, value} -> put(acc, key, value)
            :error -> acc
          end
        end)
      end

      def to_list(dict) do
        reduce(dict, {:cont, []}, fn kv, acc -> {:cont, [kv | acc]} end)
        |> elem(1)
        |> :lists.reverse()
      end

      def keys(dict) do
        reduce(dict, {:cont, []}, fn {k, _}, acc -> {:cont, [k | acc]} end)
        |> elem(1)
        |> :lists.reverse()
      end

      def values(dict) do
        reduce(dict, {:cont, []}, fn {_, v}, acc -> {:cont, [v | acc]} end)
        |> elem(1)
        |> :lists.reverse()
      end

      def equal?(dict1, dict2) do
        # Use this import to avoid conflicts in the user code
        import Kernel, except: [size: 1]

        case size(dict1) == size(dict2) do
          false ->
            false

          true ->
            reduce(dict1, {:cont, true}, fn {k, v}, _acc ->
              case fetch(dict2, k) do
                {:ok, ^v} -> {:cont, true}
                _ -> {:halt, false}
              end
            end)
            |> elem(1)
        end
      end

      def merge(dict1, dict2, fun \\ fn _k, _v1, v2 -> v2 end) do
        # Use this import to avoid conflicts in the user code
        import Kernel, except: [size: 1]

        if size(dict1) < size(dict2) do
          reduce(dict1, {:cont, dict2}, fn {k, v1}, acc ->
            {:cont, update(acc, k, v1, &fun.(k, v1, &1))}
          end)
        else
          reduce(dict2, {:cont, dict1}, fn {k, v2}, acc ->
            {:cont, update(acc, k, v2, &fun.(k, &1, v2))}
          end)
        end
        |> elem(1)
      end

      def update(dict, key, initial, fun) do
        case fetch(dict, key) do
          {:ok, value} ->
            put(dict, key, fun.(value))

          :error ->
            put(dict, key, initial)
        end
      end

      def update!(dict, key, fun) do
        case fetch(dict, key) do
          {:ok, value} ->
            put(dict, key, fun.(value))

          :error ->
            raise KeyError, key: key, term: dict
        end
      end

      def pop(dict, key, default \\ nil) do
        case fetch(dict, key) do
          {:ok, value} ->
            {value, delete(dict, key)}

          :error ->
            {default, dict}
        end
      end

      def pop_lazy(dict, key, fun) when is_function(fun, 0) do
        case fetch(dict, key) do
          {:ok, value} ->
            {value, delete(dict, key)}

          :error ->
            {fun.(), dict}
        end
      end

      def split(dict, keys) do
        Enum.reduce(keys, {new(), dict}, fn key, {inc, exc} = acc ->
          case fetch(exc, key) do
            {:ok, value} ->
              {put(inc, key, value), delete(exc, key)}

            :error ->
              acc
          end
        end)
      end

      defoverridable merge: 2,
                     merge: 3,
                     equal?: 2,
                     to_list: 1,
                     keys: 1,
                     values: 1,
                     take: 2,
                     drop: 2,
                     get: 2,
                     get: 3,
                     fetch!: 2,
                     has_key?: 2,
                     put_new: 3,
                     pop: 2,
                     pop: 3,
                     split: 2,
                     update: 4,
                     update!: 3,
                     get_and_update: 3,
                     get_lazy: 3,
                     pop_lazy: 3,
                     put_new_lazy: 3
    end
  end

  defmacrop target(dict) do
    quote do
      case unquote(dict) do
        %module{} -> module
        %{} -> Map
        dict when is_list(dict) -> Keyword
        dict -> unsupported_dict(dict)
      end
    end
  end

  @spec keys(t) :: [key]
  def keys(dict) do
    target(dict).keys(dict)
  end

  @spec values(t) :: [value]
  def values(dict) do
    target(dict).values(dict)
  end

  @spec size(t) :: non_neg_integer
  def size(dict) do
    target(dict).size(dict)
  end

  @spec has_key?(t, key) :: boolean
  def has_key?(dict, key) do
    target(dict).has_key?(dict, key)
  end

  @spec get(t, key, value) :: value
  def get(dict, key, default \\ nil) do
    target(dict).get(dict, key, default)
  end

  @spec get_lazy(t, key, (-> value)) :: value
  def get_lazy(dict, key, fun) do
    target(dict).get_lazy(dict, key, fun)
  end

  @spec get_and_update(t, key, (value -> {value, value})) :: {value, t}
  def get_and_update(dict, key, fun) do
    target(dict).get_and_update(dict, key, fun)
  end

  @spec fetch(t, key) :: value
  def fetch(dict, key) do
    target(dict).fetch(dict, key)
  end

  @spec fetch!(t, key) :: value | no_return
  def fetch!(dict, key) do
    target(dict).fetch!(dict, key)
  end

  @spec put(t, key, value) :: t
  def put(dict, key, val) do
    target(dict).put(dict, key, val)
  end

  @spec put_new(t, key, value) :: t
  def put_new(dict, key, val) do
    target(dict).put_new(dict, key, val)
  end

  @spec put_new_lazy(t, key, (-> value)) :: t
  def put_new_lazy(dict, key, fun) do
    target(dict).put_new_lazy(dict, key, fun)
  end

  @spec delete(t, key) :: t
  def delete(dict, key) do
    target(dict).delete(dict, key)
  end

  @spec merge(t, t) :: t
  def merge(dict1, dict2) do
    target1 = target(dict1)
    target2 = target(dict2)

    if target1 == target2 do
      target1.merge(dict1, dict2)
    else
      do_merge(target1, dict1, dict2, fn _k, _v1, v2 -> v2 end)
    end
  end

  @spec merge(t, t, (key, value, value -> value)) :: t
  def merge(dict1, dict2, fun) do
    target1 = target(dict1)
    target2 = target(dict2)

    if target1 == target2 do
      target1.merge(dict1, dict2, fun)
    else
      do_merge(target1, dict1, dict2, fun)
    end
  end

  defp do_merge(target1, dict1, dict2, fun) do
    Enumerable.reduce(dict2, {:cont, dict1}, fn {k, v}, acc ->
      {:cont, target1.update(acc, k, v, fn other -> fun.(k, other, v) end)}
    end)
    |> elem(1)
  end

  @spec pop(t, key, value) :: {value, t}
  def pop(dict, key, default \\ nil) do
    target(dict).pop(dict, key, default)
  end

  @spec pop_lazy(t, key, (-> value)) :: {value, t}
  def pop_lazy(dict, key, fun) do
    target(dict).pop_lazy(dict, key, fun)
  end

  @spec update!(t, key, (value -> value)) :: t
  def update!(dict, key, fun) do
    target(dict).update!(dict, key, fun)
  end

  @spec update(t, key, value, (value -> value)) :: t
  def update(dict, key, initial, fun) do
    target(dict).update(dict, key, initial, fun)
  end

  @spec split(t, [key]) :: {t, t}
  def split(dict, keys) do
    target(dict).split(dict, keys)
  end

  @spec drop(t, [key]) :: t
  def drop(dict, keys) do
    target(dict).drop(dict, keys)
  end

  @spec take(t, [key]) :: t
  def take(dict, keys) do
    target(dict).take(dict, keys)
  end

  @spec empty(t) :: t
  def empty(dict) do
    target(dict).empty(dict)
  end

  @spec equal?(t, t) :: boolean
  def equal?(dict1, dict2) do
    target1 = target(dict1)
    target2 = target(dict2)

    cond do
      target1 == target2 ->
        target1.equal?(dict1, dict2)

      target1.size(dict1) == target2.size(dict2) ->
        Enumerable.reduce(dict2, {:cont, true}, fn {k, v}, _acc ->
          case target1.fetch(dict1, k) do
            {:ok, ^v} -> {:cont, true}
            _ -> {:halt, false}
          end
        end)
        |> elem(1)

      true ->
        false
    end
  end

  @spec to_list(t) :: list
  def to_list(dict) do
    target(dict).to_list(dict)
  end

  @spec unsupported_dict(t) :: no_return
  defp unsupported_dict(dict) do
    raise ArgumentError, "unsupported dict: #{inspect(dict)}"
  end
end

defmodule Dict do
  @moduledoc ~S"""
  WARNING: this module is deprecated.

  If you need a general dictionary, use the `Map` module.
  If you need to manipulate keyword lists, use `Keyword`.

  To convert maps into keywords and vice-versa, use the
  `new` function in the respective modules.
  """

  @type key :: any
  @type value :: any
  @type t :: list | map

  # TODO: Remove by 2.0
  # (hard-deprecated in elixir_dispatch)

  defmacro __using__(_) do
    # Use this import to guarantee proper code expansion
    import Kernel, except: [size: 1]

    quote do
      def get(dict, key, default \\ nil) do
        case fetch(dict, key) do
          {:ok, value} -> value
          :error -> default
        end
      end

      def get_lazy(dict, key, fun) when is_function(fun, 0) do
        case fetch(dict, key) do
          {:ok, value} -> value
          :error -> fun.()
        end
      end

      def get_and_update(dict, key, fun) do
        current_value = get(dict, key)
        {get, new_value} = fun.(current_value)
        {get, put(dict, key, new_value)}
      end

      def fetch!(dict, key) do
        case fetch(dict, key) do
          {:ok, value} -> value
          :error -> raise KeyError, key: key, term: dict
        end
      end

      def has_key?(dict, key) do
        match? {:ok, _}, fetch(dict, key)
      end

      def put_new(dict, key, value) do
        case has_key?(dict, key) do
          true  -> dict
          false -> put(dict, key, value)
        end
      end

      def put_new_lazy(dict, key, fun) when is_function(fun, 0) do
        case has_key?(dict, key) do
          true  -> dict
          false -> put(dict, key, fun.())
        end
      end

      def drop(dict, keys) do
        Enum.reduce(keys, dict, &delete(&2, &1))
      end

      def take(dict, keys) do
        Enum.reduce(keys, new(), fn key, acc ->
          case fetch(dict, key) do
            {:ok, value} -> put(acc, key, value)
            :error -> acc
          end
        end)
      end

      def to_list(dict) do
        reduce(dict, {:cont, []}, fn
          kv, acc -> {:cont, [kv | acc]}
        end) |> elem(1) |> :lists.reverse
      end

      def keys(dict) do
        reduce(dict, {:cont, []}, fn
          {k, _}, acc -> {:cont, [k | acc]}
        end) |> elem(1) |> :lists.reverse
      end

      def values(dict) do
        reduce(dict, {:cont, []}, fn
          {_, v}, acc -> {:cont, [v | acc]}
        end) |> elem(1) |> :lists.reverse
      end

      def equal?(dict1, dict2) do
        # Use this import to avoid conflicts in the user code
        import Kernel, except: [size: 1]

        case size(dict1) == size(dict2) do
          false -> false
          true  ->
            reduce(dict1, {:cont, true}, fn({k, v}, _acc) ->
              case fetch(dict2, k) do
                {:ok, ^v} -> {:cont, true}
                _ -> {:halt, false}
              end
            end) |> elem(1)
        end
      end

      def merge(dict1, dict2, fun \\ fn(_k, _v1, v2) -> v2 end) do
        # Use this import to avoid conflicts in the user code
        import Kernel, except: [size: 1]

        if size(dict1) < size(dict2) do
          reduce(dict1, {:cont, dict2}, fn {k, v1}, acc ->
            {:cont, update(acc, k, v1, &fun.(k, v1, &1))}
          end)
        else
          reduce(dict2, {:cont, dict1}, fn {k, v2}, acc ->
            {:cont, update(acc, k, v2, &fun.(k, &1, v2))}
          end)
        end |> elem(1)
      end

      def update(dict, key, initial, fun) do
        case fetch(dict, key) do
          {:ok, value} ->
            put(dict, key, fun.(value))
          :error ->
            put(dict, key, initial)
        end
      end

      def update!(dict, key, fun) do
        case fetch(dict, key) do
          {:ok, value} ->
            put(dict, key, fun.(value))
          :error ->
            raise KeyError, key: key, term: dict
        end
      end

      def pop(dict, key, default \\ nil) do
        case fetch(dict, key) do
          {:ok, value} ->
            {value, delete(dict, key)}
          :error ->
            {default, dict}
        end
      end

      def pop_lazy(dict, key, fun) when is_function(fun, 0) do
        case fetch(dict, key) do
          {:ok, value} ->
            {value, delete(dict, key)}
          :error ->
            {fun.(), dict}
        end
      end

      def split(dict, keys) do
        Enum.reduce(keys, {new(), dict}, fn key, {inc, exc} = acc ->
          case fetch(exc, key) do
            {:ok, value} ->
              {put(inc, key, value), delete(exc, key)}
            :error ->
              acc
          end
        end)
      end

      defoverridable merge: 2, merge: 3, equal?: 2, to_list: 1, keys: 1,
                     values: 1, take: 2, drop: 2, get: 2, get: 3, fetch!: 2,
                     has_key?: 2, put_new: 3, pop: 2, pop: 3, split: 2,
                     update: 4, update!: 3, get_and_update: 3, get_lazy: 3,
                     pop_lazy: 3, put_new_lazy: 3
    end
  end

  defmacrop target(dict) do
    quote do
      case unquote(dict) do
        %module{} ->
          module
        %{} ->
          Map
        dict when is_list(dict) ->
          Keyword
        dict ->
          unsupported_dict(dict)
      end
    end
  end

  @spec keys(t) :: [key]
  def keys(dict) do
    target(dict).keys(dict)
  end

  @spec values(t) :: [value]
  def values(dict) do
    target(dict).values(dict)
  end

  @spec size(t) :: non_neg_integer
  def size(dict) do
    target(dict).size(dict)
  end

  @spec has_key?(t, key) :: boolean
  def has_key?(dict, key) do
    target(dict).has_key?(dict, key)
  end

  @spec get(t, key, value) :: value
  def get(dict, key, default \\ nil) do
    target(dict).get(dict, key, default)
  end

  @spec get_lazy(t, key, (() -> value)) :: value
  def get_lazy(dict, key, fun) do
    target(dict).get_lazy(dict, key, fun)
  end

  @spec get_and_update(t, key, (value -> {value, value})) :: {value, t}
  def get_and_update(dict, key, fun) do
    target(dict).get_and_update(dict, key, fun)
  end

  @spec fetch(t, key) :: value
  def fetch(dict, key) do
    target(dict).fetch(dict, key)
  end

  @spec fetch!(t, key) :: value | no_return
  def fetch!(dict, key) do
    target(dict).fetch!(dict, key)
  end

  @spec put(t, key, value) :: t
  def put(dict, key, val) do
    target(dict).put(dict, key, val)
  end

  @spec put_new(t, key, value) :: t
  def put_new(dict, key, val) do
    target(dict).put_new(dict, key, val)
  end

  @spec put_new_lazy(t, key, (() -> value)) :: t
  def put_new_lazy(dict, key, fun) do
    target(dict).put_new_lazy(dict, key, fun)
  end

  @spec delete(t, key) :: t
  def delete(dict, key) do
    target(dict).delete(dict, key)
  end

  @spec merge(t, t) :: t
  def merge(dict1, dict2) do
    target1 = target(dict1)
    target2 = target(dict2)

    if target1 == target2 do
      target1.merge(dict1, dict2)
    else
      do_merge(target1, dict1, dict2, fn(_k, _v1, v2) -> v2 end)
    end
  end

  @spec merge(t, t, (key, value, value -> value)) :: t
  def merge(dict1, dict2, fun) do
    target1 = target(dict1)
    target2 = target(dict2)

    if target1 == target2 do
      target1.merge(dict1, dict2, fun)
    else
      do_merge(target1, dict1, dict2, fun)
    end
  end

  defp do_merge(target1, dict1, dict2, fun) do
    Enumerable.reduce(dict2, {:cont, dict1}, fn({k, v}, acc) ->
      {:cont, target1.update(acc, k, v, fn(other) -> fun.(k, other, v) end)}
    end) |> elem(1)
  end

  @spec pop(t, key, value) :: {value, t}
  def pop(dict, key, default \\ nil) do
    target(dict).pop(dict, key, default)
  end

  @spec pop_lazy(t, key, (() -> value)) :: {value, t}
  def pop_lazy(dict, key, fun) do
    target(dict).pop_lazy(dict, key, fun)
  end

  @spec update!(t, key, (value -> value)) :: t
  def update!(dict, key, fun) do
    target(dict).update!(dict, key, fun)
  end

  @spec update(t, key, value, (value -> value)) :: t
  def update(dict, key, initial, fun) do
    target(dict).update(dict, key, initial, fun)
  end

  @spec split(t, [key]) :: {t, t}
  def split(dict, keys) do
    target(dict).split(dict, keys)
  end

  @spec drop(t, [key]) :: t
  def drop(dict, keys) do
    target(dict).drop(dict, keys)
  end

  @spec take(t, [key]) :: t
  def take(dict, keys) do
    target(dict).take(dict, keys)
  end

  @spec empty(t) :: t
  def empty(dict) do
    target(dict).empty(dict)
  end

  @spec equal?(t, t) :: boolean
  def equal?(dict1, dict2) do
    target1 = target(dict1)
    target2 = target(dict2)

    cond do
      target1 == target2 ->
        target1.equal?(dict1, dict2)

      target1.size(dict1) == target2.size(dict2) ->
        Enumerable.reduce(dict2, {:cont, true}, fn({k, v}, _acc) ->
          case target1.fetch(dict1, k) do
            {:ok, ^v} -> {:cont, true}
            _         -> {:halt, false}
          end
        end) |> elem(1)

      true ->
        false
    end
  end

  @spec to_list(t) :: list
  def to_list(dict) do
    target(dict).to_list(dict)
  end

  @spec unsupported_dict(t) :: no_return
  defp unsupported_dict(dict) do
    raise ArgumentError, "unsupported dict: #{inspect dict}"
  end
end