-module(yaws_multipart).
-export([read_multipart_form/2]).

-include("../include/yaws.hrl").
-include("../include/yaws_api.hrl").

-record(upload, {
          fd,
          filename,
          fixed_filename,
          last = false,
          param_name,
          param_running_value,
          params,
          running_file_size = 0,
          max_file_size,
          no_temp_file,
          temp_dir = yaws:tmpdir("/tmp"),
          temp_file,
          headers = [],
          data_type = list,
          return_error_file_path = false
         }).

read_multipart_form(A, Options) when A#arg.state == undefined ->
    State = #upload{params = dict:new()},
    NewState = read_options(Options,State),
    multipart(A, NewState);
read_multipart_form(A, _Options) ->
    multipart(A, A#arg.state).

read_options([], State) -> State;
read_options([Option|Rest], State) ->
    NewState = case Option of
                   {max_file_size, SizeInBytes} ->
                       State#upload{max_file_size = SizeInBytes};
                   no_temp_file ->
                       State#upload{no_temp_file = true};
                   {temp_file, FullPath} ->
                       State#upload{fixed_filename = FullPath};
                   {temp_dir, Dir} ->
                       true = filelib:is_dir(Dir),
                       State#upload{temp_dir = Dir};
                   list ->
                       State#upload{data_type = list};
                   binary ->
                       State#upload{data_type = binary};
                   return_error_file_path ->
                       State#upload{return_error_file_path = true}
               end,
    read_options(Rest, NewState).

multipart(A, State) ->
    Parse = yaws_api:parse_multipart_post(A, [State#upload.data_type]),
    case Parse of
        {cont, Cont, Res} ->
            case add_file_chunk(A, Res, State) of
                {done, NewState} ->
                    {done, NewState#upload.params};
                {cont, NewState} ->
                    {get_more, Cont, NewState};
                Error={error, _Reason} ->
                    Error
            end;
        {result, Res} ->
            case add_file_chunk(A, Res, State#upload{last=true}) of
                {done, S2} ->
                    {done,S2#upload.params};
                Error={error, _Reason} ->
                    Error
            end;
        Error={error, _Reason} ->
            Error
    end.


add_file_chunk(A, [{part_body, Data}|Res], State) ->
    add_file_chunk(A, [{body, Data}|Res], State);

add_file_chunk(_A, [], State) when State#upload.last == true ->
    {done, close_previous_param(State)};

add_file_chunk(_A, [], State) ->
    {cont, State};

add_file_chunk(A, [{head, {_Name, Opts}}|Res], State ) ->
    S1 = close_previous_param(State),
    S2 = lists:foldl(
           fun(_, Error={error, _Reason}) ->
                   Error;
              ({"filename", Fname0}, RunningState) ->
                   case create_temp_file(State) of
                       [undefined, undefined] ->
                           %% values will be stored in memory as
                           %% dictated by state#upload.no_temp_file
                           RunningState#upload{
                             filename            = Fname0,
                             param_running_value = undefined,
                             running_file_size   = 0};
                       [Fname, Fd] ->
                           RunningState#upload{
                             fd                  = Fd,
                             filename            = Fname0,
                             temp_file           = Fname,
                             param_running_value = undefined,
                             running_file_size   = 0};
                       Error={error, _Reason} ->
                           Error
                   end;
              ({"name", ParamName}, RunningState) ->
                   RunningState#upload{
                     param_name          = ParamName,
                     param_running_value = undefined};
              (HdrVal, RunningState) ->
                   RunningState#upload{
                     headers = [HdrVal | RunningState#upload.headers]}
           end,
           S1,
           Opts),
    case S2 of
        Error={error, _Reason} ->
            Error;
        _ ->
            add_file_chunk(A,Res,S2)
    end;

add_file_chunk(A, [{body, Data}|Res],State) when State#upload.fd /= undefined ->
    NewSize = compute_new_size(State, Data),
    Check   = check_param_size(State, NewSize),
    case Check of
        ok ->
            Fd = State#upload.fd,
            case file:write(Fd, Data) of
                ok ->
                    add_file_chunk(A, Res, State#upload{running_file_size = NewSize});
                {error, Reason} ->
                    file:close(Fd),
                    {error, make_error_reason(Reason, State)}
            end;
        Error={error, _Reason} ->
            Error
    end;

add_file_chunk(A, [{body, Data}|Res], State) ->
    NewSize = compute_new_size(State, Data),
    Check   = check_param_size(State, NewSize),
    case Check of
        ok ->
            NewState =
                case State#upload.param_running_value of
                    undefined ->
                        State#upload{param_running_value = Data};
                    PrevValue ->
                        NewData = compute_new_value(PrevValue, Data),
                        State#upload{param_running_value = NewData}
                end,
            add_file_chunk(A, Res,NewState#upload{running_file_size = NewSize});
        Error={error, _Reason} ->
            Error
    end.

create_temp_file(State) ->
    case State#upload.no_temp_file of
        true ->
            [undefined, undefined];
        _ ->
            FilePath =
                case State#upload.fixed_filename of
                    undefined ->
                        {A, B, C} = yaws:unique_triple(),
                        FileName = yaws:join_sep(["yaws",
                                                  integer_to_list(A),
                                                  integer_to_list(B),
                                                  integer_to_list(C)], "_"),
                        filename:join([State#upload.temp_dir, FileName]);
                    Filename ->
                        Filename
                end,
            case file:open(FilePath, [write,binary]) of
                {ok, Fd} -> [FilePath, Fd];
                {error, Reason} ->
                    {error, make_error_reason(Reason, State)}
            end
    end.

close_previous_param(#upload{param_name = undefined} = State) ->
    State;
close_previous_param(#upload{param_name = ParamName} = State) ->
    S2 = case State#upload.filename of
             undefined ->
                 ParamValue = State#upload.param_running_value,
                 State#upload{
                   params = dict:store(ParamName, ParamValue,
                                       State#upload.params)};
             _ ->
                 ParamInfo = [{"filename", State#upload.filename}],
                 ParamInfo2 = case State#upload.fd of
                                  undefined ->
                                      lists:append(
                                        ParamInfo,
                                        [{value,
                                          State#upload.param_running_value}]);
                                  Fd ->
                                      file:close(Fd),
                                      lists:append(ParamInfo,
                                                   [{temp_file,
                                                     State#upload.temp_file}])
                              end,
                 ParamInfo3 = lists:append(ParamInfo2, State#upload.headers),
                 State#upload{
                   filename = undefined,
                   fd       = undefined,
                   temp_file= undefined,
                   running_file_size = 0,
                   params   = dict:store(ParamName, ParamInfo3,
                                         State#upload.params)
                  }
         end,
    S2#upload{param_name          = undefined,
              param_running_value = undefined,
              headers             = []}.

compute_new_size(State, Data) ->
    case Data of
        undefined ->
            State#upload.running_file_size;
        _ ->
            State#upload.running_file_size + iolist_size(Data)
    end.

check_param_size(State, NewSize) ->
    case State#upload.max_file_size of
        undefined -> ok;
        MaxSizeInBytes ->
            case NewSize > MaxSizeInBytes of
                false ->
                    ok;
                true ->
                    file:close(State#upload.fd),
                    {error, make_error_reason(max_file_size_exceeded, State)}
            end
    end.

compute_new_value(PrevValue, NewData) ->
    case NewData of
        Data when is_binary(NewData) ->
            <<PrevValue/binary, Data/binary>>;
        Data when is_list(NewData) ->
            PrevValue ++ Data
    end.

make_error_reason(Reason, State) ->
    Elements = [Reason, State#upload.param_name |
                case State#upload.return_error_file_path of
                    true ->
                        [State#upload.fixed_filename, State#upload.temp_file];
                    false ->
                        []
                end],
    list_to_tuple([E || E <- Elements, E /= undefined]).
