// Copyright (c) 2017-2025 Advanced Micro Devices, Inc. All rights reserved.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.

#ifndef ROCPRIM_BLOCK_DETAIL_BLOCK_ADJACENT_DIFFERENCE_IMPL_HPP_
#define ROCPRIM_BLOCK_DETAIL_BLOCK_ADJACENT_DIFFERENCE_IMPL_HPP_

#include "../../config.hpp"
#include "../../detail/various.hpp"
#include "../../intrinsics/thread.hpp"

#include <type_traits>

#include <cassert>

BEGIN_ROCPRIM_NAMESPACE

namespace detail
{

// Wrapping function that allows to call BinaryFunction of any of these signatures:
// with b_index (a, b, b_index) or without it (a, b).
// Only in the case of discontinuity (when flags_style is true) is the operator allowed to take an
// index
// block_discontinuity and block_adjacent difference only differ in their implementations by the
// order the operators parameters are passed, so this method deals with this as well
template<class T, class BinaryFunction>
ROCPRIM_DEVICE ROCPRIM_INLINE
auto apply(BinaryFunction op,
           const T&       a,
           const T&       b,
           unsigned int   index,
           bool_constant<true> /*as_flags*/,
           bool_constant<false> /*reversed*/) -> decltype(op(b, a, index))
{
    return op(a, b, index);
}

template<class T, class BinaryFunction>
ROCPRIM_DEVICE ROCPRIM_INLINE
auto apply(BinaryFunction op,
           const T&       a,
           const T&       b,
           unsigned int   index,
           bool_constant<true> /*as_flags*/,
           bool_constant<true> /*reversed*/) -> decltype(op(b, a, index))
{
    return op(b, a, index);
}

template<typename T, typename BinaryFunction, bool AsFlags>
ROCPRIM_DEVICE ROCPRIM_INLINE
auto apply(BinaryFunction op,
           const T&       a,
           const T&       b,
           unsigned int,
           bool_constant<AsFlags> /*as_flags*/,
           bool_constant<false> /*reversed*/) -> decltype(op(b, a))
{
    return op(a, b);
}

template<typename T, typename BinaryFunction, bool AsFlags>
ROCPRIM_DEVICE ROCPRIM_INLINE
auto apply(BinaryFunction op,
           const T&       a,
           const T&       b,
           unsigned int,
           bool_constant<AsFlags> /*as_flags*/,
           bool_constant<true> /*reversed*/) -> decltype(op(b, a))
{
    return op(b, a);
}

template<typename T,
         unsigned int BlockSizeX,
         unsigned int BlockSizeY = 1,
         unsigned int BlockSizeZ = 1>
class block_adjacent_difference_impl
{
public:
    static constexpr unsigned int BlockSize = BlockSizeX * BlockSizeY * BlockSizeZ;
    struct storage_type
    {
        T items[BlockSize];
    };

    template<bool         AsFlags,
             bool         Reversed,
             bool         WithTilePredecessor,
             unsigned int ItemsPerThread,
             typename Output,
             typename BinaryFunction>
    ROCPRIM_DEVICE
    void apply_left(const T (&input)[ItemsPerThread],
                    Output (&output)[ItemsPerThread],
                    BinaryFunction op,
                    const T        tile_predecessor_item,
                    storage_type&  storage)
    {
        static constexpr auto as_flags = bool_constant<AsFlags>{};
        static constexpr auto reversed = bool_constant<Reversed>{};

        const unsigned int flat_id
            = ::rocprim::flat_block_thread_id<BlockSizeX, BlockSizeY, BlockSizeZ>();

        // Save the last item of each thread
        storage.items[flat_id] = input[ItemsPerThread - 1];

        ROCPRIM_UNROLL
        for(unsigned int i = ItemsPerThread - 1; i > 0; --i)
        {
            output[i] = detail::apply(op,
                                      input[i - 1],
                                      input[i],
                                      flat_id * ItemsPerThread + i,
                                      as_flags,
                                      reversed);
        }
        ::rocprim::syncthreads();

        if constexpr(WithTilePredecessor)
        {
            T predecessor_item = tile_predecessor_item;
            if(flat_id != 0)
            {
                predecessor_item = storage.items[flat_id - 1];
            }

            output[0] = detail::apply(op,
                                      predecessor_item,
                                      input[0],
                                      flat_id * ItemsPerThread,
                                      as_flags,
                                      reversed);
        }
        else
        {
            output[0] = get_default_item(input, 0, as_flags);
            if(flat_id != 0)
            {
                output[0] = detail::apply(op,
                                          storage.items[flat_id - 1],
                                          input[0],
                                          flat_id * ItemsPerThread,
                                          as_flags,
                                          reversed);
            }
        }
    }

