%% @author Robert Ahrens <rahrens@basho.com>
%% @author Justin Sheehy <justin@basho.com>
%% @copyright 2007-2009 Basho Technologies
%%
%%    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.

%% @doc Module for URL-dispatch by pattern matching.

-module(webmachine_dispatcher).
-author('Robert Ahrens <rahrens@basho.com>').
-author('Justin Sheehy <justin@basho.com>').
-author('Bryan Fink <bryan@basho.com>').

-export([dispatch/2, dispatch/3]).

-define(SEPARATOR, $\/).
-define(MATCH_ALL, '*').

%% @spec dispatch(Path::string(), DispatchList::[matchterm()]) ->
%%                                            dispterm() | dispfail()
%% @doc Interface for URL dispatching.
%% See also http://bitbucket.org/justin/webmachine/wiki/DispatchConfiguration
dispatch(PathAsString, DispatchList) ->
    dispatch([], PathAsString, DispatchList).

%% @spec dispatch(Host::string(), Path::string(),
%%                DispatchList::[matchterm()]) ->
%%         dispterm() | dispfail()
%% @doc Interface for URL dispatching.
%% See also http://bitbucket.org/justin/webmachine/wiki/DispatchConfiguration
dispatch(HostAsString, PathAsString, DispatchList) ->
    Path = string:tokens(PathAsString, [?SEPARATOR]),
    % URIs that end with a trailing slash are implicitly one token
    % "deeper" than we otherwise might think as we are "inside"
    % a directory named by the last token.
    ExtraDepth = case lists:last(PathAsString) == ?SEPARATOR of
		     true -> 1;
		     _ -> 0
		 end,
    {Host, Port} = split_host_port(HostAsString),
    try_host_binding(DispatchList, lists:reverse(Host), Port,
                     Path, ExtraDepth).

split_host_port(HostAsString) ->
    case string:tokens(HostAsString, ":") of
        [HostPart, PortPart] ->
            {split_host(HostPart), list_to_integer(PortPart)};
        [HostPart] ->
            {split_host(HostPart), 80};
        [] ->
            %% no host header
            {[], 80}
    end.

split_host(HostAsString) ->
    string:tokens(HostAsString, ".").

%% @type matchterm() = hostmatchterm() | pathmatchterm()
% The dispatch configuration is a list of these terms, and the
% first one whose host and path terms match the input is used.
% Using a pathmatchterm() here is equivalent to using a hostmatchterm()
% of the form {{['*'],'*'}, [pathmatchterm()]}.

%% @type hostmatchterm() = {hostmatch(), [pathmatchterm()]}
% The dispatch configuration contains a list of these terms, and the
% first one whose host and one pathmatchterm match is used.

%% @type hostmatch() = [hostterm()] | {[hostterm()], portterm()}
% A host header (Host, X-Forwarded-For, etc.) will be matched against
% this term.  Using a raws [hostterm()] list is equivalent to using
% {[hostterm()], '*'}.

%% @type hostterm() = '*' | string() | atom()
% A list of hostterms is matched against a '.'-separated hostname.
% The '*' hosterm matches all remaining tokens, and is only allowed at
% the head of the list.
% A string hostterm will match a token of exactly the same string.
% Any atom hostterm other than '*' will match any token and will
% create a binding in the result if a complete match occurs.

%% @type portterm() = '*' | integer() | atom()
% A portterm is matched against the integer port after any ':' in
% the hostname, or 80 if no port is found.
% The '*' portterm patches any port
% An integer portterm will match a port of exactly the same integer.
% Any atom portterm other than '*' will match any port and will
% create a binding in the result if a complete match occurs.

%% @type pathmatchterm() = {[pathterm()], matchmod(), matchopts()}.
% The dispatch configuration contains a list of these terms, and the
% first one whose list of pathterms matches the input path is used.

%% @type pathterm() = '*' | string() | atom().
% A list of pathterms is matched against a '/'-separated input path.
% The '*' pathterm matches all remaining tokens.
% A string pathterm will match a token of exactly the same string.
% Any atom pathterm other than '*' will match any token and will
% create a binding in the result if a complete match occurs.

