% 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(couch_server).
-behaviour(gen_server).

-export([open/2,create/2,delete/2,get_version/0,get_version/1,get_uuid/0]).
-export([all_databases/0, all_databases/2]).
-export([init/1, handle_call/3,sup_start_link/0]).
-export([handle_cast/2,code_change/3,handle_info/2,terminate/2]).
-export([dev_start/0,is_admin/2,has_admins/0,get_stats/0]).

-include("couch_db.hrl").

-record(server,{
    root_dir = [],
    dbname_regexp,
    max_dbs_open=100,
    dbs_open=0,
    start_time=""
    }).

dev_start() ->
    couch:stop(),
    up_to_date = make:all([load, debug_info]),
    couch:start().

get_version() ->
    Apps = application:loaded_applications(),
    case lists:keysearch(couch, 1, Apps) of
    {value, {_, _, Vsn}} ->
        Vsn;
    false ->
        "0.0.0"
    end.
get_version(short) ->
  %% strip git hash from version string
  [Version|_Rest] = string:tokens(get_version(), "+"),
  Version.


get_uuid() ->
    case couch_config:get("couchdb", "uuid", nil) of
        nil ->
            UUID = couch_uuids:random(),
            couch_config:set("couchdb", "uuid", ?b2l(UUID)),
            UUID;
        UUID -> ?l2b(UUID)
    end.

