Make an accidentally-public function private

defmodule Xandra.Protocol do
  @moduledoc false

  use Bitwise

  alias Xandra.{Batch, Error, Frame, Prepared, Page, Simple, TypeParser}
  alias Xandra.Cluster.StatusChange

  # We need these two macros to make
  # a single match context possible.

  defmacrop decode_string({:<-, _, [value, buffer]}) do
    quote do
      <<size::16, unquote(value)::size(size)-bytes, unquote(buffer)::bits>> = unquote(buffer)
    end
  end

  defmacrop decode_value({:<-, _, [value, buffer]}, type, do: block) do
    quote do
      <<size::32-signed, unquote(buffer)::bits>> = unquote(buffer)
      if size < 0 do
        unquote(value) = nil
        unquote(block)
      else
        <<data::size(size)-bytes, unquote(buffer)::bits>> = unquote(buffer)
        unquote(value) = decode_value(data, unquote(type))
        unquote(block)
      end
    end
  end

  @spec encode_request(Frame.t(kind), term, Keyword.t) :: Frame.t(kind) when kind: var
  def encode_request(frame, params, options \\ [])

  def encode_request(%Frame{kind: :options} = frame, nil, _options) do
    %{frame | body: []}
  end

  def encode_request(%Frame{kind: :startup} = frame, requested_options, _options)
      when is_map(requested_options) do
    %{frame | body: encode_string_map(requested_options)}
  end

  def encode_request(%Frame{kind: :register} = frame, events, _options) when is_list(events) do
    %{frame | body: encode_string_list(events)}
  end

  def encode_request(%Frame{kind: :query} = frame, %Simple{} = query, options) do
    %{statement: statement, values: values} = query
    body = [
      <<byte_size(statement)::32>>,
      statement,
      encode_params([], values, options, _skip_metadata? = false),
    ]
    %{frame | body: body}
  end

  def encode_request(%Frame{kind: :prepare} = frame, %Prepared{} = prepared, _options) do
    %{statement: statement} = prepared
    %{frame | body: [<<byte_size(statement)::32>>, statement]}
  end

  def encode_request(%Frame{kind: :execute} = frame, %Prepared{} = prepared, options) do
    %{id: id, bound_columns: columns, values: values} = prepared
    skip_metadata? = prepared.result_columns != nil
    body = [
      <<byte_size(id)::16>>,
      id,
      encode_params(columns, values, options, skip_metadata?),
    ]
    %{frame | body: body}
  end

  def encode_request(%Frame{kind: :batch} = frame, %Batch{} = batch, options) do
    %{queries: queries, type: type} = batch

    consistency = Keyword.get(options, :consistency, :one)
    serial_consistency = Keyword.get(options, :serial_consistency)
    timestamp = Keyword.get(options, :timestamp)

    flags =
      0x00
      |> set_flag(_serial_consistency = 0x10, serial_consistency)
      |> set_flag(_default_timestamp = 0x20, timestamp)

    encoded_queries =
      for query <- queries, into: [<<length(queries)::16>>], do: encode_query_in_batch(query)

    body = [
      encode_batch_type(type),
      encoded_queries,
      encode_consistency_level(consistency),
      flags,
      encode_serial_consistency(serial_consistency),
      if(timestamp, do: <<timestamp::64>>, else: []),
    ]
    %{frame | body: body}
  end

  defp encode_batch_type(:logged), do: 0
  defp encode_batch_type(:unlogged), do: 1
  defp encode_batch_type(:counter), do: 2

  defp encode_string_list(list) do
    for string <- list, into: [<<length(list)::16>>] do
      [<<byte_size(string)::16>>, string]
    end
  end

  defp encode_string_map(map) do
    for {key, value} <- map, into: [<<map_size(map)::16>>] do
      [<<byte_size(key)::16>>, key, <<byte_size(value)::16>>, value]
    end
  end

  consistency_levels = %{
    0x0000 => :any,
    0x0001 => :one,
    0x0002 => :two,
    0x0003 => :three,
    0x0004 => :quorum,
    0x0005 => :all,
    0x0006 => :local_quorum,
    0x0007 => :each_quorum,
    0x0008 => :serial,
    0x0009 => :local_serial,
    0x000A => :local_one,
  }
  for {spec, level} <- consistency_levels do
    defp encode_consistency_level(unquote(level)) do
      <<unquote(spec)::16>>
    end
  end

  defp set_flag(mask, bit, value) do
    if value do
      mask ||| bit
    else
      mask
    end
  end

  defp set_query_values_flag(mask, values) do
    cond do
      values == [] or values == %{} ->
        mask
      is_list(values) ->
        mask ||| 0x01
      is_map(values) ->
        mask ||| 0x01 ||| 0x40
    end
  end

  defp encode_params(columns, values, options, skip_metadata?) do
    consistency = Keyword.get(options, :consistency, :one)
    page_size = Keyword.get(options, :page_size, 10_000)
    paging_state = Keyword.get(options, :paging_state)
    serial_consistency = Keyword.get(options, :serial_consistency)
    timestamp = Keyword.get(options, :timestamp)

    flags =
      0x00
      |> set_query_values_flag(values)
      |> set_flag(_page_size = 0x04, true)
      |> set_flag(_metadata_presence = 0x02, skip_metadata?)
      |> set_flag(_paging_state = 0x08, paging_state)
      |> set_flag(_serial_consistency = 0x10, serial_consistency)
      |> set_flag(_default_timestamp = 0x20, timestamp)

    encoded_values =
      if values == [] or values == %{} do
        []
      else
        encode_query_values(columns, values)
      end

    [
      encode_consistency_level(consistency),
      flags,
      encoded_values,
      <<page_size::32>>,
      encode_paging_state(paging_state),
      encode_serial_consistency(serial_consistency),
      if(timestamp, do: <<timestamp::64>>, else: []),
    ]
  end

  defp encode_paging_state(value) do
    if value do
      [<<byte_size(value)::32>>, value]
    else
      []
    end
  end

  defp encode_serial_consistency(nil) do
    []
  end

  defp encode_serial_consistency(consistency) when consistency in [:serial, :local_serial] do
    encode_consistency_level(consistency)
  end

  defp encode_serial_consistency(other) do
    raise ArgumentError,
      "the :serial_consistency option must be either :serial or :local_serial, " <>
      "got: #{inspect(other)}"
  end

  defp encode_query_in_batch(%Simple{statement: statement, values: values}) do
    [
      _kind = 0,
      <<byte_size(statement)::32>>,
      statement,
      encode_query_values([], values),
    ]
  end

  defp encode_query_in_batch(%Prepared{id: id, bound_columns: bound_columns, values: values}) do
    [
      _kind = 1,
      <<byte_size(id)::16>>,
      id,
      encode_query_values(bound_columns, values),
    ]
  end

  defp encode_query_values([], values) when is_list(values) do
    for value <- values, into: [<<length(values)::16>>] do
      encode_query_value(value)
    end
  end

  defp encode_query_values([], values) when is_map(values) do
    for {name, value} <- values, into: [<<map_size(values)::16>>] do
      name = to_string(name)
      [<<byte_size(name)::16>>, name, encode_query_value(value)]
    end
  end

  defp encode_query_values(columns, values) when is_list(values) do
    encode_bound_values(columns, values, [<<length(columns)::16>>])
  end

  defp encode_query_values(columns, values) when map_size(values) == length(columns) do
    for {_keyspace, _table, name, type} <- columns, into: [<<map_size(values)::16>>] do
      value = Map.fetch!(values, name)
      [<<byte_size(name)::16>>, name, encode_query_value(type, value)]
    end
  end

  defp encode_bound_values([], [], acc) do
    acc
  end

  defp encode_bound_values([column | columns], [value | values], acc) do
    {_keyspace, _table, _name, type} = column
    acc = [acc | encode_query_value(type, value)]
    encode_bound_values(columns, values, acc)
  end

  defp encode_query_value({type, value}) when is_binary(type) do
    encode_query_value(TypeParser.parse(type), value)
  end

  defp encode_query_value(_type, nil) do
    <<-1::32>>
  end

  defp encode_query_value(type, value) do
    acc = encode_value(type, value)
    [<<IO.iodata_length(acc)::32>>, acc]
  end

  defp encode_value(:ascii, ascii) when is_binary(ascii) do
    ascii
  end

  defp encode_value(:bigint, bigint) when is_integer(bigint) do
    <<bigint::64>>
  end

  defp encode_value(:blob, blob) when is_binary(blob) do
    blob
  end

  defp encode_value(:boolean, boolean) when is_boolean(boolean) do
    if boolean, do: [1], else: [0]
  end

  defp encode_value(:date, date) when date in 0..0xFFFFFFFF do
    <<date::32>>
  end

  defp encode_value(:decimal, {value, scale}) do
    [encode_value(:int, scale), encode_value(:varint, value)]
  end

  defp encode_value(:double, double) when is_float(double) do
    <<double::64-float>>
  end

  defp encode_value(:float, float) when is_float(float) do
    <<float::32-float>>
  end

  defp encode_value(:inet, {n1, n2, n3, n4} = _inet) do
    <<n1, n2, n3, n4>>
  end

  defp encode_value(:inet, {n1, n2, n3, n4, n5, n6, n7, n8} = _inet) do
    <<n1::4-bytes, n2::4-bytes, n3::4-bytes, n4::4-bytes,
      n5::4-bytes, n6::4-bytes, n7::4-bytes, n8::4-bytes>>
  end

  defp encode_value(:int, int) when is_integer(int) do
    <<int::32>>
  end

  defp encode_value({:list, [items_type]}, list) when is_list(list) do
    for item <- list,
        into: [<<length(list)::32>>],
        do: encode_query_value(items_type, item)
  end

  defp encode_value({:map, [key_type, value_type]}, map) when is_map(map) do
    for {key, value} <- map, into: [<<map_size(map)::32>>] do
      [
        encode_query_value(key_type, key),
        encode_query_value(value_type, value),
      ]
    end
  end

  defp encode_value({:set, inner_type}, %MapSet{} = set) do
    encode_value({:list, inner_type}, MapSet.to_list(set))
  end

  defp encode_value(:smallint, int) when is_integer(int) do
    <<int::16>>
  end

  defp encode_value(:text, text) when is_binary(text) do
    text
  end

  defp encode_value(:time, time) when time in 0..86399999999999 do
    <<time::64>>
  end

  defp encode_value(:timestamp, timestamp) do
    <<timestamp::64>>
  end

  defp encode_value(:tinyint, int) when is_integer(int) do
    <<int>>
  end

  defp encode_value({:udt, fields}, value) when is_map(value) do
    for {field_name, field_type} <- fields do
      encode_query_value(field_type, Map.get(value, field_name))
    end
  end

  defp encode_value(:uuid, uuid) when is_binary(uuid) do
    <<part1::8-bytes, ?-,
      part2::4-bytes, ?-,
      part3::4-bytes, ?-,
      part4::4-bytes, ?-,
      part5::12-bytes>> = uuid
    <<decode_base16(part1)::4-bytes,
      decode_base16(part2)::2-bytes,
      decode_base16(part3)::2-bytes,
      decode_base16(part4)::2-bytes,
      decode_base16(part5)::6-bytes>>
  end

  # Alias of :text
  defp encode_value(:varchar, varchar) do
    encode_value(:text, varchar)
  end

  defp encode_value(:varint, varint) when is_integer(varint) do
    size = varint_byte_size(varint)
    <<varint::size(size)-unit(8)>>
  end

  defp encode_value(:timeuuid, timeuuid) when is_binary(timeuuid) do
    encode_value(:uuid, timeuuid)
  end

  defp encode_value({:tuple, types}, tuple) when length(types) == tuple_size(tuple) do
    for {type, item} <- Enum.zip(types, Tuple.to_list(tuple)),
        do: encode_query_value(type, item)
  end

  defp varint_byte_size(value) when value in -128..127,
    do: 1
  defp varint_byte_size(value) when value > 127,
    do: 1 + varint_byte_size(value >>> 8)
  defp varint_byte_size(value) when value < -128,
    do: varint_byte_size(-value - 1)

  @compile {:inline, decode_base16: 1}
  defp decode_base16(value) do
    Base.decode16!(value, case: :mixed)
  end

  error_codes = %{
    0x0000 => :server_failure,
    0x000A => :protocol_violation,
    0x0100 => :invalid_credentials,
    0x1000 => :unavailable,
    0x1001 => :overloaded,
    0x1002 => :bootstrapping,
    0x1003 => :truncate_failure,
    0x1100 => :write_timeout,
    0x1200 => :read_timeout,
    0x2000 => :invalid_syntax,
    0x2100 => :unauthorized,
    0x2200 => :invalid,
    0x2300 => :invalid_config,
    0x2400 => :already_exists,
    0x2500 => :unprepared,
  }
  for {code, reason} <- error_codes do
    defp decode_error_reason(<<unquote(code)::32-signed, buffer::bytes>>) do
      {unquote(reason), buffer}
    end
  end

  defp decode_error_message(_reason, buffer) do
    decode_string(message <- buffer)
    _ = buffer
    message
  end

  @spec decode_response(Frame.t(:error), term) :: Error.t
  @spec decode_response(Frame.t(:ready), nil) :: :ok
  @spec decode_response(Frame.t(:supported), nil) :: %{optional(String.t) => [String.t]}
  @spec decode_response(Frame.t(:result), Simple.t | Prepared.t | Batch.t) :: Xandra.result | Prepared.t
  def decode_response(frame, query \\ nil)

  def decode_response(%Frame{kind: :error, body: body} , _query) do
    {reason, buffer} = decode_error_reason(body)
    Error.new(reason, decode_error_message(reason, buffer))
  end

  def decode_response(%Frame{kind: :ready, body: <<>>}, nil) do
    :ok
  end

  def decode_response(%Frame{kind: :supported, body: body}, nil) do
    {value, <<>>} = decode_string_multimap(body)
    value
  end

  def decode_response(%Frame{kind: :event, body: body}, nil) do
    decode_string(event <- body)
    "STATUS_CHANGE" = event
    decode_string(effect <- body)
    {address, port, <<>>} = decode_inet(body)
    %StatusChange{effect: effect, address: address, port: port}
  end

  def decode_response(%Frame{kind: :result, body: body}, %kind{} = query)
      when kind in [Simple, Prepared, Batch] do
    decode_result_response(body, query)
  end

  defp decode_inet(<<size, data::size(size), buffer::bits>>) do
    address = decode_value(data, :inet)
    <<port::32, buffer::bits>> = buffer
    {address, port, buffer}
  end

  # Void
  defp decode_result_response(<<0x0001::32-signed>>, _query) do
    %Xandra.Void{}
  end

  # Page
  defp decode_result_response(<<0x0002::32-signed, buffer::bits>>, query) do
    page = new_page(query)
    {page, buffer} = decode_metadata(buffer, page)
    content = decode_page_content(buffer, page.columns)
    %{page | content: content}
  end

  # SetKeyspace
  defp decode_result_response(<<0x0003::32-signed, buffer::bits>>, _query) do
    decode_string(keyspace <- buffer)
    <<>> = buffer
    %Xandra.SetKeyspace{keyspace: keyspace}
  end

  # Prepared
  defp decode_result_response(<<0x0004::32-signed, buffer::bits>>, %Prepared{} = prepared) do
    decode_string(id <- buffer)
    {%{columns: bound_columns}, buffer} = decode_metadata(buffer, %Page{})
    {%{columns: result_columns}, <<>>} = decode_metadata(buffer, %Page{})
    %{prepared | id: id, bound_columns: bound_columns, result_columns: result_columns}
  end

  # SchemaChange
  defp decode_result_response(<<0x0005::32-signed, buffer::bits>>, _query) do
    decode_string(effect <- buffer)
    decode_string(target <- buffer)
    options = decode_change_options(buffer, target)
    %Xandra.SchemaChange{effect: effect, target: target, options: options}
  end

  # Since SELECT statements are not allowed in BATCH queries, there's no need to
  # support %Batch{} in this function.
  defp new_page(%Simple{}),
    do: %Page{}
  defp new_page(%Prepared{result_columns: result_columns}),
    do: %Page{columns: result_columns}

  defp decode_change_options(<<buffer::bits>>, "KEYSPACE") do
    decode_string(keyspace <- buffer)
    <<>> = buffer
    %{keyspace: keyspace}
  end

  defp decode_change_options(<<buffer::bits>>, target) when target in ["TABLE", "TYPE"] do
    decode_string(keyspace <- buffer)
    decode_string(subject <- buffer)
    <<>> = buffer
    %{keyspace: keyspace, subject: subject}
  end

  defp decode_metadata(<<flags::4-bytes, column_count::32-signed, buffer::bits>>, page) do
    <<_::29, no_metadata::1, has_more_pages::1, global_table_spec::1>> = flags
    {page, buffer} = decode_paging_state(buffer, page, has_more_pages)

    cond do
      no_metadata == 1 ->
        {page, buffer}
      global_table_spec == 1 ->
        decode_string(keyspace <- buffer)
        decode_string(table <- buffer)
        {columns, buffer} = decode_columns(buffer, column_count, {keyspace, table}, [])
        {%{page | columns: columns}, buffer}
      true ->
        {columns, buffer} = decode_columns(buffer, column_count, nil, [])
        {%{page | columns: columns}, buffer}
    end
  end

  defp decode_paging_state(<<buffer::bits>>, page, 0) do
    {page, buffer}
  end

  defp decode_paging_state(<<buffer::bits>>, page, 1) do
    <<size::32, paging_state::size(size)-bytes, buffer::bits>> = buffer
    {%{page | paging_state: paging_state}, buffer}
  end

  defp decode_page_content(<<row_count::32-signed, buffer::bits>>, columns) do
    decode_page_content(buffer, row_count, columns, columns, [[]])
  end

  defp decode_page_content(<<>>, 0, columns, columns, [[] | acc]) do
    Enum.reverse(acc)
  end

  defp decode_page_content(<<buffer::bits>>, row_count, columns, [], [values | acc]) do
    decode_page_content(buffer, row_count - 1, columns, columns, [[], Enum.reverse(values) | acc])
  end

  defp decode_page_content(<<buffer::bits>>, row_count, columns, [{_, _, _, type} | rest], [values | acc]) do
    decode_value(value <- buffer, type) do
      values = [value | values]
      decode_page_content(buffer, row_count, columns, rest, [values | acc])
    end
  end

  defp decode_value(<<value::bits>>, :ascii), do: value

  defp decode_value(<<value::64-signed>>, :bigint), do: value

  defp decode_value(<<value::bits>>, :blob), do: value

  defp decode_value(<<value::8>>, :boolean), do: Kernel.!=(value, 0)

  defp decode_value(<<value::32>>, :date), do: value

  defp decode_value(<<scale::32-signed, rest::bits>>, :decimal) do
    {decode_value(rest, :varint), scale}
  end

  defp decode_value(<<value::64-float>>, :double), do: value

  defp decode_value(<<value::32-float>>, :float), do: value

  defp decode_value(<<data::4-bytes>>, :inet) do
    <<n1, n2, n3, n4>> = data
    {n1, n2, n3, n4}
  end

  defp decode_value(<<data::16-bytes>>, :inet) do
    <<n1, n2, n3, n4, n5, n6, n7, n8, n9, n10, n11, n12, n13, n14, n15, n16>> = data
    {n1, n2, n3, n4, n5, n6, n7, n8, n9, n10, n11, n12, n13, n14, n15, n16}
  end

  defp decode_value(<<value::32-signed>>, :int), do: value

  defp decode_value(<<count::32-signed, data::bits>>, {:list, [type]}) do
    decode_value_list(data, count, type, [])
  end

  defp decode_value(<<count::32-signed, data::bits>>, {:map, [key_type, value_type]}) do
    decode_value_map_key(data, count, key_type, value_type, [])
  end

  defp decode_value(<<count::32-signed, data::bits>>, {:set, [type]}) do
    data
    |> decode_value_list(count, type, [])
    |> MapSet.new()
  end

  defp decode_value(<<value::16-signed>>, :smallint), do: value

  defp decode_value(<<data::bits>>, {:tuple, types}) do
    decode_value_tuple(data, types, [])
  end

  defp decode_value(<<value::16-bytes>>, type) when type in [:timeuuid, :uuid], do: value

  defp decode_value(<<value::bits>>, :varchar), do: value

  defp decode_value(<<data::bits>>, :varint) do
    size = bit_size(data)
    <<value::size(size)-signed>> = data
    value
  end

  defp decode_value(<<value::64>>, :time), do: value

  defp decode_value(<<data::bits>>, {:udt, fields}) do
    decode_value_udt(data, fields, [])
  end

  defp decode_value(<<value::64-signed>>, :timestamp), do: value

  defp decode_value(<<value::signed>>, :tinyint), do: value

  defp decode_value_udt(<<buffer::bits>>, [{field_name, field_type} | rest], acc) do
    decode_value(value <- buffer, field_type) do
      decode_value_udt(buffer, rest, [{field_name, value} | acc])
    end
  end

  defp decode_value_udt(<<>>, [], acc) do
    Map.new(acc)
  end

  defp decode_value_list(<<>>, 0, _type, acc) do
    Enum.reverse(acc)
  end

  defp decode_value_list(<<buffer::bits>>, count, type, acc) do
    decode_value(item <- buffer, type) do
      decode_value_list(buffer, count - 1, type, [item | acc])
    end
  end

  defp decode_value_map_key(<<>>, 0, _key_type, _value_type, acc) do
    Map.new(acc)
  end

  defp decode_value_map_key(<<buffer::bits>>, count, key_type, value_type, acc) do
    decode_value(key <- buffer, key_type) do
      decode_value_map_value(buffer, count, key_type, value_type, [key | acc])
    end
  end

  defp decode_value_map_value(<<buffer::bits>>, count, key_type, value_type, [key | acc]) do
    decode_value(value <- buffer, value_type) do
      decode_value_map_key(buffer, count - 1, key_type, value_type, [{key, value} | acc])
    end
  end

  defp decode_value_tuple(<<buffer::bits>>, [type | types], acc) do
    decode_value(item <- buffer, type) do
      decode_value_tuple(buffer, types, [item | acc])
    end
  end

  defp decode_value_tuple(<<>>, [], acc) do
    acc |> Enum.reverse |> List.to_tuple
  end

  defp decode_columns(buffer, 0, _table_spec, acc) do
    {Enum.reverse(acc), buffer}
  end

  defp decode_columns(<<buffer::bits>>, column_count, nil, acc) do
    decode_string(keyspace <- buffer)
    decode_string(table <- buffer)
    decode_string(name <- buffer)
    {type, buffer} = decode_type(buffer)
    entry = {keyspace, table, name, type}
    decode_columns(buffer, column_count - 1, nil, [entry | acc])
  end

  defp decode_columns(<<buffer::bits>>, column_count, table_spec, acc) do
    {keyspace, table} = table_spec
    decode_string(name <- buffer)
    {type, buffer} = decode_type(buffer)
    entry = {keyspace, table, name, type}
    decode_columns(buffer, column_count - 1, table_spec, [entry | acc])
  end

  defp decode_type(<<0x0000::16, buffer::bits>>) do
    decode_string(class <- buffer)
    {custom_type_to_native(class), buffer}
  end

  defp decode_type(<<0x0001::16, buffer::bits>>) do
    {:ascii, buffer}
  end

  defp decode_type(<<0x0002::16, buffer::bits>>) do
    {:bigint, buffer}
  end

  defp decode_type(<<0x0003::16, buffer::bits>>) do
    {:blob, buffer}
  end

  defp decode_type(<<0x0004::16, buffer::bits>>) do
    {:boolean, buffer}
  end

  defp decode_type(<<0x0005::16, buffer::bits>>) do
    {:counter, buffer}
  end

  defp decode_type(<<0x0006::16, buffer::bits>>) do
    {:decimal, buffer}
  end

  defp decode_type(<<0x0007::16, buffer::bits>>) do
    {:double, buffer}
  end

  defp decode_type(<<0x0008::16, buffer::bits>>) do
    {:float, buffer}
  end

  defp decode_type(<<0x0009::16, buffer::bits>>) do
    {:int, buffer}
  end

  defp decode_type(<<0x000B::16, buffer::bits>>) do
    {:timestamp, buffer}
  end

  defp decode_type(<<0x000C::16, buffer::bits>>) do
    {:uuid, buffer}
  end

  defp decode_type(<<0x000D::16, buffer::bits>>) do
    {:varchar, buffer}
  end

  defp decode_type(<<0x000E::16, buffer::bits>>) do
    {:varint, buffer}
  end

  defp decode_type(<<0x000F::16, buffer::bits>>) do
    {:timeuuid, buffer}
  end

  defp decode_type(<<0x0010::16, buffer::bits>>) do
    {:inet, buffer}
  end

  defp decode_type(<<0x0020::16, buffer::bits>>) do
    {type, buffer} = decode_type(buffer)
    {{:list, [type]}, buffer}
  end

  defp decode_type(<<0x0021::16, buffer::bits>>) do
    {key_type, buffer} = decode_type(buffer)
    {value_type, buffer} = decode_type(buffer)
    {{:map, [key_type, value_type]}, buffer}
  end

  defp decode_type(<<0x0022::16, buffer::bits>>) do
    {type, buffer} = decode_type(buffer)
    {{:set, [type]}, buffer}
  end

  defp decode_type(<<0x0030::16, buffer::bits>>) do
    decode_string(_keyspace <- buffer)
    decode_string(_name <- buffer)
    <<count::16, buffer::bits>> = buffer
    decode_type_udt(buffer, count, [])
  end

  defp decode_type(<<0x0031::16, count::16, buffer::bits>>) do
    decode_type_tuple(buffer, count, [])
  end

  custom_types = %{
    "org.apache.cassandra.db.marshal.SimpleDateType" => :date,
    "org.apache.cassandra.db.marshal.ShortType" => :smallint,
    "org.apache.cassandra.db.marshal.ByteType" => :tinyint,
    "org.apache.cassandra.db.marshal.TimeType" => :time,
  }
  for {class, type} <- custom_types do
    defp custom_type_to_native(unquote(class)) do
      unquote(type)
    end
  end

  defp custom_type_to_native(class) do
    raise "cannot decode custom type #{inspect(class)}"
  end

  defp decode_type_udt(<<buffer::bits>>, 0, acc) do
    {{:udt, Enum.reverse(acc)}, buffer}
  end

  defp decode_type_udt(<<buffer::bits>>, count, acc) do
    decode_string(field_name <- buffer)
    {field_type, buffer} = decode_type(buffer)
    decode_type_udt(buffer, count - 1, [{field_name, field_type} | acc])
  end

  defp decode_type_tuple(<<buffer::bits>>, 0, acc) do
    {{:tuple, Enum.reverse(acc)}, buffer}
  end

  defp decode_type_tuple(<<buffer::bits>>, count, acc) do
    {type, buffer} = decode_type(buffer)
    decode_type_tuple(buffer, count - 1, [type | acc])
  end

  defp decode_string_multimap(<<count::16, buffer::bits>>) do
    decode_string_multimap(buffer, count, [])
  end

  defp decode_string_multimap(<<buffer::bits>>, 0, acc) do
    {Map.new(acc), buffer}
  end

  defp decode_string_multimap(<<buffer::bits>>, count, acc) do
    decode_string(key <- buffer)
    {value, buffer} = decode_string_list(buffer)
    decode_string_multimap(buffer, count - 1, [{key, value} | acc])
  end

  defp decode_string_list(<<count::16, buffer::bits>>) do
    decode_string_list(buffer, count, [])
  end

  defp decode_string_list(<<buffer::bits>>, 0, acc) do
    {Enum.reverse(acc), buffer}
  end

  defp decode_string_list(<<buffer::bits>>, count, acc) do
    decode_string(item <- buffer)
    decode_string_list(buffer, count - 1, [item | acc])
  end
