defmodule Mariaex.Messages do
  @moduledoc false

  import Record, only: [defrecord: 2]
  use Mariaex.Coder
  require Decimal

  # @protocol_vsn_major 3
  # @protocol_vsn_minor 0

  defrecord :packet, [:size, :seqnum, :msg, :body]


  @auth_types [ ok: 0, kerberos: 2, cleartext: 3, md5: 5, scm: 6, gss: 7,
                sspi: 9, gss_cont: 8 ]
  _ = @auth_types # Drop warning for now

  @error_fields [ severity: ?S, code: ?C, message: ?M, detail: ?D, hint: ?H,
                  position: ?P, internal_position: ?p, internal_query: ?q,
                  where: ?W, schema: ?s, table: ?t, column: ?c, data_type: ?d,
                  constraint: ?n, file: ?F, line: ?L, routine: ?R ]
  _ = @error_fields # Drop warning for now

  @commands [ com_sleep: 0x00, com_quit: 0x01, com_init_db: 0x02,
              com_query: 0x03, com_field_list: 0x04, com_create_db: 0x05,
              com_drop_db: 0x06, com_refresh: 0x07, com_shutdown: 0x08,
              com_statistics: 0x09, com_process_info: 0x0a, com_connect: 0x0b,
              com_process_kill: 0x0c, com_debug: 0x0d, com_ping: 0x0e,
              com_time: 0x0f, com_delayed_inser: 0x10, com_change_use: 0x11,
              com_binlog_dump: 0x12, com_table_dump: 0x13, com_connect_out: 0x14,
              com_register_slave: 0x15, com_stmt_prepare: 0x16, com_stmt_execute: 0x17,
              com_stmt_send_long_data: 0x18, com_stmt_close: 0x19, com_stmt_reset: 0x1a,
              com_set_option: 0x1b, com_stmt_fetch: 0x1c, com_daemon: 0x1d,
              com_binlog_dump_gtid: 0x1e, com_reset_connection: 0x1f]

  for {command, number} <- @commands do
    defmacro unquote(command)(), do: unquote(number)
  end

  @types [float:
           [field_type_float: 0x04,
            field_type_double: 0x05],
          decimal:
           [field_type_decimal: 0x00,
            field_type_newdecimal: 0xf6],
          integer:
           [field_type_tiny: 0x01,
            field_type_short: 0x02,
            field_type_long: 0x03,
            field_type_int24: 0x09,
            field_type_year: 0x0d,
            field_type_longlong: 0x08],
          timestamp:
           [field_type_timestamp: 0x07,
            field_type_datetime: 0x0c],
          date:
           [field_type_date: 0x0a],
          time:
           [field_type_time: 0x0b],
          bit:
           [field_type_bit: 0x10],
          string:
           [field_type_varchar: 0x0f,
            field_type_tiny_blob: 0xf9,
            field_type_medium_blob: 0xfa,
            field_type_long_blob: 0xfb,
            field_type_blob: 0xfc,
            field_type_var_string: 0xfd,
            field_type_string: 0xfe],
          json:
           [field_type_json: 0xf5],
          geometry:
            [field_type_geometry: 0xff],
          null:
           [field_type_null: 0x06]
         ]

  def __type__(:decode, _type, nil), do: nil

  for {_type, list} <- @types,
      {name, id}    <- list   do
    def __type__(:id, unquote(name)), do: unquote(id)
  end
  for {type, list}  <- @types,
      {name, id}    <- list   do
    def __type__(:type, unquote(id)), do: {unquote(type), unquote(name)}
  end

  defcoder :handshake do
    protocol_version 1
    server_version :string
    connection_id 4
    auth_plugin_data1 :string
    capability_flags_1 2
    character_set 1
    status_flags 2
    capability_flags_2 2
    # length_auth_plugin_data and the following ten bytes in the spec are rolled into the following field
    auth_plugin_data2 {__MODULE__, :auth_plugin_data2}
    plugin :string_eof
  end

  defcoder :handshake_resp do
    capability_flags 4
    max_size 4
    character_set 1
    _ 23
    username :string
    password :length_string
    database :string
    plugin   :string_eof
  end

  defcoder :handshake_no_database_resp do
    capability_flags 4
    max_size 4
    character_set 1
    _ 23
    username :string
    password :length_string
    plugin   :string_eof
  end

  defcoder :ssl_connection_request do
    capability_flags 4
    max_size 4
    character_set 1
    _ 23
  end

  defcoder :old_password do
    password :string
  end

  defcoder :ok_resp do
    header 1
    affected_rows :length_encoded_integer
    last_insert_id :length_encoded_integer
    status_flags 2
    warnings 2
    message :string_eof
  end

  defcoder :eof_resp do
    header 1
    warnings 2
    status_flags 2
    ## Since version MySQL of 5.7.X, MySQL sends bigger eof messages without any documentation, what is
    ## added.
    message :string_eof
  end

  defcoder :error_resp do
    header 1
    error_code 2
    sql_state_marker 1, :string
    sql_state 5, :string
    error_message :string_eof
  end

  defcoder :text_cmd do
    command 1
    statement :string_eof
  end

  defcoder :stmt_prepare_ok do
    status 1
    statement_id 4
    num_columns 2
    num_params 2
    _ 1
    warning_count 2
  end

  defcoder :stmt_execute do
    command 1
    statement_id 4
    flags 1
    iteration_count 4
    parameters :string_eof
  end

  defcoder :stmt_fetch do
    command 1
    statement_id 4
    num_rows 4
  end

  defcoder :stmt_close do
    command 1
    statement_id 4
  end

  defcoder :stmt_reset do
    command 1
    statement_id 4
  end

  defcoder :column_count do
    column_count :length_encoded_integer
  end

  defcoder :bin_row do
    row :string_eof
  end

  defcoder :text_row do
    row :string_eof
  end

  defcoder :column_definition_41 do
    catalog   :length_encoded_string
    schema    :length_encoded_string
    table     :length_encoded_string
    org_table :length_encoded_string
    name      :length_encoded_string
    org_name  :length_encoded_string
    length_of_fixed :length_encoded_integer
    character_set 2
    column_length 4
    type 1
    flags 2
    decimals 1
    _ 2
  end

  defcoder :auth_switch do
    plugin :string
    salt :string
  end

  defcoder :auth_switch_request do
    password :string_eof
  end

  defcoder :caching_sha2_send_password do
    password :string
  end

  # Encoding

  def encode(msg, seqnum) do
    body = encode_msg(msg)
    <<(byte_size(body)) :: 24-little, seqnum :: 8, body :: binary>>
  end

  defp encode_msg(rec),
    do: __encode__(rec)

  # Decoding

  def decode(<< len :: size(24)-little-integer, seqnum :: size(8)-integer, body :: size(len)-binary, rest :: binary>>, state) do
    msg = decode_msg(body, state)
    {packet(size: len, seqnum: seqnum, msg: msg, body: body), rest}
  end
  def decode(rest, _state),
    do: {nil, rest}

  def decode_bin_rows(<< len :: size(24)-little-integer, seqnum :: size(8)-integer, body :: size(len)-binary, rest :: binary>>,
                      fields, nullbin_size, rows, datetime, json_library) do
    case body do
      <<0 :: 8, nullbin::size(nullbin_size)-little-unit(8), values :: binary>> ->
        row = Mariaex.RowParser.decode_bin_rows(values, fields, nullbin, datetime, json_library)
        decode_bin_rows(rest, fields, nullbin_size, [row | rows], datetime, json_library)
      body ->
        msg = decode_msg(body, :bin_rows)
        {:ok, packet(size: len, seqnum: seqnum, msg: msg, body: body), rows, rest}
    end
  end
  def decode_bin_rows(<<rest :: binary>>, _fields, _nullbin_size, rows, _datetime, _json_library) do
    {:more, rows, rest}
  end

  def decode_text_rows(<< len :: size(24)-little-integer, seqnum :: size(8)-integer, body :: size(len)-binary, rest :: binary>>,
                      fields, rows, datetime, json_library) do
    case body do
      << 254 :: 8, _ :: binary >> = body when byte_size(body) < 9 ->
        msg = decode_msg(body, :text_rows)
        {:ok, packet(size: len, seqnum: seqnum, msg: msg, body: body), rows, rest}
      body ->
        row = Mariaex.RowParser.decode_text_rows(body, fields, datetime, json_library)
        decode_text_rows(rest, fields, [row | rows], datetime, json_library)
    end
  end
  def decode_text_rows(<<rest :: binary>>, _fields, rows, _datetime, _json_library) do
    {:more, rows, rest}
  end

  defp decode_msg(<< 1 :: 8, 3 :: 8>>, _),                                         do: :fast_auth_success
  defp decode_msg(<< 1 :: 8, 4 :: 8>>, _),                                         do: :perform_full_authentication
  defp decode_msg(<< 255 :: 8, _ :: binary >> = body, _),                          do: __decode__(:error_resp, body)
  defp decode_msg(body, :handshake),                                               do: __decode__(:handshake, body)
  defp decode_msg(<< 0 :: 8, _ :: binary >> = body, :bin_rows),                    do: __decode__(:bin_row, body)
  defp decode_msg(<< 0 :: 8, _ :: binary >> = body, :prepare_send),                do: __decode__(:stmt_prepare_ok, body)
  defp decode_msg(<< 0 :: 8, _ :: binary >> = body, _),                            do: __decode__(:ok_resp, body)
  defp decode_msg(<< 254 >> = _body, _),                                           do: :mysql_old_password
  defp decode_msg(<< 254 :: 8, _ :: binary >> = body, _) when byte_size(body) < 9, do: __decode__(:eof_resp, body)
  defp decode_msg(<< 254 :: 8, rest :: binary >>, _),                              do: __decode__(:auth_switch, rest)
  defp decode_msg(<< _ :: binary >> = body, :text_rows),                           do: __decode__(:text_row, body)
  defp decode_msg(body, :column_count),                                            do: __decode__(:column_count, body)
  defp decode_msg(body, :column_definitions),                                      do: __decode__(:column_definition_41, body)

  @doc """
  Decode auth plugin data2, which need some additional context checking.
  """
  def auth_plugin_data2(<< length_auth_plugin_data :: 8, _ :: 80, next :: binary >>) do
    length = max(13, length_auth_plugin_data - 8)
    length_nul_terminated = length - 1
    << null_terminated? :: size(length)-binary, next :: binary >> = next
    auth_plugin_data2 = case null_terminated? do
                          << contents :: size(length_nul_terminated)-binary, 0 :: 8 >> -> contents
                          contents -> contents
                        end
    {String.trim(auth_plugin_data2, "\0"), next}
  end
end
