// This file is part of Eigen, a lightweight C++ template library
// for linear algebra.
//
// Mehdi Goli    Codeplay Software Ltd.
// Ralph Potter  Codeplay Software Ltd.
// Luke Iwanski  Codeplay Software Ltd.
// Contact: <eigen@codeplay.com>
// Copyright (C) 2016 Benoit Steiner <benoit.steiner.goog@gmail.com>

//
// This Source Code Form is subject to the terms of the Mozilla
// Public License v. 2.0. If a copy of the MPL was not distributed
// with this file, You can obtain one at http://mozilla.org/MPL/2.0/.

#if defined(EIGEN_USE_SYCL) && !defined(EIGEN_CXX11_TENSOR_TENSOR_DEVICE_SYCL_H)
#define EIGEN_CXX11_TENSOR_TENSOR_DEVICE_SYCL_H
#include <unordered_set>

#include "./InternalHeaderCheck.h"

namespace Eigen {

namespace TensorSycl {
namespace internal {

/// Cache all the device information needed
struct SyclDeviceInfo {
  SyclDeviceInfo(cl::sycl::queue queue)
      : local_mem_type(
            queue.get_device()
                .template get_info<cl::sycl::info::device::local_mem_type>()),
        max_work_item_sizes(
            queue.get_device()
                .template get_info<
                    cl::sycl::info::device::max_work_item_sizes>()),
        max_mem_alloc_size(
            queue.get_device()
                .template get_info<
                    cl::sycl::info::device::max_mem_alloc_size>()),
        max_compute_units(queue.get_device()
                              .template get_info<
                                  cl::sycl::info::device::max_compute_units>()),
        max_work_group_size(
            queue.get_device()
                .template get_info<
                    cl::sycl::info::device::max_work_group_size>()),
        local_mem_size(
            queue.get_device()
                .template get_info<cl::sycl::info::device::local_mem_size>()),
        platform_name(queue.get_device()
                          .get_platform()
                          .template get_info<cl::sycl::info::platform::name>()),
        device_name(queue.get_device()
                        .template get_info<cl::sycl::info::device::name>()),
        device_vendor(
            queue.get_device()
                .template get_info<cl::sycl::info::device::vendor>()) {}

  cl::sycl::info::local_mem_type local_mem_type;
  cl::sycl::id<3> max_work_item_sizes;
  unsigned long max_mem_alloc_size;
  unsigned long max_compute_units;
  unsigned long max_work_group_size;
  size_t local_mem_size;
  std::string platform_name;
  std::string device_name;
  std::string device_vendor;
};

}  // end namespace internal
}  // end namespace TensorSycl

typedef TensorSycl::internal::buffer_data_type_t buffer_scalar_t;
// All devices (even AMD CPU with intel OpenCL runtime) that support OpenCL and
// can consume SPIR or SPIRV can use the Eigen SYCL backend and consequently
// TensorFlow via the Eigen SYCL Backend.
EIGEN_STRONG_INLINE auto get_sycl_supported_devices()
    -> decltype(cl::sycl::device::get_devices()) {
#ifdef EIGEN_SYCL_USE_DEFAULT_SELECTOR
  return {cl::sycl::device(cl::sycl::default_selector())};
#else
  std::vector<cl::sycl::device> supported_devices;
  auto platform_list = cl::sycl::platform::get_platforms();
  for (const auto &platform : platform_list) {
    auto device_list = platform.get_devices();
    auto platform_name =
        platform.template get_info<cl::sycl::info::platform::name>();
    std::transform(platform_name.begin(), platform_name.end(),
                   platform_name.begin(), ::tolower);
    for (const auto &device : device_list) {
      auto vendor = device.template get_info<cl::sycl::info::device::vendor>();
      std::transform(vendor.begin(), vendor.end(), vendor.begin(), ::tolower);
      bool unsupported_condition =
          (device.is_cpu() && platform_name.find("amd") != std::string::npos &&
           vendor.find("apu") == std::string::npos) ||
          (platform_name.find("experimental") != std::string::npos) ||
          device.is_host();
      if (!unsupported_condition) {
        supported_devices.push_back(device);
      }
    }
  }
  return supported_devices;
#endif
}

class QueueInterface {
 public:
  /// Creating device by using cl::sycl::selector or cl::sycl::device.
  template <typename DeviceOrSelector>
  explicit QueueInterface(
      const DeviceOrSelector &dev_or_sel, cl::sycl::async_handler handler,
      unsigned num_threads = std::thread::hardware_concurrency())
      : m_queue(dev_or_sel, handler),
#ifdef EIGEN_SYCL_USE_PROGRAM_CLASS
        m_prog(m_queue.get_context(), get_sycl_supported_devices()),
#endif
        m_thread_pool(num_threads),
        m_device_info(m_queue) {
#ifdef EIGEN_SYCL_USE_PROGRAM_CLASS
    m_prog.build_with_kernel_type<DeviceOrSelector>();
    auto f = [&](cl::sycl::handler &cgh) {
      cgh.single_task<DeviceOrSelector>(m_prog.get_kernel<DeviceOrSelector>(),
                                        [=]() {})
    };
    EIGEN_SYCL_TRY_CATCH(m_queue.submit(f));
#endif
  }

  template <typename DeviceOrSelector>
  explicit QueueInterface(
      const DeviceOrSelector &dev_or_sel,
      unsigned num_threads = std::thread::hardware_concurrency())
      : QueueInterface(dev_or_sel,
                       [this](cl::sycl::exception_list l) {
                         this->exception_caught_ = this->sycl_async_handler(l);
                       },
                       num_threads) {}
  
  explicit QueueInterface(
      const cl::sycl::queue& q, unsigned num_threads = std::thread::hardware_concurrency())
      : m_queue(q),
#ifdef EIGEN_SYCL_USE_PROGRAM_CLASS
        m_prog(m_queue.get_context(), get_sycl_supported_devices()),
#endif
        m_thread_pool(num_threads),
        m_device_info(m_queue) {}

#ifdef EIGEN_SYCL_USE_PROGRAM_CLASS
  EIGEN_STRONG_INLINE cl::sycl::program &program() const { return m_prog; }
#endif

