% 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(chttpd_sup).
-behaviour(supervisor).

-behaviour(config_listener).

-export([init/1]).

-export([start_link/1]).

-export([handle_config_change/5, handle_config_terminate/3]).

%% Helper macro for declaring children of supervisor
-define(CHILD(I, Type), {I, {I, start_link, []}, permanent, 100, Type, [I]}).
-define(DEFAULT_SERVER_OPTIONS, "[{recbuf, undefined}]").

start_link(Args) ->
    case supervisor:start_link({local, ?MODULE}, ?MODULE, Args) of
        {ok, _} = Resp ->
            notify_started(),
            notify_uris(),
            write_uris(),
            Resp;
        Else ->
            notify_error(Else),
            Else
    end.

init([]) ->
    Children = [
        {
            config_listener_mon,
            {config_listener_mon, start_link, [?MODULE, settings()]},
            permanent,
            5000,
            worker,
            [config_listener_mon]
        },
        ?CHILD(chttpd, worker),
        ?CHILD(chttpd_auth_cache, worker),
        {chttpd_auth_cache_lru, {ets_lru, start_link, [chttpd_auth_cache_lru, lru_opts()]},
            permanent, 5000, worker, [ets_lru]}
    ],

    {ok, {{one_for_one, 3, 10}, couch_epi:register_service(chttpd_epi, Children)}}.

handle_config_change("chttpd", "bind_address", Value, _, Settings) ->
    maybe_replace(bind_address, Value, Settings);
handle_config_change("chttpd", "port", Value, _, Settings) ->
    maybe_replace(port, Value, Settings);
handle_config_change("chttpd", "server_options", Value, _, Settings) ->
    maybe_replace(server_options, Value, Settings);
handle_config_change(_, _, _, _, Settings) ->
    {ok, Settings}.

handle_config_terminate(_Server, _Reason, _State) ->
    ok.

settings() ->
    DefaultOpts = chttpd:get_default_server_options(),
    [
        {bind_address, config:get("chttpd", "bind_address")},
        {port, config:get("chttpd", "port")},
        {server_options, config:get("chttpd", "server_options", DefaultOpts)}
    ].

maybe_replace(Key, Value, Settings) ->
    case couch_util:get_value(Key, Settings) of
        Value ->
            {ok, Settings};
        _ ->
            chttpd:stop(),
            {ok, lists:keyreplace(Key, 1, Settings, {Key, Value})}
    end.

lru_opts() ->
    lists:foldl(fun append_if_set/2, [], [
        {max_objects, config:get_integer("chttpd_auth_cache", "max_objects", 0)},
        {max_size, config:get_integer("chttpd_auth_cache", "max_size", 104857600)},
        {max_lifetime, config:get_integer("chttpd_auth_cache", "max_lifetime", 600000)}
    ]).

append_if_set({Key, Value}, Opts) when Value > 0 ->
    [{Key, Value} | Opts];
append_if_set({_Key, 0}, Opts) ->
    Opts;
append_if_set({Key, Value}, Opts) ->
    couch_log:error(
        "The value for `~s` should be string convertible "
        "to integer which is >= 0 (got `~p`)",
        [Key, Value]
    ),
    Opts.

notify_started() ->
    couch_log:info("Apache CouchDB has started. Time to relax.~n", []).

notify_error(Error) ->
    couch_log:error("Error starting Apache CouchDB:~n~n    ~p~n~n", [Error]).

notify_uris() ->
    lists:foreach(
        fun(Uri) ->
            couch_log:info("Apache CouchDB has started on ~s", [Uri])
        end,
        get_uris()
    ).

write_uris() ->
    case config:get("couchdb", "uri_file", undefined) of
        undefined ->
            ok;
        UriFile ->
            Lines = [io_lib:format("~s~n", [Uri]) || Uri <- get_uris()],
            write_file(UriFile, Lines)
    end.

get_uris() ->
    Ip = config:get("chttpd", "bind_address"),
    lists:flatmap(
        fun(Uri) ->
            case get_uri(Uri, Ip) of
                undefined -> [];
                Else -> [Else]
            end
        end,
        [chttpd, couch_httpd, https]
    ).

get_uri(Name, Ip) ->
    case get_port(Name) of
        undefined ->
            undefined;
        Port ->
            io_lib:format("~s://~s:~w/", [get_scheme(Name), Ip, Port])
    end.

get_scheme(chttpd) -> "http";
get_scheme(couch_httpd) -> "http";
get_scheme(https) -> "https".

get_port(Name) ->
    try
        mochiweb_socket_server:get(Name, port)
    catch
        exit:{noproc, _} ->
            undefined
    end.

write_file(FileName, Contents) ->
    case file:write_file(FileName, Contents) of
        ok ->
            ok;
        {error, Reason} ->
            Args = [FileName, file:format_error(Reason)],
            couch_log:error("Failed ot write ~s :: ~s", Args),
            throw({error, Reason})
    end.
