// SPDX-License-Identifier: Apache-2.0
//
// Copyright 2008-2016 Conrad Sanderson (http://conradsanderson.id.au)
// Copyright 2008-2016 National ICT Australia (NICTA)
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// ------------------------------------------------------------------------

//! \addtogroup memory
//! @{

class memory {
 public:
  template <typename eT>
  arma_malloc inline static eT* acquire(const uword n_elem);

  template <typename eT>
  arma_inline static void release(eT* mem);

  template <typename eT>
  arma_inline static bool is_aligned(const eT* mem);
  template <typename eT>
  arma_inline static void mark_as_aligned(eT*& mem);
  template <typename eT>
  arma_inline static void mark_as_aligned(const eT*& mem);
};

template <typename eT>
arma_malloc inline eT* memory::acquire(const uword n_elem) {
  if (n_elem == 0) {
    return nullptr;
  }

  arma_debug_check((size_t(n_elem) > (std::numeric_limits<size_t>::max() / sizeof(eT))),
                   "arma::memory::acquire(): requested size is too large");

  eT* out_memptr;

#if defined(ARMA_ALIEN_MEM_ALLOC_FUNCTION)
  { out_memptr = (eT*)ARMA_ALIEN_MEM_ALLOC_FUNCTION(sizeof(eT) * n_elem); }
#elif defined(ARMA_USE_TBB_ALLOC)
  { out_memptr = (eT*)scalable_malloc(sizeof(eT) * n_elem); }
#elif defined(ARMA_USE_MKL_ALLOC)
  { out_memptr = (eT*)mkl_malloc(sizeof(eT) * n_elem, 32); }
#elif defined(ARMA_HAVE_POSIX_MEMALIGN)
  {
    eT* memptr = nullptr;

    const size_t n_bytes = sizeof(eT) * size_t(n_elem);
    const size_t alignment = (n_bytes >= size_t(1024)) ? size_t(32) : size_t(16);

    // TODO: investigate apparent memory leak when using alignment >= 64 (as shown on
    // Fedora 28, glibc 2.27)
    int status = posix_memalign(
        (void**)&memptr, ((alignment >= sizeof(void*)) ? alignment : sizeof(void*)),
        n_bytes);

    out_memptr = (status == 0) ? memptr : nullptr;
  }
#elif defined(_MSC_VER)
  {
    // Windoze is too primitive to handle C++17 std::aligned_alloc()

    // out_memptr = (eT *) malloc(sizeof(eT)*n_elem);
    // out_memptr = (eT *) _aligned_malloc( sizeof(eT)*n_elem, 16 );  // lives in malloc.h

    const size_t n_bytes = sizeof(eT) * size_t(n_elem);
    const size_t alignment = (n_bytes >= size_t(1024)) ? size_t(32) : size_t(16);

    out_memptr = (eT*)_aligned_malloc(n_bytes, alignment);
  }
#else
  {
    // return ( new(std::nothrow) eT[n_elem] );
    out_memptr = (eT*)malloc(sizeof(eT) * n_elem);
  }
#endif

  // TODO: for mingw, use __mingw_aligned_malloc

  arma_check_bad_alloc((out_memptr == nullptr), "arma::memory::acquire(): out of memory");

  return out_memptr;
}

template <typename eT>
arma_inline void memory::release(eT* mem) {
  if (mem == nullptr) {
    return;
  }

#if defined(ARMA_ALIEN_MEM_FREE_FUNCTION)
  { ARMA_ALIEN_MEM_FREE_FUNCTION((void*)(mem)); }
#elif defined(ARMA_USE_TBB_ALLOC)
  { scalable_free((void*)(mem)); }
#elif defined(ARMA_USE_MKL_ALLOC)
  { mkl_free((void*)(mem)); }
#elif defined(ARMA_HAVE_POSIX_MEMALIGN)
  { free((void*)(mem)); }
#elif defined(_MSC_VER)
  {
    // free( (void *)(mem) );
    _aligned_free((void*)(mem));
  }
#else
  {
    // delete [] mem;
    free((void*)(mem));
  }
#endif

  // TODO: for mingw, use __mingw_aligned_free
}

template <typename eT>
arma_inline bool memory::is_aligned(const eT* mem) {
#if (defined(ARMA_HAVE_ICC_ASSUME_ALIGNED) || defined(ARMA_HAVE_GCC_ASSUME_ALIGNED)) && \
    !defined(ARMA_DONT_CHECK_ALIGNMENT)
  {
    return (sizeof(std::size_t) >= sizeof(eT*)) ? ((std::size_t(mem) & 0x0F) == 0)
                                                : false;
  }
#else
  {
    arma_ignore(mem);

    return false;
  }
#endif
}

template <typename eT>
arma_inline void memory::mark_as_aligned(eT*& mem) {
#if defined(ARMA_HAVE_ICC_ASSUME_ALIGNED)
  { __assume_aligned(mem, 16); }
#elif defined(ARMA_HAVE_GCC_ASSUME_ALIGNED)
  { mem = (eT*)__builtin_assume_aligned(mem, 16); }
#else
  { arma_ignore(mem); }
#endif

  // TODO: look into C++20 std::assume_aligned()
  // TODO: https://en.cppreference.com/w/cpp/memory/assume_aligned

  // TODO: MSVC?  __assume( (mem & 0x0F) == 0 );
  //
  // http://comments.gmane.org/gmane.comp.gcc.patches/239430
  // GCC __builtin_assume_aligned is similar to ICC's __assume_aligned,
  // so for lvalue first argument ICC's __assume_aligned can be emulated using
  // #define __assume_aligned(lvalueptr, align) lvalueptr = __builtin_assume_aligned
  // (lvalueptr, align)
  //
  // http://www.inf.ethz.ch/personal/markusp/teaching/263-2300-ETH-spring11/slides/class19.pdf
  // http://software.intel.com/sites/products/documentation/hpc/composerxe/en-us/cpp/lin/index.htm
  // http://d3f8ykwhia686p.cloudfront.net/1live/intel/CompilerAutovectorizationGuide.pdf
}

template <typename eT>
arma_inline void memory::mark_as_aligned(const eT*& mem) {
#if defined(ARMA_HAVE_ICC_ASSUME_ALIGNED)
  { __assume_aligned(mem, 16); }
#elif defined(ARMA_HAVE_GCC_ASSUME_ALIGNED)
  { mem = (const eT*)__builtin_assume_aligned(mem, 16); }
#else
  { arma_ignore(mem); }
#endif
}

//! @}