  /// Attach an existing buffer to the pointer map, Eigen will not reuse it
  EIGEN_STRONG_INLINE void *attach_buffer(
      cl::sycl::buffer<buffer_scalar_t, 1> &buf) const {
    std::lock_guard<std::mutex> lock(pmapper_mutex_);
    return static_cast<void *>(pMapper.add_pointer(buf));
  }

  /// Detach previously attached buffer
  EIGEN_STRONG_INLINE void detach_buffer(void *p) const {
    std::lock_guard<std::mutex> lock(pmapper_mutex_);
    TensorSycl::internal::SYCLfree<false>(p, pMapper);
  }

  /// Allocating device pointer. This pointer is actually an 8 bytes host
  /// pointer used as key to access the sycl device buffer. The reason is that
  /// we cannot use device buffer as a pointer as a m_data in Eigen leafNode
  /// expressions. So we create a key pointer to be used in Eigen expression
  /// construction. When we convert the Eigen construction into the sycl
  /// construction we use this pointer as a key in our buffer_map and we make
  /// sure that we dedicate only one buffer only for this pointer. The device
  /// pointer would be deleted by calling deallocate function.
  EIGEN_STRONG_INLINE void *allocate(size_t num_bytes) const {
#if EIGEN_MAX_ALIGN_BYTES > 0
    size_t align = num_bytes % EIGEN_MAX_ALIGN_BYTES;
    if (align > 0) {
      num_bytes += EIGEN_MAX_ALIGN_BYTES - align;
    }
#endif
    std::lock_guard<std::mutex> lock(pmapper_mutex_);
    return TensorSycl::internal::SYCLmalloc(num_bytes, pMapper);
  }

  EIGEN_STRONG_INLINE void *allocate_temp(size_t num_bytes) const {
#if EIGEN_MAX_ALIGN_BYTES > 0
    size_t align = num_bytes % EIGEN_MAX_ALIGN_BYTES;
    if (align > 0) {
      num_bytes += EIGEN_MAX_ALIGN_BYTES - align;
    }
#endif
    std::lock_guard<std::mutex> lock(pmapper_mutex_);
#ifndef EIGEN_SYCL_NO_REUSE_BUFFERS
    if (scratch_buffers.empty()) {
      return TensorSycl::internal::SYCLmalloc(num_bytes, pMapper);
      ;
    } else {
      for (auto it = scratch_buffers.begin(); it != scratch_buffers.end();) {
        auto buff = pMapper.get_buffer(*it);
        if (buff.get_size() >= num_bytes) {
          auto ptr = *it;
          scratch_buffers.erase(it);
          return ptr;
        } else {
          ++it;
        }
      }
      return TensorSycl::internal::SYCLmalloc(num_bytes, pMapper);
    }
#else
    return TensorSycl::internal::SYCLmalloc(num_bytes, pMapper);
#endif
  }
  template <typename data_t>
  EIGEN_DEVICE_FUNC EIGEN_STRONG_INLINE TensorSycl::internal::RangeAccess<
      cl::sycl::access::mode::read_write, data_t>
  get(data_t *data) const {
    return get_range_accessor<cl::sycl::access::mode::read_write, data_t>(data);
  }
  template <typename data_t>
  EIGEN_DEVICE_FUNC EIGEN_STRONG_INLINE data_t *get(
      TensorSycl::internal::RangeAccess<cl::sycl::access::mode::read_write,
                                        data_t>
          data) const {
    return static_cast<data_t *>(data.get_virtual_pointer());
  }

  EIGEN_STRONG_INLINE void deallocate_temp(void *p) const {
    std::lock_guard<std::mutex> lock(pmapper_mutex_);
#ifndef EIGEN_SYCL_NO_REUSE_BUFFERS
    scratch_buffers.insert(p);
#else
    TensorSycl::internal::SYCLfree(p, pMapper);
#endif
  }
  template <cl::sycl::access::mode AcMd, typename T>
  EIGEN_STRONG_INLINE void deallocate_temp(
      const TensorSycl::internal::RangeAccess<AcMd, T> &p) const {
    deallocate_temp(p.get_virtual_pointer());
  }

  /// This is used to deallocate the device pointer. p is used as a key inside
  /// the map to find the device buffer and delete it.
  EIGEN_STRONG_INLINE void deallocate(void *p) const {
    std::lock_guard<std::mutex> lock(pmapper_mutex_);
    TensorSycl::internal::SYCLfree(p, pMapper);
  }

  EIGEN_STRONG_INLINE void deallocate_all() const {
    std::lock_guard<std::mutex> lock(pmapper_mutex_);
    TensorSycl::internal::SYCLfreeAll(pMapper);
#ifndef EIGEN_SYCL_NO_REUSE_BUFFERS
    scratch_buffers.clear();
#endif
  }

  /// The memcpyHostToDevice is used to copy the data from host to device
  /// The destination pointer could be deleted before the copy happened which is
  /// why a callback function is needed. By default if none is provided, the
  /// function is blocking.
  EIGEN_STRONG_INLINE void memcpyHostToDevice(
      void *dst, const void *src, size_t n,
      std::function<void()> callback) const {
    static const auto write_mode = cl::sycl::access::mode::discard_write;
    static const auto global_access = cl::sycl::access::target::global_buffer;
    typedef cl::sycl::accessor<buffer_scalar_t, 1, write_mode, global_access>
        write_accessor;
    if (n == 0) {
      if (callback) callback();
      return;
    }
    n /= sizeof(buffer_scalar_t);
    auto f = [&](cl::sycl::handler &cgh) {
      write_accessor dst_acc = get_range_accessor<write_mode>(cgh, dst, n);
      buffer_scalar_t const *ptr = static_cast<buffer_scalar_t const *>(src);
      auto non_deleter = [](buffer_scalar_t const *) {};
      std::shared_ptr<const buffer_scalar_t> s_ptr(ptr, non_deleter);
      cgh.copy(s_ptr, dst_acc);
    };
    cl::sycl::event e;
    EIGEN_SYCL_TRY_CATCH(e = m_queue.submit(f));
    synchronize_and_callback(e, callback);
  }

