-module(ephp_lib_pcre).

-author('').

-behaviour(ephp_lib).

-export([init_func/0, init_config/0, init_const/0, preg_match/7, preg_replace/7,
         preg_quote/4]).

-define(PREG_OFFSET_CAPTURE, 256).

-include("ephp.hrl").
-include("ephp_parser.hrl").

-spec init_func() -> ephp_lib:php_function_results().
init_func() ->
    [{preg_match,
      [{args,
        {2, 5, false, [string, string, {raw, ephp_array:new()}, {integer, 0}, {integer, 0}]}}]},
     {preg_replace,
      [{args, {3, 5, undefined, [mixed, mixed, mixed, {integer, -1}, {integer, 0}]}}]},
     {preg_quote, [{args, {1, 2, undefined, [string, {string, <<>>}]}}]}].

-spec init_config() -> ephp_lib:php_config_results().
init_config() ->
    [{<<"pcre.backtrack_limit">>, 1000000}, {<<"pcre.recurssion_limit">>, 100000}].

-ifndef(PCRE_VERSION).

  keep it only for some editors ( like VSCode )
-define(PCRE_VERSION, "8.02 2010-03-19").

-endif.

-spec init_const() -> ephp_lib:php_const_results().
init_const() ->
    [{<<"PREG_PATTERN_ORDER">>, 1},
     {<<"PREG_SET_ORDER">>, 2},
     {<<"PREG_OFFSET_CAPTURE">>, ?PREG_OFFSET_CAPTURE},
     {<<"PREG_SPLIT_NO_EMPTY">>, 1},
     {<<"PREG_SPLIT_DELIM_CAPTURE">>, 2},
     {<<"PREG_SPLIT_OFFSET_CAPTURE">>, 4},
     {<<"PREG_NO_ERROR">>, 0},
     {<<"PREG_INTERNAL_ERROR">>, 1},
     {<<"PREG_BACKTRACK_LIMIT_ERROR">>, 2},
     {<<"PREG_RECURSION_LIMIT_ERROR">>, 3},
     {<<"PREG_BAD_UTF8_ERROR">>, 4},
     {<<"PREG_BAD_UTF8_OFFSET_ERROR">>, 5},
     {<<"PCRE_VERSION">>, <<?PCRE_VERSION>>}].

-spec preg_replace(ephp:context_id(),
                   line(),
                   Pattern :: var_value(),
                   Replacement :: var_value(),
                   Subject :: var_value(),
                   Limit :: var_value(),
                   Count :: var_value()) ->
                      binary().
%% TODO: check when Pattern, Replacement and Subject are arrays
preg_replace(_Context,
             _Line,
             {_, Pattern},
             {_, Replacement},
             {_, Subject},
             {_, Limit},
             {undefined, _}) ->
    {RegExp, Flags} = get_parts(Pattern),
    PMFlags =
        case Limit of
            -1 ->
                Flags;
            _ ->
                [{match_limit, Limit} | Flags]
        end,
    Replace = parse_replace(Replacement),
    S = re:replace(Subject, RegExp, Replace, [{return, binary} | PMFlags]),
    iolist_to_binary(S).

-spec preg_match(ephp:context_id(),
                 line(),
                 Pattern :: var_value(),
                 Subject :: var_value(),
                 Matches :: var_value(),
                 Flags :: var_value(),
                 Offset :: var_value()) ->
                    pos_integer().
preg_match(_Context,
           _Line,
           {_, Pattern},
           {_, Subject},
           {undefined, _},
           {_, _},
           {_, Offset}) ->
    {RegExp, Flags} = get_parts(Pattern),
    PMFlags = [{offset, Offset}],
    case re:run(Subject, RegExp, Flags ++ PMFlags) of
        {match, _} ->
            1;
        nomatch ->
            0
    end;
preg_match(Context,
           _Line,
           {_, Pattern},
           {_, Subject},
           {VarMatches, _},
           {_, Flag},
           {_, Offset}) ->
    {RegExp, Flags} = get_parts(Pattern),
    PMFlags = [{capture, all, binary}, {offset, Offset}],
    ValMatches =
        case re:run(Subject, RegExp, Flags ++ PMFlags) of
            {match, Matches} ->
                if Flag =:= ?PREG_OFFSET_CAPTURE ->
                       PMFlags2 = [{capture, all, index}, {offset, Offset}],
                       {match, Idx} = re:run(Subject, RegExp, Flags ++ PMFlags2),
                       Elements =
                           lists:zipwith(fun({I, _}, Chunk) -> ephp_array:from_list([Chunk, I]) end,
                                         Idx,
                                         Matches),
                       ephp_array:from_list(Elements);
                   true ->
                       ephp_array:from_list(Matches)
                end;
            nomatch ->
                ephp_array:new()
        end,
    ephp_context:set(Context, VarMatches, ValMatches),
    ephp_array:size(ValMatches).

