%% Copyright (c) 2009-2012
%% Bill Warnecke <bill@rupture.com>,
%% Jacob Vorreuter <jacob.vorreuter@gmail.com>,
%% Henning Diedrich <hd2010@eonblast.com>,
%% Eonblast Corporation <http://www.eonblast.com>
%%
%% Permission is  hereby  granted,  free of charge,  to any person
%% obtaining  a copy of this software and associated documentation
%% files (the "Software"),to deal in the Software without restric-
%% tion,  including  without  limitation the rights to use,  copy,
%% modify, merge,  publish,  distribute,  sublicense,  and/or sell
%% copies  of the  Software,  and to  permit  persons to  whom the
%% Software  is  furnished  to do  so,  subject  to the  following
%% conditions:
%%
%% The above  copyright notice and this permission notice shall be
%% included in all copies or substantial portions of the Software.
%%
%% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
%% EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
%% OF  MERCHANTABILITY,  FITNESS  FOR  A  PARTICULAR  PURPOSE  AND
%% NONINFRINGEMENT. IN  NO  EVENT  SHALL  THE AUTHORS OR COPYRIGHT
%% HOLDERS  BE  LIABLE FOR  ANY CLAIM, DAMAGES OR OTHER LIABILITY,
%% WHETHER IN AN ACTION OF CONTRACT,  TORT  OR OTHERWISE,  ARISING
%% FROM,  OUT OF OR IN CONNECTION WITH THE SOFTWARE  OR THE USE OR
%% OTHER DEALINGS IN THE SOFTWARE.
%% @private
-module(emysql_conn).
-export([set_database/2, set_database/3, set_encoding/2, set_encoding/3,
        execute/3, execute/4,
        preprepare/3, preprepare/4,
        prepare/3, prepare/4,
        unprepare/2, unprepare/3,
        open_connections/1, open_connection/1,
        reset_connection/3, close_connection/1,
        open_n_connections/2, hstate/1,
        ping_connection/2,
        test_connection/2, test_connection/3, need_test_connection/1
]).

-include("emysql.hrl").


set_database(Connection, Database) ->
    DefaultTimeout = emysql_app:default_timeout(),
    set_database(Connection, Database, DefaultTimeout).

