%% Copyright 2015 Kolab Systems AG (http://www.kolabsys.com)
%%
%% Aaron Seigo (Kolab Systems) <seigo a kolabsys.com>
%%
%% This program is free software: you can redistribute it and/or modify
%% it under the terms of the GNU General Public License as published by
%% the Free Software Foundation, either version 3 of the License, or
%% (at your option) any later version.
%%
%% This program is distributed in the hope that it will be useful,
%% but WITHOUT ANY WARRANTY; without even the implied warranty of
%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
%% GNU General Public License for more details.
%%
%% You should have received a copy of the GNU General Public License
%% along with this program.  If not, see <http://www.gnu.org/licenses/>.

-module(kolab_guam_rule_filter_groupware).
-export([new/1, applies/4, imap_data/3, apply_to_client_message/4, apply_to_server_message/3]).
-behavior(kolab_guam_rule).

-include("kolab_guam_rule_filter_groupware.hrl").

new(_Config) -> #state { blacklist = undefined }.

applies(_ConnectionDetails, _Buffer, { _Tag, Command, Data }, State) ->
    Applies = apply_if_id_matches(Command, Data, State),
    %lager:debug("********** Checking ...~n    Command: ~s ~s, Result ~p", [Command, Data, Applies]),
    { Applies, State }.

apply_to_client_message(_ImapSession, Buffer, undefined, State) ->
    { Buffer, State };

apply_to_client_message(ImapSession, Buffer, { Tag, Command, Data }, State) ->
    { Active, StateTag } =
    case is_triggering_command(Command, Data, State) of
        true -> fetch_metadata(ImapSession, State), { true, Tag };
        _ -> { false, <<>> }
    end,
    %lager:info("Client sent: ~s ~s ~p", [Command, Data, Active]),
    { Buffer, State#state{ active = Active, tag = StateTag }}.

apply_to_server_message(_ImapSession, Buffer, #state{ active = true } = State) ->
    filter_folders(Buffer, State);
apply_to_server_message(_ImapSession, Buffer, State) -> { Buffer, State }.

imap_data(_ResponseToken, { error, _Reason }, State) -> State;
imap_data(_ResponseToken, Response, State) ->
    %TODO: we don't need Foo/Bar if we already have Foo, so filter folders-of-groupwarefolders
    Blacklist = lists:foldl(fun({ _Folder, [ { _Property, null } ]}, Acc) -> Acc;
                               ({ _Folder, [ { _Property, <<"mail", _Rest/binary>> } ]}, Acc) -> Acc;
                               ({ Folder, _ }, Acc) -> [{ Folder, <<Folder/binary, "/">> }|Acc] end,
                            [], Response),
    State#state{ blacklist = Blacklist }.

%%PRIVATE
is_triggering_command(Command, Data, #state{ trigger_commands = TriggerCommands }) ->
    %% if the command is in the list of trigger commands and the ending is not "" (which means "send me
    %% the root and separator" according to RFC 3501), then it is treated as a triggering event
    lists:any(fun(T) -> (Command =:= T) andalso (binary:longest_common_suffix([Data, <<"\"\"">>]) =/= 2) end,
              TriggerCommands).


fetch_metadata(none, #state{ blacklist = undefined }) -> ok;
fetch_metadata(ImapSession, #state{ blacklist = undefined }) ->
    eimap:get_folder_metadata(ImapSession, self(), { rule_data, ?MODULE, blacklist }, "*", ["/shared/vendor/kolab/folder-type"]);
fetch_metadata(_ImapSession, _State) -> ok.

apply_if_id_matches(<<"ID">>, Data, _State) ->
    apply_if_found_kolab(string:str(string:to_lower(binary_to_list(Data)), "/kolab"));
apply_if_id_matches(Command, Data, State) ->
    case is_triggering_command(Command, Data, State) of
        true -> true;
        _ -> notyet
    end.

apply_if_found_kolab(0) -> true;
apply_if_found_kolab(_) -> false.

filter_folders(<<>>, State) ->
    { <<>>, State#state{ active = true } };
filter_folders(Buffer, #state{ last_chunk = LeftOvers } = State) ->
    % Add the left overs from the previous buffer to the current buffer
    FullBuffer = <<LeftOvers/binary, Buffer/binary>>,

    {FullLines, LastChunk} = eimap_utils:split_lines_with_literals(FullBuffer),

    { Response, More } = filter_folders(State, FullLines, { <<>>, true }),

    {<<Response/binary>>, State#state{ active = More, last_chunk = LastChunk}}.

filter_folders(_State, [], Return) -> Return;
filter_folders(_State, _Folders, { Acc, false }) -> { Acc, false };
filter_folders(State, [Unfiltered|Folders], { Acc, _More }) -> filter_folders(State, Folders, filter_folder(State, Unfiltered, Acc)).

filter_folder(_State, <<>>, Acc) -> { Acc, true };
filter_folder(State, <<"* LIST ", Details/binary>> = Response, Acc) -> { filter_on_details(State, Response, Acc, Details), true };
filter_folder(State, <<"* XLIST ", Details/binary>> = Response, Acc) -> { filter_on_details(State, Response, Acc, Details), true };
filter_folder(State, <<"* LSUB ", Details/binary>> = Response, Acc) -> { filter_on_details(State, Response, Acc, Details), true };
filter_folder(State, <<"* STATUS ", Details/binary>> = Response, Acc) -> { filter_on_details(State, Response, Acc, Details), true };
filter_folder(#state{ tag = Tag }, Response, Acc) ->
    HasMore =
    case byte_size(Tag) =< byte_size(Response) of
        true ->
            case binary:match(Response, Tag, [{ scope, { 0, byte_size(Tag) } }]) of
                nomatch -> true;
                _ -> false % we have found our closing tag!
            end;
        false -> true
    end,
    { add_response(Response, Acc), HasMore }.

filter_on_details(#state{ blacklist = Blacklist }, Response, Acc, Details) ->
    %% Remove "*" and extract response command name
    { _, Start, _ } = pop_token(Response), %% asterisk
    { Cmd, _, _ } = pop_token(Start), %% command

    %% Extract folder name
    Suffix =
    case Cmd =:= <<"STATUS">> of
        true -> Details;
        _ ->
            { Pos, _Length } = binary:match(Details, [<<")">>], []),
            { _Delimiter, Rest, _} = pop_token(binary:part(Details, Pos + 2, byte_size(Details) - Pos - 2)),
            Rest
    end,
    { Folder, _, _ } = pop_token(Suffix),

    %% Check the folder in blacklist
    %% io:format("COMPARING ~p ??? ~p~n", [Folder, in_blacklist(Folder, Blacklist)]),
    case in_blacklist(Folder, Blacklist) of
        true -> Acc;
        _ -> add_response(Response, Acc)
    end.

add_response(Response, <<>>) -> Response;
add_response(Response, Acc) -> <<Acc/binary, Response/binary>>.

%% FIXME: This (none, _Pattern) is triggered by a test. It likely means we're not parsing utf-8 sequences correctly,
%% but since we still pass it through correctly it doesn't matter as long as we don't attempt
%% to create filters that have to match that.
in_blacklist(none, _Pattern) -> false;
in_blacklist(_Folder, undefined) -> false;
in_blacklist(_Folder, []) -> false;
in_blacklist(Folder, [{ Literal, Prefix }|List]) ->
    case Literal =:= Folder of
        true -> true;
        _ -> case binary:match(Folder, Prefix) of
                 { 0, _ } -> true;
                 _ -> in_blacklist(Folder, List)
             end
    end.

%% pop_token from https://github.com/MainframeHQ/switchboard/blob/master/src/imap.erl (BSD Lic.)
%% with some small changes by Aleksander Machniak <machniak@kolabsys.com>
pop_token(Data) ->
    pop_token(Data, none).

pop_token(<<>>, State) ->
    {none, <<>>, State};

%% Consume hanging spaces
pop_token(<<" ", Rest/binary>>, none) ->
    pop_token(Rest, none);

%% \r\n
pop_token(<<$\r, $\n, Rest/binary>>, none) ->
    {crlf, Rest, none};

%% NIL
pop_token(<<"NIL", Rest/binary>>, none) ->
    {nil, Rest, none};

%% ( | ) | [ | ]
pop_token(<<$(, Rest/binary>>, none) ->
    {'(', Rest, none};
pop_token(<<$), Rest/binary>>, none) ->
    {')', Rest, none};


%% Atom
pop_token(<<C, _/binary>> = Data, {atom, AtomAcc}) when
  C =:= 32; C =:= 40; C =:= 41; C =:= $(; C =:= $) ->
    {AtomAcc, Data, none};
pop_token(<<$\r, $\n, _/binary>> = Data, {atom, AtomAcc}) ->
    {AtomAcc, Data, none};
pop_token(<<C, Rest/binary>>, none) when C >= 35, C < 123 ->
    pop_token(Rest, {atom, <<C>>});
pop_token(<<C, Rest/binary>>, {atom, AtomAcc}) when C >= 35, C < 123 ->
    pop_token(Rest, {atom, <<AtomAcc/binary, C>>});

%% Literal Strings
pop_token(<<${, Rest/binary>>, none) ->
    pop_token(Rest, {literal, <<>>});
pop_token(<<$}, $\r, $\n, Rest/binary>>, {literal, ByteAcc}) ->
    pop_token(Rest, {literal, binary_to_integer(ByteAcc), <<>>});
pop_token(<<D, Rest/binary>>, {literal, ByteAcc}) when D >= 48, D < 58 ->
    pop_token(Rest, {literal, <<ByteAcc/binary, D>>});
pop_token(Binary, {literal, Bytes, LiteralAcc}) when is_integer(Bytes) ->
    case Binary of
        <<Literal:Bytes/binary, Rest/binary>> ->
            {<<LiteralAcc/binary, Literal/binary>>, Rest, none};
        _ ->
            %% If the binary is too short, accumulate it in the state
            pop_token(<<>>, {literal, Bytes - size(Binary), <<LiteralAcc/binary, Binary/binary>>})
    end;

%% Quoted Strings
pop_token(<<$", Rest/binary>>, none) ->
    pop_token(Rest, {quoted, <<>>});
pop_token(<<$\\, C, Rest/binary>>, {quoted, Acc}) ->
    pop_token(Rest, {quoted, <<Acc/binary, C>>});
pop_token(<<$", Rest/binary>>, {quoted, Acc}) ->
    {Acc, Rest, none};
pop_token(<<$\r, $\n, _>>, {quoted, _}) ->
    throw({error, crlf_in_quoted});
pop_token(<<C, Rest/binary>>, {quoted, Acc}) ->
    pop_token(Rest, {quoted, <<Acc/binary, C>>});

pop_token(Binary, _) ->
    {none, Binary, none}.