-spec preg_quote(ephp:context_id(), line(), Str :: var_value(), Delim :: var_value()) ->
                    binary().
preg_quote(_Context, _Line, {_, Str}, {_, DelimBin}) ->
    Delims = binary_to_list(<<".\\+*?[^]$(){}=!<>|:-", DelimBin/binary>>),
    escape(Str, <<>>, Delims).

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

escape(<<>>, Result, _) ->
    Result;
escape(<<A/utf8, Str/binary>>, StrRes, Delims) ->
    case lists:member(A, Delims) of
        true ->
            escape(Str, <<StrRes/binary, "\\", A/utf8>>, Delims);
        false ->
            escape(Str, <<StrRes/binary, A/utf8>>, Delims)
    end.

get_parts(<<InitDelim:8, Rest/binary>>) ->
    EndDelim = get_end_delimiter(InitDelim),
    {RegExp, RawFlags} = parse_regexp(Rest, EndDelim),
    Flags = parse_flags(RawFlags),
    {RegExp, Flags}.

parse_replace(Replacement) ->
    parse_replace(Replacement, <<>>).

parse_replace(<<>>, Result) ->
    Result;
parse_replace(<<"\\", Rest/binary>>, Result) ->
    parse_replace(Rest, <<Result/binary, "\\\\">>);
parse_replace(<<"${", Rest/binary>>, Result) ->
    case binary:split(Rest, <<"}">>) of
        [Num, NewRest] ->
            parse_replace(NewRest, <<Result/binary, "\\", Num/binary>>);
        _ ->
            %% TODO: error?
            ok
    end;
parse_replace(<<"$", Rest/binary>>, Result) ->
    {NewRest, Num} = parse_number(Rest, <<>>),
    parse_replace(NewRest, <<Result/binary, "\\", Num/binary>>);
parse_replace(<<A/utf8, Rest/binary>>, Result) ->
    parse_replace(Rest, <<Result/binary, A/utf8>>).

parse_number(<<A:8, Rest/binary>>, Result) when A >= 0 andalso A =< 9 ->
    parse_number(Rest, <<Result/binary, A:8>>);
parse_number(Rest, Result) ->
    {Rest, Result}.

parse_regexp(String, EndDelim) ->
    parse_regexp(String, <<>>, EndDelim).

parse_regexp(<<>>, _Result, _EndDelim) ->
    throw({error, enodelim});
parse_regexp(<<EndDelim:8, Rest/binary>>, Result, EndDelim) ->
    {Result, Rest};
parse_regexp(<<"\\", EndDelim:8, Rest/binary>>, Result, EndDelim) ->
    parse_regexp(Rest, <<Result/binary, "\\", EndDelim:8>>, EndDelim);
parse_regexp(<<"(", Rest/binary>>, Result, EndDelim) ->
    {NewResult, NewRest} = parse_regexp(Rest, $)),
    parse_regexp(NewRest, <<Result/binary, "(", NewResult/binary, ")">>, EndDelim);
parse_regexp(<<"[", Rest/binary>>, Result, EndDelim) ->
    {NewResult, NewRest} = parse_regexp(Rest, $]),
    parse_regexp(NewRest, <<Result/binary, "[", NewResult/binary, "]">>, EndDelim);
parse_regexp(<<"<", Rest/binary>>, Result, EndDelim) ->
    {NewResult, NewRest} = parse_regexp(Rest, $>),
    parse_regexp(NewRest, <<Result/binary, "<", NewResult/binary, ">">>, EndDelim);
parse_regexp(<<"{", Rest/binary>>, Result, EndDelim) ->
    {NewResult, NewRest} = parse_regexp(Rest, $}),
    parse_regexp(NewRest, <<Result/binary, "{", NewResult/binary, "}">>, EndDelim);
parse_regexp(<<A:8, Rest/binary>>, Result, EndDelim) ->
    parse_regexp(Rest, <<Result/binary, A:8>>, EndDelim).

parse_flags(RawFlags) ->
    parse_flags(RawFlags, []).

