%% This file is a copy of http_uri.erl from the R13B-1 Erlang/OTP
%% distribution with several modifications.

%% All modifications are Copyright (c) 2007-2024 Broadcom. The term “Broadcom” refers to Broadcom Inc. and/or its subsidiaries. All rights reserved.

%% ``The contents of this file are subject to the Erlang Public License,
%% Version 1.1, (the "License"); you may not use this file except in
%% compliance with the License. You should have received a copy of the
%% Erlang Public License along with this software. If not, it can be
%% retrieved via the world wide web at https://www.erlang.org/.
%%
%% Software distributed under the License is distributed on an "AS IS"
%% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See
%% the License for the specific language governing rights and limitations
%% under the License.
%%
%% The Initial Developer of the Original Code is Ericsson Utvecklings AB.
%% Portions created by Ericsson are Copyright 1999, Ericsson Utvecklings
%% AB. All Rights Reserved.''

%% See https://tools.ietf.org/html/rfc3986

-module(uri_parser).

-export([parse/2]).

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

%% Returns a key list of elements extracted from the URI. Note that
%% only 'scheme' is guaranteed to exist. Key-Value pairs from the
%% Defaults list will be used absence of a non-empty value extracted
%% from the URI. The values extracted are strings, except for 'port'
%% which is an integer, 'userinfo' which is a list of strings (split
%% on $:), and 'query' which is a list of strings where no $= char
%% found, or a {key,value} pair where a $= char is found (initial
%% split on $& and subsequent optional split on $=). Possible keys
%% are: 'scheme', 'userinfo', 'host', 'port', 'path', 'query',
%% 'fragment'.

-spec parse(AbsURI, Defaults :: list())
    -> [{atom(), string()}] | {error, no_scheme | {malformed_uri, AbsURI, any()}}
    when AbsURI :: string() | binary().
parse(AbsURI, Defaults) ->
    AbsUriString = rabbit_data_coercion:to_list(AbsURI),
    case parse_scheme(AbsUriString) of
	{error, Reason} ->
	    {error, Reason};
	{Scheme, Rest} ->
            case (catch parse_uri_rest(Rest, true)) of
                [_|_] = List ->
                    merge_keylists([{scheme, Scheme} | List], Defaults);
                E ->
                    {error, {malformed_uri, AbsURI, E}}
            end
    end.

%%%========================================================================
%%% Internal functions
%%%========================================================================
parse_scheme(AbsURI) ->
    split_uri(AbsURI, ":", {error, no_scheme}).

parse_uri_rest("//" ++ URIPart, true) ->
    %% we have an authority
    {Authority, PathQueryFrag} =
	split_uri(URIPart, "/|\\?|#", {URIPart, ""}, 1, 0),
    AuthorityParts = parse_authority(Authority),
    parse_uri_rest(PathQueryFrag, false) ++ AuthorityParts;
parse_uri_rest(PathQueryFrag, _Bool) ->
    %% no authority, just a path and maybe query
    {PathQuery, Frag} = split_uri(PathQueryFrag, "#", {PathQueryFrag, ""}),
    {Path, QueryString} = split_uri(PathQuery, "\\?", {PathQuery, ""}),
    QueryPropList = split_query(QueryString),
    [{path, Path}, {'query', QueryPropList}, {fragment, Frag}].

parse_authority(Authority) ->
    {UserInfo, HostPort} = split_uri(Authority, "@", {"", Authority}),
    UserInfoSplit = case re:split(UserInfo, ":", [{return, list}]) of
                        [""] -> [];
                        UIS  -> UIS
                    end,
    [{userinfo, UserInfoSplit} | parse_host_port(HostPort)].

parse_host_port("[" ++ HostPort) -> %ipv6
    {Host, ColonPort} = split_uri(HostPort, "\\]", {HostPort, ""}),
    [{host, Host} | case split_uri(ColonPort, ":", not_found, 0, 1) of
                        not_found -> case ColonPort of
                                         [] -> [];
                                         _  -> throw({invalid_port, ColonPort})
                                     end;
                        {_, Port} -> [{port, list_to_integer(Port)}]
                    end];

parse_host_port(HostPort) ->
    {Host, Port} = split_uri(HostPort, ":", {HostPort, not_found}),
    [{host, Host} | case Port of
                        not_found -> [];
                        _         -> [{port, list_to_integer(Port)}]
                    end].

split_query(Query) ->
    case re:split(Query, "&", [{return, list}]) of
        [""]    -> [];
        QParams -> [split_uri(Param, "=", Param) || Param <- QParams]
    end.

split_uri(UriPart, SplitChar, NoMatchResult) ->
    split_uri(UriPart, SplitChar, NoMatchResult, 1, 1).

split_uri(UriPart, SplitChar, NoMatchResult, SkipLeft, SkipRight) ->
    case re:run(UriPart, SplitChar) of
	{match, [{Match, _}]} ->
	    {string:substr(UriPart, 1, Match + 1 - SkipLeft),
	     string:substr(UriPart, Match + 1 + SkipRight, length(UriPart))};
	nomatch ->
	    NoMatchResult
    end.

merge_keylists(A, B) ->
    {AEmpty, ANonEmpty} = lists:partition(fun ({_Key, V}) -> V =:= [] end, A),
    [AEmptyS, ANonEmptyS, BS] =
        [lists:ukeysort(1, X) || X <- [AEmpty, ANonEmpty, B]],
    lists:ukeymerge(1, lists:ukeymerge(1, ANonEmptyS, BS), AEmptyS).
