src/support/z_dispatcher.erl

%% @author Marc Worrell <marc@worrell.nl>
%% @copyright 2009-2025 Marc Worrell
%% @doc Manage dispatch lists (aka definitions for url patterns). Constructs named urls from dispatch lists.
%% @end

%% Copyright 2009-2025 Marc Worrell
%%
%% Licensed under the Apache License, Version 2.0 (the "License");
%% you may not use this file except in compliance with the License.
%% You may obtain a copy of the License at
%%
%%     http://www.apache.org/licenses/LICENSE-2.0
%%
%% Unless required by applicable law or agreed to in writing, software
%% distributed under the License is distributed on an "AS IS" BASIS,
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
%% See the License for the specific language governing permissions and
%% limitations under the License.

-module(z_dispatcher).
-author("Marc Worrell <marc@worrell.nl>").

-behaviour(gen_server).

%% gen_server exports
-export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]).
-export([start_link/1]).

%% z_dispatch exports
-export([
    dispatcher_args/0,
    url_for/2,
    url_for/3,
    url_for/4,
    hostname/1,
    hostname_port/1,
    hostname_ssl_port/1,
    abs_url/2,
    dispatchinfo/1,
    update/1,
    reload/1,
    reload/2,
    drop_port/1
]).

-include_lib("zotonic.hrl").

-record(state, {
    dispatch_list :: [ z_sites_dispatcher:dispatch_rule() ],
    page_paths :: #{ binary() | atom() => z_sites_dispatcher:dispatch_rsc_rule() },
    lookup :: #{  },
    context :: z:context(),
    site :: atom(),
    hostname :: binary() | undefined,
    hostname_port :: binary() | undefined,
    hostname_ssl_port :: binary() | undefined,
    smtphost :: binary() | undefined,
    hostalias :: list( binary() ),
    redirect = true
}).

-record(dispatch_url, {url, dispatch_options}).

%%====================================================================
%% API
%%====================================================================

%% @doc A list of dispatch rule arguments that shouldn't be considered with redirects.
%%      Used by controller_file_id and controller_redirect
%% TODO: this behaviour should be changed to an _inclusive_ list instead of a filter list
dispatcher_args() ->
    [
        is_permanent, dispatch, q, qargs,
        zotonic_dispatch, ssl, protocol, session_id, set_session_id,
        zotonic_dispatch_file, zotonic_dispatch_module,
        auth_options, auth_expires, csp_nonce
    ].

%% @doc Starts the dispatch server
start_link(Site) ->
    Name = name(Site),
    gen_server:start_link({local, Name}, ?MODULE, Site, []).

%% @doc Construct an uri from a named dispatch, assuming no parameters. Uses html escape.
-spec url_for(Name, Context) -> Url when
    Name :: atom() | binary(),
    Context :: z:context(),
    Url :: binary() | undefined.
url_for(Name, Context) ->
    return_url(
        opt_abs_url(
            rewrite(make_url_for(Name, [], html, Context),
                    Name, [], Context),
            Name, [], Context)).


%% @doc Construct an uri from a named dispatch and the parameters. Uses html escape.
-spec url_for(Name, Args, Context) -> Url when
    Name :: atom() | binary(),
    Args :: proplists:proplist(),
    Context :: z:context(),
    Url :: binary() | undefined.
url_for(Name, Args, Context) ->
    Args1 = append_extra_args(Args, Context),
    return_url(
        opt_abs_url(
            rewrite(make_url_for(Name, Args1, html, Context),
                    Name, Args1, Context),
            Name, Args1, Context)).


%% @doc Construct an uri from a named dispatch and the parameters
-spec url_for(Name, Args, Escape, Context) -> Url when
    Name :: atom() | binary(),
    Args :: proplists:proplist(),
    Escape :: html | xml | none,
    Context :: z:context(),
    Url :: binary() | undefined.
url_for(Name, Args, Escape, Context) ->
    Args1 = append_extra_args(Args, Context),
    return_url(
        opt_abs_url(
            rewrite(make_url_for(Name, Args1, Escape, Context),
                    Name, Args1, Context),
            Name, Args1, Context)).