set_database(_, undefined, _) -> ok;
set_database(_, Empty, _) when Empty == ""; Empty == <<>> -> ok;
set_database(Connection, Database, Timeout) ->
    Packet = <<?COM_QUERY, "use `", (iolist_to_binary(Database))/binary, "`">>,  % todo: utf8?
    emysql_tcp:send_and_recv_packet(Connection#emysql_connection.socket, Packet, 0, Timeout).

set_encoding(Connection, Encoding) ->
    DefaultTimeout = emysql_app:default_timeout(),
    set_encoding(Connection, Encoding, DefaultTimeout).

set_encoding(_, undefined, _) -> ok;
set_encoding(Connection, Encoding, Timeout) ->
    Packet = <<?COM_QUERY, "set names '", (erlang:atom_to_binary(Encoding, utf8))/binary, "'">>,
    emysql_tcp:send_and_recv_packet(Connection#emysql_connection.socket, Packet, 0, Timeout).

%% @todo This can go away once the underlying socket accepts IOData
canonicalize_query(Q) when is_binary(Q) -> Q;
canonicalize_query(QL) when is_list(QL) -> iolist_to_binary(QL).

execute(Connection, Query, Args) ->
    DefaultTimeout = emysql_app:default_timeout(),
    execute(Connection, Query, Args, DefaultTimeout).

execute(Connection, StmtName, [], Timeout) when is_atom(StmtName) ->
    prepare_statement(Connection, StmtName, Timeout),
    StmtNameBin = atom_to_binary(StmtName, utf8),
    Packet = <<?COM_QUERY, "EXECUTE ", StmtNameBin/binary>>,
    emysql_tcp:send_and_recv_packet(Connection#emysql_connection.socket, Packet, 0, Timeout);
execute(#emysql_connection { socket = Sock }, Query, [], Timeout) ->
    QB = canonicalize_query(Query),
    Packet = <<?COM_QUERY, QB/binary>>,
    emysql_tcp:send_and_recv_packet(Sock, Packet, 0, Timeout);
execute(Connection, Query, Args, Timeout) when (is_list(Query) orelse is_binary(Query)) andalso is_list(Args) ->
    StmtName = "stmt_"++integer_to_list(erlang:phash2(Query)),
    ok = prepare(Connection, StmtName, Query, Timeout),
    Ret =
    case set_params(Connection, 1, Args, undefined, Timeout) of
        OK when is_record(OK, ok_packet) ->
            ParamNamesBin = list_to_binary(string:join([[$@ | integer_to_list(I)] || I <- lists:seq(1, length(Args))], ", ")),  % todo: utf8?
            Packet = <<?COM_QUERY, "EXECUTE ", (list_to_binary(StmtName))/binary, " USING ", ParamNamesBin/binary>>,  % todo: utf8?
            emysql_tcp:send_and_recv_packet(Connection#emysql_connection.socket, Packet, 0, Timeout);
        Error ->
            Error
    end,
    unprepare(Connection, StmtName, Timeout),
    Ret;

execute(Connection, StmtName, Args, Timeout) when is_atom(StmtName), is_list(Args) ->
    prepare_statement(Connection, StmtName, Timeout),
    case set_params(Connection, 1, Args, undefined, Timeout) of
        OK when is_record(OK, ok_packet) ->
            ParamNamesBin = list_to_binary(string:join([[$@ | integer_to_list(I)] || I <- lists:seq(1, length(Args))], ", ")),  % todo: utf8?
            StmtNameBin = atom_to_binary(StmtName, utf8),
            Packet = <<?COM_QUERY, "EXECUTE ", StmtNameBin/binary, " USING ", ParamNamesBin/binary>>,
            emysql_tcp:send_and_recv_packet(Connection#emysql_connection.socket, Packet, 0, Timeout);
        Error ->
            Error
    end.

preprepare(Connection, StmtName, Statement) ->
    DefaultTimeout = emysql_app:default_timeout(),
    preprepare(Connection, StmtName, Statement, DefaultTimeout).

preprepare(#emysql_connection{}, StmtName, Statement, Timeout) when is_atom(StmtName) andalso (is_list(Statement) orelse is_binary(Statement)) ->
    emysql_statements:add(StmtName, Statement, Timeout).

prepare(Connection, Name, Statement) ->
    DefaultTimeout = emysql_app:default_timeout(),
    prepare(Connection, Name, Statement, DefaultTimeout).

prepare(Connection, Name, Statement, Timeout) when is_atom(Name) ->
    prepare(Connection, atom_to_list(Name), Statement, Timeout);
prepare(Connection, Name, Statement, Timeout) ->
    StatementBin = encode(Statement, binary),
    Packet = <<?COM_QUERY, "PREPARE ", (list_to_binary(Name))/binary, " FROM ", StatementBin/binary>>,  % todo: utf8?
    case emysql_tcp:send_and_recv_packet(Connection#emysql_connection.socket, Packet, 0, Timeout) of
        OK when is_record(OK, ok_packet) ->
            ok;
        Err when is_record(Err, error_packet) ->
            exit({failed_to_prepare_statement, Err#error_packet.msg})
    end.

unprepare(Connection, Name) ->
    DefaultTimeout = emysql_app:default_timeout(),
    unprepare(Connection, Name, DefaultTimeout).

unprepare(Connection, Name, Timeout) when is_atom(Name)->
    unprepare(Connection, atom_to_list(Name), Timeout);
unprepare(Connection, Name, Timeout) ->
    Packet = <<?COM_QUERY, "DEALLOCATE PREPARE ", (list_to_binary(Name))/binary>>,  % todo: utf8?
    emysql_tcp:send_and_recv_packet(Connection#emysql_connection.socket, Packet, 0, Timeout).

open_n_connections(PoolId, N) ->
    case emysql_conn_mgr:find_pool(PoolId, emysql_conn_mgr:pools()) of
        {Pool, _} ->
            lists:foldl(fun(_, {Conns, Reasons}) ->
                %% Catch {'EXIT',_} errors so newly opened connections are not orphaned.
                %% We do not want to close all the connections here like in
                %% open_connections/2. Struggle to keep working.
                case catch open_connection_for_manager(Pool) of
                    #emysql_connection{} = Connection ->
                        {[Connection | Conns], Reasons};
                    {'EXIT', Reason} ->
                        {Conns, [Reason | Reasons]}
                end
            end, {[], []}, lists:seq(1, N));
        _ ->
            exit(pool_not_found)
    end.

%% @doc Opens connections for the necessary pool.
%%
%% If connection opening fails, removes all connections from the pool
%% Does not remove pool from emysql_conn_mgr due to a possible deadlock.
%% Caller must do it by itself.
open_connections(Pool) ->
     %-% io:format("open connections loop: .. "),
    case (queue:len(Pool#pool.available) + gb_trees:size(Pool#pool.locked)) < Pool#pool.size of
        true ->
            case catch open_connection_for_manager(Pool) of
                #emysql_connection{} = Conn ->
                    open_connections(Pool#pool{available = queue:in(Conn, Pool#pool.available)});
                {'EXIT', Reason} ->
                    AllConns = lists:append(
                        queue:to_list(Pool#pool.available),
                        gb_trees:values(Pool#pool.locked)
                    ),
                    lists:foreach(fun emysql_conn:close_connection/1, AllConns),
                    {error, Reason}
			end;
        false ->
            {ok, Pool}
    end.

open_connection(#pool{pool_id=PoolId, host=Host, port=Port, user=User,
        password=Password, database=Database, encoding=Encoding,
                      start_cmds=StartCmds, connect_timeout=ConnectTimeout} = Pool) ->
     %-% io:format("~p open connection for pool ~p host ~p port ~p user ~p base ~p~n", [self(), PoolId, Host, Port, User, Database]),
     %-% io:format("~p open connection: ... connect ... ~n", [self()]),
    case gen_tcp:connect(Host, Port, [binary, {packet, raw}, {active, false}, {recbuf, ?TCP_RECV_BUFFER}], ConnectTimeout) of
        {ok, Sock} ->
            #greeting {
               server_version = Version,
               thread_id = ThreadId,
               caps = Caps,
               language = Language
              } = handshake(Sock, User, Password, ConnectTimeout),
            Connection = #emysql_connection{
                            id = erlang:port_to_list(Sock),
                            pool_id = PoolId,
                            encoding = Encoding,
                            socket = Sock,
                            version = Version,
                            thread_id = ThreadId,
                            caps = Caps,
                            language = Language,
                            test_period = Pool#pool.conn_test_period,
                            last_test_time = now_seconds()
                           },
            %%-% io:format("~p open connection: ... set db ...~n", [self()]),
            ok = set_database_or_die(Connection, Database, ConnectTimeout),
            ok = set_encoding_or_die(Connection, Encoding, ConnectTimeout),
            ok = run_startcmds_or_die(Connection, StartCmds, ConnectTimeout),
            Connection;
        {error, Reason} ->
             %-% io:format("~p open connection: ... ERROR ~p~n", [self(), Reason]),
             %-% io:format("~p open connection: ... exit with failed_to_connect_to_database~n", [self()]),
            exit({failed_to_connect_to_database, Reason})
    end.

handshake(Sock, User, Password, Timeout) ->
   case emysql_auth:handshake(Sock, User, Password, Timeout) of
       {ok, #greeting{} = G} -> G;
       {error, Reason} ->
           gen_tcp:close(Sock),
           exit(Reason)
   end.

open_connection_for_manager(#pool{} = Pool) ->
    Connection = #emysql_connection{socket = Sock} = open_connection(Pool),
    ok = give_manager_control(Sock), 
    Connection.

give_manager_control(Socket) ->
    case emysql_conn_mgr:give_manager_control(Socket) of
        {error, Reason} ->
            gen_tcp:close(Socket),
            exit({Reason,
                 "Failed to find conn mgr when opening connection. Make sure crypto is started and emysql.app is in the Erlang path."});
        ok -> ok
   end.

set_database_or_die(#emysql_connection { socket = Socket } = Connection, Database, Timeout) ->
    case set_database(Connection, Database, Timeout) of
        ok -> ok;
        OK1 when is_record(OK1, ok_packet) -> ok;
        Err1 when is_record(Err1, error_packet) ->
             gen_tcp:close(Socket),
             exit({failed_to_set_database, Err1#error_packet.msg})
    end.

run_startcmds_or_die(#emysql_connection{socket=Socket}, StartCmds, Timeout) ->
    lists:foreach(
        fun(Cmd) ->
                Packet = <<?COM_QUERY, Cmd/binary>>,
                case emysql_tcp:send_and_recv_packet(Socket, Packet, 0, Timeout) of
                    OK when OK =:= ok orelse is_record(OK, ok_packet) ->
                        ok;
                    #error_packet{msg=Msg} ->
                        gen_tcp:close(Socket),
                        exit({failed_to_run_cmd, Msg})
                end
        end,
        StartCmds
    ).
 
set_encoding_or_die(#emysql_connection { socket = Socket } = Connection, Encoding, Timeout) ->
    case set_encoding(Connection, Encoding, Timeout) of
        ok -> ok;
        OK2 when is_record(OK2, ok_packet) -> ok;
        Err2 when is_record(Err2, error_packet) ->
            gen_tcp:close(Socket),
            exit({failed_to_set_encoding, Err2#error_packet.msg})
    end.
 
reset_connection(Pools, Conn, StayLocked) ->
    %% if a process dies or times out while doing work
    %% the socket must be closed and the connection reset
    %% in the conn_mgr state. Also a new connection needs
    %% to be opened to replace the old one. If that fails,
    %% we queue the old as available for the next try
    %% by the next caller process coming along. So the
    %% pool can't run dry, even though it can freeze.
    %-% io:format("resetting connection~n"),
    MonitorRef = Conn#emysql_connection.monitor_ref,
    close_connection(Conn),
    %% OPEN NEW SOCKET
    case emysql_conn_mgr:find_pool(Conn#emysql_connection.pool_id, Pools) of
        {Pool, _} ->
            case catch open_connection_for_manager(Pool) of
                #emysql_connection{} = NewConn when StayLocked == pass ->
                    NewConn2 = add_monitor_ref(NewConn, MonitorRef),
                    ok = emysql_conn_mgr:replace_connection_as_available(Conn, NewConn2),
                    NewConn2;
                #emysql_connection{} = NewConn when StayLocked == keep ->
                    NewConn2 = add_monitor_ref(NewConn, MonitorRef),
                    ok = emysql_conn_mgr:replace_connection_as_locked(Conn, NewConn2),
                    NewConn2;
                {'EXIT', Reason} ->
                    DeadConn = Conn#emysql_connection { alive = false, last_test_time = 0 },
                    emysql_conn_mgr:replace_connection_as_available(Conn, DeadConn),
                    {error, {cannot_reopen_in_reset, Reason}}
            end;
        undefined ->
            exit(pool_not_found)
    end.

add_monitor_ref(Conn, MonitorRef) ->
    Conn#emysql_connection{monitor_ref = MonitorRef}.

close_connection(Conn) ->
	%% garbage collect statements
	emysql_statements:remove(Conn#emysql_connection.id),
	ok = gen_tcp:close(Conn#emysql_connection.socket).

ping_connection(Conn, Timeout) ->
  case catch emysql_tcp:send_and_recv_packet(Conn#emysql_connection.socket, <<?COM_PING>>, 0, Timeout) of
    {'EXIT', _} ->
        error;
    _ ->
        ok
  end.

test_connection(Conn, StayLocked) ->
    DefaultTimeout = emysql_app:default_timeout(),
    test_connection(Conn, StayLocked, DefaultTimeout).

test_connection(Conn, StayLocked, Timeout) ->
  case ping_connection(Conn, Timeout) of
    error ->
      case reset_connection(emysql_conn_mgr:pools(), Conn, StayLocked) of
        NewConn when is_record(NewConn, emysql_connection) ->
          NewConn;
        {error, FailedReset} ->
          exit({connection_down, {and_conn_reset_failed, FailedReset}})
      end;
    ok ->
       NewConn = Conn#emysql_connection{last_test_time = now_seconds()},
       case StayLocked of
         pass -> emysql_conn_mgr:replace_connection_as_available(Conn, NewConn);
         keep -> emysql_conn_mgr:replace_connection_as_locked(Conn, NewConn)
       end,
       NewConn
  end.

need_test_connection(Conn) ->
   (Conn#emysql_connection.test_period =:= 0) orelse
     (Conn#emysql_connection.last_test_time =:= 0) orelse
     (Conn#emysql_connection.last_test_time + Conn#emysql_connection.test_period < now_seconds()).

now_seconds() ->
   {M, S, _} = os:timestamp(),
   M * 1000000 + S.

%%--------------------------------------------------------------------
%%% Internal functions
%%--------------------------------------------------------------------

set_params(_, _, [], Result, _) -> Result;
set_params(Connection, Num, Values, _, Timeout) ->
	Packet = set_params_packet(Num, Values),
	emysql_tcp:send_and_recv_packet(Connection#emysql_connection.socket, Packet, 0, Timeout).

set_params_packet(NumStart, Values) ->
	BinValues = [encode(Val, binary) || Val <- Values],
	BinNums = [encode(Num, binary) || Num <- lists:seq(NumStart, NumStart + length(Values) - 1)],
	BinPairs = lists:zip(BinNums, BinValues),
	Parts = [<<"@", NumBin/binary, "=", ValBin/binary>> || {NumBin, ValBin} <- BinPairs], 
	Sets = list_to_binary(join(Parts, <<",">>)),
	<<?COM_QUERY, "SET ", Sets/binary>>.

%% @doc Join elements of list with Sep
%%
%% 1> join([1,2,3], 0).
%% [1,0,2,0,3]

join([], _Sep) -> [];
join(L, Sep) -> join(L, Sep, []).

join([H], _Sep, Acc)  -> lists:reverse([H|Acc]);
join([H|T], Sep, Acc) -> join(T, Sep, [Sep, H|Acc]).

prepare_statement(Connection, StmtName, Timeout) ->
    case emysql_statements:fetch(StmtName, Timeout) of
        undefined ->
            exit(statement_has_not_been_prepared);
        {Version, Statement} ->
            case emysql_statements:version(Connection#emysql_connection.id, StmtName, Timeout) of
                Version ->
                    ok;
                _ ->
                    ok = prepare(Connection, StmtName, Statement, Timeout),
                    emysql_statements:prepare(Connection#emysql_connection.id, StmtName, Version, Timeout)
            end
    end.

% human readable string rep of the server state flag
%% @private
hstate(State) ->
       case (State band ?SERVER_STATUS_AUTOCOMMIT)   of 0 -> ""; _-> "AUTOCOMMIT " end
    ++ case (State band ?SERVER_MORE_RESULTS_EXIST)  of 0 -> ""; _-> "MORE_RESULTS_EXIST " end
    ++ case (State band ?SERVER_QUERY_NO_INDEX_USED) of 0 -> ""; _-> "NO_INDEX_USED " end.

%% @doc Encode a value so that it can be included safely in a MySQL query.
%% @spec encode(term(), list | binary) -> string() | binary() | {error, Error}
encode(null, list) ->
    "null";
encode(undefined, list) ->
    "null";
encode(null, binary)  ->
    <<"null">>;
encode(undefined, binary)  ->
    <<"null">>;
encode(Val, list) when is_binary(Val) ->
    quote(binary_to_list(Val));
encode(Val, binary) when is_atom(Val) ->
    encode(atom_to_list(Val), binary);
encode(Val, binary) when is_list(Val) ->
    list_to_binary(quote(Val));
encode(Val, binary) when is_binary(Val) ->
    list_to_binary(quote(binary_to_list(Val)));
encode(Val, list) when is_list(Val) ->
    quote(Val);
encode(Val, list) when is_integer(Val) ->
    integer_to_list(Val);
encode(Val, binary) when is_integer(Val) ->
    list_to_binary(integer_to_list(Val));
encode(Val, list) when is_float(Val) ->
    [Res] = io_lib:format("~w", [Val]),
    Res;
encode(Val, binary) when is_float(Val) ->
    iolist_to_binary(io_lib:format("~w", [Val]));
encode({datetime, Val}, ReturnType) ->
    encode(Val, ReturnType);
encode({date, Val}, ReturnType) ->
    encode(Val, ReturnType);
encode({time, Val}, ReturnType) ->
    encode(Val, ReturnType);
encode({{Year, Month, Day}, {Hour, Minute, Second}}, list) ->
    Res = io_lib:format("'~4.4.0w-~2.2.0w-~2.2.0w ~2.2.0w:~2.2.0w:~2.2.0w'",
                        [Year, Month, Day, Hour, Minute, Second]),
    lists:flatten(Res);
encode({{_Year, _Month, _Day}, {_Hour, _Minute, _Second}}=Val, binary) ->
    list_to_binary(encode(Val, list));
encode({Time1, Time2, Time3}, list) ->
    Res = two_digits([Time1, Time2, Time3]),
    lists:flatten(Res);
encode({_Time1, _Time2, _Time3}=Val, binary) ->
    list_to_binary(encode(Val, list));
encode(Val, _) ->
    {error, {unrecognized_value, Val}}.
    
%% @private
two_digits(Nums) when is_list(Nums) ->
    [two_digits(Num) || Num <- Nums];
two_digits(Num) ->
    [Str] = io_lib:format("~b", [Num]),
    case length(Str) of
        1 -> [$0 | Str];
        _ -> Str
    end.

%% @doc Quote a string or binary value so that it can be included safely in a
%% MySQL query. For the quoting, a binary is converted to a list and back.
%% For this, it's necessary to know the encoding of the binary.
%% @spec quote(x()) -> x()
%%       x() = list() | binary()
%% @end
%% hd/11,12
quote(String) when is_list(String) ->
    [39 | lists:reverse([39 | quote_loop(String)])]. %% 39 is $'

%% @doc  Make MySQL-safe backslash escapes before 10, 13, \, 26, 34, 39.
%% @spec quote_loop(list()) -> list()
%% @private
%% @end
%% hd/11,12
quote_loop(List) ->
    quote_loop(List, []).

quote_loop([], Acc) ->
    Acc;

quote_loop([0 | Rest], Acc) ->
    quote_loop(Rest, [$0, $\\ | Acc]);

quote_loop([10 | Rest], Acc) ->
    quote_loop(Rest, [$n, $\\ | Acc]);

quote_loop([13 | Rest], Acc) ->
    quote_loop(Rest, [$r, $\\ | Acc]);

quote_loop([$\\ | Rest], Acc) ->
    quote_loop(Rest, [$\\ , $\\ | Acc]);

quote_loop([39 | Rest], Acc) -> %% 39 is $'
    quote_loop(Rest, [39, $\\ | Acc]); %% 39 is $'

quote_loop([34 | Rest], Acc) -> %% 34 is $"
    quote_loop(Rest, [34, $\\ | Acc]); %% 34 is $"

quote_loop([26 | Rest], Acc) ->
    quote_loop(Rest, [$Z, $\\ | Acc]);

quote_loop([C | Rest], Acc) ->
    quote_loop(Rest, [C | Acc]).
