%%%-------------------------------------------------------------------
%%% File    : p1_mysql_bin.erl
%%% Author  : Pawel Chmielowski <pawel@process-one.net>
%%% Descrip.: Binary protocol and prepared statement support.
%%% Ref.    : https://dev.mysql.com/doc/dev/mysql-server/latest/page_protocol_binary_resultset.html

-module(p1_mysql_bin).

-export([prepare_and_execute/6]).

%%--------------------------------------------------------------------
%% External exports
%%--------------------------------------------------------------------

-include("p1_mysql.hrl").
-include("p1_mysql_consts.hrl").
-include("p1_mysql_state.hrl").
-include_lib("kernel/include/inet.hrl").

-define(MYSQL_4_0, 40). %% Support for MySQL 4.0.x
-define(MYSQL_4_1, 41). %% Support for MySQL 4.1.x et 5.0.x

-define(TYPE_DECIMAL, 0).
-define(TYPE_TINY, 1).
-define(TYPE_SHORT, 2).
-define(TYPE_LONG, 3).
-define(TYPE_FLOAT, 4).
-define(TYPE_DOUBLE, 5).
-define(TYPE_NULL, 6).
-define(TYPE_TIMESTAMP, 7).
-define(TYPE_LONGLONG, 8).
-define(TYPE_INT24, 9).
-define(TYPE_DATE, 10).
-define(TYPE_TIME, 11).
-define(TYPE_DATETIME, 12).
-define(TYPE_YEAR, 13).
%-define(TYPE_NEWDATE, 14).
-define(TYPE_VARCHAR, 15).
-define(TYPE_BIT, 16).
%-define(TYPE_TIMESTAMP2, 17).
%-define(TYPE_TIME2, 18).
%-define(TYPE_ARRAY, 19).
%-define(TYPE_INVALID, 243).
%-define	(TYPE_BOOL, 244).
-define(TYPE_JSON, 245).
-define(TYPE_NEWDECIMAL, 246).
-define(TYPE_ENUM, 247).
-define(TYPE_SET, 248).
-define(TYPE_TINY_BLOB, 249).
-define(TYPE_MEDIUM_BLOB, 250).
-define(TYPE_LONG_BLOB, 251).
-define(TYPE_BLOB, 252).
-define(TYPE_VAR_STRING, 253).
-define(TYPE_STRING, 254).
-define(TYPE_GEOMETRY, 255).

-define(OFFSET, 2).

prepare_and_execute(#state{prepared = Prep} = State,
		    Query, QueryId, Args, Types, Options) ->
    case maps:get(QueryId, Prep, none) of
	none ->
	    case prepare(State, Query, Options) of
		{ok, NState, StmtId} ->
		    NState2 = NState#state{prepared = Prep#{QueryId => StmtId}},
		    execute(NState2, StmtId, Args, Types, Options);
		{error, NState, Error} ->
		    {NState, {error, Error}}
	    end;
	StmtId ->
	    execute(State, StmtId, Args, Types, Options)
    end.