end

defmodule Xandra.Protocol do
  @moduledoc false

  use Bitwise

  alias Xandra.{Batch, Error, Frame, Prepared, Page, Simple, TypeParser}
  alias Xandra.Cluster.StatusChange

  # We need these two macros to make
  # a single match context possible.

  defmacrop decode_string({:<-, _, [value, buffer]}) do
    quote do
      <<size::16, unquote(value)::size(size)-bytes, unquote(buffer)::bits>> = unquote(buffer)
    end
  end

  defmacrop decode_value({:<-, _, [value, buffer]}, type, do: block) do
    quote do
      <<size::32-signed, unquote(buffer)::bits>> = unquote(buffer)
      if size < 0 do
        unquote(value) = nil
        unquote(block)
      else
        <<data::size(size)-bytes, unquote(buffer)::bits>> = unquote(buffer)
        unquote(value) = decode_value(data, unquote(type))
        unquote(block)
      end
    end
  end

  @spec encode_request(Frame.t(kind), term, Keyword.t) :: Frame.t(kind) when kind: var
  def encode_request(frame, params, options \\ [])

  def encode_request(%Frame{kind: :options} = frame, nil, _options) do
    %{frame | body: []}
  end

  def encode_request(%Frame{kind: :startup} = frame, requested_options, _options)
      when is_map(requested_options) do
    %{frame | body: encode_string_map(requested_options)}
  end

  def encode_request(%Frame{kind: :register} = frame, events, _options) when is_list(events) do
    %{frame | body: encode_string_list(events)}
  end

  def encode_request(%Frame{kind: :query} = frame, %Simple{} = query, options) do
    %{statement: statement, values: values} = query
    body = [
      <<byte_size(statement)::32>>,
      statement,
      encode_params([], values, options, _skip_metadata? = false),
    ]
    %{frame | body: body}
  end

  def encode_request(%Frame{kind: :prepare} = frame, %Prepared{} = prepared, _options) do
    %{statement: statement} = prepared
    %{frame | body: [<<byte_size(statement)::32>>, statement]}
  end

  def encode_request(%Frame{kind: :execute} = frame, %Prepared{} = prepared, options) do
    %{id: id, bound_columns: columns, values: values} = prepared
    skip_metadata? = prepared.result_columns != nil
    body = [
      <<byte_size(id)::16>>,
      id,
      encode_params(columns, values, options, skip_metadata?),
    ]
    %{frame | body: body}
  end

  def encode_request(%Frame{kind: :batch} = frame, %Batch{} = batch, options) do
    %{queries: queries, type: type} = batch

    consistency = Keyword.get(options, :consistency, :one)
    serial_consistency = Keyword.get(options, :serial_consistency)
    timestamp = Keyword.get(options, :timestamp)

    flags =
      0x00
      |> set_flag(_serial_consistency = 0x10, serial_consistency)
      |> set_flag(_default_timestamp = 0x20, timestamp)

    encoded_queries =
      for query <- queries, into: [<<length(queries)::16>>], do: encode_query_in_batch(query)

    body = [
      encode_batch_type(type),
      encoded_queries,
      encode_consistency_level(consistency),
      flags,
      encode_serial_consistency(serial_consistency),
      if(timestamp, do: <<timestamp::64>>, else: []),
    ]
    %{frame | body: body}
  end

  defp encode_batch_type(:logged), do: 0
  defp encode_batch_type(:unlogged), do: 1
  defp encode_batch_type(:counter), do: 2

  defp encode_string_list(list) do
    for string <- list, into: [<<length(list)::16>>] do
      [<<byte_size(string)::16>>, string]
    end
  end

  defp encode_string_map(map) do
    for {key, value} <- map, into: [<<map_size(map)::16>>] do
      [<<byte_size(key)::16>>, key, <<byte_size(value)::16>>, value]
    end
  end

  consistency_levels = %{
    0x0000 => :any,
    0x0001 => :one,
    0x0002 => :two,
    0x0003 => :three,
    0x0004 => :quorum,
    0x0005 => :all,
    0x0006 => :local_quorum,
    0x0007 => :each_quorum,
    0x0008 => :serial,
    0x0009 => :local_serial,
    0x000A => :local_one,
  }
  for {spec, level} <- consistency_levels do
    defp encode_consistency_level(unquote(level)) do
      <<unquote(spec)::16>>
    end
  end

  defp set_flag(mask, bit, value) do
    if value do
      mask ||| bit
    else
      mask
    end
  end

  defp set_query_values_flag(mask, values) do
    cond do
      values == [] or values == %{} ->
        mask
      is_list(values) ->
        mask ||| 0x01
      is_map(values) ->
        mask ||| 0x01 ||| 0x40
    end
  end

  defp encode_params(columns, values, options, skip_metadata?) do
    consistency = Keyword.get(options, :consistency, :one)
    page_size = Keyword.get(options, :page_size, 10_000)
    paging_state = Keyword.get(options, :paging_state)
    serial_consistency = Keyword.get(options, :serial_consistency)
    timestamp = Keyword.get(options, :timestamp)

    flags =
      0x00
      |> set_query_values_flag(values)
      |> set_flag(_page_size = 0x04, true)
      |> set_flag(_metadata_presence = 0x02, skip_metadata?)
      |> set_flag(_paging_state = 0x08, paging_state)
      |> set_flag(_serial_consistency = 0x10, serial_consistency)
      |> set_flag(_default_timestamp = 0x20, timestamp)

    encoded_values =
      if values == [] or values == %{} do
        []
      else
        encode_query_values(columns, values)
      end

    [
      encode_consistency_level(consistency),
      flags,
      encoded_values,
      <<page_size::32>>,
      encode_paging_state(paging_state),
      encode_serial_consistency(serial_consistency),
      if(timestamp, do: <<timestamp::64>>, else: []),
    ]
  end

  defp encode_paging_state(value) do
    if value do
      [<<byte_size(value)::32>>, value]
    else
      []
    end
  end

  defp encode_serial_consistency(nil) do
    []
  end

  defp encode_serial_consistency(consistency) when consistency in [:serial, :local_serial] do
    encode_consistency_level(consistency)
  end

  defp encode_serial_consistency(other) do
    raise ArgumentError,
      "the :serial_consistency option must be either :serial or :local_serial, " <>
      "got: #{inspect(other)}"
  end

  defp encode_query_in_batch(%Simple{statement: statement, values: values}) do
    [
      _kind = 0,
      <<byte_size(statement)::32>>,
      statement,
      encode_query_values([], values),
    ]
  end

  defp encode_query_in_batch(%Prepared{id: id, bound_columns: bound_columns, values: values}) do
    [
      _kind = 1,
      <<byte_size(id)::16>>,
      id,
      encode_query_values(bound_columns, values),
    ]
  end

  defp encode_query_values([], values) when is_list(values) do
    for value <- values, into: [<<length(values)::16>>] do
      encode_query_value(value)
    end
  end

  defp encode_query_values([], values) when is_map(values) do
    for {name, value} <- values, into: [<<map_size(values)::16>>] do
      name = to_string(name)
      [<<byte_size(name)::16>>, name, encode_query_value(value)]
    end
  end

  defp encode_query_values(columns, values) when is_list(values) do
    encode_bound_values(columns, values, [<<length(columns)::16>>])
  end

  defp encode_query_values(columns, values) when map_size(values) == length(columns) do
    for {_keyspace, _table, name, type} <- columns, into: [<<map_size(values)::16>>] do
      value = Map.fetch!(values, name)
      [<<byte_size(name)::16>>, name, encode_query_value(type, value)]
    end
  end

  defp encode_bound_values([], [], acc) do
    acc
  end

  defp encode_bound_values([column | columns], [value | values], acc) do
    {_keyspace, _table, _name, type} = column
    acc = [acc | encode_query_value(type, value)]
    encode_bound_values(columns, values, acc)
  end

  defp encode_query_value({type, value}) when is_binary(type) do
    encode_query_value(TypeParser.parse(type), value)
  end

  defp encode_query_value(_type, nil) do
    <<-1::32>>
  end

  defp encode_query_value(type, value) do
    acc = encode_value(type, value)
    [<<IO.iodata_length(acc)::32>>, acc]
  end

  defp encode_value(:ascii, ascii) when is_binary(ascii) do
    ascii
  end

  defp encode_value(:bigint, bigint) when is_integer(bigint) do
    <<bigint::64>>
  end

  defp encode_value(:blob, blob) when is_binary(blob) do
    blob
  end

  defp encode_value(:boolean, boolean) when is_boolean(boolean) do
    if boolean, do: [1], else: [0]
  end

  defp encode_value(:date, date) when date in 0..0xFFFFFFFF do
    <<date::32>>
  end

  defp encode_value(:decimal, {value, scale}) do
    [encode_value(:int, scale), encode_value(:varint, value)]
  end

  defp encode_value(:double, double) when is_float(double) do
    <<double::64-float>>
  end

  defp encode_value(:float, float) when is_float(float) do
    <<float::32-float>>
  end

  defp encode_value(:inet, {n1, n2, n3, n4} = _inet) do
    <<n1, n2, n3, n4>>
  end

  defp encode_value(:inet, {n1, n2, n3, n4, n5, n6, n7, n8} = _inet) do
    <<n1::4-bytes, n2::4-bytes, n3::4-bytes, n4::4-bytes,
      n5::4-bytes, n6::4-bytes, n7::4-bytes, n8::4-bytes>>
  end

  defp encode_value(:int, int) when is_integer(int) do
    <<int::32>>
  end

  defp encode_value({:list, [items_type]}, list) when is_list(list) do
    for item <- list,
        into: [<<length(list)::32>>],
        do: encode_query_value(items_type, item)
  end

  defp encode_value({:map, [key_type, value_type]}, map) when is_map(map) do
    for {key, value} <- map, into: [<<map_size(map)::32>>] do
      [
        encode_query_value(key_type, key),
        encode_query_value(value_type, value),
      ]
    end
  end

  defp encode_value({:set, inner_type}, %MapSet{} = set) do
    encode_value({:list, inner_type}, MapSet.to_list(set))
  end

  defp encode_value(:smallint, int) when is_integer(int) do
    <<int::16>>
  end

  defp encode_value(:text, text) when is_binary(text) do
    text
  end

  defp encode_value(:time, time) when time in 0..86399999999999 do
    <<time::64>>
  end

  defp encode_value(:timestamp, timestamp) do
    <<timestamp::64>>
  end

  defp encode_value(:tinyint, int) when is_integer(int) do
    <<int>>
  end

  defp encode_value({:udt, fields}, value) when is_map(value) do
    for {field_name, field_type} <- fields do
      encode_query_value(field_type, Map.get(value, field_name))
    end
  end

  defp encode_value(:uuid, uuid) when is_binary(uuid) do
    <<part1::8-bytes, ?-,
      part2::4-bytes, ?-,
      part3::4-bytes, ?-,
      part4::4-bytes, ?-,
      part5::12-bytes>> = uuid
    <<decode_base16(part1)::4-bytes,
      decode_base16(part2)::2-bytes,
      decode_base16(part3)::2-bytes,
      decode_base16(part4)::2-bytes,
      decode_base16(part5)::6-bytes>>
  end

  # Alias of :text
  defp encode_value(:varchar, varchar) do
    encode_value(:text, varchar)
  end

  defp encode_value(:varint, varint) when is_integer(varint) do
    size = varint_byte_size(varint)
    <<varint::size(size)-unit(8)>>
  end

  defp encode_value(:timeuuid, timeuuid) when is_binary(timeuuid) do
    encode_value(:uuid, timeuuid)
  end

  defp encode_value({:tuple, types}, tuple) when length(types) == tuple_size(tuple) do
    for {type, item} <- Enum.zip(types, Tuple.to_list(tuple)),
        do: encode_query_value(type, item)
  end

  defp varint_byte_size(value) when value in -128..127,
    do: 1
  defp varint_byte_size(value) when value > 127,
    do: 1 + varint_byte_size(value >>> 8)
  defp varint_byte_size(value) when value < -128,
    do: varint_byte_size(-value - 1)

  @compile {:inline, decode_base16: 1}
  defp decode_base16(value) do
    Base.decode16!(value, case: :mixed)
  end

  error_codes = %{
    0x0000 => :server_failure,
    0x000A => :protocol_violation,
    0x0100 => :invalid_credentials,
    0x1000 => :unavailable,
    0x1001 => :overloaded,
    0x1002 => :bootstrapping,
    0x1003 => :truncate_failure,
    0x1100 => :write_timeout,
    0x1200 => :read_timeout,
    0x2000 => :invalid_syntax,
    0x2100 => :unauthorized,
    0x2200 => :invalid,
    0x2300 => :invalid_config,
    0x2400 => :already_exists,
    0x2500 => :unprepared,
  }
  for {code, reason} <- error_codes do
    defp decode_error_reason(<<unquote(code)::32-signed, buffer::bytes>>) do
      {unquote(reason), buffer}
    end
  end

  defp decode_error_message(_reason, buffer) do
    decode_string(message <- buffer)
    _ = buffer
    message
  end

  @spec decode_response(Frame.t(:error), term) :: Error.t
  @spec decode_response(Frame.t(:ready), nil) :: :ok
  @spec decode_response(Frame.t(:supported), nil) :: %{optional(String.t) => [String.t]}
  @spec decode_response(Frame.t(:result), Simple.t | Prepared.t | Batch.t) :: Xandra.result | Prepared.t
  def decode_response(frame, query \\ nil)

  def decode_response(%Frame{kind: :error, body: body} , _query) do
    {reason, buffer} = decode_error_reason(body)
    Error.new(reason, decode_error_message(reason, buffer))
  end

  def decode_response(%Frame{kind: :ready, body: <<>>}, nil) do
    :ok
  end

  def decode_response(%Frame{kind: :supported, body: body}, nil) do
    {value, <<>>} = decode_string_multimap(body)
    value
  end

  def decode_response(%Frame{kind: :event, body: body}, nil) do
    decode_string(event <- body)
    "STATUS_CHANGE" = event
    decode_string(effect <- body)
    {address, port, <<>>} = decode_inet(body)
    %StatusChange{effect: effect, address: address, port: port}
  end

  def decode_response(%Frame{kind: :result, body: body}, %kind{} = query)
      when kind in [Simple, Prepared, Batch] do
    decode_result_response(body, query)
  end

  defp decode_inet(<<size, data::size(size), buffer::bits>>) do
    address = decode_value(data, :inet)
    <<port::32, buffer::bits>> = buffer
    {address, port, buffer}
  end

  # Void
  defp decode_result_response(<<0x0001::32-signed>>, _query) do
    %Xandra.Void{}
  end

  # Page
  defp decode_result_response(<<0x0002::32-signed, buffer::bits>>, query) do
    page = new_page(query)
    {page, buffer} = decode_metadata(buffer, page)
    content = decode_page_content(buffer, page.columns)
    %{page | content: content}
  end

  # SetKeyspace
  defp decode_result_response(<<0x0003::32-signed, buffer::bits>>, _query) do
    decode_string(keyspace <- buffer)
    <<>> = buffer
    %Xandra.SetKeyspace{keyspace: keyspace}
  end

  # Prepared
  defp decode_result_response(<<0x0004::32-signed, buffer::bits>>, %Prepared{} = prepared) do
    decode_string(id <- buffer)
    {%{columns: bound_columns}, buffer} = decode_metadata(buffer, %Page{})
    {%{columns: result_columns}, <<>>} = decode_metadata(buffer, %Page{})
    %{prepared | id: id, bound_columns: bound_columns, result_columns: result_columns}
  end

  # SchemaChange
  defp decode_result_response(<<0x0005::32-signed, buffer::bits>>, _query) do
    decode_string(effect <- buffer)
    decode_string(target <- buffer)
    options = decode_change_options(buffer, target)
    %Xandra.SchemaChange{effect: effect, target: target, options: options}
  end

  # Since SELECT statements are not allowed in BATCH queries, there's no need to
  # support %Batch{} in this function.
  defp new_page(%Simple{}),
    do: %Page{}
  defp new_page(%Prepared{result_columns: result_columns}),
    do: %Page{columns: result_columns}

  defp decode_change_options(<<buffer::bits>>, "KEYSPACE") do
    decode_string(keyspace <- buffer)
    <<>> = buffer
    %{keyspace: keyspace}
  end

  defp decode_change_options(<<buffer::bits>>, target) when target in ["TABLE", "TYPE"] do
    decode_string(keyspace <- buffer)
    decode_string(subject <- buffer)
    <<>> = buffer
    %{keyspace: keyspace, subject: subject}
  end

  defp decode_metadata(<<flags::4-bytes, column_count::32-signed, buffer::bits>>, page) do
    <<_::29, no_metadata::1, has_more_pages::1, global_table_spec::1>> = flags
    {page, buffer} = decode_paging_state(buffer, page, has_more_pages)

    cond do
      no_metadata == 1 ->
        {page, buffer}
      global_table_spec == 1 ->
        decode_string(keyspace <- buffer)
        decode_string(table <- buffer)
        {columns, buffer} = decode_columns(buffer, column_count, {keyspace, table}, [])
        {%{page | columns: columns}, buffer}
      true ->
        {columns, buffer} = decode_columns(buffer, column_count, nil, [])
        {%{page | columns: columns}, buffer}
    end
  end

  defp decode_paging_state(<<buffer::bits>>, page, 0) do
    {page, buffer}
  end

  defp decode_paging_state(<<buffer::bits>>, page, 1) do
    <<size::32, paging_state::size(size)-bytes, buffer::bits>> = buffer
    {%{page | paging_state: paging_state}, buffer}
  end

  defp decode_page_content(<<row_count::32-signed, buffer::bits>>, columns) do
    decode_page_content(buffer, row_count, columns, columns, [[]])
  end

  def decode_page_content(<<>>, 0, columns, columns, [[] | acc]) do
    Enum.reverse(acc)
  end

  def decode_page_content(<<buffer::bits>>, row_count, columns, [], [values | acc]) do
    decode_page_content(buffer, row_count - 1, columns, columns, [[], Enum.reverse(values) | acc])
  end

  def decode_page_content(<<buffer::bits>>, row_count, columns, [{_, _, _, type} | rest], [values | acc]) do
    decode_value(value <- buffer, type) do
      values = [value | values]
      decode_page_content(buffer, row_count, columns, rest, [values | acc])
    end
  end

  defp decode_value(<<value::bits>>, :ascii), do: value

  defp decode_value(<<value::64-signed>>, :bigint), do: value

  defp decode_value(<<value::bits>>, :blob), do: value

  defp decode_value(<<value::8>>, :boolean), do: Kernel.!=(value, 0)

  defp decode_value(<<value::32>>, :date), do: value

  defp decode_value(<<scale::32-signed, rest::bits>>, :decimal) do
    {decode_value(rest, :varint), scale}
  end

  defp decode_value(<<value::64-float>>, :double), do: value

  defp decode_value(<<value::32-float>>, :float), do: value

  defp decode_value(<<data::4-bytes>>, :inet) do
    <<n1, n2, n3, n4>> = data
    {n1, n2, n3, n4}
  end

  defp decode_value(<<data::16-bytes>>, :inet) do
    <<n1, n2, n3, n4, n5, n6, n7, n8, n9, n10, n11, n12, n13, n14, n15, n16>> = data
    {n1, n2, n3, n4, n5, n6, n7, n8, n9, n10, n11, n12, n13, n14, n15, n16}
  end

  defp decode_value(<<value::32-signed>>, :int), do: value

  defp decode_value(<<count::32-signed, data::bits>>, {:list, [type]}) do
    decode_value_list(data, count, type, [])
  end

  defp decode_value(<<count::32-signed, data::bits>>, {:map, [key_type, value_type]}) do
    decode_value_map_key(data, count, key_type, value_type, [])
  end

  defp decode_value(<<count::32-signed, data::bits>>, {:set, [type]}) do
    data
    |> decode_value_list(count, type, [])
    |> MapSet.new()
  end

  defp decode_value(<<value::16-signed>>, :smallint), do: value

  defp decode_value(<<data::bits>>, {:tuple, types}) do
    decode_value_tuple(data, types, [])
  end

  defp decode_value(<<value::16-bytes>>, type) when type in [:timeuuid, :uuid], do: value

  defp decode_value(<<value::bits>>, :varchar), do: value

  defp decode_value(<<data::bits>>, :varint) do
    size = bit_size(data)
    <<value::size(size)-signed>> = data
    value
  end

  defp decode_value(<<value::64>>, :time), do: value

  defp decode_value(<<data::bits>>, {:udt, fields}) do
    decode_value_udt(data, fields, [])
  end

  defp decode_value(<<value::64-signed>>, :timestamp), do: value

  defp decode_value(<<value::signed>>, :tinyint), do: value

  defp decode_value_udt(<<buffer::bits>>, [{field_name, field_type} | rest], acc) do
    decode_value(value <- buffer, field_type) do
      decode_value_udt(buffer, rest, [{field_name, value} | acc])
    end
  end

  defp decode_value_udt(<<>>, [], acc) do
    Map.new(acc)
  end

  defp decode_value_list(<<>>, 0, _type, acc) do
    Enum.reverse(acc)
  end

  defp decode_value_list(<<buffer::bits>>, count, type, acc) do
    decode_value(item <- buffer, type) do
      decode_value_list(buffer, count - 1, type, [item | acc])
    end
  end

  defp decode_value_map_key(<<>>, 0, _key_type, _value_type, acc) do
    Map.new(acc)
  end

  defp decode_value_map_key(<<buffer::bits>>, count, key_type, value_type, acc) do
    decode_value(key <- buffer, key_type) do
      decode_value_map_value(buffer, count, key_type, value_type, [key | acc])
    end
  end

  defp decode_value_map_value(<<buffer::bits>>, count, key_type, value_type, [key | acc]) do
    decode_value(value <- buffer, value_type) do
      decode_value_map_key(buffer, count - 1, key_type, value_type, [{key, value} | acc])
    end
  end

  defp decode_value_tuple(<<buffer::bits>>, [type | types], acc) do
    decode_value(item <- buffer, type) do
      decode_value_tuple(buffer, types, [item | acc])
    end
  end

  defp decode_value_tuple(<<>>, [], acc) do
    acc |> Enum.reverse |> List.to_tuple
  end

  defp decode_columns(buffer, 0, _table_spec, acc) do
    {Enum.reverse(acc), buffer}
  end

  defp decode_columns(<<buffer::bits>>, column_count, nil, acc) do
    decode_string(keyspace <- buffer)
    decode_string(table <- buffer)
    decode_string(name <- buffer)
    {type, buffer} = decode_type(buffer)
    entry = {keyspace, table, name, type}
    decode_columns(buffer, column_count - 1, nil, [entry | acc])
  end

  defp decode_columns(<<buffer::bits>>, column_count, table_spec, acc) do
    {keyspace, table} = table_spec
    decode_string(name <- buffer)
    {type, buffer} = decode_type(buffer)
    entry = {keyspace, table, name, type}
    decode_columns(buffer, column_count - 1, table_spec, [entry | acc])
  end

  defp decode_type(<<0x0000::16, buffer::bits>>) do
    decode_string(class <- buffer)
    {custom_type_to_native(class), buffer}
  end

  defp decode_type(<<0x0001::16, buffer::bits>>) do
    {:ascii, buffer}
  end

  defp decode_type(<<0x0002::16, buffer::bits>>) do
    {:bigint, buffer}
  end

  defp decode_type(<<0x0003::16, buffer::bits>>) do
    {:blob, buffer}
  end

  defp decode_type(<<0x0004::16, buffer::bits>>) do
    {:boolean, buffer}
  end

  defp decode_type(<<0x0005::16, buffer::bits>>) do
    {:counter, buffer}
  end

  defp decode_type(<<0x0006::16, buffer::bits>>) do
    {:decimal, buffer}
  end

  defp decode_type(<<0x0007::16, buffer::bits>>) do
    {:double, buffer}
  end

  defp decode_type(<<0x0008::16, buffer::bits>>) do
    {:float, buffer}
  end

  defp decode_type(<<0x0009::16, buffer::bits>>) do
    {:int, buffer}
  end

  defp decode_type(<<0x000B::16, buffer::bits>>) do
    {:timestamp, buffer}
  end

  defp decode_type(<<0x000C::16, buffer::bits>>) do
    {:uuid, buffer}
  end

  defp decode_type(<<0x000D::16, buffer::bits>>) do
    {:varchar, buffer}
  end

  defp decode_type(<<0x000E::16, buffer::bits>>) do
    {:varint, buffer}
  end

  defp decode_type(<<0x000F::16, buffer::bits>>) do
    {:timeuuid, buffer}
  end

  defp decode_type(<<0x0010::16, buffer::bits>>) do
    {:inet, buffer}
  end

  defp decode_type(<<0x0020::16, buffer::bits>>) do
    {type, buffer} = decode_type(buffer)
    {{:list, [type]}, buffer}
  end

  defp decode_type(<<0x0021::16, buffer::bits>>) do
    {key_type, buffer} = decode_type(buffer)
    {value_type, buffer} = decode_type(buffer)
    {{:map, [key_type, value_type]}, buffer}
  end

  defp decode_type(<<0x0022::16, buffer::bits>>) do
    {type, buffer} = decode_type(buffer)
    {{:set, [type]}, buffer}
  end

  defp decode_type(<<0x0030::16, buffer::bits>>) do
    decode_string(_keyspace <- buffer)
    decode_string(_name <- buffer)
    <<count::16, buffer::bits>> = buffer
    decode_type_udt(buffer, count, [])
  end

  defp decode_type(<<0x0031::16, count::16, buffer::bits>>) do
    decode_type_tuple(buffer, count, [])
  end

  custom_types = %{
    "org.apache.cassandra.db.marshal.SimpleDateType" => :date,
    "org.apache.cassandra.db.marshal.ShortType" => :smallint,
    "org.apache.cassandra.db.marshal.ByteType" => :tinyint,
    "org.apache.cassandra.db.marshal.TimeType" => :time,
  }
  for {class, type} <- custom_types do
    defp custom_type_to_native(unquote(class)) do
      unquote(type)
    end
  end

  defp custom_type_to_native(class) do
    raise "cannot decode custom type #{inspect(class)}"
  end

  defp decode_type_udt(<<buffer::bits>>, 0, acc) do
    {{:udt, Enum.reverse(acc)}, buffer}
  end

  defp decode_type_udt(<<buffer::bits>>, count, acc) do
    decode_string(field_name <- buffer)
    {field_type, buffer} = decode_type(buffer)
    decode_type_udt(buffer, count - 1, [{field_name, field_type} | acc])
  end

  defp decode_type_tuple(<<buffer::bits>>, 0, acc) do
    {{:tuple, Enum.reverse(acc)}, buffer}
  end

  defp decode_type_tuple(<<buffer::bits>>, count, acc) do
    {type, buffer} = decode_type(buffer)
    decode_type_tuple(buffer, count - 1, [type | acc])
  end

  defp decode_string_multimap(<<count::16, buffer::bits>>) do
    decode_string_multimap(buffer, count, [])
  end

  defp decode_string_multimap(<<buffer::bits>>, 0, acc) do
    {Map.new(acc), buffer}
  end

  defp decode_string_multimap(<<buffer::bits>>, count, acc) do
    decode_string(key <- buffer)
    {value, buffer} = decode_string_list(buffer)
    decode_string_multimap(buffer, count - 1, [{key, value} | acc])
  end

  defp decode_string_list(<<count::16, buffer::bits>>) do
    decode_string_list(buffer, count, [])
  end

  defp decode_string_list(<<buffer::bits>>, 0, acc) do
    {Enum.reverse(acc), buffer}
  end

  defp decode_string_list(<<buffer::bits>>, count, acc) do
    decode_string(item <- buffer)
    decode_string_list(buffer, count - 1, [item | acc])
  end