  /// The memcpyDeviceToHost is used to copy the data from device to host.
  /// The source pointer could be deleted before the copy happened which is
  /// why a callback function is needed. By default if none is provided, the
  /// function is blocking.
  EIGEN_STRONG_INLINE void memcpyDeviceToHost(
      void *dst, const void *src, size_t n,
      std::function<void()> callback) const {
    static const auto read_mode = cl::sycl::access::mode::read;
    static const auto global_access = cl::sycl::access::target::global_buffer;
    typedef cl::sycl::accessor<buffer_scalar_t, 1, read_mode, global_access>
        read_accessor;
    if (n == 0) {
      if (callback) callback();
      return;
    }
    n /= sizeof(buffer_scalar_t);
    auto f = [&](cl::sycl::handler &cgh) {
      read_accessor src_acc = get_range_accessor<read_mode>(cgh, src, n);
      buffer_scalar_t *ptr = static_cast<buffer_scalar_t *>(dst);
      auto non_deleter = [](buffer_scalar_t *) {};
      std::shared_ptr<buffer_scalar_t> s_ptr(ptr, non_deleter);
      cgh.copy(src_acc, s_ptr);
    };
    cl::sycl::event e;
    EIGEN_SYCL_TRY_CATCH(e = m_queue.submit(f));
    synchronize_and_callback(e, callback);
  }

  /// The memcpy function.
  /// No callback is required here as both arguments are on the device
  /// and SYCL can handle the dependency.
  EIGEN_STRONG_INLINE void memcpy(void *dst, const void *src, size_t n) const {
    static const auto read_mode = cl::sycl::access::mode::read;
    static const auto write_mode = cl::sycl::access::mode::discard_write;
    if (n == 0) {
      return;
    }
    n /= sizeof(buffer_scalar_t);
    auto f = [&](cl::sycl::handler &cgh) {
      auto src_acc = get_range_accessor<read_mode>(cgh, src, n);
      auto dst_acc = get_range_accessor<write_mode>(cgh, dst, n);
      cgh.copy(src_acc, dst_acc);
    };
    cl::sycl::event e;
    EIGEN_SYCL_TRY_CATCH(e = m_queue.submit(f));
    async_synchronize(e);
  }

  /// the memset function.
  /// No callback is required here as both arguments are on the device
  /// and SYCL can handle the dependency.
  EIGEN_STRONG_INLINE void memset(void *data, int c, size_t n) const {
    static const auto write_mode = cl::sycl::access::mode::discard_write;
    if (n == 0) {
      return;
    }
    auto f = [&](cl::sycl::handler &cgh) {
      // Get a typed range accesser to ensure we fill each byte, in case
      // `buffer_scalar_t` is not (u)int8_t.
      auto dst_acc = get_typed_range_accessor<write_mode, uint8_t>(cgh, data, n);
      cgh.fill(dst_acc, static_cast<uint8_t>(c));
    };
    cl::sycl::event e;
    EIGEN_SYCL_TRY_CATCH(e = m_queue.submit(f));
    async_synchronize(e);
  }

  template<typename T>
  EIGEN_STRONG_INLINE void fill(T* begin, T* end, const T& value) const {
    static const auto write_mode = cl::sycl::access::mode::discard_write;
    if (begin == end) {
      return;
    }
    const ptrdiff_t count = end - begin;
    auto f = [&](cl::sycl::handler &cgh) {
      auto dst_acc = get_typed_range_accessor<write_mode, T>(cgh, begin, count);
      cgh.fill(dst_acc, value);
    };
    cl::sycl::event e;
    EIGEN_SYCL_TRY_CATCH(e = m_queue.submit(f));
    async_synchronize(e);
  }

  /// Get a range accessor to the virtual pointer's device memory. This range
  /// accessor will allow access to the memory from the pointer to the end of
  /// the buffer.
  ///
  /// NOTE: Inside a kernel the range accessor will always be indexed from the
  /// start of the buffer, so the offset in the accessor is only used by
  /// methods like handler::copy and will not be available inside a kernel.
  template <cl::sycl::access::mode AcMd, typename T>
  EIGEN_STRONG_INLINE TensorSycl::internal::RangeAccess<AcMd, T>
  get_range_accessor(const void *ptr) const {
    static const auto global_access = cl::sycl::access::target::global_buffer;
    static const auto is_place_holder = cl::sycl::access::placeholder::true_t;
    typedef TensorSycl::internal::RangeAccess<AcMd, T> ret_type;
    typedef const TensorSycl::internal::buffer_data_type_t *internal_ptr_t;

    std::lock_guard<std::mutex> lock(pmapper_mutex_);

    auto original_buffer = pMapper.get_buffer(ptr);
    const ptrdiff_t offset = pMapper.get_offset(ptr);
    eigen_assert(offset % sizeof(T) == 0 && "The offset must be a multiple of sizeof(T)");
    eigen_assert(original_buffer.get_size() % sizeof(T) == 0 && "The buffer size must be a multiple of sizeof(T)");
    const ptrdiff_t typed_offset = offset / sizeof(T);
    eigen_assert(typed_offset >= 0);
    const auto typed_size = original_buffer.get_size() / sizeof(T);
    auto buffer = original_buffer.template reinterpret<
        std::remove_const_t<T>>(
        cl::sycl::range<1>(typed_size));
    const ptrdiff_t size = buffer.get_count() - typed_offset;
    eigen_assert(size >= 0);
    typedef cl::sycl::accessor<std::remove_const_t<T>,
                               1, AcMd, global_access, is_place_holder>
        placeholder_accessor_t;
    const auto start_ptr = static_cast<internal_ptr_t>(ptr) - offset;
    return ret_type(placeholder_accessor_t(buffer, cl::sycl::range<1>(size),
                                           cl::sycl::id<1>(typed_offset)),
                    static_cast<size_t>(typed_offset),
                    reinterpret_cast<std::intptr_t>(start_ptr));
  }

