%% Copyright (c) 2011-2012 Basho Technologies, Inc.  All Rights Reserved.
%%
%% This file is provided to you 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
%%
%%   https://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 Helper functions for webmachine's default log handlers

-module(webmachine_log).

-include("webmachine_logger.hrl").
-include_lib("kernel/include/logger.hrl").

-export([add_handler/2,
         call/2,
         call/3,
         datehour/0,
         datehour/1,
         defer_refresh/1,
         delete_handler/1,
         fix_log/2,
         fmt_ip/1,
         fmtnow/0,
         log_access/1,
         log_close/3,
         log_open/1,
         log_open/2,
         log_write/2,
         maybe_rotate/3,
         month/1,
         refresh/2,
         suffix/1,
         zeropad/2,
         zone/0]).

-record(state, {hourstamp :: datehour(),
                filename :: string(),
                handle :: file:io_device()}).

%% @doc Add a handler to receive log events
-type add_handler_result() :: ok | {'EXIT', term()} | term().
-spec add_handler(atom() | {atom(), term()}, term()) -> add_handler_result().
add_handler(Mod, Args) ->
    gen_event:add_handler(?EVENT_LOGGER, Mod, Args).

%% @doc Make a synchronous call directly to a specific event handler
%% module
-type error() :: {error, bad_module} | {'EXIT', term()} | term().
-spec call(atom(), term()) -> term() | error().
call(Mod, Msg) ->
    gen_event:call(?EVENT_LOGGER, Mod, Msg).

%% @doc Make a synchronous call directly to a specific event handler
%% module
-spec call(atom(), term(), timeout()) -> term() | error().
call(Mod, Msg, Timeout) ->
    gen_event:call(?EVENT_LOGGER, Mod, Msg, Timeout).

%% Should be
%%   -type datehour() :: {calendar:year(), calendar:month(), calendar:day(), calendar:hour()}.
%% but calendar types are not exported
-type datehour() :: {non_neg_integer(), 1..12, 1..31, 0..23}.

%% @doc Return a four-tuple containing year, month, day, and hour
%% of the current time.
-spec datehour() -> datehour().
datehour() ->
    datehour(os:timestamp()).

%% @doc Return a four-tuple containing year, month, day, and hour
%% of the specified time.
-spec datehour(erlang:timestamp()) -> datehour().
datehour(TS) ->
    {{Y, M, D}, {H, _, _}} = calendar:now_to_universal_time(TS),
    {Y, M, D, H}.

%% @doc Defer the refresh of a log file.
-spec defer_refresh(atom()) -> {ok, timer:tref()} | {error, term()}.
defer_refresh(Mod) ->
    {_, {_, M, S}} = calendar:universal_time(),
    Time = 1000 * (3600 - ((M * 60) + S)),
    timer:apply_after(Time, ?MODULE, refresh, [Mod, os:timestamp()]).

%% @doc Remove a log handler
-type delete_handler_result() :: term() | {error, module_not_found} | {'EXIT', term()}.
-spec delete_handler(atom() | {atom(), term()}) -> delete_handler_result().
delete_handler(Mod) ->
    gen_event:delete_handler(?EVENT_LOGGER, Mod, []).

%% Seek backwards to the last valid log entry
-spec fix_log(file:io_device(), non_neg_integer()) -> ok.
fix_log(_FD, 0) ->
    ok;
fix_log(FD, 1) ->
    {ok, 0} = file:position(FD, 0),
    ok;
fix_log(FD, Location) ->
    case file:pread(FD, Location - 1, 1) of
        {ok, [$\n | _]} ->
            ok;
        {ok, _} ->
            fix_log(FD, Location - 1)
    end.

%% @doc Format an IP address or host name
-spec fmt_ip(undefined | string() | inet:ip4_address() | inet:ip6_address()) -> string().
fmt_ip(IP) when is_tuple(IP) ->
    inet_parse:ntoa(IP);
fmt_ip(undefined) ->
    "0.0.0.0";
fmt_ip(HostName) ->
    HostName.

%% @doc Format the current time into a string
-spec fmtnow() -> io_lib:chars().
fmtnow() ->
    {{Year, Month, Date}, {Hour, Min, Sec}} = calendar:local_time(),
    io_lib:format("[~2..0w/~ts/~4..0w:~2..0w:~2..0w:~2..0w ~ts]",
                  [Date,month(Month),Year, Hour, Min, Sec, zone()]).