get_stats() ->
    {ok, #server{start_time=Time,dbs_open=Open}} =
            gen_server:call(couch_server, get_server),
    [{start_time, ?l2b(Time)}, {dbs_open, Open}].

sup_start_link() ->
    gen_server:start_link({local, couch_server}, couch_server, [], []).

open(DbName, Options0) ->
    Options = maybe_add_sys_db_callbacks(DbName, Options0),
    case gen_server:call(couch_server, {open, DbName, Options}, infinity) of
    {ok, Db} ->
        Ctx = couch_util:get_value(user_ctx, Options, #user_ctx{}),
        {ok, Db#db{user_ctx=Ctx}};
    Error ->
        Error
    end.

create(DbName, Options0) ->
    Options = maybe_add_sys_db_callbacks(DbName, Options0),
    case gen_server:call(couch_server, {create, DbName, Options}, infinity) of
    {ok, Db} ->
        Ctx = couch_util:get_value(user_ctx, Options, #user_ctx{}),
        {ok, Db#db{user_ctx=Ctx}};
    Error ->
        Error
    end.

delete(DbName, Options) ->
    gen_server:call(couch_server, {delete, DbName, Options}, infinity).

maybe_add_sys_db_callbacks(DbName, Options) when is_binary(DbName) ->
    maybe_add_sys_db_callbacks(?b2l(DbName), Options);
maybe_add_sys_db_callbacks(DbName, Options) ->
    case couch_config:get("replicator", "db", "_replicator") of
    DbName ->
        [
            {before_doc_update, fun couch_replicator_manager:before_doc_update/2},
            {after_doc_read, fun couch_replicator_manager:after_doc_read/2},
            sys_db | Options
        ];
    _ ->
        case couch_config:get("couch_httpd_auth", "authentication_db", "_users") of
        DbName ->
        [
            {before_doc_update, fun couch_users_db:before_doc_update/2},
            {after_doc_read, fun couch_users_db:after_doc_read/2},
            sys_db | Options
        ];
        _ ->
            Options
        end
    end.

check_dbname(#server{dbname_regexp=RegExp}, DbName) ->
    case re:run(DbName, RegExp, [{capture, none}]) of
    nomatch ->
        case DbName of
            "_users" -> ok;
            "_replicator" -> ok;
            _Else ->
                {error, illegal_database_name, DbName}
            end;
    match ->
        ok
    end.

is_admin(User, ClearPwd) ->
    case couch_config:get("admins", User) of
    "-hashed-" ++ HashedPwdAndSalt ->
        [HashedPwd, Salt] = string:tokens(HashedPwdAndSalt, ","),
        couch_util:to_hex(crypto:hash(sha, ClearPwd ++ Salt)) == HashedPwd;
    _Else ->
        false
    end.

has_admins() ->
    couch_config:get("admins") /= [].

get_full_filename(Server, DbName) ->
    filename:join([Server#server.root_dir, "./" ++ DbName ++ ".couch"]).

hash_admin_passwords() ->
    hash_admin_passwords(true).

hash_admin_passwords(Persist) ->
    lists:foreach(
        fun({User, ClearPassword}) ->
            HashedPassword = couch_passwords:hash_admin_password(ClearPassword),
            couch_config:set("admins", User, ?b2l(HashedPassword), Persist)
        end, couch_passwords:get_unhashed_admins()).

init([]) ->
    % read config and register for configuration changes

    % just stop if one of the config settings change. couch_server_sup
    % will restart us and then we will pick up the new settings.

    RootDir = couch_config:get("couchdb", "database_dir", "."),
    MaxDbsOpen = list_to_integer(
            couch_config:get("couchdb", "max_dbs_open")),
    Self = self(),
    ok = couch_config:register(
        fun("couchdb", "database_dir") ->
            exit(Self, config_change)
        end),
    ok = couch_config:register(
        fun("couchdb", "max_dbs_open", Max) ->
            gen_server:call(couch_server,
                    {set_max_dbs_open, list_to_integer(Max)})
        end),
    ok = couch_file:init_delete_dir(RootDir),
    hash_admin_passwords(),
    ok = couch_config:register(
        fun("admins", _Key, _Value, Persist) ->
            % spawn here so couch_config doesn't try to call itself
            spawn(fun() -> hash_admin_passwords(Persist) end)
        end, false),
    {ok, RegExp} = re:compile("^[a-z][a-z0-9\\_\\$()\\+\\-\\/]*$"),
    ets:new(couch_dbs_by_name, [set, private, named_table]),
    ets:new(couch_dbs_by_pid, [set, private, named_table]),
    ets:new(couch_dbs_by_lru, [ordered_set, private, named_table]),
    ets:new(couch_sys_dbs, [set, private, named_table]),
    process_flag(trap_exit, true),
    {ok, #server{root_dir=RootDir,
                dbname_regexp=RegExp,
                max_dbs_open=MaxDbsOpen,
                start_time=couch_util:rfc1123_date()}}.

terminate(_Reason, _Srv) ->
    lists:foreach(
        fun({_, {_, Pid, _}}) ->
                couch_util:shutdown_sync(Pid)
        end,
        ets:tab2list(couch_dbs_by_name)).

all_databases() ->
    {ok, DbList} = all_databases(
        fun(DbName, Acc) -> {ok, [DbName | Acc]} end, []),
    {ok, lists:usort(DbList)}.

all_databases(Fun, Acc0) ->
    {ok, #server{root_dir=Root}} = gen_server:call(couch_server, get_server),
    NormRoot = couch_util:normpath(Root),
    FinalAcc = try
        filelib:fold_files(Root, "^[a-z0-9\\_\\$()\\+\\-]*[\\.]couch$", true,
            fun(Filename, AccIn) ->
                NormFilename = couch_util:normpath(Filename),
                case NormFilename -- NormRoot of
                [$/ | RelativeFilename] -> ok;
                RelativeFilename -> ok
                end,
                case Fun(?l2b(filename:rootname(RelativeFilename, ".couch")), AccIn) of
                {ok, NewAcc} -> NewAcc;
                {stop, NewAcc} -> throw({stop, Fun, NewAcc})
                end
            end, Acc0)
    catch throw:{stop, Fun, Acc1} ->
         Acc1
    end,
    {ok, FinalAcc}.


maybe_close_lru_db(#server{dbs_open=NumOpen, max_dbs_open=MaxOpen}=Server)
        when NumOpen < MaxOpen ->
    {ok, Server};
maybe_close_lru_db(#server{dbs_open=NumOpen}=Server) ->
    % must free up the lru db.
    case try_close_lru(now()) of
    ok ->
        {ok, Server#server{dbs_open=NumOpen - 1}};
    Error -> Error
    end.

try_close_lru(StartTime) ->
    LruTime = get_lru(),
    if LruTime > StartTime ->
        % this means we've looped through all our opened dbs and found them
        % all in use.
        {error, all_dbs_active};
    true ->
        [{_, DbName}] = ets:lookup(couch_dbs_by_lru, LruTime),
        [{_, {opened, MainPid, LruTime}}] = ets:lookup(couch_dbs_by_name, DbName),
        case couch_db:is_idle(MainPid) of
        true ->
            ok = shutdown_idle_db(DbName, MainPid, LruTime);
        false ->
            % this still has referrers. Go ahead and give it a current lru time
            % and try the next one in the table.
            NewLruTime = now(),
            true = ets:insert(couch_dbs_by_name, {DbName, {opened, MainPid, NewLruTime}}),
            true = ets:insert(couch_dbs_by_pid, {MainPid, DbName}),
            true = ets:delete(couch_dbs_by_lru, LruTime),
            true = ets:insert(couch_dbs_by_lru, {NewLruTime, DbName}),
            try_close_lru(StartTime)
        end
    end.

get_lru() ->
    get_lru(ets:first(couch_dbs_by_lru)).

get_lru(LruTime) ->
    [{LruTime, DbName}] = ets:lookup(couch_dbs_by_lru, LruTime),
    case ets:member(couch_sys_dbs, DbName) of
    false ->
        LruTime;
    true ->
        [{_, {opened, MainPid, _}}] = ets:lookup(couch_dbs_by_name, DbName),
        case couch_db:is_idle(MainPid) of
        true ->
            NextLru = ets:next(couch_dbs_by_lru, LruTime),
            ok = shutdown_idle_db(DbName, MainPid, LruTime),
            get_lru(NextLru);
        false ->
            get_lru(ets:next(couch_dbs_by_lru, LruTime))
        end
    end.

shutdown_idle_db(DbName, MainPid, LruTime) ->
    couch_util:shutdown_sync(MainPid),
    true = ets:delete(couch_dbs_by_lru, LruTime),
    true = ets:delete(couch_dbs_by_name, DbName),
    true = ets:delete(couch_dbs_by_pid, MainPid),
    true = ets:delete(couch_sys_dbs, DbName),
    ok.

open_async(Server, From, DbName, Filepath, Options) ->
    Parent = self(),
    Opener = spawn_link(fun() ->
            Res = couch_db:start_link(DbName, Filepath, Options),
            gen_server:call(
                Parent, {open_result, DbName, Res, Options}, infinity
            ),
            unlink(Parent),
            case Res of
            {ok, DbReader} ->
                unlink(DbReader);
            _ ->
                ok
            end
        end),
    true = ets:insert(couch_dbs_by_name, {DbName, {opening, Opener, [From]}}),
    true = ets:insert(couch_dbs_by_pid, {Opener, DbName}),
    DbsOpen = case lists:member(sys_db, Options) of
    true ->
        true = ets:insert(couch_sys_dbs, {DbName, true}),
        Server#server.dbs_open;
    false ->
        Server#server.dbs_open + 1
    end,
    Server#server{dbs_open = DbsOpen}.

handle_call({set_max_dbs_open, Max}, _From, Server) ->
    {reply, ok, Server#server{max_dbs_open=Max}};
handle_call(get_server, _From, Server) ->
    {reply, {ok, Server}, Server};
handle_call({open_result, DbName, {ok, OpenedDbPid}, Options}, _From, Server) ->
    link(OpenedDbPid),
    [{DbName, {opening,Opener,Froms}}] = ets:lookup(couch_dbs_by_name, DbName),
    lists:foreach(fun({FromPid,_}=From) ->
        gen_server:reply(From,
                catch couch_db:open_ref_counted(OpenedDbPid, FromPid))
    end, Froms),
    LruTime = now(),
    true = ets:insert(couch_dbs_by_name,
            {DbName, {opened, OpenedDbPid, LruTime}}),
    true = ets:delete(couch_dbs_by_pid, Opener),
    true = ets:insert(couch_dbs_by_pid, {OpenedDbPid, DbName}),
    true = ets:insert(couch_dbs_by_lru, {LruTime, DbName}),
    case lists:member(create, Options) of
    true ->
        couch_db_update_notifier:notify({created, DbName});
    false ->
        ok
    end,
    {reply, ok, Server};
handle_call({open_result, DbName, {error, eexist}, Options}, From, Server) ->
    handle_call({open_result, DbName, file_exists, Options}, From, Server);
handle_call({open_result, DbName, Error, Options}, _From, Server) ->
    [{DbName, {opening,Opener,Froms}}] = ets:lookup(couch_dbs_by_name, DbName),
    lists:foreach(fun(From) ->
        gen_server:reply(From, Error)
    end, Froms),
    true = ets:delete(couch_dbs_by_name, DbName),
    true = ets:delete(couch_dbs_by_pid, Opener),
    DbsOpen = case lists:member(sys_db, Options) of
    true ->
        true = ets:delete(couch_sys_dbs, DbName),
        Server#server.dbs_open;
    false ->
        Server#server.dbs_open - 1
    end,
    {reply, ok, Server#server{dbs_open = DbsOpen}};
handle_call({open, DbName, Options}, {FromPid,_}=From, Server) ->
    LruTime = now(),
    case ets:lookup(couch_dbs_by_name, DbName) of
    [] ->
        open_db(DbName, Server, Options, From);
    [{_, {opening, Opener, Froms}}] ->
        true = ets:insert(couch_dbs_by_name, {DbName, {opening, Opener, [From|Froms]}}),
        {noreply, Server};
    [{_, {opened, MainPid, PrevLruTime}}] ->
        true = ets:insert(couch_dbs_by_name, {DbName, {opened, MainPid, LruTime}}),
        true = ets:delete(couch_dbs_by_lru, PrevLruTime),
        true = ets:insert(couch_dbs_by_lru, {LruTime, DbName}),
        {reply, couch_db:open_ref_counted(MainPid, FromPid), Server}
    end;
handle_call({create, DbName, Options}, From, Server) ->
    case ets:lookup(couch_dbs_by_name, DbName) of
    [] ->
        open_db(DbName, Server, [create | Options], From);
    [_AlreadyRunningDb] ->
        {reply, file_exists, Server}
    end;
handle_call({delete, DbName, _Options}, _From, Server) ->
    DbNameList = binary_to_list(DbName),
    case check_dbname(Server, DbNameList) of
    ok ->
        FullFilepath = get_full_filename(Server, DbNameList),
        UpdateState =
        case ets:lookup(couch_dbs_by_name, DbName) of
        [] -> false;
        [{_, {opening, Pid, Froms}}] ->
            couch_util:shutdown_sync(Pid),
            true = ets:delete(couch_dbs_by_name, DbName),
            true = ets:delete(couch_dbs_by_pid, Pid),
            [gen_server:reply(F, not_found) || F <- Froms],
            true;
        [{_, {opened, Pid, LruTime}}] ->
            couch_util:shutdown_sync(Pid),
            true = ets:delete(couch_dbs_by_name, DbName),
            true = ets:delete(couch_dbs_by_pid, Pid),
            true = ets:delete(couch_dbs_by_lru, LruTime),
            true
        end,
        Server2 = case UpdateState of
        true ->
            DbsOpen = case ets:member(couch_sys_dbs, DbName) of
            true ->
                true = ets:delete(couch_sys_dbs, DbName),
                Server#server.dbs_open;
            false ->
                Server#server.dbs_open - 1
            end,
            Server#server{dbs_open = DbsOpen};
        false ->
            Server
        end,

        %% Delete any leftover .compact files.  If we don't do this a subsequent
        %% request for this DB will try to open the .compact file and use it.
        couch_file:delete(Server#server.root_dir, FullFilepath ++ ".compact"),

        case couch_file:delete(Server#server.root_dir, FullFilepath) of
        ok ->
            couch_db_update_notifier:notify({deleted, DbName}),
            {reply, ok, Server2};
        {error, enoent} ->
            {reply, not_found, Server2};
        Else ->
            {reply, Else, Server2}
        end;
    Error ->
        {reply, Error, Server}
    end.

handle_cast(Msg, _Server) ->
    exit({unknown_cast_message, Msg}).

code_change(_OldVsn, State, _Extra) ->
    {ok, State}.
    
handle_info({'EXIT', _Pid, config_change}, Server) ->
    {noreply, shutdown, Server};
handle_info({'EXIT', Pid, Reason}, Server) ->
    Server2 = case ets:lookup(couch_dbs_by_pid, Pid) of
    [{Pid, DbName}] ->

        % If the Pid is known, the name should be as well.
        % If not, that's an error, which is why there is no [] clause.
        case ets:lookup(couch_dbs_by_name, DbName) of
        [{_, {opening, Pid, Froms}}] ->
            Msg = case Reason of
            snappy_nif_not_loaded ->
                io_lib:format(
                    "To open the database `~s`, Apache CouchDB "
                    "must be built with Erlang OTP R13B04 or higher.",
                    [DbName]
                );
            true ->
                io_lib:format("Error opening database ~p: ~p", [DbName, Reason])
            end,
            ?LOG_ERROR(Msg, []),
            lists:foreach(
              fun(F) -> gen_server:reply(F, {bad_otp_release, Msg}) end,
              Froms
            );
        [{_, {opened, Pid, LruTime}}] ->
            ?LOG_ERROR(
                "Unexpected exit of database process ~p [~p]: ~p",
                [Pid, DbName, Reason]
            ),
            true = ets:delete(couch_dbs_by_lru, LruTime)
        end,

        true = ets:delete(couch_dbs_by_pid, DbName),
        true = ets:delete(couch_dbs_by_name, DbName),

        case ets:lookup(couch_sys_dbs, DbName) of
        [{DbName, _}] ->
            true = ets:delete(couch_sys_dbs, DbName),
            Server;
        [] ->
            Server#server{dbs_open = Server#server.dbs_open - 1}
        end
    end,
    {noreply, Server2};
handle_info(Error, _Server) ->
    ?LOG_ERROR("Unexpected message, restarting couch_server: ~p", [Error]),
    exit(kill).

open_db(DbName, Server, Options, From) ->
    DbNameList = binary_to_list(DbName),
    case check_dbname(Server, DbNameList) of
    ok ->
        Filepath = get_full_filename(Server, DbNameList),
        case lists:member(sys_db, Options) of
        true ->
            {noreply, open_async(Server, From, DbName, Filepath, Options)};
        false ->
            case maybe_close_lru_db(Server) of
            {ok, Server2} ->
                {noreply, open_async(Server2, From, DbName, Filepath, Options)};
            CloseError ->
                {reply, CloseError, Server}
            end
        end;
     Error ->
        {reply, Error, Server}
     end.