  /// Get a range accessor to the virtual pointer's device memory with a
  /// specified size.
  template <cl::sycl::access::mode AcMd, typename Index>
  EIGEN_STRONG_INLINE cl::sycl::accessor<
      buffer_scalar_t, 1, AcMd, cl::sycl::access::target::global_buffer>
  get_range_accessor(cl::sycl::handler &cgh, const void *ptr,
                     const Index n_bytes) const {
    static const auto global_access = cl::sycl::access::target::global_buffer;
    eigen_assert(n_bytes >= 0);
    std::lock_guard<std::mutex> lock(pmapper_mutex_);
    auto buffer = pMapper.get_buffer(ptr);
    const ptrdiff_t offset = pMapper.get_offset(ptr);
    eigen_assert(offset >= 0);
    eigen_assert(offset + n_bytes <= buffer.get_size());
    return buffer.template get_access<AcMd, global_access>(
        cgh, cl::sycl::range<1>(n_bytes), cl::sycl::id<1>(offset));
  }

  /// Get a range accessor to the virtual pointer's device memory with a
  /// specified type and count.
  template <cl::sycl::access::mode AcMd, typename T, typename Index>
  EIGEN_STRONG_INLINE cl::sycl::accessor<
      T, 1, AcMd, cl::sycl::access::target::global_buffer>
  get_typed_range_accessor(cl::sycl::handler &cgh, const void *ptr,
                     const Index count) const {
    static const auto global_access = cl::sycl::access::target::global_buffer;
    eigen_assert(count >= 0);
    std::lock_guard<std::mutex> lock(pmapper_mutex_);
    auto buffer = pMapper.get_buffer(ptr);
    const ptrdiff_t offset = pMapper.get_offset(ptr);
    eigen_assert(offset >= 0);

    // Technically we should create a subbuffer for the desired range,
    // then reinterpret that.  However, I was not able to get changes to reflect
    // in the original buffer (only the subbuffer and reinterpretted buffer).
    // This current implementation now has the restriction that the buffer
    // offset and original buffer size must be a multiple of sizeof(T).
    // Note that get_range_accessor(void*) currently has the same restriction.
    //
    // auto subbuffer = cl::sycl::buffer<buffer_scalar_t, 1>(buffer, 
    //     cl::sycl::id<1>(offset), cl::sycl::range<1>(n_bytes));
    eigen_assert(offset % sizeof(T) == 0 && "The offset must be a multiple of sizeof(T)");
    eigen_assert(buffer.get_size() % sizeof(T) == 0 && "The buffer size must be a multiple of sizeof(T)");
    const ptrdiff_t typed_offset = offset / sizeof(T);
    const size_t typed_size = buffer.get_size() / sizeof(T);
    auto reint = buffer.template reinterpret<
        std::remove_const_t<T>>(
        cl::sycl::range<1>(typed_size));
    return reint.template get_access<AcMd, global_access>(
        cgh, cl::sycl::range<1>(count), cl::sycl::id<1>(typed_offset));
  }

  /// Creation of sycl accessor for a buffer. This function first tries to find
  /// the buffer in the buffer_map. If found it gets the accessor from it, if
  /// not, the function then adds an entry by creating a sycl buffer for that
  /// particular pointer.
  template <cl::sycl::access::mode AcMd>
  EIGEN_STRONG_INLINE cl::sycl::accessor<
      buffer_scalar_t, 1, AcMd, cl::sycl::access::target::global_buffer>
  get_sycl_accessor(cl::sycl::handler &cgh, const void *ptr) const {
    std::lock_guard<std::mutex> lock(pmapper_mutex_);
    return pMapper.get_buffer(ptr)
        .template get_access<AcMd, cl::sycl::access::target::global_buffer>(
            cgh);
  }

  EIGEN_STRONG_INLINE cl::sycl::buffer<buffer_scalar_t, 1> get_sycl_buffer(
      const void *ptr) const {
    std::lock_guard<std::mutex> lock(pmapper_mutex_);
    return pMapper.get_buffer(ptr);
  }

  EIGEN_STRONG_INLINE ptrdiff_t get_offset(const void *ptr) const {
    std::lock_guard<std::mutex> lock(pmapper_mutex_);
    return pMapper.get_offset(ptr);
  }

  template <typename OutScalar, typename sycl_kernel, typename Lhs,
            typename Rhs, typename OutPtr, typename Range, typename Index,
            typename... T>
  EIGEN_ALWAYS_INLINE void binary_kernel_launcher(const Lhs &lhs,
                                                  const Rhs &rhs, OutPtr outptr,
                                                  Range thread_range,
                                                  Index scratchSize,
                                                  T... var) const {
    auto kernel_functor = [=](cl::sycl::handler &cgh) {
      // binding the placeholder accessors to a commandgroup handler
      lhs.bind(cgh);
      rhs.bind(cgh);
      outptr.bind(cgh);
      typedef cl::sycl::accessor<OutScalar, 1,
                                 cl::sycl::access::mode::read_write,
                                 cl::sycl::access::target::local>
          LocalAccessor;

      LocalAccessor scratch(cl::sycl::range<1>(scratchSize), cgh);
      cgh.parallel_for(
#ifdef EIGEN_SYCL_USE_PROGRAM_CLASS
          program().template get_kernel<sycl_kernel>(),
#endif
          thread_range, sycl_kernel(scratch, lhs, rhs, outptr, var...));
    };
    cl::sycl::event e;
    EIGEN_SYCL_TRY_CATCH(e = m_queue.submit(kernel_functor));
    async_synchronize(e);
  }

  template <typename OutScalar, typename sycl_kernel, typename InPtr,
            typename OutPtr, typename Range, typename Index, typename... T>
  EIGEN_ALWAYS_INLINE void unary_kernel_launcher(const InPtr &inptr,
                                                 OutPtr &outptr,
                                                 Range thread_range,
                                                 Index scratchSize,
                                                 T... var) const {
    auto kernel_functor = [=](cl::sycl::handler &cgh) {
      // binding the placeholder accessors to a commandgroup handler
      inptr.bind(cgh);
      outptr.bind(cgh);
      typedef cl::sycl::accessor<OutScalar, 1,
                                 cl::sycl::access::mode::read_write,
                                 cl::sycl::access::target::local>
          LocalAccessor;

      LocalAccessor scratch(cl::sycl::range<1>(scratchSize), cgh);
      cgh.parallel_for(
#ifdef EIGEN_SYCL_USE_PROGRAM_CLASS
          program().template get_kernel<sycl_kernel>(),
#endif
          thread_range, sycl_kernel(scratch, inptr, outptr, var...));
    };
    cl::sycl::event e;
    EIGEN_SYCL_TRY_CATCH(e = m_queue.submit(kernel_functor));
    async_synchronize(e);
  }