    template<bool         AsFlags,
             bool         Reversed,
             bool         WithTilePredecessor,
             unsigned int ItemsPerThread,
             typename Output,
             typename BinaryFunction>
    ROCPRIM_DEVICE
    void apply_left_partial(const T (&input)[ItemsPerThread],
                            Output (&output)[ItemsPerThread],
                            BinaryFunction     op,
                            const T            tile_predecessor_item,
                            const unsigned int valid_items,
                            storage_type&      storage)
    {
        static constexpr auto as_flags = bool_constant<AsFlags>{};
        static constexpr auto reversed = bool_constant<Reversed>{};

        const unsigned int flat_id
            = ::rocprim::flat_block_thread_id<BlockSizeX, BlockSizeY, BlockSizeZ>();

        // Save the last item of each thread
        storage.items[flat_id] = input[ItemsPerThread - 1];

        ROCPRIM_UNROLL
        for(unsigned int i = ItemsPerThread - 1; i > 0; --i)
        {
            const unsigned int index = flat_id * ItemsPerThread + i;
            output[i]                = get_default_item(input, i, as_flags);
            if(index < valid_items)
            {
                output[i] = detail::apply(op, input[i - 1], input[i], index, as_flags, reversed);
            }
        }
        ::rocprim::syncthreads();

        const unsigned int index = flat_id * ItemsPerThread;

        if constexpr(WithTilePredecessor)
        {
            T predecessor_item = tile_predecessor_item;
            if(flat_id != 0)
            {
                predecessor_item = storage.items[flat_id - 1];
            }

            output[0] = get_default_item(input, 0, as_flags);
            if(index < valid_items)
            {
                output[0]
                    = detail::apply(op, predecessor_item, input[0], index, as_flags, reversed);
            }
        }
        else
        {
            output[0] = get_default_item(input, 0, as_flags);
            if(flat_id != 0 && index < valid_items)
            {
                output[0] = detail::apply(op,
                                          storage.items[flat_id - 1],
                                          input[0],
                                          flat_id * ItemsPerThread,
                                          as_flags,
                                          reversed);
            }
        }
    }

    template<bool         AsFlags,
             bool         Reversed,
             bool         WithTileSuccessor,
             unsigned int ItemsPerThread,
             typename Output,
             typename BinaryFunction>
    ROCPRIM_DEVICE
    void apply_right(const T (&input)[ItemsPerThread],
                     Output (&output)[ItemsPerThread],
                     BinaryFunction op,
                     const T        tile_successor_item,
                     storage_type&  storage)
    {
        static constexpr auto as_flags = bool_constant<AsFlags>{};
        static constexpr auto reversed = bool_constant<Reversed>{};

        const unsigned int flat_id
            = ::rocprim::flat_block_thread_id<BlockSizeX, BlockSizeY, BlockSizeZ>();

        // Save the first item of each thread
        storage.items[flat_id] = input[0];

        ROCPRIM_UNROLL
        for(unsigned int i = 0; i < ItemsPerThread - 1; ++i)
        {
            output[i] = detail::apply(op,
                                      input[i],
                                      input[i + 1],
                                      flat_id * ItemsPerThread + i + 1,
                                      as_flags,
                                      reversed);
        }
        ::rocprim::syncthreads();

        if constexpr(WithTileSuccessor)
        {
            T successor_item = tile_successor_item;
            if(flat_id != BlockSize - 1)
            {
                successor_item = storage.items[flat_id + 1];
            }

            output[ItemsPerThread - 1] = detail::apply(op,
                                                       input[ItemsPerThread - 1],
                                                       successor_item,
                                                       flat_id * ItemsPerThread + ItemsPerThread,
                                                       as_flags,
                                                       reversed);
        }
        else
        {
            output[ItemsPerThread - 1] = get_default_item(input, ItemsPerThread - 1, as_flags);
            if(flat_id != BlockSize - 1)
            {
                output[ItemsPerThread - 1]
                    = detail::apply(op,
                                    input[ItemsPerThread - 1],
                                    storage.items[flat_id + 1],
                                    flat_id * ItemsPerThread + ItemsPerThread,
                                    as_flags,
                                    reversed);
            }
        }
    }
    template<bool         AsFlags,
             bool         Reversed,
             unsigned int ItemsPerThread,
             typename Output,
             typename BinaryFunction>
    ROCPRIM_DEVICE
    void apply_right_partial(const T (&input)[ItemsPerThread],
                             Output (&output)[ItemsPerThread],
                             BinaryFunction     op,
                             const unsigned int valid_items,
                             storage_type&      storage)
    {
        static constexpr auto as_flags = bool_constant<AsFlags>{};
        static constexpr auto reversed = bool_constant<Reversed>{};

        const unsigned int flat_id
            = ::rocprim::flat_block_thread_id<BlockSizeX, BlockSizeY, BlockSizeZ>();

        // Save the first item of each thread
        storage.items[flat_id] = input[0];

        ROCPRIM_UNROLL
        for(unsigned int i = 0; i < ItemsPerThread - 1; ++i)
        {
            const unsigned int index = flat_id * ItemsPerThread + i + 1;
            output[i]                = get_default_item(input, i, as_flags);
            if(index < valid_items)
            {
                output[i] = detail::apply(op, input[i], input[i + 1], index, as_flags, reversed);
            }
        }
        ::rocprim::syncthreads();

        output[ItemsPerThread - 1] = get_default_item(input, ItemsPerThread - 1, as_flags);

        const unsigned int next_thread_index = flat_id * ItemsPerThread + ItemsPerThread;
        if(next_thread_index < valid_items)
        {
            output[ItemsPerThread - 1] = detail::apply(op,
                                                       input[ItemsPerThread - 1],
                                                       storage.items[flat_id + 1],
                                                       next_thread_index,
                                                       as_flags,
                                                       reversed);
        }
    }

private:
    template<unsigned int ItemsPerThread>
    ROCPRIM_DEVICE
    int get_default_item(const T (&)[ItemsPerThread],
                         unsigned int /*index*/,
                         bool_constant<true> /*as_flags*/)
    {
        return 1;
    }

    template<unsigned int ItemsPerThread>
    ROCPRIM_DEVICE
    T get_default_item(const T (&input)[ItemsPerThread],
                       const unsigned int index,
                       bool_constant<false> /*as_flags*/)
    {
        return input[index];
    }
};

} // namespace detail

END_ROCPRIM_NAMESPACE

#endif // ROCPRIM_BLOCK_DETAIL_BLOCK_ADJACENT_DIFFERENCE_IMPL_HPP_
