%%%  This program is free software; you can redistribute it and/or modify
%%%  it under the terms of the GNU General Public License as published by
%%%  the Free Software Foundation; either version 2 of the License, or
%%%  (at your option) any later version.
%%%
%%%  This program is distributed in the hope that it will be useful,
%%%  but WITHOUT ANY WARRANTY; without even the implied warranty of
%%%  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
%%%  GNU General Public License for more details.
%%%
%%%  You should have received a copy of the GNU General Public License
%%%  along with this program; if not, write to the Free Software
%%%  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA.
%%%

%%%  In addition, as a special exception, you have the permission to
%%%  link the code of this program with any library released under
%%%  the EPL license and distribute linked combinations including
%%%  the two.

%%% File    : ts_search.erl
%%% Author  : Mickael Remond <mickael.remond@erlang-fr.org>
%%% Description : Add dynamic / Differenciated parameters in tsung
%%%               request and response
%%%               The function subst is intended to be called for each
%%%               relevant field in ts_protocol implementation.
%%% Created : 22 Mar 2004 by Mickael Remond <mickael.remond@erlang-fr.org>

%%% Nicolas Niclausse: add dynamic variable and matching

-module(ts_search).
-vc('$Id$ ').

-export([subst/2, match/4, parse_dynvar/2]).

-include("ts_profile.hrl").

%% ----------------------------------------------------------------------
%% Function: subst/2
%% Purpose: search into a given string and replace %%Mod:Fun%% strings
%%          by the result of the call to Mod:Fun(Pid) where Pid is the
%%          Pid of the client The substitution tag are intended to
%%          be used in tsung.xml scenarii files.
%% Returns: new string
%% ----------------------------------------------------------------------
subst(Int, _DynVar) when is_integer(Int) ->
    Int;
subst(Atom, _DynVar) when is_atom(Atom) ->
    Atom;
subst(Binary, DynVar) when is_binary(Binary) ->
    list_to_binary(subst(binary_to_list(Binary), DynVar));
subst(String, DynVar) ->
    subst(String, DynVar, []).

subst([], _DynVar, Acc) ->
    lists:reverse(Acc);
subst([$%,$%,$_|Rest], DynVar, Acc) ->
    extract_variable(Rest, DynVar, Acc, []);
subst([$%,$%|Rest], DynVar, Acc) ->
    extract_module(Rest,  DynVar, Acc, []);
subst([H|Tail], DynVar, Acc) ->
    subst(Tail, DynVar, [H|Acc]).

%% Search for the module string in the subst markup
extract_module([],_DynVar, Acc,_) ->
    lists:reverse(Acc);
extract_module([$:|Tail],DynVar, Acc, Mod) ->
    ?DebugF("found module name: ~p~n",[lists:reverse(Mod)]),
    extract_function(Tail,DynVar, Acc,lists:reverse(Mod),[]);
extract_module([H|Tail],DynVar, Acc, Mod) ->
    extract_module(Tail,DynVar, Acc,[H|Mod]).

%% Search for the module string in the subst markup
extract_variable([],_DynVar,Acc,_) ->
    lists:reverse(Acc);
extract_variable([$%,$%|Tail], DynVar, Acc, Var) ->
    VarName = list_to_atom(lists:reverse(Var)),
    case ts_dynvars:lookup(VarName,DynVar) of
        {ok, ResultTmp} ->
            Result=ts_utils:term_to_list(ResultTmp),
            ?DebugF("found value ~p for name ~p~n",[Result,VarName]),
            subst(Tail, DynVar,lists:reverse(Result) ++ Acc);
        false ->
            ?LOGF("DynVar: no value found for var ~p~n",[VarName],?WARN),
            subst(Tail, DynVar,lists:reverse("undefined") ++ Acc)
    end;

extract_variable([H|Tail],DynVar,Acc,Mod) ->
    extract_variable(Tail,DynVar,Acc,[H|Mod]).

%% Search for the function string and do the real substitution before
%% keeping on the parsing
extract_function([], _DynVar, Acc, _Mod, _Fun) ->
    lists:reverse(Acc);
extract_function([$%,$%|Tail], DynVar, Acc, Mod, Fun) ->
    ?DebugF("found function name: ~p~n",[lists:reverse(Fun)]),
    Module = list_to_atom(Mod),
    Function = list_to_atom(lists:reverse(Fun)),
    Result = case Module:Function({self(), DynVar }) of
                 Int when is_integer(Int) ->
                     lists:reverse(integer_to_list(Int));
                 Str when is_list(Str) ->
                     Str;
                 _Val ->
                     ?LOGF("extract fun:bad result ~p~n",[_Val],?WARN),
                     []
             end,
    subst(Tail, DynVar, lists:reverse(Result) ++ Acc);