    template <typename OutScalar, typename sycl_kernel, typename InPtr,
           typename Range, typename Index, typename... T>
  EIGEN_ALWAYS_INLINE void nullary_kernel_launcher(const InPtr &inptr,
                                                 Range thread_range,
                                                 Index scratchSize,
                                                 T... var) const {
    auto kernel_functor = [=](cl::sycl::handler &cgh) {
      // binding the placeholder accessors to a commandgroup handler
      inptr.bind(cgh);
      typedef cl::sycl::accessor<OutScalar, 1,
                                 cl::sycl::access::mode::read_write,
                                 cl::sycl::access::target::local>
          LocalAccessor;

      LocalAccessor scratch(cl::sycl::range<1>(scratchSize), cgh);
      cgh.parallel_for(
#ifdef EIGEN_SYCL_USE_PROGRAM_CLASS
          program().template get_kernel<sycl_kernel>(),
#endif
          thread_range, sycl_kernel(scratch, inptr, var...));
    };
    cl::sycl::event e;
    EIGEN_SYCL_TRY_CATCH(e = m_queue.submit(kernel_functor));
    async_synchronize(e);
  }


  EIGEN_STRONG_INLINE void synchronize() const {
#ifdef EIGEN_EXCEPTIONS
    m_queue.wait_and_throw();
#else
    m_queue.wait();
#endif
  }


  EIGEN_STRONG_INLINE void async_synchronize(cl::sycl::event e) const {
    set_latest_event(e);
#ifndef EIGEN_SYCL_ASYNC_EXECUTION
    synchronize();
#endif
  }

  template <typename Index>
  EIGEN_STRONG_INLINE void parallel_for_setup(Index n, Index &tileSize,
                                              Index &rng, Index &GRange) const {
    tileSize = static_cast<Index>(getNearestPowerOfTwoWorkGroupSize());
    tileSize = std::min(static_cast<Index>(EIGEN_SYCL_LOCAL_THREAD_DIM0 *
                                           EIGEN_SYCL_LOCAL_THREAD_DIM1),
                        static_cast<Index>(tileSize));
    rng = n;
    if (rng == 0) rng = static_cast<Index>(1);
    GRange = rng;
    if (tileSize > GRange)
      tileSize = GRange;
    else if (GRange > tileSize) {
      Index xMode = static_cast<Index>(GRange % tileSize);
      if (xMode != 0) GRange += static_cast<Index>(tileSize - xMode);
    }
  }

  /// This is used to prepare the number of threads and also the number of
  /// threads per block for sycl kernels
  template <typename Index>
  EIGEN_STRONG_INLINE void parallel_for_setup(
      const std::array<Index, 2> &input_dim, cl::sycl::range<2> &global_range,
      cl::sycl::range<2> &local_range) const {
    std::array<Index, 2> input_range = input_dim;
    Index max_workgroup_Size =
        static_cast<Index>(getNearestPowerOfTwoWorkGroupSize());
    max_workgroup_Size =
        std::min(static_cast<Index>(EIGEN_SYCL_LOCAL_THREAD_DIM0 *
                                    EIGEN_SYCL_LOCAL_THREAD_DIM1),
                 static_cast<Index>(max_workgroup_Size));
    Index pow_of_2 = static_cast<Index>(std::log2(max_workgroup_Size));
    local_range[1] =
        static_cast<Index>(std::pow(2, static_cast<Index>(pow_of_2 / 2)));
    input_range[1] = input_dim[1];
    if (input_range[1] == 0) input_range[1] = static_cast<Index>(1);
    global_range[1] = input_range[1];
    if (local_range[1] > global_range[1])
      local_range[1] = global_range[1];
    else if (global_range[1] > local_range[1]) {
      Index xMode = static_cast<Index>(global_range[1] % local_range[1]);
      if (xMode != 0)
        global_range[1] += static_cast<Index>(local_range[1] - xMode);
    }
    local_range[0] = static_cast<Index>(max_workgroup_Size / local_range[1]);
    input_range[0] = input_dim[0];
    if (input_range[0] == 0) input_range[0] = static_cast<Index>(1);
    global_range[0] = input_range[0];
    if (local_range[0] > global_range[0])
      local_range[0] = global_range[0];
    else if (global_range[0] > local_range[0]) {
      Index xMode = static_cast<Index>(global_range[0] % local_range[0]);
      if (xMode != 0)
        global_range[0] += static_cast<Index>(local_range[0] - xMode);
    }
  }