end


Move main pipeline steps

Move main pipeline steps

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

  @doc """
  The `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
            ],
            cli_aliases: [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: %{}

  @type t :: %__MODULE__{}

  @execution_pipeline_key __MODULE__
  @execution_pipeline [
    __pre__: [
      {Credo.Execution.Task.AppendDefaultConfig, []},
      {Credo.Execution.Task.ParseOptions, []},
      {Credo.Execution.Task.ConvertCLIOptionsToConfig, []},
      {Credo.Execution.Task.InitializePlugins, []}
    ],
    parse_cli_options: [
      {Credo.Execution.Task.ParseOptions, []}
    ],
    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, use_strict_parser: true}
    ],
    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 a re-run with the the given `argv`, noting to just analyse the `files_that_changed`."
  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 "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
  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: 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
  def get_path(exec) do
    exec.cli_options.path
  end

  # Commands

  @doc "Returns the name of the command, which should be run by the given execution."
  def get_command_name(exec) do
    exec.cli_options.command
  end

  @doc "Returns all valid command names."
  def get_valid_command_names(exec) do
    Map.keys(exec.commands)
  end

  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}
  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

  def get_plugin_param(exec, plugin_mod, param_name) do
    exec.plugins[plugin_mod][param_name]
  end

  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 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, 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

  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

  # Assigns

  @doc "Returns the assign with the given `name` for the given `exec` struct (or return the given `default` value)."
  def get_assign(exec, name, default \\ nil) do
    Map.get(exec.assigns, name, default)
  end

  @doc "Puts the given `value` with the given `name` as assign into the given `exec` struct."
  def put_assign(exec, name, value) do
    %__MODULE__{exec | assigns: Map.put(exec.assigns, name, value)}
  end

  # Config Files

  @doc "Returns all config files for the given `exec` struct."
  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."
  def get_source_files(exec) do
    Credo.Execution.ExecutionSourceFiles.get(exec)
  end

  @doc "Puts the given `source_files` into the given `exec` struct."
  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 set_issues(exec, issues) do
    ExecutionIssues.set(exec, issues)

    exec
  end

  # Results

  @doc "Returns the result with the given `name` for the given `exec` struct (or return the given `default` value)."
  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."
  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."
  def halt(exec) do
    %__MODULE__{exec | halted: true}
  end

  @doc false
  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

  @doc false
  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)
      end)
    end)
  end

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

  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 false
  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

  @doc false
  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, __MODULE__, pipeline)
  end

  @doc false
  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

  @doc false
  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, pipeline_key, pipeline)
  end

  defp put_builtin_command(exec, name, command_mod) do
    put_command(exec, 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()
      |> 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 a `Execution` struct, which is created and
  manipulated via the `Credo.Execution` module.
  """

  @doc """
  The `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
            ],
            cli_aliases: [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: %{}

  @type t :: %__MODULE__{}

  @execution_pipeline_key __MODULE__
  @execution_pipeline [
    __pre__: [
      {Credo.Execution.Task.AppendDefaultConfig, []},
      {Credo.Execution.Task.ParseOptions, []},
      {Credo.Execution.Task.ConvertCLIOptionsToConfig, []},
      {Credo.Execution.Task.InitializePlugins, []}
    ],
    parse_cli_options: [
      {Credo.Execution.Task.ParseOptions, []}
    ],
    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
    ],
    pre_determine_command: [
      {Credo.Execution.Task.DetermineCommand, []}
    ],
    pre_set_default_command: [
      {Credo.Execution.Task.SetDefaultCommand, []}
    ],
    initialize_command: [
      {Credo.Execution.Task.InitializeCommand, []}
    ],
    parse_cli_options_final: [
      {Credo.Execution.Task.ParseOptions, use_strict_parser: true}
    ],
    validate_cli_options: [
      {Credo.Execution.Task.ValidateOptions, []}
    ],
    convert_cli_options_to_config: [
      {Credo.Execution.Task.ConvertCLIOptionsToConfig, []}
    ],
    determine_command: [
      # {Credo.Execution.Task.DetermineCommand, []}
    ],
    set_default_command: [
      # {Credo.Execution.Task.SetDefaultCommand, []}
    ],
    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 a re-run with the the given `argv`, noting to just analyse the `files_that_changed`."
  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 "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
  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: 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
  def get_path(exec) do
    exec.cli_options.path
  end

  # Commands

  @doc "Returns the name of the command, which should be run by the given execution."
  def get_command_name(exec) do
    exec.cli_options.command
  end

  @doc "Returns all valid command names."
  def get_valid_command_names(exec) do
    Map.keys(exec.commands)
  end

  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}
  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

  def get_plugin_param(exec, plugin_mod, param_name) do
    exec.plugins[plugin_mod][param_name]
  end

  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 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, 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

  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

  # Assigns

  @doc "Returns the assign with the given `name` for the given `exec` struct (or return the given `default` value)."
  def get_assign(exec, name, default \\ nil) do
    Map.get(exec.assigns, name, default)
  end

  @doc "Puts the given `value` with the given `name` as assign into the given `exec` struct."
  def put_assign(exec, name, value) do
    %__MODULE__{exec | assigns: Map.put(exec.assigns, name, value)}
  end

  # Config Files

  @doc "Returns all config files for the given `exec` struct."
  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."
  def get_source_files(exec) do
    Credo.Execution.ExecutionSourceFiles.get(exec)
  end

  @doc "Puts the given `source_files` into the given `exec` struct."
  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 set_issues(exec, issues) do
    ExecutionIssues.set(exec, issues)

    exec
  end

  # Results

  @doc "Returns the result with the given `name` for the given `exec` struct (or return the given `default` value)."
  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."
  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."
  def halt(exec) do
    %__MODULE__{exec | halted: true}
  end

  @doc false
  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

  @doc false
  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)
      end)
    end)
  end

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

  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 false
  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

  @doc false
  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, __MODULE__, pipeline)
  end

  @doc false
  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

  @doc false
  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, pipeline_key, pipeline)
  end

  defp put_builtin_command(exec, name, command_mod) do
    put_command(exec, 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()
      |> 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