execute(#state{mysql_version = Version, log_fun = LogFun, socket = Sock} = State,
	StmtId, Args, Types, Options) ->
    Packet = generate_execute_stmt_packet(StmtId, Args, Types),
    case p1_mysql_conn:do_send(Sock, Packet, 0, LogFun) of
	ok ->
	    case get_execute_stmt_response(State, Version, Options) of
		{Tag, Result, NState2} ->
		    {NState2, {Tag, Result}};
		E -> {State, E}
	    end;
	{error, Reason} ->
	    Msg = io_lib:format("Failed sending data on socket : ~p", [Reason]),
	    {State, {error, #p1_mysql_result{error = Msg}}}
    end.

prepare(#state{mysql_version = Version, log_fun = LogFun, socket = Sock} = State,
	Query, Options) ->
    QueryStr = if is_function(Query) -> iolist_to_binary(Query());
		   is_list(Query) -> iolist_to_binary(Query);
		   true -> Query
	       end,
    Packet = generate_prepare_packet(QueryStr),
    case p1_mysql_conn:do_send(Sock, Packet, 0, LogFun) of
	ok ->
	    get_prepare_response(State, Version, Options);
	{error, Reason} ->
	    Msg = io_lib:format("Failed sending data on socket : ~p", [Reason]),
	    {error, State, #p1_mysql_result{error = Msg}}
    end.

generate_execute_stmt_packet(Id, Params, ParamsType) ->
    {ParamsBin, TypesBin, _, BitMap2} = lists:foldl(
	fun({null, _}, {AccParam, AccType, Bit, BitMap}) ->
	    {AccParam, AccType, Bit*2, BitMap bor Bit};
	   ({Param, Type}, {AccParam, AccType, Bit, BitMap}) ->
	       {TypeB, ParamB} = encode_binary_value(Type, Param),
	       {<<AccParam/binary, ParamB/binary>>,
		<<AccType/binary, TypeB/binary>>,
		Bit*2, BitMap}
	end, {<<>>, <<>>, 1, 0}, lists:zip(Params, ParamsType)),

    Len = length(Params),
    LenBit = ((Len + 7) div 8) * 8,
    NullBitmapBin = <<BitMap2:LenBit>>,

    <<16#17, Id:32/little-integer, 0, 1:32/little,
      NullBitmapBin/binary, 1, TypesBin/binary, ParamsBin/binary>>.

get_execute_stmt_response(State, Version, _Options) ->
    case do_recv(State) of
	{ok, <<T:8, Rest/binary>>, _, NState} when T == 0; T == 254 ->
	    {AffectedRows, _} = decode_var_int(Rest),
	    {updated, #p1_mysql_result{affectedrows = AffectedRows}, NState};
	{ok, <<255, _ErrCode:16/little, $#, _State:5/binary, Rest/binary>>, _, _NState}
	    when Version == ?MYSQL_4_1 ->
	    {error, #p1_mysql_result{error = binary_to_list(Rest)}};
	{ok, <<255, _ErrCode:16/little, Rest/binary>>, _, _NState} ->
	    {error, #p1_mysql_result{error = binary_to_list(Rest)}};
	{ok, Data, _, NState} ->
	    {ColumnsCount, _} = decode_var_int(Data),
	    case get_columns_definitions(NState, Version, {[], []}) of
		{ok, Columns, ColTypes, NState2} ->
		    case get_resultset_rows(NState2, ColTypes,
					    ((ColumnsCount + 7 + ?OFFSET) div 8) * 8, []) of
			{ok, Rows, NState3} ->
			    {data, #p1_mysql_result{fieldinfo = Columns, rows = Rows}, NState3};
			{error, E} ->
			    {error, #p1_mysql_result{error = E}}
		    end;
		{error, E} ->
		    {error, #p1_mysql_result{error = E}}
	    end;
	{error, Reason} ->
	    {error, #p1_mysql_result{error = Reason}}
    end.

generate_prepare_packet(Query) ->
    <<16#16, Query/binary>>.

get_prepare_response(State, Version, _Options) ->
    case do_recv(State) of
	{ok, <<0, StmtID:32/little, NumColumns:16/little, NumParams:16/little, _Rest/binary>>, _, NState} ->
	    case receive_to_eof(NumParams, NState) of
		{ok, NState2} ->
		    case receive_to_eof(NumColumns, NState2) of
			{ok, NState3} ->
			    {ok, NState3, StmtID};
			E -> E
		    end;
		E -> E
	    end;
	{ok, <<255, _ErrCode:16/little, $#, _State:5/binary, Rest/binary>>, _, NState}
	    when Version == ?MYSQL_4_1 ->
	    {error, NState, #p1_mysql_result{error = binary_to_list(Rest)}};
	{ok, <<255, _ErrCode:16/little, Rest/binary>>, _, NState} ->
	    {error, NState, #p1_mysql_result{error = binary_to_list(Rest)}};
	{error, Reason} ->
	    {error, State, #p1_mysql_result{error = Reason}}
    end.

do_recv(State) ->
    p1_mysql_conn:do_recv(State#state.log_fun, State, undefined).

encode_var_string(String) ->
    <<(encode_var_int(size(String)))/binary, String/binary>>.

decode_var_string(Val) ->
    {Len, Rest} = decode_var_int(Val),
    <<Str:Len/binary, Rest2/binary>> = Rest,
    {Str, Rest2}.

encode_var_int(Val) when Val < 16#fb ->
    <<Val:8>>;
encode_var_int(Val) when Val =< 16#ffff ->
    <<16#fc, Val:16/little>>;
encode_var_int(Val) when Val =< 16#ffffff ->
    <<16#fd, Val:24/little>>;
encode_var_int(Val) ->
    <<16#fe, Val:64/little>>.

decode_var_int(<<V, Rest/binary>>) when V < 16#fb ->
    {V, Rest};
decode_var_int(<<16#fc, V:16/little, Rest/binary>>) ->
    {V, Rest};
decode_var_int(<<16#fd, V:24/little, Rest/binary>>) ->
    {V, Rest};
decode_var_int(<<16#fe, V:64/little, Rest/binary>>) ->
    {V, Rest}.

encode_binary_value(string, Value) ->
    {<<?TYPE_VAR_STRING, 0>>, encode_var_string(Value)};
encode_binary_value(integer, Value) ->
    if
	Value > -16#80 andalso Value < 16#7f ->
	    {<<?TYPE_TINY, 0>>, <<Value:8/little-signed-integer>>};
	Value > -16#8000 andalso Value < 16#7fff ->
	    {<<?TYPE_SHORT, 0>>, <<Value:16/little-signed-integer>>};
	Value > -16#80000000 andalso Value < 16#7ffffffff ->
	    {<<?TYPE_LONG, 0>>, <<Value:32/little-signed-integer>>};
	true ->
	    {<<?TYPE_LONGLONG, 0>>, <<Value:64/little-signed-integer>>}
    end;
encode_binary_value(bool, true) ->
    {<<?TYPE_TINY, 0>>, <<1>>};
encode_binary_value(bool, false) ->
    {<<?TYPE_TINY, 0>>, <<0>>};
encode_binary_value(datetime, {{Y, M, D}, {H, MM, S}}) ->
    {<<?TYPE_DATE, 0>>, <<Y:16/little, M:8, D:8, H:8, MM:8, S:8>>}.

decode_binary_value(?TYPE_TINY, <<V, Rest/binary>>) ->
    {V, Rest};
decode_binary_value(T, <<V:16/little, Rest/binary>>) when T == ?TYPE_SHORT; T == ?TYPE_YEAR ->
    {V, Rest};
decode_binary_value(T, <<V:32/little, Rest/binary>>) when T == ?TYPE_LONG; T == ?TYPE_INT24 ->
    {V, Rest};
decode_binary_value(?TYPE_FLOAT, <<V:32/little-float, Rest/binary>>) ->
    {V, Rest};
decode_binary_value(?TYPE_DOUBLE, <<V:64/little-float, Rest/binary>>) ->
    {V, Rest};
decode_binary_value(T, <<0, Rest/binary>>) when
    T == ?TYPE_TIMESTAMP; T == ?TYPE_DATETIME; T == ?TYPE_DATE ->
    {{{0, 0, 0}, {0, 0, 0, 0}}, Rest};
decode_binary_value(T, <<4, Y:16/little, M, D, Rest/binary>>) when
    T == ?TYPE_TIMESTAMP; T == ?TYPE_DATETIME; T == ?TYPE_DATE ->
    {{{Y, M, D}, {0, 0, 0, 0}}, Rest};
decode_binary_value(T, <<7, Y:16/little, M, D, H, MM, S, Rest/binary>>) when
    T == ?TYPE_TIMESTAMP; T == ?TYPE_DATETIME; T == ?TYPE_DATE ->
    {{{Y, M, D}, {H, MM, S, 0}}, Rest};
decode_binary_value(T, <<11, Y:16/little, M, D, H, MM, S, MS:32/little, Rest/binary>>)
    when T == ?TYPE_TIMESTAMP; T == ?TYPE_DATETIME; T == ?TYPE_DATE ->
    {{{Y, M, D}, {H, MM, S, MS}}, Rest};
decode_binary_value(?TYPE_LONGLONG, <<V:64/little, Rest/binary>>) ->
    {V, Rest};
decode_binary_value(?TYPE_TIME, <<0, Rest/binary>>) ->
    {{0, 0, 0, 0, 0, 0}, Rest};
decode_binary_value(?TYPE_TIME, <<8, Neg, D:32/little, H, M, S, Rest/binary>>) ->
    {{Neg, D, H, M, S, 0}, Rest};
decode_binary_value(?TYPE_TIME, <<12, Neg, D:32/little, H, M, S, MS:32/little, Rest/binary>>) ->
    {{Neg, D, H, M, S, MS}, Rest};
decode_binary_value(T, Rest) when
    T == ?TYPE_VARCHAR; T == ?TYPE_VAR_STRING; T == ?TYPE_STRING; T == ?TYPE_STRING;
    T == ?TYPE_ENUM; T == ?TYPE_SET;
    T == ?TYPE_BLOB; T == ?TYPE_TINY_BLOB; T == ?TYPE_MEDIUM_BLOB; T == ?TYPE_LONG_BLOB;
    T == ?TYPE_GEOMETRY; T == ?TYPE_JSON;
    T == ?TYPE_DECIMAL; T == ?TYPE_NEWDECIMAL ->
    {Str, Rest2} = decode_var_string(Rest),
    case T of
	%_ when T == ?TYPE_DECIMAL; T == ?TYPE_NEWDECIMAL ->
	%    {catch binary_to_float(Str), Rest2};
	_ ->
	    {Str, Rest2}
    end.

decode_column_41(Data) ->
    {_Catalog, Rest} = decode_var_string(Data),
    {_Schema, Rest1} = decode_var_string(Rest),
    {Table, Rest2} = decode_var_string(Rest1),
    {_OrgTable, Rest3} = decode_var_string(Rest2),
    {Name, Rest4} = decode_var_string(Rest3),
    {_OrgName, <<12, _CharSet:16/little, _ColLen:32/little, Type:8,
		 _Flags:16/little, _Decimals:8, _Rest7/binary>>} = decode_var_string(Rest4),
    {{Table, Name, 1, p1_mysql_conn:get_field_datatype(Type)}, Type}.

decode_column_32(Data) ->
    {Table, Rest} = decode_var_string(Data),
    {Name, <<1, Type:8, 3, _Flags:16/little, _Decimals:8, _Rest1/binary>>} = decode_var_string(Rest),
    {{Table, Name, 1, p1_mysql_conn:get_field_datatype(Type)}, Type}.

get_columns_definitions(State, Version, {Acc1, Acc2}) ->
    case do_recv(State) of
	{ok, <<254, _/binary>>, _, NState} ->
	    {ok, lists:reverse(Acc1), lists:reverse(Acc2), NState};
	{ok, Data, _, NState} ->
	    case Version of
		?MYSQL_4_1 ->
		    {Column, ColType} = decode_column_41(Data),
		    get_columns_definitions(NState, Version, {[Column | Acc1], [ColType | Acc2]});
		_ ->
		    {Column, ColType} = decode_column_32(Data),
		    get_columns_definitions(NState, Version, {[Column | Acc1], [ColType | Acc2]})
	    end;
	{error, Reason} ->
	    {error, Reason}
    end.

decode_resultset_row([], _ColumnsCount, _Null, Value, Acc) ->
    {lists:reverse(Acc), Value};
decode_resultset_row([Type | TypeRest], Column, Null, Value, Acc) ->
    case (1 bsl Column) band Null of
	0 ->
	    {Val, Rest} = decode_binary_value(Type, Value),
	    decode_resultset_row(TypeRest, Column + 1, Null, Rest, [Val | Acc]);
	_ ->
	    decode_resultset_row(TypeRest, Column + 1, Null, Value, [null | Acc])
    end.

get_resultset_rows(State, Columns, NullSize, Acc) ->
    case do_recv(State) of
	{ok, <<254, _/binary>>, _, NState} ->
	    {ok, lists:reverse(Acc), NState};
	{ok, <<0, Null:NullSize/little, Values/binary>>, _, NState} ->
	    %io:format("ROWS: ~p ~p ~p~n", [Null, NullSize, Values]),
	    % the first 2 bits of the bitmap must be ignored, this could be achieved by a
	    % 2 bits right shift of the little endian decoded integer
	    % (see https://dev.mysql.com/doc/dev/mysql-server/latest/page_protocol_binary_resultset.html)
	    {Row, _Rest} = decode_resultset_row(Columns, 0, Null bsr ?OFFSET, Values, []),
	    get_resultset_rows(NState, Columns, NullSize, [Row | Acc]);
	{error, Reason} ->
	    {error, Reason}
    end.

receive_to_eof(0, State) ->
    {ok, State};
receive_to_eof(N, State) ->
    case do_recv(State) of
	{ok, <<254, _/binary>>, _, NState} ->
	    {ok, NState};
	{ok, _, _, NState} ->
	    receive_to_eof(N, NState);
	{error, Reason} ->
	    {error, Reason}
    end.