  /// This is used to prepare the number of threads and also the number of
  /// threads per block for sycl kernels
  template <typename Index>
  EIGEN_STRONG_INLINE void parallel_for_setup(
      const std::array<Index, 3> &input_dim, cl::sycl::range<3> &global_range,
      cl::sycl::range<3> &local_range) const {
    std::array<Index, 3> input_range = input_dim;
    Index max_workgroup_Size =
        static_cast<Index>(getNearestPowerOfTwoWorkGroupSize());
    max_workgroup_Size =
        std::min(static_cast<Index>(EIGEN_SYCL_LOCAL_THREAD_DIM0 *
                                    EIGEN_SYCL_LOCAL_THREAD_DIM1),
                 static_cast<Index>(max_workgroup_Size));
    Index pow_of_2 = static_cast<Index>(std::log2(max_workgroup_Size));
    local_range[2] =
        static_cast<Index>(std::pow(2, static_cast<Index>(pow_of_2 / 3)));
    input_range[2] = input_dim[2];
    if (input_range[2] == 0) input_range[1] = static_cast<Index>(1);
    global_range[2] = input_range[2];
    if (local_range[2] > global_range[2])
      local_range[2] = global_range[2];
    else if (global_range[2] > local_range[2]) {
      Index xMode = static_cast<Index>(global_range[2] % local_range[2]);
      if (xMode != 0)
        global_range[2] += static_cast<Index>(local_range[2] - xMode);
    }
    pow_of_2 = static_cast<Index>(
        std::log2(static_cast<Index>(max_workgroup_Size / local_range[2])));
    local_range[1] =
        static_cast<Index>(std::pow(2, static_cast<Index>(pow_of_2 / 2)));
    input_range[1] = input_dim[1];
    if (input_range[1] == 0) input_range[1] = static_cast<Index>(1);
    global_range[1] = input_range[1];
    if (local_range[1] > global_range[1])
      local_range[1] = global_range[1];
    else if (global_range[1] > local_range[1]) {
      Index xMode = static_cast<Index>(global_range[1] % local_range[1]);
      if (xMode != 0)
        global_range[1] += static_cast<Index>(local_range[1] - xMode);
    }
    local_range[0] = static_cast<Index>(max_workgroup_Size /
                                        (local_range[1] * local_range[2]));
    input_range[0] = input_dim[0];
    if (input_range[0] == 0) input_range[0] = static_cast<Index>(1);
    global_range[0] = input_range[0];
    if (local_range[0] > global_range[0])
      local_range[0] = global_range[0];
    else if (global_range[0] > local_range[0]) {
      Index xMode = static_cast<Index>(global_range[0] % local_range[0]);
      if (xMode != 0)
        global_range[0] += static_cast<Index>(local_range[0] - xMode);
    }
  }

  EIGEN_STRONG_INLINE bool has_local_memory() const {
#if !defined(EIGEN_SYCL_LOCAL_MEM) && defined(EIGEN_SYCL_NO_LOCAL_MEM)
    return false;
#elif defined(EIGEN_SYCL_LOCAL_MEM) && !defined(EIGEN_SYCL_NO_LOCAL_MEM)
    return true;
#else
    return m_device_info.local_mem_type ==
           cl::sycl::info::local_mem_type::local;
#endif
  }

  EIGEN_STRONG_INLINE unsigned long max_buffer_size() const {
    return m_device_info.max_mem_alloc_size;
  }

  EIGEN_STRONG_INLINE unsigned long getNumSyclMultiProcessors() const {
    return m_device_info.max_compute_units;
  }

  EIGEN_STRONG_INLINE unsigned long maxSyclThreadsPerBlock() const {
    return m_device_info.max_work_group_size;
  }

  EIGEN_STRONG_INLINE cl::sycl::id<3> maxWorkItemSizes() const {
    return m_device_info.max_work_item_sizes;
  }

  /// No need for sycl it should act the same as CPU version
  EIGEN_STRONG_INLINE int majorDeviceVersion() const { return 1; }

  EIGEN_STRONG_INLINE unsigned long maxSyclThreadsPerMultiProcessor() const {
    // OpenCL does not have such a concept
    return 2;
  }

  EIGEN_STRONG_INLINE size_t sharedMemPerBlock() const {
    return m_device_info.local_mem_size;
  }

  // This function returns the nearest power of 2 Work-group size which is <=
  // maximum device workgroup size.
  EIGEN_STRONG_INLINE size_t getNearestPowerOfTwoWorkGroupSize() const {
    return getPowerOfTwo(m_device_info.max_work_group_size, false);
  }

  EIGEN_STRONG_INLINE std::string getPlatformName() const {
    return m_device_info.platform_name;
  }

  EIGEN_STRONG_INLINE std::string getDeviceName() const {
    return m_device_info.device_name;
  }

  EIGEN_STRONG_INLINE std::string getDeviceVendor() const {
    return m_device_info.device_vendor;
  }

  // This function returns the nearest power of 2
  // if roundup is true returns result>=wgsize
  // else it return result <= wgsize
  EIGEN_STRONG_INLINE size_t getPowerOfTwo(size_t wGSize, bool roundUp) const {
    if (roundUp) --wGSize;
    wGSize |= (wGSize >> 1);
    wGSize |= (wGSize >> 2);
    wGSize |= (wGSize >> 4);
    wGSize |= (wGSize >> 8);
    wGSize |= (wGSize >> 16);
#if EIGEN_ARCH_x86_64 || EIGEN_ARCH_ARM64 || EIGEN_OS_WIN64
    wGSize |= (wGSize >> 32);
#endif
    return ((!roundUp) ? (wGSize - (wGSize >> 1)) : ++wGSize);
  }

  EIGEN_STRONG_INLINE cl::sycl::queue &sycl_queue() const { return m_queue; }

  // This function checks if the runtime recorded an error for the
  // underlying stream device.
  EIGEN_STRONG_INLINE bool ok() const {
    if (!exception_caught_) {
      synchronize();
    }
    return !exception_caught_;
  }

  EIGEN_STRONG_INLINE cl::sycl::event get_latest_event() const {
#ifdef EIGEN_SYCL_STORE_LATEST_EVENT
    std::lock_guard<std::mutex> lock(event_mutex_);
    return latest_events_[std::this_thread::get_id()];
#else
    eigen_assert(false);
    return cl::sycl::event();
#endif
  }

  // destructor
  ~QueueInterface() {
    pMapper.clear();
#ifndef EIGEN_SYCL_NO_REUSE_BUFFERS
    scratch_buffers.clear();
#endif
  }

 protected:
  EIGEN_STRONG_INLINE void set_latest_event(cl::sycl::event e) const {
#ifdef EIGEN_SYCL_STORE_LATEST_EVENT
    std::lock_guard<std::mutex> lock(event_mutex_);
    latest_events_[std::this_thread::get_id()] = e;
#else
    EIGEN_UNUSED_VARIABLE(e);
#endif
  }

  void synchronize_and_callback(cl::sycl::event e,
                                const std::function<void()> &callback) const {
    set_latest_event(e);
    if (callback) {
      auto callback_ = [=]() {
#ifdef EIGEN_EXCEPTIONS
        cl::sycl::event(e).wait_and_throw();
#else
        cl::sycl::event(e).wait();
#endif
        callback();
      };
      m_thread_pool.Schedule(std::move(callback_));
    } else {
#ifdef EIGEN_EXCEPTIONS
      m_queue.wait_and_throw();
#else
      m_queue.wait();
#endif
    }
  }