parse_flags(<<"i", RestFlags/binary>>, Flags) ->
    parse_flags(RestFlags, [caseless | Flags]);
parse_flags(<<"m", RestFlags/binary>>, Flags) ->
    parse_flags(RestFlags, [multiline | Flags]);
parse_flags(<<"s", RestFlags/binary>>, Flags) ->
    parse_flags(RestFlags, [dotall | Flags]);
parse_flags(<<"x", RestFlags/binary>>, Flags) ->
    parse_flags(RestFlags, [extended | Flags]);
parse_flags(<<"A", RestFlags/binary>>, Flags) ->
    parse_flags(RestFlags, [anchored | Flags]);
parse_flags(<<"D", RestFlags/binary>>, Flags) ->
    parse_flags(RestFlags, [dollar_endonly | Flags]);
parse_flags(<<"U", RestFlags/binary>>, Flags) ->
    parse_flags(RestFlags, [ungreedy | Flags]);
parse_flags(<<"J", RestFlags/binary>>, Flags) ->
    parse_flags(RestFlags, [dupnames | Flags]);
parse_flags(<<"u", RestFlags/binary>>, Flags) ->
    parse_flags(RestFlags, [unicode, bsr_unicode | Flags]);
parse_flags(<<"X", RestFlags/binary>>, Flags) ->
  TODO
    parse_flags(RestFlags, Flags);
parse_flags(<<"S", RestFlags/binary>>, Flags) ->
  TODO
    parse_flags(RestFlags, Flags);
parse_flags(<<>>, Flags) ->
    Flags.

get_end_delimiter($() ->
    $);
get_end_delimiter($<) ->
    $>;
get_end_delimiter($[) ->
    $];
get_end_delimiter(${) ->
    $};
get_end_delimiter($\\) ->
    throw({error, edelimiter});
get_end_delimiter(A) when ?IS_ALPHA(A) ->
    throw({error, edelimiter});
get_end_delimiter(A) when ?IS_NUMBER(A) ->
    throw({error, edelimiter});
get_end_delimiter(A) when ?IS_SPACE(A) ->
    throw({error, edelimiter});
get_end_delimiter(A) ->
    A.

 TODO: check when Pattern, Replacement and Subject are arrays
------------------------------------------------------------------------------
 TODO: error?
-module(ephp_lib_pcre).

-author('').

-behaviour(ephp_lib).

-export([init_func/0, init_config/0, init_const/0, preg_match/7, preg_replace/7,
         preg_quote/4]).

-define(PREG_OFFSET_CAPTURE, 256).

-include("ephp.hrl").
-include("ephp_parser.hrl").

-spec init_func() -> ephp_lib:php_function_results().
init_func() ->
    [{preg_match,
      [{args,
        {2, 5, false, [string, string, {raw, ephp_array:new()}, {integer, 0}, {integer, 0}]}}]},
     {preg_replace,
      [{args, {3, 5, undefined, [mixed, mixed, mixed, {integer, -1}, {integer, 0}]}}]},
     {preg_quote, [{args, {1, 2, undefined, [string, {string, <<>>}]}}]}].

-spec init_config() -> ephp_lib:php_config_results().
init_config() ->
    [{<<"pcre.backtrack_limit">>, 1000000}, {<<"pcre.recurssion_limit">>, 100000}].

-ifndef(PCRE_VERSION).

  keep it only for some editors ( like VSCode )
-define(PCRE_VERSION, "8.02 2010-03-19").

-endif.

-spec init_const() -> ephp_lib:php_const_results().
init_const() ->
    [{<<"PREG_PATTERN_ORDER">>, 1},
     {<<"PREG_SET_ORDER">>, 2},
     {<<"PREG_OFFSET_CAPTURE">>, ?PREG_OFFSET_CAPTURE},
     {<<"PREG_SPLIT_NO_EMPTY">>, 1},
     {<<"PREG_SPLIT_DELIM_CAPTURE">>, 2},
     {<<"PREG_SPLIT_OFFSET_CAPTURE">>, 4},
     {<<"PREG_NO_ERROR">>, 0},
     {<<"PREG_INTERNAL_ERROR">>, 1},
     {<<"PREG_BACKTRACK_LIMIT_ERROR">>, 2},
     {<<"PREG_RECURSION_LIMIT_ERROR">>, 3},
     {<<"PREG_BAD_UTF8_ERROR">>, 4},
     {<<"PREG_BAD_UTF8_OFFSET_ERROR">>, 5},
     {<<"PCRE_VERSION">>, <<?PCRE_VERSION>>}].

