/*
    drjit/morton.h -- Morton/Z-order curve encoding and decoding routines

    Dr.Jit is a C++ template library that enables transparent vectorization
    of numerical kernels using SIMD instruction sets available on current
    processor architectures.

    Copyright (c) 2021 Wenzel Jakob <wenzel.jakob@epfl.ch>
    Includes contributions by Sebastien Speierer

    All rights reserved. Use of this source code is governed by a BSD-style
    license that can be found in the LICENSE file.
*/

#pragma once

#include <drjit/packet.h>

#if defined(_MSC_VER)
#  pragma warning (push)
#  pragma warning (disable: 4310) // cast truncates constant value
#endif

NAMESPACE_BEGIN(drjit)
NAMESPACE_BEGIN(detail)

/// Generate bit masks for the functions \ref scatter_bits() and \ref gather_bits()
template <typename Value> constexpr Value morton_magic(size_t dim, size_t level) {
    size_t n_bits = sizeof(Value) * 8;
    size_t max_block_size = n_bits / dim;
    size_t block_size = size_t(1) << (level - 1);
    size_t count = 0;

    if (block_size > max_block_size)
        block_size = max_block_size;

    Value mask  = Value(1) << (n_bits - 1),
         value = Value(0);

    for (size_t i = 0; i < n_bits; ++i) {
        value >>= 1;

        if (count < max_block_size && (i / block_size) % dim == 0) {
            count++;
            value |= mask;
        }
    }

    return value;
}

/// Bit scatter function. \c Dimension defines the final distance between two output bits
template <size_t, typename Value, size_t Level, enable_if_t<Level == 0> = 0>
DRJIT_INLINE Value scatter_bits(Value x) { return x; }

template <size_t Dimension, typename Value,
          size_t Level = clog2i(sizeof(Value) * 8),
          enable_if_t<Level != 0 && (!(has_avx2 && has_x86_64) || !std::is_integral_v<Value>)> = 0>
DRJIT_INLINE Value scatter_bits(Value x) {
    using Scalar = scalar_t<Value>;

    constexpr Scalar magic = morton_magic<Scalar>(Dimension, Level);
    constexpr size_t shift_maybe = (1 << (Level - 1)) * (Dimension - 1);
    constexpr size_t shift = (shift_maybe < sizeof(Scalar) * 8) ? shift_maybe : 0;

    if constexpr (shift != 0)
        x |= sl<shift>(x);

    x &= magic;

    return scatter_bits<Dimension, Value, Level - 1>(x);
}

template <size_t, typename Value, size_t Level,
          enable_if_t<Level == 0> = 0>
DRJIT_INLINE Value gather_bits(Value x) { return x; }

/// Bit gather function. \c Dimension defines the final distance between two input bits
template <size_t Dimension, typename Value,
          size_t Level = clog2i(sizeof(Value) * 8),
          enable_if_t<Level != 0 && (!(has_avx2 && has_x86_64) || !std::is_integral_v<Value>)> = 0>
DRJIT_INLINE Value gather_bits(Value x) {
    using Scalar = scalar_t<Value>;

    constexpr size_t ilevel = clog2i(sizeof(Value) * 8) - Level + 1;
    constexpr Scalar magic = morton_magic<Scalar>(Dimension, ilevel);
    constexpr size_t shift_maybe = (1 << (ilevel - 1)) * (Dimension - 1);
    constexpr size_t shift = (shift_maybe < sizeof(Scalar) * 8) ? shift_maybe : 0;

    x &= magic;

    if constexpr (shift != 0)
        x |= sr<shift>(x);

    return gather_bits<Dimension, Value, Level - 1>(x);
}

#if defined(DRJIT_X86_AVX2) && defined(DRJIT_X86_64)
template <size_t Dimension, typename Value,
          enable_if_t<std::is_integral_v<Value>> = 0>
DRJIT_INLINE Value scatter_bits(Value x) {
    constexpr Value magic = morton_magic<Value>(Dimension, 1);
    if constexpr (sizeof(Value) <= 4)
        return Value(_pdep_u32((uint32_t) x, (uint32_t) magic));
    else
        return Value(_pdep_u64((uint64_t) x, (uint64_t) magic));
}

template <size_t Dimension, typename Value,
          enable_if_t<std::is_integral_v<Value>> = 0>
DRJIT_INLINE Value gather_bits(Value x) {
    constexpr Value magic = morton_magic<Value>(Dimension, 1);
    if constexpr (sizeof(Value) <= 4)
        return Value(_pext_u32((uint32_t) x, (uint32_t) magic));
    else
        return Value(_pext_u64((uint64_t) x, (uint64_t) magic));
}
#endif

template <typename Array, size_t Index,
          enable_if_t<Index == 0> = 0>
DRJIT_INLINE void morton_decode_helper(value_t<Array> value, Array &out) {
    out.entry(0) = gather_bits<Array::Size>(value);
}

template <typename Array, size_t Index = array_size_v<Array> - 1,
          enable_if_t<Index != 0> = 0>
DRJIT_INLINE void morton_decode_helper(value_t<Array> value, Array &out) {
    out.entry(Index) = gather_bits<Array::Size>(sr<Index>(value));
    morton_decode_helper<Array, Index - 1>(value, out);
}

NAMESPACE_END(detail)

/// Convert a N-dimensional integer array into the Morton/Z-order curve encoding
template <typename Array, size_t Index, enable_if_t<Index == 0> = 0>
DRJIT_INLINE value_t<Array> morton_encode(Array a) {
    return detail::scatter_bits<Array::Size>(a.entry(0));
}

/// Convert a N-dimensional integer array into the Morton/Z-order curve encoding
template <typename Array, size_t Index = array_size_v<Array> - 1, enable_if_t<Index != 0> = 0>
DRJIT_INLINE value_t<Array> morton_encode(Array a) {
    static_assert(std::is_unsigned_v<scalar_t<Array>>, "morton_encode() requires unsigned arguments");
    return sl<Index>(detail::scatter_bits<Array::Size>(a.entry(Index))) |
                     morton_encode<Array, Index - 1>(a);
}

/// Convert Morton/Z-order curve encoding into a N-dimensional integer array
template <typename Array>
DRJIT_INLINE Array morton_decode(value_t<Array> value) {
    static_assert(std::is_unsigned_v<scalar_t<Array>>, "morton_decode() requires unsigned arguments");
    Array result;
    detail::morton_decode_helper(value, result);
    return result;
}

NAMESPACE_END(drjit)

#if defined(_MSC_VER)
#  pragma warning (pop)
#endif