  bool sycl_async_handler(cl::sycl::exception_list exceptions) const {
    bool exception_caught = false;
    for (const auto &e : exceptions) {
      if (e) {
        exception_caught = true;
        EIGEN_THROW_X(e);
      }
    }
    return exception_caught;
  }

  /// class members:
  bool exception_caught_ = false;

  mutable std::mutex pmapper_mutex_;

#ifdef EIGEN_SYCL_STORE_LATEST_EVENT
  mutable std::mutex event_mutex_;
  mutable std::unordered_map<std::thread::id, cl::sycl::event> latest_events_;
#endif

  /// std::map is the container used to make sure that we create only one buffer
  /// per pointer. The lifespan of the buffer now depends on the lifespan of
  /// SyclDevice. If a non-read-only pointer is needed to be accessed on the
  /// host we should manually deallocate it.
  mutable TensorSycl::internal::PointerMapper pMapper;
#ifndef EIGEN_SYCL_NO_REUSE_BUFFERS
  mutable std::unordered_set<void *> scratch_buffers;
#endif
  /// sycl queue
  mutable cl::sycl::queue m_queue;
#ifdef EIGEN_SYCL_USE_PROGRAM_CLASS
  mutable cl::sycl::program m_prog;
#endif

  /// The thread pool is used to wait on events and call callbacks
  /// asynchronously
  mutable Eigen::ThreadPool m_thread_pool;

  const TensorSycl::internal::SyclDeviceInfo m_device_info;
};

struct SyclDeviceBase {
  /// QueueInterface is not owned. it is the caller's responsibility to destroy
  /// it
  const QueueInterface *m_queue_stream;
  explicit SyclDeviceBase(const QueueInterface *queue_stream)
      : m_queue_stream(queue_stream) {}
  EIGEN_STRONG_INLINE const QueueInterface *queue_stream() const {
    return m_queue_stream;
  }
};

// Here is a sycl device struct which accept the sycl queue interface
// as an input
struct SyclDevice : public SyclDeviceBase {
  explicit SyclDevice(const QueueInterface *queue_stream)
      : SyclDeviceBase(queue_stream) {}

  // this is the accessor used to construct the evaluator
  template <cl::sycl::access::mode AcMd, typename T>
  EIGEN_STRONG_INLINE TensorSycl::internal::RangeAccess<AcMd, T>
  get_range_accessor(const void *ptr) const {
    return queue_stream()->template get_range_accessor<AcMd, T>(ptr);
  }

  // get sycl accessor
  template <cl::sycl::access::mode AcMd>
  EIGEN_STRONG_INLINE cl::sycl::accessor<
      buffer_scalar_t, 1, AcMd, cl::sycl::access::target::global_buffer>
  get_sycl_accessor(cl::sycl::handler &cgh, const void *ptr) const {
    return queue_stream()->template get_sycl_accessor<AcMd>(cgh, ptr);
  }

  /// Accessing the created sycl device buffer for the device pointer
  EIGEN_STRONG_INLINE cl::sycl::buffer<buffer_scalar_t, 1> get_sycl_buffer(
      const void *ptr) const {
    return queue_stream()->get_sycl_buffer(ptr);
  }

  /// This is used to prepare the number of threads and also the number of
  /// threads per block for sycl kernels
  template <typename Index>
  EIGEN_STRONG_INLINE void parallel_for_setup(Index n, Index &tileSize,
                                              Index &rng, Index &GRange) const {
    queue_stream()->parallel_for_setup(n, tileSize, rng, GRange);
  }

  /// This is used to prepare the number of threads and also the number of
  /// threads per block for sycl kernels
  template <typename Index>
  EIGEN_STRONG_INLINE void parallel_for_setup(
      const std::array<Index, 2> &input_dim, cl::sycl::range<2> &global_range,
      cl::sycl::range<2> &local_range) const {
    queue_stream()->parallel_for_setup(input_dim, global_range, local_range);
  }

  /// This is used to prepare the number of threads and also the number of
  /// threads per block for sycl kernels
  template <typename Index>
  EIGEN_STRONG_INLINE void parallel_for_setup(
      const std::array<Index, 3> &input_dim, cl::sycl::range<3> &global_range,
      cl::sycl::range<3> &local_range) const {
    queue_stream()->parallel_for_setup(input_dim, global_range, local_range);
  }

  /// allocate device memory
  EIGEN_STRONG_INLINE void *allocate(size_t num_bytes) const {
    return queue_stream()->allocate(num_bytes);
  }

  EIGEN_STRONG_INLINE void *allocate_temp(size_t num_bytes) const {
    return queue_stream()->allocate_temp(num_bytes);
  }

  /// deallocate device memory
  EIGEN_STRONG_INLINE void deallocate(void *p) const {
    queue_stream()->deallocate(p);
  }

  EIGEN_STRONG_INLINE void deallocate_temp(void *buffer) const {
    queue_stream()->deallocate_temp(buffer);
  }
  template <cl::sycl::access::mode AcMd, typename T>
  EIGEN_STRONG_INLINE void deallocate_temp(
      const TensorSycl::internal::RangeAccess<AcMd, T> &buffer) const {
    queue_stream()->deallocate_temp(buffer);
  }
  EIGEN_STRONG_INLINE void deallocate_all() const {
    queue_stream()->deallocate_all();
  }

  template <typename data_t>
  EIGEN_DEVICE_FUNC EIGEN_STRONG_INLINE TensorSycl::internal::RangeAccess<
      cl::sycl::access::mode::read_write, data_t>
  get(data_t *data) const {
    return queue_stream()->get(data);
  }
  template <typename data_t>
  EIGEN_DEVICE_FUNC EIGEN_STRONG_INLINE data_t *get(
      TensorSycl::internal::RangeAccess<cl::sycl::access::mode::read_write,
                                        data_t>
          data) const {
    return queue_stream()->get(data);
  }