-spec preg_replace(ephp:context_id(),
                   line(),
                   Pattern :: var_value(),
                   Replacement :: var_value(),
                   Subject :: var_value(),
                   Limit :: var_value(),
                   Count :: var_value()) ->
                      binary().
preg_replace(_Context,
             _Line,
             {_, Pattern},
             {_, Replacement},
             {_, Subject},
             {_, Limit},
             {undefined, _}) ->
    {RegExp, Flags} = get_parts(Pattern),
    PMFlags =
        case Limit of
            -1 ->
                Flags;
            _ ->
                [{match_limit, Limit} | Flags]
        end,
    Replace = parse_replace(Replacement),
    S = re:replace(Subject, RegExp, Replace, [{return, binary} | PMFlags]),
    iolist_to_binary(S).

-spec preg_match(ephp:context_id(),
                 line(),
                 Pattern :: var_value(),
                 Subject :: var_value(),
                 Matches :: var_value(),
                 Flags :: var_value(),
                 Offset :: var_value()) ->
                    pos_integer().
preg_match(_Context,
           _Line,
           {_, Pattern},
           {_, Subject},
           {undefined, _},
           {_, _},
           {_, Offset}) ->
    {RegExp, Flags} = get_parts(Pattern),
    PMFlags = [{offset, Offset}],
    case re:run(Subject, RegExp, Flags ++ PMFlags) of
        {match, _} ->
            1;
        nomatch ->
            0
    end;
preg_match(Context,
           _Line,
           {_, Pattern},
           {_, Subject},
           {VarMatches, _},
           {_, Flag},
           {_, Offset}) ->
    {RegExp, Flags} = get_parts(Pattern),
    PMFlags = [{capture, all, binary}, {offset, Offset}],
    ValMatches =
        case re:run(Subject, RegExp, Flags ++ PMFlags) of
            {match, Matches} ->
                if Flag =:= ?PREG_OFFSET_CAPTURE ->
                       PMFlags2 = [{capture, all, index}, {offset, Offset}],
                       {match, Idx} = re:run(Subject, RegExp, Flags ++ PMFlags2),
                       Elements =
                           lists:zipwith(fun({I, _}, Chunk) -> ephp_array:from_list([Chunk, I]) end,
                                         Idx,
                                         Matches),
                       ephp_array:from_list(Elements);
                   true ->
                       ephp_array:from_list(Matches)
                end;
            nomatch ->
                ephp_array:new()
        end,
    ephp_context:set(Context, VarMatches, ValMatches),
    ephp_array:size(ValMatches).

-spec preg_quote(ephp:context_id(), line(), Str :: var_value(), Delim :: var_value()) ->
                    binary().
preg_quote(_Context, _Line, {_, Str}, {_, DelimBin}) ->
    Delims = binary_to_list(<<".\\+*?[^]$(){}=!<>|:-", DelimBin/binary>>),
    escape(Str, <<>>, Delims).

  Internal functions

escape(<<>>, Result, _) ->
    Result;
escape(<<A/utf8, Str/binary>>, StrRes, Delims) ->
    case lists:member(A, Delims) of
        true ->
            escape(Str, <<StrRes/binary, "\\", A/utf8>>, Delims);
        false ->
            escape(Str, <<StrRes/binary, A/utf8>>, Delims)
    end.

get_parts(<<InitDelim:8, Rest/binary>>) ->
    EndDelim = get_end_delimiter(InitDelim),
    {RegExp, RawFlags} = parse_regexp(Rest, EndDelim),
    Flags = parse_flags(RawFlags),
    {RegExp, Flags}.

parse_replace(Replacement) ->
    parse_replace(Replacement, <<>>).

parse_replace(<<>>, Result) ->
    Result;
parse_replace(<<"\\", Rest/binary>>, Result) ->
    parse_replace(Rest, <<Result/binary, "\\\\">>);
parse_replace(<<"${", Rest/binary>>, Result) ->
    case binary:split(Rest, <<"}">>) of
        [Num, NewRest] ->
            parse_replace(NewRest, <<Result/binary, "\\", Num/binary>>);
        _ ->
            ok
    end;
parse_replace(<<"$", Rest/binary>>, Result) ->
    {NewRest, Num} = parse_number(Rest, <<>>),
    parse_replace(NewRest, <<Result/binary, "\\", Num/binary>>);
