#pragma once

// The types of these two will be replaced during compilation by the Spiral code generator. 
// It matches on `using default_int = ` and `;` with the inner part being replaced so the form should be kept as is. 
// The two statements need to begin at the start of a line.
using default_int = int;
using default_uint = unsigned int;

#ifndef __NVRTC__
// NVRTC has these includes by default so they need to be left out if it is used as the compiler.
#include <new>
#include <assert.h>
#include <stdio.h>
#endif

#include <utility> // For std::move
#include <stdlib.h> // For exit
// ws: upstream includes <cuda_runtime.h> unconditionally, so the CppHost `.cpp` did not build with a plain
// g++/clang++ on a machine without the CUDA SDK. Include it under nvcc/NVRTC as before, and under a host compiler
// only when the header is reachable (then the CppHost object can still link with the nvcc-built `.cu`).
#if defined(__CUDACC__) || defined(__NVRTC__)
#include <cuda_runtime.h> // So we can link with Cuda libs.
#elif defined(__has_include)
#if __has_include(<cuda_runtime.h>)
#include <cuda_runtime.h> // So we can link with Cuda libs.
#endif
#endif
// ws: nvcc and NVRTC provide log/exp/tanh/sqrt/sin/cos/pow/isnan as builtins; a host compiler needs the header.
#if !defined(__CUDACC__) && !defined(__NVRTC__)
#include <math.h>
#endif

// The defines are so the .hpp library is compatible between the backends.
#ifndef __host__
#define __host__
#endif
#ifndef __device__
#define __device__
#endif

// ws: the generator prefixes methods named `noinline...` with `__noinline__`, which only nvcc knows. Empty on a host
// compiler: libstdc++ spells `__attribute__((__noinline__))`, which stays valid as `__attribute__(())`.
#if !defined(__CUDACC__) && !defined(__NVRTC__) && !defined(__noinline__)
#define __noinline__
#endif

#ifdef __CUDACC__
// For error checking on the host.
#define gpuErrchk(ans) { gpuAssert((ans), __FILE__, __LINE__); }
template <typename T> inline __host__ __device__ void destroy(T& obj) { obj.~T(); }
inline void gpuAssert(cudaError error, const char *file, int line, bool abort=true) {
    if (error != cudaSuccess) {
        fprintf(stderr, "GPUassert: %s %s %d\n", cudaGetErrorString(error), file, line);
        if (abort) exit(error);
    }
}
#endif

template <typename el>
struct sptr // Shared pointer for the Spiral datatypes. They have to have the refc field inside them to work.
{
    el* base;

    __host__ __device__ sptr() : base(nullptr) {}
    __host__ __device__ sptr(el* ptr) : base(ptr) { this->base->refc++; }

    __host__ __device__ ~sptr()
    {
        if (this->base != nullptr && --this->base->refc == 0)
        {
            delete this->base;
            this->base = nullptr;
        }
    }

    __host__ __device__ sptr(const sptr& x)
    {
        this->base = x.base;
        this->base->refc++;
    }

    __host__ __device__ sptr(sptr&& x)
    {
        this->base = x.base;
        x.base = nullptr;
    }

    __host__ __device__ sptr& operator=(const sptr& x)
    {
        if (this->base != x.base)
        {
            if (this->base != nullptr && --this->base->refc == 0) { delete this->base; }
            this->base = x.base;
            this->base->refc++;
        }
        return *this;
    }
    
    __host__ __device__ sptr& operator=(sptr&& x)
    {
        if (this->base != x.base)
        {
            if (this->base != nullptr && --this->base->refc == 0) { delete this->base; }
            this->base = x.base;
            x.base = nullptr;
        }
        return *this;
    }
};

template <typename el>
struct csptr : public sptr<el>
{ // Shared pointer for closures specifically.
    using sptr<el>::sptr;
    template <typename... Args>
    __host__ __device__ auto operator()(Args... args) -> decltype(this->base->operator()(args...))
    {
        return this->base->operator()(args...);
    }
};

template <typename el, default_int max_length>
struct static_array
{
    el ptr[max_length];
    __host__ __device__ el& operator[](default_int i) {
        assert("The index has to be in range." && 0 <= i && i < max_length);
        return this->ptr[i];
    }
};

template <typename el, default_int max_length>
struct static_array_list
{
    default_int length{ 0 };
    el ptr[max_length];