  /// attach existing buffer
  EIGEN_STRONG_INLINE void *attach_buffer(
      cl::sycl::buffer<buffer_scalar_t, 1> &buf) const {
    return queue_stream()->attach_buffer(buf);
  }
  /// detach buffer
  EIGEN_STRONG_INLINE void detach_buffer(void *p) const {
    queue_stream()->detach_buffer(p);
  }
  EIGEN_STRONG_INLINE ptrdiff_t get_offset(const void *ptr) const {
    return queue_stream()->get_offset(ptr);
  }

  // some runtime conditions that can be applied here
  EIGEN_STRONG_INLINE bool isDeviceSuitable() const { return true; }

  /// memcpyHostToDevice
  template <typename Index>
  EIGEN_STRONG_INLINE void memcpyHostToDevice(
      Index *dst, const Index *src, size_t n,
      std::function<void()> callback = {}) const {
    queue_stream()->memcpyHostToDevice(dst, src, n, callback);
  }
  /// memcpyDeviceToHost
  template <typename Index>
  EIGEN_STRONG_INLINE void memcpyDeviceToHost(
      void *dst, const Index *src, size_t n,
      std::function<void()> callback = {}) const {
    queue_stream()->memcpyDeviceToHost(dst, src, n, callback);
  }
  /// the memcpy function
  template <typename Index>
  EIGEN_STRONG_INLINE void memcpy(void *dst, const Index *src, size_t n) const {
    queue_stream()->memcpy(dst, src, n);
  }
  /// the memset function
  EIGEN_STRONG_INLINE void memset(void *data, int c, size_t n) const {
    queue_stream()->memset(data, c, n);
  }
  /// the fill function
  template<typename T>
  EIGEN_STRONG_INLINE void fill(T* begin, T* end, const T& value) const {
    queue_stream()->fill(begin, end, value);
  }
  /// returning the sycl queue
  EIGEN_STRONG_INLINE cl::sycl::queue &sycl_queue() const {
    return queue_stream()->sycl_queue();
  }
#ifdef EIGEN_SYCL_USE_PROGRAM_CLASS
  EIGEN_STRONG_INLINE cl::sycl::program &program() const {
    return queue_stream()->program();
  }
#endif

  EIGEN_STRONG_INLINE size_t firstLevelCacheSize() const { return 48 * 1024; }

  EIGEN_STRONG_INLINE size_t lastLevelCacheSize() const {
    // We won't try to take advantage of the l2 cache for the time being, and
    // there is no l3 cache on sycl devices.
    return firstLevelCacheSize();
  }
  EIGEN_STRONG_INLINE unsigned long getNumSyclMultiProcessors() const {
    return queue_stream()->getNumSyclMultiProcessors();
  }
  EIGEN_STRONG_INLINE unsigned long maxSyclThreadsPerBlock() const {
    return queue_stream()->maxSyclThreadsPerBlock();
  }
  EIGEN_STRONG_INLINE cl::sycl::id<3> maxWorkItemSizes() const {
    return queue_stream()->maxWorkItemSizes();
  }
  EIGEN_STRONG_INLINE unsigned long maxSyclThreadsPerMultiProcessor() const {
    // OpenCL does not have such a concept
    return queue_stream()->maxSyclThreadsPerMultiProcessor();
  }
  EIGEN_STRONG_INLINE size_t sharedMemPerBlock() const {
    return queue_stream()->sharedMemPerBlock();
  }
  EIGEN_STRONG_INLINE size_t getNearestPowerOfTwoWorkGroupSize() const {
    return queue_stream()->getNearestPowerOfTwoWorkGroupSize();
  }

  EIGEN_STRONG_INLINE size_t getPowerOfTwo(size_t val, bool roundUp) const {
    return queue_stream()->getPowerOfTwo(val, roundUp);
  }
  /// No need for sycl it should act the same as CPU version
  EIGEN_STRONG_INLINE int majorDeviceVersion() const {
    return queue_stream()->majorDeviceVersion();
  }

  EIGEN_STRONG_INLINE void synchronize() const {
    queue_stream()->synchronize();
  }
  EIGEN_STRONG_INLINE void async_synchronize(
      cl::sycl::event e = cl::sycl::event()) const {
    queue_stream()->async_synchronize(e);
  }
  EIGEN_STRONG_INLINE cl::sycl::event get_latest_event() const {
    return queue_stream()->get_latest_event();
  }

  // This function checks if the runtime recorded an error for the
  // underlying stream device.
  EIGEN_STRONG_INLINE bool ok() const { return queue_stream()->ok(); }

  EIGEN_STRONG_INLINE bool has_local_memory() const {
    return queue_stream()->has_local_memory();
  }
  EIGEN_STRONG_INLINE long max_buffer_size() const {
    return queue_stream()->max_buffer_size();
  }
  EIGEN_STRONG_INLINE std::string getPlatformName() const {
    return queue_stream()->getPlatformName();
  }
  EIGEN_STRONG_INLINE std::string getDeviceName() const {
    return queue_stream()->getDeviceName();
  }
  EIGEN_STRONG_INLINE std::string getDeviceVendor() const {
    return queue_stream()->getDeviceVendor();
  }
  template <typename OutScalar, typename KernelType, typename... T>
  EIGEN_ALWAYS_INLINE void binary_kernel_launcher(T... var) const {
    queue_stream()->template binary_kernel_launcher<OutScalar, KernelType>(
        var...);
  }
  template <typename OutScalar, typename KernelType, typename... T>
  EIGEN_ALWAYS_INLINE void unary_kernel_launcher(T... var) const {
    queue_stream()->template unary_kernel_launcher<OutScalar, KernelType>(
        var...);
  }

  template <typename OutScalar, typename KernelType, typename... T>
  EIGEN_ALWAYS_INLINE void nullary_kernel_launcher(T... var) const {
    queue_stream()->template nullary_kernel_launcher<OutScalar, KernelType>(
        var...);
  }
};
}  // end namespace Eigen

#endif  // EIGEN_CXX11_TENSOR_TENSOR_DEVICE_SYCL_H
