%%% @doc: Elli example callback
%%%
%%% Your callback needs to implement two functions, {@link handle/2} and
%%% {@link handle_event/3}. For every request, Elli will call your handle
%%% function with the request. When an event happens, like Elli
%%% completed a request, there was a parsing error or your handler
%%% threw an error, {@link handle_event/3} is called.

-module(cloudi_x_elli_example_callback).
-export([handle/2, handle_event/3]).
-export([chunk_loop/1]).

-include("cloudi_x_elli.hrl").
-include("cloudi_x_elli_util.hrl").
-behaviour(cloudi_x_elli_handler).

-include_lib("kernel/include/file.hrl").

%%
%% ELLI REQUEST CALLBACK
%%

%% @doc Handle a `Req'uest.
%% Delegate to our handler function.
%% @see handle/3
-spec handle(Req, _Args) -> Result when
      Req    :: cloudi_x_elli:req(),
      _Args  :: cloudi_x_elli_handler:callback_args(),
      Result :: cloudi_x_elli_handler:result().
handle(Req, _Args) -> handle(Req#req.method, cloudi_x_elli_request:path(Req), Req).



%% @doc Route `Method' and `Path' to the appropriate clause.
%%
%% `ok' can be used instead of `200' to signal success.
%%
%% If you return any of the following HTTP headers, you can
%% override the default behaviour of Elli:
%%
%%  * **Connection**:     By default Elli will use `keep-alive' if the protocol
%%                        supports it, setting `<<"close">>' will close the
%%                        connection immediately after Elli has sent the
%%                        response. If the client has already sent pipelined
%%                        requests, these will be discarded.
%%
%%  * **Content-Length**: By default Elli looks at the size of the body you
%%                        returned to determine the `Content-Length' header.
%%                        Explicitly including your own `Content-Length' (with
%%                        the value as `integer()', `binary()' or `list()')
%%                        allows you to return an empty body. Useful for
%%                        implementing the `"304 Not Modified"' response.
%%
%% @see cloudi_x_elli_request:get_arg/3
%% @see cloudi_x_elli_request:post_arg/3
%% @see cloudi_x_elli_request:post_arg_decoded/3
%% @see cloudi_x_elli_request:get_header/3
%% @see cloudi_x_elli_request:get_arg_decoded/3
%% @see cloudi_x_elli_request:get_args_decoded/1
%% @see cloudi_x_elli_util:file_size/1
%% @see cloudi_x_elli_request:get_range/1
%% @see cloudi_x_elli_request:normalize_range/2
%% @see cloudi_x_elli_request:encode_range/2
%% @see cloudi_x_elli_request:chunk_ref/1
%% @see chunk_loop/1
-spec handle(Method, Path, Req) -> cloudi_x_elli_handler:result() when
      Method :: cloudi_x_elli:http_method(),
      Path   :: [binary()],
      Req    :: cloudi_x_elli:req().
handle('GET', [<<"hello">>, <<"world">>], _Req) ->
    %% Reply with a normal response.
    timer:sleep(1000),
    {ok, [], <<"Hello World!">>};

handle('GET', [<<"hello">>], Req) ->
    %% Fetch a GET argument from the URL.
    Name = cloudi_x_elli_request:get_arg(<<"name">>, Req, <<"undefined">>),
    {ok, [], <<"Hello ", Name/binary>>};

handle('POST', [<<"hello">>], Req) ->
    %% Fetch a POST argument from the POST body.
    Name = cloudi_x_elli_request:post_arg(<<"name">>, Req, <<"undefined">>),
    %% Fetch and decode
    City = cloudi_x_elli_request:post_arg_decoded(<<"city">>, Req, <<"undefined">>),
    {ok, [], <<"Hello ", Name/binary, " of ", City/binary>>};

handle('GET', [<<"hello">>, <<"iolist">>], Req) ->
    %% Iolists will be kept as iolists all the way to the socket.
    Name = cloudi_x_elli_request:get_arg(<<"name">>, Req),
    {ok, [], [<<"Hello ">>, Name]};

handle('GET', [<<"type">>], Req) ->
    Name = cloudi_x_elli_request:get_arg(<<"name">>, Req),
    %% Fetch a header.
    case cloudi_x_elli_request:get_header(<<"Accept">>, Req, <<"text/plain">>) of
        <<"text/plain">> ->
            {ok, [{<<"Content-type">>, <<"text/plain; charset=ISO-8859-1">>}],
             <<"name: ", Name/binary>>};
        <<"application/json">> ->
            {ok, [{<<"Content-type">>,
                   <<"application/json; charset=ISO-8859-1">>}],
             <<"{\"name\" : \"", Name/binary, "\"}">>}
    end;

handle('GET', [<<"headers.html">>], _Req) ->
    %% Set custom headers, for example 'Content-Type'
    {ok, [{<<"X-Custom">>, <<"foobar">>}], <<"see headers">>};

%% See note in function doc re: overriding Elli's default behaviour
%% via Connection and Content-Length headers.
handle('GET', [<<"user">>, <<"defined">>, <<"behaviour">>], _Req) ->
    {304, [{<<"Connection">>, <<"close">>},
           {<<"Content-Length">>, <<"123">>}], <<"ignored">>};

handle('GET', [<<"user">>, <<"content-length">>], _Req) ->
    {200, [{<<"Content-Length">>, 123}], <<"foobar">>};

handle('GET', [<<"crash">>], _Req) ->
    %% Throwing an exception results in a 500 response and
    %% request_throw being called
    throw(foobar);

handle('GET', [<<"decoded-hello">>], Req) ->
    %% Fetch a URI decoded GET argument from the URL.
    Name = cloudi_x_elli_request:get_arg_decoded(<<"name">>, Req, <<"undefined">>),
    {ok, [], <<"Hello ", Name/binary>>};

handle('GET', [<<"decoded-list">>], Req) ->
    %% Fetch a URI decoded GET argument from the URL.
    [{<<"name">>, Name}, {<<"foo">>, true}] =
        cloudi_x_elli_request:get_args_decoded(Req),
    {ok, [], <<"Hello ", Name/binary>>};


handle('GET', [<<"sendfile">>], _Req) ->
    %% Returning {file, "/path/to/file"} instead of the body results
    %% in Elli using sendfile.
    F    = "README.md",
    {ok, [], {file, F}};

handle('GET', [<<"send_no_file">>], _Req) ->
    %% Returning {file, "/path/to/file"} instead of the body results
    %% in Elli using sendfile.
    F    = "README",
    {ok, [], {file, F}};

handle('GET', [<<"sendfile">>, <<"error">>], _Req) ->
    F    = "test",
    {ok, [], {file, F}};

handle('GET', [<<"sendfile">>, <<"range">>], Req) ->
    %% Read the Range header of the request and use the normalized
    %% range with sendfile, otherwise send the entire file when
    %% no range is present, or respond with a 416 if the range is invalid.
    F     = "README.md",
    {ok, [], {file, F, cloudi_x_elli_request:get_range(Req)}};

handle('GET', [<<"compressed">>], _Req) ->
    %% Body with a byte size over 1024 are automatically gzipped by
    %% cloudi_x_elli_middleware_compress
    {ok, binary:copy(<<"Hello World!">>, 86)};

handle('GET', [<<"compressed-io_list">>], _Req) ->
    %% Body with a iolist size over 1024 are automatically gzipped by
    %% cloudi_x_elli_middleware_compress
    {ok, lists:duplicate(86, [<<"Hello World!">>])};


handle('HEAD', [<<"head">>], _Req) ->
    {200, [], <<"body must be ignored">>};

handle('GET', [<<"chunked">>], Req) ->
    %% Start a chunked response for streaming real-time events to the
    %% browser.
    %%
    %% Calling cloudi_x_elli_request:send_chunk(ChunkRef, Body) will send that
    %% part to the client. cloudi_x_elli_request:close_chunk(ChunkRef) will
    %% close the response.
    %%
    %% Return immediately {chunk, Headers} to signal we want to chunk.
    Ref = cloudi_x_elli_request:chunk_ref(Req),
    spawn(fun() -> ?MODULE:chunk_loop(Ref) end),
    {chunk, [{<<"Content-Type">>, <<"text/event-stream">>}]};

handle('GET', [<<"shorthand">>], _Req) ->
    {200, <<"hello">>};

handle('GET', [<<"ip">>], Req) ->
    {<<"200 OK">>, cloudi_x_elli_request:peer(Req)};

handle('GET', [<<"304">>], _Req) ->
    %% A "Not Modified" response is exactly like a normal response (so
    %% Content-Length is included), but the body will not be sent.
    {304, [{<<"Etag">>, <<"foobar">>}], <<"Ignored">>};

handle('GET', [<<"302">>], _Req) ->
    {302, [{<<"Location">>, <<"/hello/world">>}], <<>>};

handle('GET', [<<"403">>], _Req) ->
    %% Exceptions formatted as return codes can be used to
    %% short-circuit a response, for example in case of
    %% authentication/authorization
    throw({403, [], <<"Forbidden">>});

handle('GET', [<<"invalid_return">>], _Req) ->
    {invalid_return};

handle(_, _, _Req) ->
    {404, [], <<"Not Found">>}.



%% @doc Send 10 separate chunks to the client.
%% @equiv chunk_loop(Ref, 10)
chunk_loop(Ref) ->
    chunk_loop(Ref, 10).

%% @doc If `N > 0', send a chunk to the client, checking for errors,
%% as the user might have disconnected.
%% When `N == 0', call {@link cloudi_x_elli_request:close_chunk/1.
%% cloudi_x_elli_request:close_chunk(Ref)}.
chunk_loop(Ref, 0) ->
    cloudi_x_elli_request:close_chunk(Ref);
chunk_loop(Ref, N) ->
    timer:sleep(10),

    case cloudi_x_elli_request:send_chunk(Ref, [<<"chunk">>, integer_to_binary(N)]) of
        ok              -> ok;
        {error, Reason} -> ?LOG_ERROR("error in sending chunk: ~p~n", [Reason])
    end,

    chunk_loop(Ref, N-1).


%%
%% ELLI EVENT CALLBACKS
%%


%% @doc Handle Elli events, fired throughout processing a request.
%%
%% `elli_startup' is sent when Elli is starting up. If you are
%% implementing a middleware, you can use it to spawn processes,
%% create ETS tables or start supervised processes in a supervisor
%% tree.
%%
%% `request_complete' fires *after* Elli has sent the response to the
%% client. `Timings' contains timestamps (native units) of events like when the
%% connection was accepted, when headers/body parsing finished, when the
%% user callback returns, response sent, etc. `Sizes' contains response sizes
%% like response headers size, response body or file size.
%% This allows you to collect performance statistics for monitoring your app.
%%
%% `request_throw', `request_error' and `request_exit' events are sent if
%% the user callback code throws an exception, has an error or
%% exits. After triggering this event, a generated response is sent to
%% the user.
%%
%% `invalid_return' is sent if the user callback code returns a term not
%% understood by cloudi_x_elli, see {@link cloudi_x_elli_http:execute_callback/1}.
%% After triggering this event, a generated response is sent to the user.
%%
%% `chunk_complete' fires when a chunked response is completely
%% sent. It's identical to the `request_complete' event, except instead
%% of the response body you get the atom `client' or `server'
%% depending on who closed the connection. `Sizes' will have the key `chunks',
%% which is the total size of all chunks plus encoding overhead.
%%
%% `request_closed' is sent if the client closes the connection when
%% Elli is waiting for the next request on a keep alive connection.
%%
%% `request_timeout' is sent if the client times out when
%% Elli is waiting for the request.
%%
%% `request_parse_error' fires if the request is invalid and cannot be parsed by
%% [`erlang:decode_packet/3`][decode_packet/3] or it contains a path Elli cannot
%% parse or does not support.
%%
%% [decode_packet/3]: http://erlang.org/doc/man/erlang.html#decode_packet-3
%%
%% `client_closed' can be sent from multiple parts of the request
%% handling. It's sent when the client closes the connection or if for
%% any reason the socket is closed unexpectedly. The `Where' atom
%% tells you in which part of the request processing the closed socket
%% was detected: `receiving_headers', `receiving_body' or `before_response'.
%%
%% `client_timeout' can as with `client_closed' be sent from multiple
%% parts of the request handling. If Elli tries to receive data from
%% the client socket and does not receive anything within a timeout,
%% this event fires and the socket is closed.
%%
%% `bad_request' is sent when Elli detects a request is not well
%% formatted or does not conform to the configured limits. Currently
%% the `Reason' variable can be `{too_many_headers, Headers}'
%% or `{body_size, ContentLength}'.
%%
%% `file_error' is sent when the user wants to return a file as a
%% response, but for some reason it cannot be opened.
-spec handle_event(Event, Args, Config) -> ok when
      Event  :: cloudi_x_elli:event(),
      Args   :: cloudi_x_elli_handler:callback_args(),
      Config :: [tuple()].
handle_event(elli_startup, [], _) -> ok;
handle_event(request_complete, [_Request,
                                _ResponseCode, _ResponseHeaders, _ResponseBody,
                                {_Timings, _Sizes}], _) -> ok;
handle_event(request_throw, [_Request, _Exception, _Stacktrace], _) -> ok;
handle_event(request_error, [_Request, _Exception, _Stacktrace], _) -> ok;
handle_event(request_exit, [_Request, _Exception, _Stacktrace], _) -> ok;

handle_event(invalid_return, [_Request, _ReturnValue], _) -> ok;

handle_event(chunk_complete, [_Request,
                              _ResponseCode, _ResponseHeaders, _ClosingEnd,
                              {_Timings, _Sizes}], _) -> ok;

handle_event(request_closed, [], _) -> ok;

handle_event(request_timeout, [], _) -> ok;

handle_event(request_parse_error, [_], _) -> ok;

handle_event(client_closed, [_Where], _) -> ok;

handle_event(client_timeout, [_Where], _) -> ok;

handle_event(bad_request, [_Reason], _) -> ok;

handle_event(file_error, [_ErrorReason], _) -> ok.