%% @doc Fetch the preferred hostname for this site
-spec hostname( z:context() ) -> binary() | undefined.
hostname(#context{ dispatcher = Dispatcher }) ->
    try
        gen_server:call(Dispatcher, 'hostname', infinity)
    catch
        exit:{noproc, {gen_server, call, _}} ->
            undefined
    end.

%% @doc Fetch the preferred hostname, including port, for this site
-spec hostname_port( z:context() ) -> binary() | undefined.
hostname_port(#context{dispatcher=Dispatcher}) ->
    try
        gen_server:call(Dispatcher, 'hostname_port', infinity)
    catch
        exit:{noproc, {gen_server, call, _}} ->
            undefined
    end.

%% @doc Fetch the preferred hostname for SSL, including port, for this site
-spec hostname_ssl_port( z:context() ) -> binary() | undefined.
hostname_ssl_port(#context{dispatcher=Dispatcher}) ->
    try
        gen_server:call(Dispatcher, 'hostname_ssl_port', infinity)
    catch
        exit:{noproc, {gen_server, call, _}} ->
            undefined
    end.

%% @doc Make the url an absolute url
abs_url(Url, Context) ->
    abs_url(Url, undefined, [], Context).

%% @doc Fetch the dispatchlist for the site.
-spec dispatchinfo( z:context() | pid() | atom() ) -> {ok, DispatchInfo} | {error, noproc} when
    DispatchInfo :: #{
        site := atom(),
        hostname := binary() | undefined,
        smtphost := binary() | undefined,
        hostalias := [ binary() ],
        redirect := boolean(),
        dispatch_list := [ z_sites_dispatcher:dispatch_rule() ],
        page_paths := #{ atom() | binary() => z_sites_dispatcher:dispatch_rsc_rule() }
    }.
dispatchinfo(#context{dispatcher=Dispatcher}) ->
    dispatchinfo(Dispatcher);
dispatchinfo(Server) when is_pid(Server) orelse is_atom(Server) ->
    try
        DispatchInfo = gen_server:call(Server, 'dispatchinfo', infinity),
        {ok, DispatchInfo}
    catch
        exit:{noproc, {gen_server, call, _}} ->
            {error, noproc}
    end.


%% @doc Update the dispatch list but don't reload it yet. Used when flushing all sites, see z:flush/0
update(#context{dispatcher=Dispatcher}) ->
    gen_server:call(Dispatcher, 'reload', infinity).


%% @doc Reload all dispatch lists.  Finds new dispatch lists and adds them to the dispatcher
reload(#context{dispatcher=Dispatcher}) ->
    gen_server:call(Dispatcher, 'reload', infinity),
    z_sites_dispatcher:update_dispatchinfo().

reload(module_ready, Context) ->
    reload(Context).

name(#context{ dispatcher = Dispatcher }) when Dispatcher =/= undefined ->
    Dispatcher;
name(SiteOrContext) ->
    z_utils:name_for_site(?MODULE, SiteOrContext).

%%====================================================================
%% Support routines, called outside the gen_server
%%====================================================================

%% @doc Rewrite the generated urls. Checks for zotonic_http_accept and checks modules.
rewrite({page_url, #dispatch_url{ url = Url } = D}, _Dispatch, Args, _Context) ->
    Url1 = iolist_to_binary(Url),
    D#dispatch_url{
        url = check_http_options(Url1, Args)
    };
rewrite(#dispatch_url{url = undefined} = D, _Dispatch, _Args, _Context) ->
    D;
rewrite(#dispatch_url{url = Url} = D, Dispatch, Args, Context) ->
    Url1 = iolist_to_binary(Url),
    Url2 = check_http_options(Url1, Args),
    D#dispatch_url{
        url = z_notifier:foldl(#url_rewrite{dispatch = Dispatch, args = Args}, Url2, Context)
    }.

check_http_options(Url, Args) ->
    case lists:keyfind(zotonic_http_accept, 1, Args) of
        {zotonic_http_accept, undefined} ->
            Url;
        {zotonic_http_accept, Mime} ->
            Mime1 = cow_qs:urlencode(z_convert:to_binary(Mime)),
            <<"/http-accept/", Mime1/binary, Url/binary>>;
        false ->
            Url
    end.


%% @doc Optionally make the url an absolute url
opt_abs_url(#dispatch_url{url=undefined} = D, _Dispatch, _Args, _Context) ->
    D;
opt_abs_url(#dispatch_url{url=Url, dispatch_options=DispatchOptions} = D, Dispatch, Args, Context) ->
    case use_absolute_url(Args, DispatchOptions, Context) of
        true -> D#dispatch_url{url=abs_url(Url, Dispatch, DispatchOptions, Context)};
        false -> D
    end.

abs_url(Url, Dispatch, DispatchOptions, Context) ->
    case z_notifier:first(#url_abs{dispatch=Dispatch, url=Url, dispatch_options=DispatchOptions}, Context) of
        undefined -> z_convert:to_binary(z_context:abs_url(Url, Context));
        AbsUrl -> z_convert:to_binary(AbsUrl)
    end.

%% @doc Convenience function, return the generated Url as a binary (or undefined if none).
return_url(#dispatch_url{ url = undefined }) -> undefined;
return_url(#dispatch_url{ url = Url }) -> iolist_to_binary(Url).

%% @doc Check if an url should be made an absolute url
use_absolute_url(Args, Options, Context) ->
    case to_bool(proplists:get_value(absolute_url, Args)) of
        false -> false;
        true -> true;
        undefined ->
            case to_bool(proplists:get_value(absolute_url, Options)) of
                false -> false;
                true -> true;
                undefined ->
                    case to_bool(z_context:get(absolute_url, Context)) of
                        false -> false;
                        true -> true;
                        undefined -> false
                    end
            end
    end.


to_bool(undefined) -> undefined;
to_bool(N) -> z_convert:to_bool(N).

%%====================================================================
%% gen_server callbacks
%%====================================================================

%% @doc Initiates the server, loads the dispatch list into the webmachine dispatcher
init(Site) ->
    logger:set_process_metadata(#{
        site => Site,
        module => ?MODULE
    }),
    ets:new(name(Site), [named_table, set, {keypos, 1}, protected, {read_concurrency, true}]),
    Context = z_context:new(Site),
    Hostname0 = m_site:get(hostname, Context),
    Hostname = drop_port(Hostname0),
    Smtphost = drop_port(m_site:get(smtphost, Context)),
    HostAlias = case m_site:get(hostalias, Context) of
        undefined -> [];
        HA -> HA
    end,
    Alias = lists:filtermap(
        fun(Alias) ->
            case drop_port(Alias) of
                undefined -> false;
                Alias1 -> {true, Alias1}
            end
        end,
        HostAlias),
    process_flag(trap_exit, true),
    IsRedirect = z_context:is_hostname_redirect_configured(Context),
    State  = #state{
                dispatch_list = [],
                page_paths = #{},
                lookup = #{},
                context = Context,
                site = Site,
                smtphost = Smtphost,
                hostname = Hostname,
                hostname_port = add_port(Hostname, http, z_config:get(port)),
                hostname_ssl_port = add_port(Hostname, https, z_config:get(ssl_port)),
                hostalias = Alias,
                redirect = IsRedirect
    },
    z_notifier:observe(module_ready, {?MODULE, reload}, Context),
    {ok, State}.

%% @doc Return the preferred hostname for the site
handle_call('hostname', _From, State) ->
    {reply, State#state.hostname, State};

%% @doc Return the preferred hostname, and port, for the site
handle_call('hostname_port', _From, State) ->
    {reply, State#state.hostname_port, State};

%% @doc Return the preferred hostname for ssl, and port, for the site
handle_call('hostname_ssl_port', _From, State) ->
    {reply, State#state.hostname_ssl_port, State};

%% @doc Return the dispatchinfo for the site.
handle_call('dispatchinfo', _From, State) ->
    {reply, #{
        site => State#state.site,
        hostname => State#state.hostname,
        smtphost => State#state.smtphost,
        hostalias => State#state.hostalias,
        redirect => State#state.redirect,
        dispatch_list => State#state.dispatch_list,
        page_paths => State#state.page_paths
     },
     State};

%% @doc Reload the dispatch list, signal the sites supervisor that the dispatch list has been changed.
%% The site supervisor will collect all dispatch lists and compile a new dispatcher module using
%% dispatch_compiler.
handle_call('reload', _From, State) ->
    State1 = reload_dispatch_list(State),
    {reply, ok, State1}.

%% @doc Handle casts.
handle_cast(_Msg, State) ->
    {noreply, State}.

%% @doc Handling all non call/cast messages
handle_info(_Info, State) ->
    {noreply, State}.


%% @doc This function is called by a gen_server when it is about to
%% terminate. It should be the opposite of Module:init/1 and do any necessary
%% cleaning up. When it returns, the gen_server terminates with Reason.
%% The return value is ignored.
terminate(_Reason, State) ->
    z_notifier:detach(module_ready, State#state.context),
    ok.

%% @doc Convert process state when code is changed
code_change(_OldVsn, State, _Extra) ->
    {ok, State}.


%%====================================================================
%% support functions
%%====================================================================


% @doc Drop the portnumber from the hostname
-spec drop_port( undefined | none | string() | binary() ) -> undefined | binary().
drop_port(undefined) -> undefined;
drop_port(none) -> undefined;
drop_port(<<>>) -> undefined;
drop_port(Hostname) when is_binary(Hostname) ->
    hd(binary:split(Hostname, <<":">>));
drop_port(Hostname) when is_list(Hostname) ->
    drop_port(z_convert:to_binary(Hostname)).

-spec add_port( binary() | undefined, http | https, pos_integer() ) -> binary() | undefined.
add_port(undefined, _Protocol, _Port) -> undefined;
add_port(_Hostname, _Protocol, none) -> undefined;
add_port(Hostname, http, 80) -> Hostname;
add_port(Hostname, https, 443) -> Hostname;
add_port(Hostname, _, Port) ->
    iolist_to_binary([ Hostname, $:, integer_to_list(Port) ]).

%% @doc Reload the dispatch list and update the ets table for the URL generation.
reload_dispatch_list(#state{ context = Context } = State) ->
    DispatchRules = try
        collect_dispatch_lists(Context)
    catch
        _:{error, _Msg} ->
           State#state.dispatch_list
    end,
    LookupMap = dispatch_for_uri_lookup(DispatchRules),
    update_ets(LookupMap, Context),
    {DispatchList, RscPathList} = lists:partition(fun is_dispatch/1, DispatchRules),
    PagePathsLookup = page_path_lookup(RscPathList),
    State#state{
        dispatch_list = DispatchList,
        page_paths = PagePathsLookup,
        lookup = LookupMap
    }.

update_ets(LookupMap, Context) ->
    Name = name(Context),
    LookupList = maps:to_list(LookupMap),
    Current = ets:match(Name, {'$1', '_'}),
    NewKeys = maps:keys(LookupMap),
    NewKeys1 = NewKeys ++ [ z_convert:to_binary(K) || K <- NewKeys ],
    DelKeys = lists:flatten(Current) -- NewKeys1,
    lists:foreach(fun(K) -> ets:delete(Name, K) end, DelKeys),
    ets:insert(Name, LookupList),
    ets:insert(Name, [ {z_convert:to_binary(K), Ls} || {K, Ls} <- LookupList ]).

is_dispatch({_Name, Path, _Controller, _Options}) when is_list(Path) -> true;
is_dispatch({_Name, RscName, _Controller, _Options}) when is_atom(RscName) -> false.

page_path_lookup(RscPathList) ->
    lists:foldr(
        fun({_Name, RscName, _Controller, _Options} = Disp, Acc) ->
            NameBin = z_convert:to_binary(RscName),
            Acc#{
                RscName => Disp,
                NameBin => Disp
            }
        end,
        #{},
        RscPathList).

%% @doc Collect all dispatch lists.  Checks priv/dispatch for all dispatch list definitions.
%% Dispatch rules are ordered by highest prio first.
collect_dispatch_lists(Context) ->
    ModDispOnPrio = lists:concat(
        lists:map(
            fun({Mod, ModFiles}) ->
                lists:sort( [ {F, Mod} || F <- ModFiles ] )
            end,
            z_module_indexer:dispatch(Context))
        ),
    Dispatch = lists:map(fun get_file_dispatch/1, ModDispOnPrio),
    lists:flatten(Dispatch).


%% @doc Read a dispatch file, the file should contain a valid Erlang dispatch datastructure.
get_file_dispatch({File, Mod}) ->
    try
        case filelib:is_regular(File)
            andalso not zotonic_filewatcher_handler:is_file_blocked(File)
        of
            true ->
                Basename = filename:basename(File),
                case Basename of
                    "." ++ _ ->
                        [];
                    <<".", _/binary>> ->
                        [];
                    _Other  ->
                        {ok, Disp} = file:consult(File),
                        Disp1 = split_paths(lists:flatten(Disp), Mod, File),
                        add_mod_to_options(Disp1, Mod, filename:basename(File))
                end;
            false ->
                []
        end
    catch
        M:E ->
            ?LOG_ERROR(#{
                text => <<"Dispatch file parse error">>,
                in => zotonic_core,
                result => M,
                reason => E,
                module => Mod,
                file => File
            }),
            throw({error, "Parse error in " ++ z_convert:to_list(File)})
    end.

%% @doc Check all dispatch rules for proper format, complain about non matching ones.
%% Split paths like "/path/to/:var" into a list of path segments.
%% Drop all non conforming dispatch paths, and log errors for those.
split_paths(Disp, Mod, File) ->
    lists:filtermap(
        fun
            ({Name, [ C | _ ] = Path, Controller, Opts} = Rule) when is_integer(C) ->
                try
                    Path1 = split_binary_path(unicode:characters_to_binary(Path)),
                    {true, {Name, Path1, Controller, Opts}}
                catch
                    E:R:S ->
                        ?LOG_ERROR(#{
                            in => zotonic_core,
                            text => <<"Error in dispatch rule - can not convert string to binary">>,
                            result => E,
                            reason => R,
                            modules => Mod,
                            dispatch_file => File,
                            dispatch_rule => Rule,
                            stack => S
                        }),
                        false
                end;
            ({Name, Path, Controller, Opts}) when is_atom(Name), is_binary(Path), is_atom(Controller), is_list(Opts) ->
                Path1 = split_binary_path(Path),
                {true, {Name, Path1, Controller, Opts}};
            ({Name, Path, Controller, Opts}) when is_atom(Name), is_list(Path), is_atom(Controller), is_list(Opts) ->
                {true, {Name, Path, Controller, Opts}};
            ({Name, RscName, Controller, Opts}) when is_atom(Name), is_atom(RscName), is_atom(Controller), is_list(Opts) ->
                {true, {Name, RscName, Controller, Opts}};
            (Other) ->
                ?LOG_ERROR(#{
                    in => zotonic_core,
                    text => <<"Unrecognized dispatch rule">>,
                    result => error,
                    reason => format,
                    dispatch_rule => Other,
                    dispatch_file => File,
                    module => Mod
                }),
                false
        end,
        Disp).

split_binary_path(Path) ->
    Ps = binary:split(Path, <<"/">>, [ global ]),
    lists:filtermap(
        fun
            (<<>>) -> false;
            (<<"*">>) -> {true, '*'};
            (<<":", Var/binary>>) -> {true, binary_to_atom(Var, utf8)};
            (P) -> {true, P}
        end,
        Ps).

add_mod_to_options(Disp, Mod, Filename) ->
    F = z_convert:to_binary(Filename),
    lists:map(
        fun({Name, Path, Controller, Opts}) ->
            Opts1 = [
                {zotonic_dispatch_module, Mod},
                {zotonic_dispatch_file, F}
                | Opts
            ],
            {Name, Path, Controller, Opts1}
        end,
        Disp).

%% @doc Transform the dispatchlist into a datastructure for building uris from name/vars
%% Datastructure needed is:   name -> [vars, pattern]
dispatch_for_uri_lookup(DispatchList) ->
    dispatch_for_uri_lookup1(DispatchList, #{}).

dispatch_for_uri_lookup1([], LookupAcc) ->
    LookupAcc;
dispatch_for_uri_lookup1([{Name, RscName, Controller, DispatchOptions}|T], LookupAcc)
    when is_atom(Name), is_atom(RscName), is_atom(Controller), is_list(DispatchOptions) ->
    Current = maps:get(Name, LookupAcc, []),
    LookupAcc1 = LookupAcc#{
        Name => [ {0, [], RscName, DispatchOptions} | Current ]
    },
    dispatch_for_uri_lookup1(T, LookupAcc1);
dispatch_for_uri_lookup1([{Name, Pattern, Controller, DispatchOptions}|T], LookupAcc)
    when is_atom(Name), is_list(Pattern), is_atom(Controller), is_list(DispatchOptions) ->
    Vars  = lists:foldl(fun(A, Acc) when is_atom(A) -> [A|Acc];
                           ({A,_RegExp}, Acc) when is_atom(A) -> [A|Acc];
                           (_, Acc) -> Acc
                        end,
                        [],
                        Pattern),
    Current = maps:get(Name, LookupAcc, []),
    LookupAcc1 = LookupAcc#{
        Name => [ {length(Vars), Vars, Pattern, DispatchOptions} | Current ]
    },
    dispatch_for_uri_lookup1(T, LookupAcc1);
dispatch_for_uri_lookup1([IllegalDispatch|T], LookupAcc) ->
    ?LOG_ERROR(#{
        text => <<"Dispatcher dropping malformed dispatch rule">>,
        in => zotonic_core,
        result => error,
        reason => malformed,
        dispatch_rule => IllegalDispatch
    }),
    dispatch_for_uri_lookup1(T, LookupAcc).


%% @doc Make an uri for the named dispatch with the given parameters
make_url_for(Name, Args, Escape, _Context) when Name =:= none; Name =:= <<"none">> ->
    QueryStringArgs = filter_empty_args(Args),
    Sep = case Escape of
            xml  -> "&amp;";
            html -> "&amp;";
            _    -> $&
          end,
    #dispatch_url{
        url = z_convert:to_binary([$?, urlencode(QueryStringArgs, Sep)]),
        dispatch_options = []
    };
make_url_for(Name, Args, Escape, Context) ->
    Args1 = filter_empty_args(Args),
    case ets:lookup(name(Context), Name) of
        [] ->
            #dispatch_url{};
        [{_, Patterns}] ->
            case make_url_for1(Args1, Patterns, Escape, undefined, Context) of
                #dispatch_url{ url = undefined } = DispUrl when Name =/= image, Name =/= <<"image">> ->
                    ?LOG_INFO(#{
                        text => <<"Dispatcher make_url_for failed">>,
                        in => zotonic_core,
                        dispatch_rule => Name,
                        args => Args1,
                        patterns => Patterns,
                        escape => Escape
                    }),
                    DispUrl;
                DispUrl ->
                    DispUrl
            end
    end.


%% @doc Filter out empty dispatch arguments before making an URL.
filter_empty_args(Args) ->
    lists:filter(
      fun
          ({_, <<>>}) -> false;
          ({_, []}) -> false;
          ({_, undefined}) -> false;
          ({absolute_url, _}) -> false;
          (absolute_url) -> false;
          ({zotonic_http_accept, _}) -> false;
          (_) -> true
      end, Args).


%% @doc Try to match all patterns with the arguments
make_url_for1(_Args, [], _Escape, undefined, _Context) ->
    #dispatch_url{};
make_url_for1(_Args, [], Escape, {QueryStringArgs, RscName, DispOpts}, Context) when is_atom(RscName) ->
    PageUrl = m_rsc:p(RscName, <<"page_url">>, Context),
    case QueryStringArgs of
        [] ->
            {page_url, #dispatch_url{
                url = z_convert:to_binary(PageUrl),
                dispatch_options = DispOpts
            }};
        _  ->
            Sep = case Escape of
                    xml  -> "&amp;";
                    html -> "&amp;";
                    _    -> $&
                  end,
            {page_url, #dispatch_url{
                url = z_convert:to_binary([PageUrl, $?, urlencode(QueryStringArgs, Sep)]),
                dispatch_options = DispOpts
            }}
    end;
make_url_for1(Args, [], Escape, {QueryStringArgs, Pattern, DispOpts}, _Context) ->
    ReplArgs =  fun
                    ('*') -> path_argval('*', Args);
                    (V) when is_atom(V) -> path_argval(V, Args);
                    ({V, _Pattern}) when is_atom(V) ->
                        mochiweb_util:quote_plus(path_argval(V, Args));
                    (S) ->
                        S
                end,
    UriParts = lists:map(ReplArgs, Pattern),
    Uri      = [$/ | lists:join($/, UriParts)],
    case QueryStringArgs of
        [] ->
            #dispatch_url{
                url = z_convert:to_binary(Uri),
                dispatch_options = DispOpts
            };
        _  ->
            Sep = case Escape of
                    xml  -> "&amp;";
                    html -> "&amp;";
                    _    -> $&
                  end,
            #dispatch_url{
                url = z_convert:to_binary([Uri, $?, urlencode(QueryStringArgs, Sep)]),
                dispatch_options = DispOpts
            }
    end;
make_url_for1(Args, [Pattern|T], Escape, Best, Context) ->
    Best1 = select_best_pattern(Args, Pattern, Best),
    make_url_for1(Args, T, Escape, Best1, Context).

path_argval('*', Args) ->
    case proplists:get_value(star, Args) of
        undefined -> <<>>;
        L when is_list(L) ->
            List1 = [ cow_qs:urlencode(z_convert:to_binary(B)) || B <- L ],
            lists:join($/, List1);
        V ->
            z_convert:to_binary(V)
    end;
path_argval(Arg, Args) ->
    B = z_convert:to_binary(proplists:get_value(Arg, Args, <<"-">>)),
    cow_qs:urlencode(B).

select_best_pattern(Args, {0, [], RscName, DispOpts}, Best) when is_atom(RscName) ->
    select_best_pattern1({Args, RscName, DispOpts}, Best);
select_best_pattern(Args, {PCount, PArgs, Pattern, DispOpts}, Best) when is_list(Pattern) ->
    if
        length(Args) >= PCount ->
            %% Check if all PArgs are part of Args
            {PathArgs, QueryStringArgs} = lists:partition(
                                            fun
                                                ({star,_}) -> lists:member('*', PArgs);
                                                ({A,_}) -> lists:member(A, PArgs)
                                            end, Args),
            case length(PathArgs) of
                PCount ->
                    % Could fill all path args, this match satisfies
                    select_best_pattern1({QueryStringArgs,Pattern,DispOpts}, Best);
                _ ->
                    Best
            end;
        true ->
            Best
    end.

select_best_pattern1(A, undefined) ->
    A;
select_best_pattern1({AQS, _APat, _AOpts}=A, {BQS, _BPat, _BOpts}=B) ->
    if
        length(BQS) >= length(AQS) -> A;
        true -> B
    end.


%% @doc URL encode the property list.
urlencode(Props, Join) ->
    RevPairs = lists:foldl(fun ({K, V}, Acc) ->
                                   [[mochiweb_util:quote_plus(K), $=, mochiweb_util:quote_plus(V)] | Acc]
                           end, [], Props),
    lists:flatten(revjoin(RevPairs, Join, [])).

revjoin([], _Separator, Acc) ->
    Acc;
revjoin([S | Rest], Separator, []) ->
    revjoin(Rest, Separator, [S]);
revjoin([S | Rest], Separator, Acc) ->
    revjoin(Rest, Separator, [S, Separator | Acc]).


%% @doc Append extra arguments to the url, depending if 'qargs' or 'varargs' is set.
append_extra_args(Args, Context) when is_map(Args) ->
    append_extra_args(maps:to_list(Args), Context);
append_extra_args(Args, Context) ->
    append_qargs(append_varargs(Args, Context), Context).


%% @doc Append all query arguments iff they are not mentioned in the arglist and if qargs parameter is set
append_qargs(Args, Context) ->
    case proplists:get_value(qargs, Args) of
        undefined ->
            Args;
        false ->
            proplists:delete(qargs, Args);
        true ->
            Args1 = proplists:delete(qargs, Args),
            merge_qargs(z_context:get_qargs(Context), Args1);
        L when is_list(L) ->
            Args1 = proplists:delete(qargs, Args),
            merge_qargs(L, Args1);
        M when is_map(M) ->
            Args1 = proplists:delete(qargs, Args),
            merge_qargs(maps:to_list(M), Args1)
    end.

merge_qargs([], Args) ->
    Args;
merge_qargs(Qs, Args) ->
    Ks = [ z_convert:to_binary(A) || {A, _} <- Args ],
    lists:foldr(
        fun({A, _} = AV, Acc) ->
            case lists:member(A, Ks) of
                true ->
                    Acc;
                false ->
                    [ AV | Acc ]
            end
        end,
        Args,
        Qs).

%% @doc Append all varargs argument, names are given in a list.
append_varargs(Args, Context) ->
    case proplists:get_value(varargs, Args) of
        undefined ->
            Args;
        Varargs ->
            append_varargs(Varargs, proplists:delete(varargs, Args), Context)
    end.

append_varargs([], Args, _Context) ->
    Args;
append_varargs([{Name, Value}|Varargs], Args, Context) ->
    append_varargs(Varargs, append_vararg(Name, Value, Args), Context);
append_varargs([[Name, Value]|Varargs], Args, Context) ->
    append_varargs(Varargs, append_vararg(Name, Value, Args), Context);
append_varargs([Name|Varargs], Args, Context) ->
    Key = z_convert:to_atom(Name),
    append_varargs(Varargs, append_vararg(Key, z_context:get(Key, Context), Args), Context).

append_vararg(Name, Value, Args) ->
    Key = z_convert:to_atom(Name),
    case proplists:is_defined(Key, Args) of
        true -> Args;
        false -> [{Key, Value}|Args]
    end.