    __host__ __device__ el& operator[](default_int i) {
        assert("The index has to be in range." && 0 <= i && i < this->length);
        return this->ptr[i];
    }
    __host__ __device__ void push(const el& x) {
        ptr[this->length++] = x;
        assert("The array after pushing should not be greater than max length." && this->length <= max_length);
    }
    __host__ __device__ void push(const el&& x) {
        ptr[this->length++] = std::move(x);
        assert("The array after pushing should not be greater than max length." && this->length <= max_length);
    }
    __host__ __device__ el pop() {
        assert("The array before popping should be greater than 0." && 0 < this->length);
        auto x = ptr[--this->length];
        ptr[this->length].~el();
        new (&ptr[this->length]) el();
        return x;
    }
    // Should be used only during initialization.
    __host__ __device__ void unsafe_set_length(default_int i) {
        assert("The new length should be in range." && 0 <= i && i <= max_length);
        this->length = i;
    }
};

template <typename el, default_int max_length>
struct dynamic_array_base
{
    int refc{ 0 };
    el* ptr;

    __host__ __device__ dynamic_array_base() : ptr(new el[max_length]) {}
    __host__ __device__ ~dynamic_array_base() { delete[] this->ptr; }

    __host__ __device__ el& operator[](default_int i) {
        // ws: upstream checks `i < this->length`, a member dynamic_array_base does not have (a compile error once indexed).
        assert("The index has to be in range." && 0 <= i && i < max_length);
        return this->ptr[i];
    }
};

template <typename el, default_int max_length>
struct dynamic_array
{
    sptr<dynamic_array_base<el, max_length>> ptr;

    __host__ __device__ dynamic_array() = default;
    __host__ __device__ dynamic_array(bool t) : ptr(new dynamic_array_base<el, max_length>()) {}
    __host__ __device__ el& operator[](default_int i) {
        return this->ptr.base->operator[](i);
    }
};

template <typename el, default_int max_length>
struct dynamic_array_list_base
{
    int refc{ 0 };
    default_int length{ 0 };
    el* ptr;

    __host__ __device__ dynamic_array_list_base() : ptr(new el[max_length]) {}
    __host__ __device__ dynamic_array_list_base(default_int l) : ptr(new el[max_length]) { this->unsafe_set_length(l); }
    __host__ __device__ ~dynamic_array_list_base() { delete[] this->ptr; }

    __host__ __device__ el& operator[](default_int i) {
        assert("The index has to be in range." && 0 <= i && i < this->length);
        return this->ptr[i];
    }
    __host__ __device__ void push(const el& x) {
        ptr[this->length++] = x;
        assert("The array after pushing should not be greater than max length." && this->length <= max_length);
    }
    __host__ __device__ void push(const el&& x) {
        ptr[this->length++] = std::move(x);
        assert("The array after pushing should not be greater than max length." && this->length <= max_length);
    }
    __host__ __device__ el pop() {
        assert("The array before popping should be greater than 0." && 0 < this->length);
        auto x = ptr[--this->length];
        ptr[this->length].~el();
        new (&ptr[this->length]) el();
        return x;
    }
    // Should be used only during initialization.
    __host__ __device__ void unsafe_set_length(default_int i) {
        assert("The new length should be in range." && 0 <= i && i <= max_length);
        this->length = i;
    }
};

template <typename el, default_int max_length>
struct dynamic_array_list
{
    sptr<dynamic_array_list_base<el, max_length>> ptr;

    __host__ __device__ dynamic_array_list() = default;
    __host__ __device__ dynamic_array_list(default_int l) : ptr(new dynamic_array_list_base<el, max_length>(l)) {}

    __host__ __device__ el& operator[](default_int i) {
        return this->ptr.base->operator[](i);
    }
    __host__ __device__ void push(const el& x) {
        this->ptr.base->push(x);
    }
    __host__ __device__ void push(const el&& x) {
        this->ptr.base->push(std::move(x));
    }
    __host__ __device__ el pop() {
        return this->ptr.base->pop();
    }
    // Should be used only during initialization.
    __host__ __device__ void unsafe_set_length(default_int i) {
        this->ptr.base->unsafe_set_length(i);
    }
    __host__ __device__ default_int length_() {
        return this->ptr.base->length;
    }
};