extract_function([H|Tail],DynVar,  Acc, Mod, Fun) ->
    extract_function(Tail, DynVar, Acc, Mod, [H|Fun]).

%%----------------------------------------------------------------------
%% @spec match(Match::#match{}, Data::binary() | list, {Counts::integer(),
%% Max::integer(), SessionId::integer(),UserId::integer()}, Dynvars::term()
%%  ) ->  Count::integer()
%% @doc search for regexp in Data; send result to ts_mon
%% @end
%%----------------------------------------------------------------------
match([], _Data, {Count, _MaxC, _SessionId, _UserId}, _DynVars) -> Count;
match([Match=#match{'skip_headers'=http}|Tail], Data, Counts,DynVars) when is_binary(Data)->
    %% keep http body only
    case re:run(Data,"\\r\\n\\r\\n(.*)",[{capture,all_but_first,binary},dotall]) of
        {match,[NewData]} ->
            match([Match#match{'skip_headers'=no}|Tail], NewData, Counts, DynVars);
        _ ->
            ?LOGF("Skip http headers failure, data was: ~p ~n",[Data], ?ERR),
            match([Match#match{'skip_headers'=no}|Tail], Data, Counts, DynVars)
        end;

match([Match=#match{'apply_to_content'=undefined}|Tail], Data, Counts,DynVars) ->
    ?DebugF("Matching Data size ~p; apply undefined~n",[size(Data)]),
    match([Match|Tail], Data, Counts, [],DynVars);
match([Match=#match{'apply_to_content'={Module,Fun}}|Tail], Data, Counts,DynVars) ->
    ?DebugF("Matching Data size ~p; apply ~p:~p~n",[size(Data),Module,Fun]),
    NewData = Module:Fun(Data),
    ?DebugF("Match: apply result =~p~n",[NewData]),
    match([Match|Tail], NewData, Counts, [],DynVars).

%% @spec match(Match::#match{}, Data::binary() | list(), Count::tuple(),
%%             Stats::list(), DynVars::term()) -> Count::integer()
match([], _Data, {Count,_, _,_}, Stats, _) ->
    %% all matches done, add stats, and return Count unchanged (continue)
    ts_mon:add(Stats),
    Count;
match([Match=#match{regexp=RawRegExp,subst=Subst, do=Action, 'when'=When}
       |Tail], Data,Counts,Stats,DynVars)->
    RegExp  = case Subst of
        true -> subst(RawRegExp, DynVars);
        _    -> RawRegExp
    end,
    ?DebugF("RegExp was ~p and now is ~p after substitution (~p)~n",[RawRegExp,RegExp,Subst]),
    case re:run(Data, RegExp) of
        {When,_} ->
            ?LOGF("Ok Match (regexp=~p) do=~p~n",[RegExp,Action], ?INFO),
            setcount(Match, Counts, [{count, match}| Stats], Data);
        When -> % nomatch
            ?LOGF("Bad Match (regexp=~p) do=~p~n",[RegExp, Action], ?INFO),
            setcount(Match, Counts, [{count, nomatch} | Stats],Data);
        {match,_} -> % match but when=nomatch
            ?LOGF("Ok Match (regexp=~p)~n",[RegExp], ?INFO),
            case Action of
                loop    -> put(loop_count, 0);
                restart -> put(restart_count, 0);
                _       -> ok
            end,
            match(Tail, Data, Counts, [{count, match} | Stats],DynVars);
        nomatch -> % nomatch but when=match
            ?LOGF("Bad Match (regexp=~p)~n",[RegExp], ?INFO),
            case Action of
                loop    -> put(loop_count, 0);
                restart -> put(restart_count, 0);
                _       -> ok
            end,
            match(Tail, Data, Counts,[{count, nomatch} | Stats],DynVars);
        {error,_Error} ->
            ?LOGF("Error while matching: bad REGEXP (~p)~n", [RegExp], ?ERR),
            match(Tail, Data, Counts,[{count, badregexp} | Stats],DynVars)
    end.

%%----------------------------------------------------------------------
%% Func: setcount/3
%% Args:  #match, Counts, Stats
%% Update the request counter after a match:
%%   - if loop is true, we must start again the same request, so add 1 to count
%%   - if restart is true, we must start again the whole session, set count to MaxCount
%%   - if stop is true, set count to 0
%%----------------------------------------------------------------------
setcount(#match{do=continue}, {Count, _MaxC, _SessionId, _UserId}, Stats,_)->
    ts_mon:add(Stats),
    Count;
setcount(#match{do=log}, {Count, MaxC, SessionId, UserId}, Stats,_)->
    ts_mon:add_match(Stats,{UserId,SessionId,MaxC-Count}),
    Count;
setcount(#match{do=dump}, {Count, MaxC, SessionId, UserId}, Stats,Data)->
    ts_mon:add_match(Stats,{UserId,SessionId,MaxC-Count, Data}),
    Count;
setcount(#match{do=restart, max_restart=MaxRestart}, {Count, MaxC,SessionId,UserId}, Stats,_)->
    CurRestart = get(restart_count),
    Ids={UserId,SessionId,MaxC-Count},
    ?LOGF("Restart on (no)match ~p~n",[CurRestart], ?INFO),
    case CurRestart of
        undefined ->
            put(restart_count,1),
            ts_mon:add_match([{count, match_restart} | Stats],Ids),
            MaxC ;
        Val when Val > MaxRestart ->
            ?LOG("Max restart reached, abort ! ~n", ?WARN),
            ts_mon:add_match([{count, match_restart_abort} | Stats],Ids),
            0;
        Val ->
            put(restart_count, Val +1),
            ts_mon:add_match([{count, match_restart} | Stats],Ids),
            MaxC
    end;
setcount(#match{do=loop,loop_back=Back,max_loop=MaxLoop,sleep_loop=Sleep},{Count,_MaxC,_SessionId,_UserId},Stats,_)->
    CurLoop = get(loop_count),
    ?LOGF("Loop on (no)match ~p~n",[CurLoop], ?INFO),
    ts_mon:add([{count, match_loop} | Stats]),
    case CurLoop of
        undefined ->
            put(loop_count,1),
            timer:sleep(Sleep),
            Count +1 + Back ;
        Val when Val >= MaxLoop ->
            ?LOG("Max Loop reached, abort loop on request! ~n", ?WARN),
            put(loop_count, 0),
            Count;
        Val ->
            put(loop_count, Val +1),
            timer:sleep(Sleep),
            Count + 1 + Back
    end;
setcount(#match{do=abort}, {Count,MaxC,SessionId,UserId}, Stats,_) ->
    ts_mon:add_match([{count, match_stop} | Stats],{UserId,SessionId,MaxC-Count}),
    0.

%%----------------------------------------------------------------------
%% Func: parse_dynvar/2
%% Args: DynVarSpecs, Data
%% Purpose: look for dynamic variables in Data
%% Returns: DynVars (List)
%%----------------------------------------------------------------------
parse_dynvar([], _Data) -> ts_dynvars:new();
parse_dynvar(DynVarSpecs, Data)  when is_binary(Data) ->
    ?DebugF("Parsing Dyn Variable (specs=~p); data is ~p~n",[DynVarSpecs,Data]),
    parse_dynvar(DynVarSpecs,Data, undefined,undefined,[]);
parse_dynvar(DynVarSpecs, {_,_,_,Data})  when is_binary(Data) ->
    ?DebugF("Parsing Dyn Variable (specs=~p); data is ~p~n",[DynVarSpecs,Data]),
    parse_dynvar(DynVarSpecs,Data, undefined,undefined,[]);
parse_dynvar(DynVarSpecs, {_,_,_,Data})  when is_list(Data) ->
    ?DebugF("Parsing Dyn Variable (specs=~p); data is ~p~n",[DynVarSpecs,Data]),
    parse_dynvar(DynVarSpecs,list_to_binary(Data), undefined,undefined,[]);
parse_dynvar(DynVarSpecs, _Data)  ->
    ?LOGF("Error while Parsing dyn Variable(~p)~n",[DynVarSpecs],?WARN),
    ts_dynvars:new().

% parse_dynvar(DynVars,BinaryData,ListData,TreeData,Accum)
%            ListData and TreeData are lazy computed when needed by
%            regexp or xpath variables respectively
parse_dynvar([],_Binary , _String,_Tree, DynVars) -> DynVars;



parse_dynvar(D=[{re,_, _}| _],Binary,undefined,Tree,DynVars) ->
    parse_dynvar(D,Binary,Binary,Tree,DynVars);
parse_dynvar([{re,VarName, RegExp}| DynVarsSpecs],Binary,Data,Tree,DynVars) ->
    case re:run(Data, RegExp,[{capture,[1],binary}]) of
        {match,[Value]} ->
            ?LOGF("DynVar (RE): Match (~p=~p) ~n",[VarName, Value], ?INFO),
            parse_dynvar(DynVarsSpecs, Binary,Data,Tree,
                            ts_dynvars:set(VarName,Value,DynVars));
        nomatch ->
            ?LOGF("Dyn Var (RE): no Match (varname=~p), ~n",[VarName], ?WARN),
            ?LOGF("Regexp was: ~p ~n",[RegExp], ?INFO),
            parse_dynvar(DynVarsSpecs, Binary,Data,Tree, ts_dynvars:set(VarName,"",DynVars))
    end;

parse_dynvar(D=[{xpath,_VarName, _Expr}| _DynVarsSpecs],
                Binary,String,undefined,DynVars) ->
    Body = extract_body(Binary),
    ToParse = case bit_size(Body) of
                  0 ->
                      Binary;
                  _ ->
                      Body
              end,
    try mochiweb_html:parse(ToParse) of
        Tree ->
            parse_dynvar(D,Binary,String,Tree,DynVars)
    catch
        Type:Exp ->
            ?LOGF("Page couldn't be parsed:(~p:~p) ~n Page:~p~n",
                    [Type,Exp,Binary],?ERR),
            parse_dynvar(D,Binary,String,xpath_error,DynVars)
    end;

parse_dynvar(D=[{jsonpath,_VarName, _Expr}| _DynVarsSpecs],
                Binary,String,undefined,DynVars) ->
    Body = extract_body(Binary),
    try mochijson2:decode(Body) of
        JSON ->
            ?LOGF("JSON decode: ~p~n", [JSON],?DEB),
            parse_dynvar(D,Binary,String,JSON,DynVars)
    catch
        Type:Exp ->
            ?LOGF("JSON couldn't be parsed:(~p:~p) ~n Page:~p~n",
                    [Type,Exp,Binary],?ERR),
            parse_dynvar(D,Binary,String,json_error,DynVars)
    end;

parse_dynvar(D=[{pgsql_expr,_VarName, _Expr}| _DynVarsSpecs],
                Binary,String,undefined,DynVars) ->
    Pairs=ts_pgsql:to_pairs(Binary),
    parse_dynvar(D,Binary,String,Pairs,DynVars);

parse_dynvar([{xpath,VarName,_Expr}|DynVarsSpecs],Binary,String,xpath_error,DynVars)->
    ?LOGF("Couldn't execute XPath: page not parsed (varname=~p)~n",
          [VarName],?ERR),
    parse_dynvar(DynVarsSpecs, Binary,String,xpath_error,DynVars);

parse_dynvar([{jsonpath,VarName,_Expr}|DynVarsSpecs],Binary,String,json_error,DynVars)->
    ?LOGF("Couldn't execute JSONPath: page not parsed (varname=~p)~n",
          [VarName],?ERR),
    parse_dynvar(DynVarsSpecs, Binary,String,json_error,DynVars);

parse_dynvar([{pgsql_expr,VarName,_Expr}|DynVarsSpecs],Binary,String,pgsql_error,DynVars)->
    ?LOGF("Couldn't decode pgsql expr from PGSQL binary (varname=~p)~n", [VarName],?ERR),
    parse_dynvar(DynVarsSpecs, Binary,String,json_error,DynVars);

parse_dynvar([{xpath,VarName, Expr}| DynVarsSpecs],Binary,String,Tree,DynVars)->
    Value = mochiweb_xpath:execute(Expr,Tree),
    ?DebugF("Xpath result: ~p~n", [Value]),
    case Value of
        [] -> ?LOGF("Dyn Var: no Match (varname=~p), ~n",[VarName],?WARN);
        _  -> ?LOGF("Dyn Var: Match (~p=~p), ~n",[VarName,Value],?INFO)
    end,
    parse_dynvar(DynVarsSpecs, Binary,String,Tree,ts_dynvars:set(VarName,Value,DynVars));

parse_dynvar([{jsonpath,VarName, Expr}| DynVarsSpecs],Binary,String,JSON,DynVars)->
    Values = ts_utils:jsonpath(Expr,JSON),
    case Values of
        undefined -> ?LOGF("Dyn Var: no Match (varname=~p), ~n",[VarName],?WARN);
        _  -> ?LOGF("Dyn Var: Match (~p=~p), ~n",[VarName,Values],?INFO)
    end,
    parse_dynvar(DynVarsSpecs, Binary,String,JSON,ts_dynvars:set(VarName,Values,DynVars));

parse_dynvar([{pgsql_expr,VarName, Expr}| DynVarsSpecs],Binary,String,PGSQL,DynVars)->
    Values = ts_pgsql:find_pair(Expr,PGSQL),
    case Values of
        undefined -> ?LOGF("Dyn Var: no Match (varname=~p), ~n",[VarName],?WARN);
        _  -> ?LOGF("Dyn Var: Match (~p=~p), ~n",[VarName,Values],?INFO)
    end,
    parse_dynvar(DynVarsSpecs, Binary,String,PGSQL,ts_dynvars:set(VarName,Values,DynVars));

parse_dynvar(Args, _Binary,_String,_Tree, _DynVars) ->
    ?LOGF("Bad args while parsing Dyn Var (~p)~n", [Args], ?ERR),
    [].

extract_body(Data) ->
    case re:run(Data,"\r\n\r\n(.*)$",[{capture,all_but_first,binary},dotall]) of
        nomatch        -> Data;
        {match, [Val]} -> Val;
        _              -> Data
    end.
