lib/time/working_time.ex


defmodule Time.WorkingTime do
    @moduledoc"""
    Module oriented to the calculation of working time, according to the business
    """

    alias Type.Type
    import Time.PWorkingTimeForBusiness

    @doc"""
    Calculates the working time elapsed between two dates. Depending on the business, hours vary.

    To add a new business and its particular hours, you must:
        1. Define a module with the name of the business.
        2. Implement the Time.PWorkingTimeForBusiness protocol, to define particular hours. Use the following template:

        ```
        define Time.PWorkingTimeForBusiness, for: Atom do
            def function_name(date, <business_name>) do
                ...
            end

            ...

        end
        ```

    ### Parameter:

        - start_date: Timex.DateTime or String. Start date.

        - end_date: Timex.DateTime or String. End date.

        - business: Atom. Business.

        - params: t. Auxiliary parameters.

        - change_timezone: Boolean. Indicate if you have to change the dates to the time use defined in the configuration.

    ### Return:

        - {:ok, Integer (seconds)} | {:error, String}

    """
    def elapsed_time(start_date, end_date, business, params, change_timezone \\ true)

    def elapsed_time(start_date, end_date, _business, _params, _change_timezone)
        when is_nil(start_date) or is_nil(end_date)
        do
            {:error, "One of the dates has null value. Start date: #{start_date}. End date: #{end_date}."}
    end

    def elapsed_time(start_date, end_date, business, params, change_timezone)
        when
            (is_binary(start_date) and start_date != "") or
            (is_binary(end_date) and end_date != "")
        do
            start_date = try do
                    Type.convert(start_date, :DateTime)
                rescue
                    _ -> start_date
                end

            end_date = try do
                    Type.convert(end_date, :DateTime)
                rescue
                    _ -> end_date
                end

            elapsed_timep(start_date, end_date, business, params, change_timezone)
    end

    def elapsed_time(start_date, end_date, business, params, change_timezone) do
        elapsed_timep(start_date, end_date, business, params, change_timezone)
    end

    defp elapsed_timep(start_date, end_date, business, params, change_timezone) do
        if not Timex.is_valid?(start_date) do
            {:error, "Not valid init date #{inspect start_date}"}

        else if not Timex.is_valid?(end_date) do
            {:error, "Not valid end date #{inspect end_date}"}

        else if Timex.diff(end_date, start_date, :seconds) < 0 do
            {:error, "Start date #{inspect start_date} is later than end date #{inspect end_date}"}

        else
            start_date = convert_to_business_datetime(start_date, business, params, change_timezone)
            end_date = convert_to_business_datetime(end_date, business, params, change_timezone)

            result = Timex.diff(end_date, start_date, :seconds) - get_non_working_time(start_date, end_date, business, params)
            {:ok, result}
        end end end
    end

    #
    # Calculate the non-labor time between two dates, in seconds
    #
    # ### Parameters:
    #
    #     - start_date: Timex.DateTime. Start date.
    #
    #     - end_date: Timex.DateTime. End date.
    #
    #     - business: Atom. Business.
    #
    #     - params: t. Auxiliary parameters.
    #
    # ### Returns:
    #
    #     - Integer
    #
    defp get_non_working_time(%{year: year, month: month, day: day}, %{year: year, month: month, day: day}, _business, _params) do
        0
    end

    defp get_non_working_time(%{year: year, month: month, day: day}, %{year: year, month: month, day: day}, _business, _params, %{year: year, month: month, day: day}) do
        0
    end

    defp get_non_working_time(start_date, end_date, business, params) do
        get_non_working_time(start_date, end_date, business, params, start_date)
    end

    defp get_non_working_time(start_date, end_date, business, params, last_working_start_date) do
        tomorrow = Timex.shift(start_date, days: 1)
        {_, {end_hour, end_minute, end_second}} = working_hours(last_working_start_date, business, params)
        {{start_hour, start_minute, start_second}, _} = working_hours(end_date, business, params)

        # It is assumed that the start time is the same for all work days
        if not is_working_day?(tomorrow, business, params) do
            # time in seconds from the starting time, until the same time of next day
            24 * 3_600
        else
            # time in seconds between end_hour:00 and start_hour:00 next day
            rem(start_hour - end_hour + 24, 24) * 3_600 -
            # time in seconds of the shift of the initial time, which was assumed it is more
            end_minute * 60 - end_second +
            # time in seconds of the racing of the final hour, which was assumed it is less
            start_minute * 60 + start_second
        end
        |> Kernel.+(get_non_working_time(
            tomorrow,
            end_date,
            business,
            params,
            if is_working_day?(tomorrow, business, params) do
                tomorrow
            else
                last_working_start_date
            end
            )
        )
    end

    @doc """
    Given a date, it returns the next date such that it is skilled in the defined working hours. Can return the same date.

    ### Parameter:

        - date: DateTime. Date.

        - business: Atom. Business.

        - params: t. Auxiliary parameters.

        - change_timezone: Boolean. Indicate if you have to change the dates to the time use defined in the configuration.

    ### Return:

        - DateTime
    """
    def convert_to_business_datetime(date, business, params, true) do
        date
        |> Timex.Timezone.convert("America/Santiago")
        |> convert_to_business_datetime(business, params, false)
    end

    def convert_to_business_datetime(%{hour: hour, minute: minute, second: seconds} = date, business, params, false) do
        is_working_hours = is_working_hours?(date, business, params)
        is_working_day = is_working_day?(date, business, params)

        {start_time, _} = date |> working_hours(business, params)

        new_date =
            case {is_working_day, is_working_hours} do
                {false, _} ->
                    date
                    |> next_working_day(business, params)

                {true, false} ->
                    if {0, 0, 0} <= {hour, minute, seconds} and {hour, minute, seconds} <= start_time do
                        date

                    else
                        date
                        |> next_working_day(business, params)
                    end

                {true, true} ->
                    date
            end

        if not(is_working_day and is_working_hours) do
            {{start_hour, start_minute, start_second}, _} =
                new_date
                |> working_hours(business, params)

            new_date
            |> Timex.set([hour: start_hour, minute: start_minute, second: start_second, microsecond: 0])

        else
            new_date
        end
    end

    #
    # Returns the closest business day after the defined date
    #
    # ### Parameters:
    #
    #     - date: Timex.DateTime. Date.
    #
    #     - business: Atom. Business.
    #
    #     - params: t. Auxiliary parameters.
    #
    # ### Return:
    #
    #     - Timex.DateTime
    #
    defp next_working_day(date, business, params) do
        # try do para el caso de que existe un desplazamiento de una fecha, sobre un cambio de uso de horario
        tomorrow =
            try do
                tmp =
                    date
                    |> Timex.shift(days: 1)
                    |> Map.get(:after) # si no da error es por temp es del tipo AmbiguosDateTime

                # si es true es xq el día anterior fue del tipo AmbiguosDateTime, pero el actual es de tipo DateTime, por lo que tiene que calcularse con normalidad
                if is_nil(tmp) do
                    raise("")
                else
                    tmp
                end
            rescue
                _ -> Timex.shift(date, days: 1)
            end

        if is_working_day?(tomorrow, business, params) do
            tomorrow
        else
            next_working_day(tomorrow, business, params)
        end
    end

    #
    # Check if the defined date is a working hours
    #
    # ### Parameter:
    #
    #     - date: Timex.DateTime. Date.
    #
    #     - business: Atom. Business.
    #
    #     - params: t. Auxiliary parameters.
    #
    #  ### Return:
    #
    #     - Boolean.
    #
    defp is_working_hours?(%{hour: hour, minute: minute, second: second} = date, business, params) do
        {start_time, end_time} = working_hours(date, business, params)
        start_time <= {hour, minute, second} and {hour, minute, second} <= end_time
    end







end