%% @doc Notify registered log event handler of an access event.
-spec log_access(tuple()) -> ok.
log_access({_, _, _}=LogData) ->
    gen_event:sync_notify(?EVENT_LOGGER, {log_access, LogData}).

%% @doc Close a log file.
-spec log_close(atom(), string(), file:io_device()) -> ok | {error, term()}.
log_close(Mod, Name, FD) ->
    ?LOG_INFO("~tp: closing log file: ~tp", [Mod, Name]),
    file:close(FD).

%% @doc Open a new log file for writing
-spec log_open(string()) -> {file:io_device(), datehour()}.
log_open(FileName) ->
    DateHour = datehour(),
    {log_open(FileName, DateHour), DateHour}.

%% @doc Open a new log file for writing
-spec log_open(string(), datehour()) -> file:io_device().
log_open(FileName, DateHour) ->
    LogName = FileName ++ suffix(DateHour),
    ?LOG_INFO("opening log file: ~tp", [LogName]),
    _ = filelib:ensure_dir(LogName),
    {ok, FD} = file:open(LogName, [read, write, raw]),
    {ok, Location} = file:position(FD, eof),
    fix_log(FD, Location),
    _ = file:truncate(FD),
    FD.

-spec log_write(file:io_device(), iolist()) -> ok | {error, term()}.
log_write(FD, IoData) ->
    file:write(FD, lists:flatten(IoData)).

%% @doc Rotate a log file if the hour it represents
%% has passed.
-spec maybe_rotate(atom(), erlang:timestamp(), #state{}) -> #state{}.
maybe_rotate(Mod, Time, State) ->
    ThisHour = datehour(Time),
    if ThisHour == State#state.hourstamp ->
            State;
       true ->
            _ = defer_refresh(Mod),
            _ = log_close(Mod, State#state.filename, State#state.handle),
            Handle = log_open(State#state.filename, ThisHour),
            State#state{hourstamp=ThisHour, handle=Handle}
    end.

%% @doc Convert numeric month value to the abbreviation
-spec month(1..12) -> string().
month(1) ->
    "Jan";
month(2) ->
    "Feb";
month(3) ->
    "Mar";
month(4) ->
    "Apr";
month(5) ->
    "May";
month(6) ->
    "Jun";
month(7) ->
    "Jul";
month(8) ->
    "Aug";
month(9) ->
    "Sep";
month(10) ->
    "Oct";
month(11) ->
    "Nov";
month(12) ->
    "Dec".

%% @doc Make a synchronous call to instruct a log handler to refresh
%% itself.
-spec refresh(atom(), erlang:timestamp()) -> ok | {error, term()}.
refresh(Mod, Time) ->
    call(Mod, {refresh, Time}, infinity).

-spec suffix(datehour()) -> string().
suffix({Y, M, D, H}) ->
    YS = zeropad(Y, 4),
    MS = zeropad(M, 2),
    DS = zeropad(D, 2),
    HS = zeropad(H, 2),
    lists:flatten([$., YS, $_, MS, $_, DS, $_, HS]).

-spec zeropad(integer(), integer()) -> string().
zeropad(Num, MinLength) ->
    NumStr = integer_to_list(Num),
    zeropad_str(NumStr, MinLength - length(NumStr)).

-spec zeropad_str(string(), integer()) -> string().
zeropad_str(NumStr, Zeros) when Zeros > 0 ->
    zeropad_str([$0 | NumStr], Zeros - 1);
zeropad_str(NumStr, _) ->
    NumStr.

-spec zone() -> string().
zone() ->
    Time = erlang:universaltime(),
    LocalTime = calendar:universal_time_to_local_time(Time),
    DiffSecs = calendar:datetime_to_gregorian_seconds(LocalTime) -
        calendar:datetime_to_gregorian_seconds(Time),
    zone((DiffSecs/3600)*100).

%% Ugly reformatting code to get times like +0000 and -1300

-spec zone(float() | integer()) -> string().
zone(Val) when Val < 0 ->
    io_lib:format("-~4..0w", [trunc(abs(Val))]);
zone(Val) when Val >= 0 ->
    io_lib:format("+~4..0w", [trunc(abs(Val))]).
