#ifndef ARRAY_H
#define ARRAY_H

#include "Core.h"
#include "Macros.h"
#include <Eigen/Dense>
#include <iomanip>
#include <math.h>
#include <random>
#include <type_traits>

#ifdef DEVICE
#define POINTER pDevice
#else
#define POINTER pHost
#endif

namespace CudaTools {

template <typename T>
using EigenMat = Eigen::Matrix<T, Eigen::Dynamic, Eigen::Dynamic, Eigen::ColMajor>;
template <typename T> using EigenMapMat = Eigen::Map<EigenMat<T>>;
template <typename T> using ConstEigenMapMat = Eigen::Map<const EigenMat<T>>;

template <typename T> struct EigenAdaptConst { typedef EigenMapMat<T> type; };
template <typename T> struct EigenAdaptConst<const T> { typedef ConstEigenMapMat<T> type; };

#define ENABLE_IF(X) std::enable_if_t<X, bool>
#define IS_INT(T) std::is_integral<T>::value
#define IS_FLOAT(T) std::is_floating_point<T>::value
#define IS_NUM(T) IS_INT(T) or IS_FLOAT(T)

template <typename T> class Array;
using Slice = std::pair<uint32_t, uint32_t>;

template <typename T> class ArrayIterator {
  private:
    template <typename U>
    friend std::ostream& operator<<(std::ostream& out, const ArrayIterator<U>& it);
    T* pData;
    Shape mShape;
    uint32_t mIndices[CUDATOOLS_ARRAY_MAX_AXES] = {0};

  public:
    HD ArrayIterator(T* p, const Shape& shape) : pData(p), mShape(shape){};

    /**
     * Moves the iterator to the next value.
     */
    HD void next() {
        bool carry = false;
        uint32_t offset = 0;
        for (uint32_t iAxis = mShape.axes() - 1; iAxis < mShape.axes(); --iAxis) {
            if (mIndices[iAxis] == mShape.dim(iAxis) - 1) {
                mIndices[iAxis] = 0;
                offset += mShape.stride(iAxis) * (mShape.dim(iAxis) - 1);
                carry = true;
            } else {
                pData += mShape.stride(iAxis);
                mIndices[iAxis] += 1;
                carry = false;
            }

            if (not carry) {
                pData -= offset;
                return;
            }
        }
        pData += 1; // "Overflow" occured, so we reached end of array.
    }

    /**
     * Moves the iterator to the previous value.
     */
    HD void prev() {
        bool carry = false;
        uint32_t offset = 0;
        for (uint32_t iAxis = mShape.axes() - 1; iAxis < mShape.axes(); --iAxis) {
            if (mIndices[iAxis] == 0) {
                mIndices[iAxis] = mShape.dim(iAxis) - 1;
                offset += mShape.stride(iAxis) * (mShape.dim(iAxis) - 1);
                carry = true;
            } else {
                pData -= mShape.stride(iAxis);
                mIndices[iAxis] += 1;
                carry = false;
            }
            if (not carry) {
                pData += offset;
                return;
            }
        }
        pData -= 1;
    }

    /**
     * Moves the iterator a specified value away.
     * \param amount the amount to advance by
     */
    HD void advance(const int32_t amount) {
        if (amount < 0) {
            for (uint32_t i = 0; i < abs(amount); ++i) {
                prev();
            }
        } else {
            for (uint32_t i = 0; i < abs(amount); ++i) {
                next();
            }
        }
    }

    HD void operator++() { next(); }; /**< Prefix increment operator. */
    HD void operator--() { prev(); }; /**< Prefix decrement operator. */

    /**< Addition operator. */
    HD ArrayIterator<T> operator+(const int32_t v) const {
        ArrayIterator<T> it = *this;
        it.advance(v);
        return it;
    };

    /** Subtraction operator.*/
    HD ArrayIterator<T> operator-(const int32_t v) const {
        ArrayIterator<T> it = *this;
        it.advance(-v);
        return it;
    };
    HD void operator+=(const int32_t v) { advance(v); };
    HD void operator-=(const int32_t v) { advance(-v); };

    HD T& operator*() { return *pData; };             /**< Dereference operator. */
    HD const T& operator*() const { return *pData; }; /**< Const dereference operator. */

    /**
     * Equals operator.
     */
    HD bool operator==(const ArrayIterator<T>& it) { return pData == it.pData; }

    /**
     * Not equals operator.
     */
    HD bool operator!=(const ArrayIterator<T>& it) { return pData != it.pData; }
};

template <typename T> std::ostream& operator<<(std::ostream& out, const ArrayIterator<T>& it) {
    return out << it.pData;
}

template <typename T> class ArrayLoader {
  private:
    ArrayIterator<T> mIterator;
    ArrayIterator<T> mIteratorEnd;

  public:
    HD ArrayLoader(const ArrayIterator<T>& it, const ArrayIterator<T>& it_end)
        : mIterator(it), mIteratorEnd(it_end){};
    HD ArrayLoader &operator,(const T value) {
        CT_ERROR_IF(mIterator, ==, mIteratorEnd, "Cannot assign more values than Array size");
        *mIterator = value;
        ++mIterator;
        return *this;
    }
};

/**
 * A container that holds a N-dimensional array, stored column major. To set the
 * maximum N, there is a compiler macro CUDATOOLS_ARRAY_MAX_DIM whose default value is 4.
 * It adapts to operations between host and device to ease memory management.
 */
template <typename T> class Array {
  private:
    template <typename U> friend std::ostream& operator<<(std::ostream&, const Array<U>&);

    Shape mShape;
    T* pHost = nullptr;
    T* pDevice = nullptr;

    bool mIsView = false;
    bool mIsSlice = false;

    uint32_t mEndOffset = 0;

    void freeArrays() {
#ifndef DEVICE
        if (not mIsView) {
            if (pDevice != nullptr) CudaTools::free(pDevice);
            if (pHost != nullptr) delete[] pHost;
        }
#endif
    };

    HD void calcEnd() {
        uint32_t offset = 0;
        for (uint32_t i = 0; i < shape().axes(); ++i) {
            offset += (shape().dim(i) - 1) * shape().stride(i);
        }
        mEndOffset = offset + 1;
    };

  public:
    HD Array() = default;

    /**
     * Constructor for an Array that creates an allocates an array with
     * the specified Shape. Construction in this format is disabled on the device.
     * \brief Host only
     * \param shape the shape of the array
     * \param noDevice whether to initialize the array on the device
     */
    Array(const Shape& shape, const bool noDevice = false) : mShape(shape), mIsView(false) {
        pHost = new T[shape.items()];
        calcEnd();
        if (noDevice) return;
        pDevice = (T*)CudaTools::malloc(shape.items() * sizeof(T));
    };

    /**
     * Constructor for an Array from an existing (preallocated) pointer.
     * \param pointer the pointer to use
     * \param shape the shape of the array
     * \param noDevice whether to initialize the array on the device
     */
    HD Array(T* const pointer, const Shape& shape, const bool noDevice = false)
        : mShape(shape), mIsView(true), mIsSlice(false) {
        POINTER = pointer;
        calcEnd();
#ifndef DEVICE
        if (noDevice) return;
        pDevice = (T*)CudaTools::malloc(shape.items() * sizeof(T));
#endif
    };

    /**
     * Constructor for making a Array view from another Array,
     * given an offset and shape.
     * \param arr the original Array
     * \param shape the shape of the new array
     * \param offset the index where to start the a view of the array
     */
    HD Array(const Array& arr, const Shape& shape, const uint32_t offset = 0)
        : mShape(shape), pHost(arr.pHost), pDevice(arr.pDevice), mIsView(true),
          mIsSlice(arr.mIsSlice) {
        calcEnd();
        if (pHost != nullptr) pHost += offset;
        if (pDevice != nullptr) pDevice += offset;
    };

    /**
     * The copy-constructor for a Array. If this is not a view, a deep copy
     * of the data will be performed on both host and device. On the device, it is always
     * treated like a view.
     */
    HD Array(const Array& arr) : mShape(arr.mShape), mIsView(arr.mIsView), mIsSlice(arr.mIsSlice) {
        calcEnd();
        if (mIsView) { // If the other array was a view (and now this one), just assign.
            pHost = arr.pHost;
            pDevice = arr.pDevice;
            return;
        }

        // Otherwise, we assume this is needs to own data.
        pHost = new T[mShape.items()];
        auto arr_it = arr.begin();
        for (auto it = begin(); it != end(); ++it) {
            *it = *arr_it;
            ++arr_it;
        }

#ifndef DEVICE
        if (arr.pDevice != nullptr) {
            pDevice = (T*)CudaTools::malloc(mShape.items() * sizeof(T));
        }
#endif
    };

    /**
     *  The move-constructor for a Array.
     */
    HD Array(Array&& arr)
        : mShape(arr.mShape), pHost(arr.pHost), pDevice(arr.pDevice), mIsView(arr.mIsView),
          mIsSlice(arr.mIsSlice) {
        calcEnd();
        // Make other object empty.
        arr.pHost = nullptr;
        arr.pDevice = nullptr;
        arr.mIsView = true;
    };

    HD ~Array() { freeArrays(); };

    /**
     * The copy-assignment operator for a Array. If this is not a view,
     * then the currently owned data will be freed, and a deep copy of the data will
     * be performed on both host and device. On the device, it is always treated like a view.
     */
    HD Array& operator=(const Array& arr) {
        if (this == &arr) return *this;

        if (mIsView) { // If this array is a view, we assign data from the right-hand side.
            auto arr_it = arr.begin();
            for (auto it = begin(); it != end() and arr_it != arr.end(); ++it) {
                *it = *arr_it;
                ++arr_it;
            }
            return *this;
        }

        // Otherwise, it is implied to be object reassignment.
        mShape = arr.mShape;
        mIsView = arr.mIsView;
        mIsSlice = arr.mIsSlice;
        calcEnd();

        // Regardless if the right-hand side is a view, we create a new copy.
        // In case that the right-hand side is a view of this array, we
        // allocate memory to copy first. Keep in mind that the right-hand side
        // array will then become undefined.

        // We can only do this on the host.
#ifndef DEVICE
        T* new_pDevice = nullptr;
        if (pDevice != nullptr) {
            new_pDevice = (T*)CudaTools::malloc(mShape.items() * sizeof(T));
        }

        T* new_pHost = new T[mShape.items()];
        memcpy(new_pHost, arr.pHost, mShape.items() * sizeof(T));

        freeArrays();
        pHost = new_pHost;
        pDevice = new_pDevice;
#else
        pHost = arr.pHost;
        pDevice = arr.pDevice;
#endif
        return *this;
    };

    /**
     * The move-assignment operator for a Array.
     */
    HD Array& operator=(Array&& arr) {
        if (this == &arr) return *this;

        if (mIsView) { // If this array is a view, we assign data from the right-hand side.
            auto arr_it = arr.begin();
            for (auto it = begin(); it != end() and arr_it != arr.end(); ++it) {
                *it = *arr_it;
                ++arr_it;
            }
            return *this;
        }

        CT_ERROR(arr.mIsView,
                 "Cannot move-assign view to a non-view (owner). This would lead to undefined "
                 "behavior.");

        // Otherwise, it is implied to be object reassignment.
        freeArrays();
        mShape = arr.mShape;
        pHost = arr.pHost;
        pDevice = arr.pDevice;
        mIsView = arr.mIsView;
        mIsSlice = arr.mIsSlice;
        calcEnd();

        // Make other array empty.
        arr.pHost = nullptr;
        arr.pDevice = nullptr;
        arr.mIsView = true;
        return *this;
    };

    /**
     * Used for indexing the Array.
     * \param index index of the first dimension
     */
    HD Array operator[](const uint32_t index) const {
        CT_ERROR_IF(index, >=, shape().dim(0), "Index exceeds axis size");
        return Array(*this, shape().subshape(1), index * shape().stride(0));
    };

    /**
     * Used for indexing the Array.
     * \param indices a list of indices to index the Array
     */
    HD Array operator[](const std::initializer_list<uint32_t> indices) const {
        CT_ERROR_IF(indices.size(), >, shape().axes(),
                    "Number of indices cannot exceed number of axes");
        auto it = indices.begin();
        uint offset = 0;
        for (uint32_t i = 0; i < indices.size(); ++i) {
            uint32_t index = *it;
            CT_ERROR_IF(index, >=, shape().dim(i), "Index exceeds axis size");
            offset += index * shape().stride(i);
            ++it;
        }
        return Array(*this, shape().subshape(indices.size()), offset);
    };

    HD ArrayLoader<T> operator<<(const T value) {
        auto it = begin();
        *it = value;
        ++it;
        return ArrayLoader<T>(it, end());
    };

    HD T operator=(const T& value) { return POINTER[0] = value; };
    HD operator T&() { return POINTER[0]; };
    HD operator const T&() const { return POINTER[0]; };

    /**
     * Used to create slices of the Array.
     * \param slices a list of slices to slice the Array
     */
    HD Array slice(const std::initializer_list<Slice> slices) const {
        CT_ERROR_IF(slices.size(), >, shape().axes(),
                    "Number of slices cannot exceed number of axes");

        uint offset = 0;
        Shape new_shape = mShape;
        auto it = slices.begin();
        for (uint32_t i = 0; i < slices.size(); ++i) {
            uint32_t from_index = it->first;
            uint32_t to_index = it->second;
            CT_ERROR_IF(from_index, >, to_index,
                        "Slice start cannot be greater than than slice end");
            CT_ERROR_IF(from_index, >=, shape().dim(i), "Slice start exceeds axis size");
            CT_ERROR_IF(to_index - 1, >=, shape().dim(i), "Slice end exceeds axis size");

            offset += from_index * shape().stride(i);
            new_shape.mAxisDim[i] = to_index - from_index;
            ++it;
        }
        new_shape.mItems = 1;
        for (uint32_t i = 0; i < shape().axes(); ++i) {
            new_shape.mItems *= new_shape.dim(i);
        }

        Array<T> arr(*this, new_shape, offset);
        arr.mIsSlice = true;
        return arr;
    };

    /**
     * Returns this Array with a different Shape. Its self assigning version is reshape.
     * If this Array is a slice of another, then it will perform a deep copy, and return
     * a new non-view array.
     */
    HD Array reshaped(const Shape& new_shape) const {
        CT_ERROR_IF(shape().items(), !=, new_shape.items(),
                    "New shape cannot have a different number of terms");
        CT_ERROR(mIsSlice, "Cannot reshape slice, a new array must be made. (Try copy first)")
        Array<T> arr = view();
        arr.mShape = new_shape;
        return arr;
    };

    HD void reshape(const Shape& new_shape) {
        CT_ERROR_IF(shape().items(), !=, new_shape.items(),
                    "New shape cannot have a different number of terms");
        CT_ERROR(mIsSlice, "Cannot reshape slice, a new array must be made. (Try copy first)")
        mShape = new_shape;
    };

    /**
     * Gets a view that is has at least two dimensions. Useful for promoting
     * single vectors to their 2D counterparts.
     */
    HD Array atLeast2D() const {
        return (shape().axes() == 1) ? reshaped({shape().length(), 1}) : view();
    };

    /**
     * Reshapes this array, making it at least 2D. Useful for promoting
     * single vectors to their 2D counterparts.
     */
    HD void asAtLeast2D() {
        if (shape().axes() == 1) reshape({shape().length(), 1});
    };

    /**
     * Returns a view of this Array that has been flattened into one dimension.
     */
    HD Array flattened() const { return reshaped({mShape.mItems}); };

    /**
     * Flattens this Array into one dimension.
     */
    HD void flatten() { reshape({mShape.mItems}); };

    /**
     * Returns the Eigen::Map of this Array.
     */
    typename EigenAdaptConst<T>::type eigenMap() const {
        uint32_t total_dim = mShape.mAxes;
        CT_ERROR(mIsSlice, "Mapping to an Eigen array cannot occur on slices")
        CT_ERROR_IF(total_dim, !=, 2,
                    "Mapping to an Eigen array can only occur on two-dimensional arrays");
        return typename EigenAdaptConst<T>::type(POINTER, mShape.rows(), mShape.cols());
    };

    /**
     * Gets the Shape of the Array.
     */
    HD Shape shape() const { return mShape; };

    /**
     * Gets the pointer to this array, depending on host or device.
     */
    HD T* data() const { return POINTER; };

    /**
     * Returns the device pointer regardless of host or device.
     */
    HD T* dataDevice() const { return pDevice; };

    HD bool isView() const { return mIsView; };   /**< Gets whether this Array is a view. */
    HD bool isSlice() const { return mIsSlice; }; /**< Gets whether this Array is a slice. */

    /**
     * Gets a view of this Array.
     */
    HD Array view() const { return Array(*this, mShape); }

    /**
     * Copies this Array and returns a new Array with the same memory.
     */
    HD Array copy() const {
        Array<T> arr(mShape, (pDevice == nullptr));

        auto arr_it = arr.begin();
        for (auto it = begin(); it != end(); ++it) {
            *arr_it = *it;
            ++arr_it;
        }
#ifndef DEVICE
        if (pDevice != nullptr) {
            CudaTools::deviceCopy(pDevice, arr.dataDevice(), mShape.items() * sizeof(T)).wait();
        }
#endif
        return arr;
    };

    /**
     * Gets the iterator to the beginning of this Array.
     */
    HD ArrayIterator<T> begin() const { return ArrayIterator<T>(POINTER, mShape); };

    /**
     * Gets the iterator to the end of this Array.
     */
    HD ArrayIterator<T> end() const { return ArrayIterator<T>(POINTER + mEndOffset, mShape); };

    /**
     * Sets the values of the entire Array to a constant. This is restricted to numerical types.
     */
    HD void setConstant(const T value) const {
        static_assert(IS_NUM(T), "Function only available on numeric types.");
        for (auto it = begin(); it != end(); ++it) {
            *it = value;
        }
    };

    /**
     * Sets the Array values with uniform random values in a specified range. This is restricted to
     * numerical types.
     * \brief Host only
     */
    void setRandom(const T min, const T max) const {
        static_assert(IS_NUM(T), "Function only available on numeric types.");
        CT_ERROR_IF(max, <, min, "Upper bound of range cannot be larger than lower bound");
        std::random_device rd;
        std::mt19937 mt(rd());
        if constexpr (IS_INT(T)) {
            std::uniform_int_distribution<T> dist(min, max);
            for (auto it = begin(); it != end(); ++it) {
                *it = dist(mt);
            }
        } else if constexpr (IS_FLOAT(T)) {
            std::uniform_real_distribution<T> dist(min, max);
            for (auto it = begin(); it != end(); ++it) {
                *it = dist(mt);
            }
        }
    };

    /**
     * Sets the Array values to start from a value and increment by a specified step. This is
     * restricted to numerical types.
     */
    HD void setRange(T min, const T step = 1) const {
        static_assert(IS_NUM(T), "Function only available on numeric types.");
        for (auto it = begin(); it != end(); ++it) {
            *it = min;
            min += step;
        }
    }
    /**
     * Sets the Array values to be evenly spaced numbers over a given interval. This is restricted
     * to floating point types.
     */
    HD void setLinspace(const T min, const T max) const {
        static_assert(IS_FLOAT(T), "Function only available on numeric floating types.");
        CT_ERROR_IF(max, <, min, "Upper bound of range cannot be larger than lower bound");
        T i = 0;
        T d = max - min;
        T items = (T)(shape().items() - 1);
        for (auto it = begin(); it != end(); ++it) {
            *it = min + d * (i / items);
            i += 1;
        }
    };

    /**
     * Returns array of given shape with constant values. This is restricted to numerical types.
     * \brief Host only
     */
    static Array constant(const Shape& shape, const T value) {
        static_assert(IS_NUM(T), "Function only available on numeric types.");
        Array<T> arr(shape);
        arr.setConstant(value);
        return arr;
    };

    /**
     * Returns array of given shape with random values in given interval. This is restricted to
     * numerical types.
     * \brief Host only
     */
    static Array random(const Shape& shape, const T min, const T max) {
        static_assert(IS_NUM(T), "Function only available on numeric types.");
        Array<T> arr(shape);
        arr.setRandom(min, max);
        return arr;
    };

    /**
     * Returns evenly spaced values within a given interval. This is restricted to numerical types.
     * \brief Host only
     */
    static Array range(const T min, const T max, const T step = 1) {
        static_assert(IS_NUM(T), "Function only available on numeric types.");
        CT_ERROR_IF(max, <, min, "Upper bound of range cannot be larger than lower bound");
        Array<T> arr({(uint32_t)((max - min) / step)});
        arr.setRange(min, step);
        return arr;
    }

    /**
     * Returns evenly spaced values within a given interval. This is restricted to floating point
     * types.
     * \brief Host only
     */
    static Array linspace(const T min, const T max, const uint32_t size) {
        static_assert(IS_FLOAT(T), "Function only available on numeric floating types.");
        Array<T> arr({size});
        arr.setLinspace(min, max);
        return arr;
    }

    /**
     * Transposes the internal data and returns the corresponding new Array.
     * Its self assigning version is transpose. This is restricted to numerical types.
     * \brief Host only
     */
    Array transposed() const {
        static_assert(IS_NUM(T), "Function only available on numeric types.");
        CT_ERROR_IF(shape().axes(), !=, 2, "Tranpose can only occur on two-dimensional arrays");
        Array<T> new_arr({mShape.rows(), mShape.cols()});
        new_arr.eigenMap() = this->eigenMap().transpose().eval();
        return new_arr;
    };

    /**
     * Transposes the intenal data. Its self assigning version is transpose.
     * This is restricted to numerical types.
     * \brief Host only
     */
    void transpose() {
        static_assert(IS_NUM(T), "Function only available on numeric types.");
        CT_ERROR_IF(shape().axes(), !=, 2, "Tranpose can only occur on two-dimensional arrays");
        Array<T> new_arr(*this, {mShape.cols(), mShape.rows()});
        new_arr.eigenMap() = this->eigenMap().transpose().eval();
        mShape = Shape({mShape.cols(), mShape.rows()});
    };

    void inverse() const {
        static_assert(IS_FLOAT(T), "Function only available on floating numeric types.");
        CT_ERROR_IF(shape().axes(), !=, 2, "Inverse can only occur on two-dimensional arrays");
        CT_ERROR_IF(shape().rows(), !=, shape().cols(),
                    "Inverse can only occur on square matrices");
        Array<T> inv(shape());
        inv.eigenMap() = this->eigenMap().inverse();
    };

    /**
     * Pins the memory (page locks) for faster memory transfer in concurrent
     * transfers.
     * \brief Host only
     */
    void pinMemory() const { CudaTools::pin(pHost, mShape.items() * sizeof(T)); };

    /**
     * Updates the host copy by copying the device data back to the host.
     * \brief Host only
     */
    StreamID updateHost(const StreamID& stream = DEF_MEM_STREAM) const {
        CT_ERROR(mIsView, "Cannot update host on a view");
        CudaTools::pull(pHost, pDevice, mShape.items() * sizeof(T), stream);
        return stream;
    };

    /**
     * Updates the device copy by copying the host data to the device.
     * \brief Host only
     */
    StreamID updateDevice(const StreamID& stream = DEF_MEM_STREAM) const {
        CT_ERROR(mIsView, "Cannot update device on a view");
        CudaTools::push(pHost, pDevice, mShape.items() * sizeof(T), stream);
        return stream;
    };
};

template <typename T>
void printAxis(std::ostream& out, const Array<T>& arr, const uint32_t axis, size_t width) {
    std::string space = std::string(2 * axis, ' ');
    if (arr.shape().axes() == 1) {
        out << "[";
        for (uint32_t i = 0; i < arr.shape().items(); ++i) {
            if constexpr (std::is_floating_point<T>::value) {
                out << std::scientific << std::setprecision(6);
            }
            if (width == 0) {
                out << ((i == 0) ? "" : " ");
            } else {
                out << std::setw((i == 0) ? width - 1 : width);
            }
            out << (T)arr[i] << ((i == arr.shape().items() - 1) ? "]" : ",");
        }
    } else if (arr.shape().axes() == 2) {
        for (uint32_t i = 0; i < arr.shape().dim(0); ++i) {
            out << space << ((i == 0) ? "[" : " ");
            printAxis(out, arr[i], axis + 1, width);
            out << ((i == arr.shape().dim(0) - 1) ? "]" : ",\n");
        }
    } else {
        out << space << "[\n";
        for (uint32_t i = 0; i < arr.shape().dim(0); ++i) {
            printAxis(out, arr[i], axis + 1, width);
            out << ((i == arr.shape().dim(0) - 1) ? "\n" : ",\n\n");
        }
        out << space << "]";
    }
}

template <typename T> std::ostream& operator<<(std::ostream& out, const Array<T>& arr) {
    size_t width = 0;
    if constexpr (IS_NUM(T)) {
        T max_val = 0;
        bool negative = false;
        for (auto it = arr.begin(); it != arr.end(); ++it) {
            if (*it < 0) negative = true;
            max_val = (abs(*it) > max_val) ? abs(*it) : max_val;
        }
        width = std::to_string(max_val).size() + 1;
        width += (negative) ? 1 : 0;
    } else if constexpr (IS_FLOAT(T)) {
        T max_val = 0;
        bool negative = false;
        for (auto it = arr.begin(); it != arr.end(); ++it) {
            if (*it < 0) negative = true;
            int exp = 0;
            frexp(*it, &exp);
            max_val = (exp > max_val) ? exp : max_val;
        }
        width = std::to_string(max_val).size() + 5;
        width += (negative) ? 1 : 0;
    }

    printAxis<T>(out, arr, 0, (arr.shape().axes() == 1) ? 0 : width);
    return out;
}

}; // namespace CudaTools

#endif // ARRAY_H