%% @type matchmod() = atom().
% This atom, if present in a successful matchterm, will appear in
% the resulting dispterm.  In Webmachine this is used to name the
% resource module that will handle the matching request.

%% @type matchopts() = [term()].
% This term, if present in a successful matchterm, will appear in
% the resulting dispterm.  In Webmachine this is used to provide
% arguments to the resource module handling the matching request.

%% @type dispterm() = {matchmod(), matchopts(), pathtokens(),
%%                bindings(), approot(), stringpath()}.

%% @type pathtokens() = [pathtoken()].
% This is the list of tokens matched by a trailing '*' pathterm.

%% @type pathtoken() = string().

%% @type bindings() = [{bindingterm(),pathtoken()}].
% This is a proplist of bindings indicated by atom terms in the
% matching spec, bound to the matching tokens in the request path.

%% @type approot() = string().

%% @type stringpath() = string().
% This is the path portion matched by a trailing '*' pathterm.

%% @type dispfail() = {no_dispatch_match, pathtokens()}.

try_host_binding([], Host, Port, Path, _Depth) ->
    {no_dispatch_match, {Host, Port}, Path};
try_host_binding([Dispatch|Rest], Host, Port, Path, Depth) ->
    {{HostSpec,PortSpec},PathSpec} =
        case Dispatch of
            {{H,P},S} -> {{H,P},S};
            {H,S}     -> {{H,?MATCH_ALL},S};
            S         -> {{[?MATCH_ALL],?MATCH_ALL},[S]}
        end,
    case bind_port(PortSpec, Port, []) of
        {ok, PortBindings} ->
            case bind(lists:reverse(HostSpec), Host, PortBindings, 0) of
                {ok, HostRemainder, HostBindings, _} ->
                    case try_path_binding(PathSpec, Path, HostBindings, Depth) of
                        {Mod, Props, PathRemainder, PathBindings,
                         AppRoot, StringPath} ->
                            {Mod, Props, HostRemainder, Port, PathRemainder,
                             PathBindings, AppRoot, StringPath};
                        {no_dispatch_match, _} ->
                            try_host_binding(Rest, Host, Port, Path, Depth)
                    end;
                fail ->
                    try_host_binding(Rest, Host, Port, Path, Depth)
            end;
        fail ->
            try_host_binding(Rest, Host, Port, Path, Depth)
    end.

bind_port(Port, Port, Bindings) -> {ok, Bindings};
bind_port(?MATCH_ALL, _Port, Bindings) -> {ok, Bindings};
bind_port(PortAtom, Port, Bindings) when is_atom(PortAtom) ->
    {ok, [{PortAtom, Port}|Bindings]};
bind_port(_, _, _) -> fail.

try_path_binding([], PathTokens, _, _) ->
    {no_dispatch_match, PathTokens};
try_path_binding([{PathSchema, Mod, Props}|Rest], PathTokens,
                 Bindings, ExtraDepth) ->
    case bind(PathSchema, PathTokens, Bindings, 0) of
        {ok, Remainder, NewBindings, Depth} ->
            {Mod, Props, Remainder, NewBindings,
             calculate_app_root(Depth + ExtraDepth), reconstitute(Remainder)};
        fail -> 
            try_path_binding(Rest, PathTokens, Bindings, ExtraDepth)
    end.

bind([], [], Bindings, Depth) ->
    {ok, [], Bindings, Depth};
bind([?MATCH_ALL], Rest, Bindings, Depth) when is_list(Rest) ->
    {ok, Rest, Bindings, Depth + length(Rest)};
bind(_, [], _, _) ->
    fail;
bind([Token|RestToken],[Match|RestMatch],Bindings,Depth) when is_atom(Token) ->
    bind(RestToken, RestMatch, [{Token, Match}|Bindings], Depth + 1);
bind([Token|RestToken], [Token|RestMatch], Bindings, Depth) ->
    bind(RestToken, RestMatch, Bindings, Depth + 1);
bind(_, _, _, _) ->
    fail.

reconstitute([]) -> "";
reconstitute(UnmatchedTokens) -> string:join(UnmatchedTokens, [?SEPARATOR]).

calculate_app_root(1) -> ".";
calculate_app_root(N) when N > 1 ->
    string:join(lists:duplicate(N, ".."), [?SEPARATOR]).