parse_replace(<<A/utf8, Rest/binary>>, Result) ->
    parse_replace(Rest, <<Result/binary, A/utf8>>).

parse_number(<<A:8, Rest/binary>>, Result) when A >= 0 andalso A =< 9 ->
    parse_number(Rest, <<Result/binary, A:8>>);
parse_number(Rest, Result) ->
    {Rest, Result}.

parse_regexp(String, EndDelim) ->
    parse_regexp(String, <<>>, EndDelim).

parse_regexp(<<>>, _Result, _EndDelim) ->
    throw({error, enodelim});
parse_regexp(<<EndDelim:8, Rest/binary>>, Result, EndDelim) ->
    {Result, Rest};
parse_regexp(<<"\\", EndDelim:8, Rest/binary>>, Result, EndDelim) ->
    parse_regexp(Rest, <<Result/binary, "\\", EndDelim:8>>, EndDelim);
parse_regexp(<<"(", Rest/binary>>, Result, EndDelim) ->
    {NewResult, NewRest} = parse_regexp(Rest, $)),
    parse_regexp(NewRest, <<Result/binary, "(", NewResult/binary, ")">>, EndDelim);
parse_regexp(<<"[", Rest/binary>>, Result, EndDelim) ->
    {NewResult, NewRest} = parse_regexp(Rest, $]),
    parse_regexp(NewRest, <<Result/binary, "[", NewResult/binary, "]">>, EndDelim);
parse_regexp(<<"<", Rest/binary>>, Result, EndDelim) ->
    {NewResult, NewRest} = parse_regexp(Rest, $>),
    parse_regexp(NewRest, <<Result/binary, "<", NewResult/binary, ">">>, EndDelim);
parse_regexp(<<"{", Rest/binary>>, Result, EndDelim) ->
    {NewResult, NewRest} = parse_regexp(Rest, $}),
    parse_regexp(NewRest, <<Result/binary, "{", NewResult/binary, "}">>, EndDelim);
parse_regexp(<<A:8, Rest/binary>>, Result, EndDelim) ->
    parse_regexp(Rest, <<Result/binary, A:8>>, EndDelim).

parse_flags(RawFlags) ->
    parse_flags(RawFlags, []).

parse_flags(<<"i", RestFlags/binary>>, Flags) ->
    parse_flags(RestFlags, [caseless | Flags]);
parse_flags(<<"m", RestFlags/binary>>, Flags) ->
    parse_flags(RestFlags, [multiline | Flags]);
parse_flags(<<"s", RestFlags/binary>>, Flags) ->
    parse_flags(RestFlags, [dotall | Flags]);
parse_flags(<<"x", RestFlags/binary>>, Flags) ->
    parse_flags(RestFlags, [extended | Flags]);
parse_flags(<<"A", RestFlags/binary>>, Flags) ->
    parse_flags(RestFlags, [anchored | Flags]);
parse_flags(<<"D", RestFlags/binary>>, Flags) ->
    parse_flags(RestFlags, [dollar_endonly | Flags]);
parse_flags(<<"U", RestFlags/binary>>, Flags) ->
    parse_flags(RestFlags, [ungreedy | Flags]);
parse_flags(<<"J", RestFlags/binary>>, Flags) ->
    parse_flags(RestFlags, [dupnames | Flags]);
parse_flags(<<"u", RestFlags/binary>>, Flags) ->
    parse_flags(RestFlags, [unicode, bsr_unicode | Flags]);
parse_flags(<<"X", RestFlags/binary>>, Flags) ->
  TODO
    parse_flags(RestFlags, Flags);
parse_flags(<<"S", RestFlags/binary>>, Flags) ->
  TODO
    parse_flags(RestFlags, Flags);
parse_flags(<<>>, Flags) ->
    Flags.

get_end_delimiter($() ->
    $);
get_end_delimiter($<) ->
    $>;
get_end_delimiter($[) ->
    $];
get_end_delimiter(${) ->
    $};
get_end_delimiter($\\) ->
    throw({error, edelimiter});
get_end_delimiter(A) when ?IS_ALPHA(A) ->
    throw({error, edelimiter});
get_end_delimiter(A) when ?IS_NUMBER(A) ->
    throw({error, edelimiter});
get_end_delimiter(A) when ?IS_SPACE(A) ->
    throw({error, edelimiter});
get_end_delimiter(A) ->
    A.
