#pragma once

#include <condition_variable>
#include <deque>
#include <mutex>

#include <glm/glm.hpp>

#ifdef _DEBUG
#define GL(stmt) stmt; check_OpenGLError(#stmt, __FILE__, __LINE__);
#else
#define GL(stmt) stmt
#endif

struct vertex_t 
{ 
    glm::vec4 pos;
    glm::vec2 uvs;
    glm::vec2 uvs2;
    glm::vec3 nor;
    vertex_t() : pos(0), uvs(0), uvs2(0), nor(0) {}
    vertex_t(glm::vec2 p) : pos(p, 0, 1), uvs(0), uvs2(0), nor(0) {}
    vertex_t(glm::vec2 p, glm::vec2 uv) : pos(p, 0, 1), uvs(uv), uvs2(0), nor(0) {}
    vertex_t(glm::vec3 p) : pos(p, 1), uvs(0), uvs2(0), nor(0) {}
    vertex_t(glm::vec3 p, glm::vec2 uv) : pos(p, 1), uvs(uv), uvs2(0), nor(0) {}
    vertex_t(glm::vec4 p) : pos(p), uvs(0), uvs2(0), nor(0) {}
    vertex_t(glm::vec4 p, glm::vec2 uv) : pos(p), uvs(uv), uvs2(0), nor(0) {}
    vertex_t(glm::vec4 p, glm::vec2 uv, glm::vec2 uv2) : pos(p), uvs(uv), uvs2(uv2), nor(0) {}
};

uint16_t constexpr const_hash(const char* input)
{
    return *input ?
        static_cast<uint16_t>(*input) + 33 * const_hash(input + 1) :
        5381;
}

inline void* aligned_malloc(size_t size, size_t align) {
    void* result;
#ifdef _MSC_VER 
    result = _aligned_malloc(size, align);
#else 
    if (posix_memalign(&result, align, size)) result = 0;
#endif
    return result;
}

inline void aligned_free(void* ptr) {
#ifdef _MSC_VER 
    _aligned_free(ptr);
#else 
    free(ptr);
#endif

}

// used as: std::vector<T, AlignmentAllocator<T, 16> > bla;
template <typename T, std::size_t N = 16>
class AlignmentAllocator {
public:
    typedef T value_type;
    typedef std::size_t size_type;
    typedef std::ptrdiff_t difference_type;

    typedef T* pointer;
    typedef const T* const_pointer;

    typedef T& reference;
    typedef const T& const_reference;

public:
    inline AlignmentAllocator() throw () { }

    template <typename T2>
    inline AlignmentAllocator(const AlignmentAllocator<T2, N>&) throw () { }

    inline ~AlignmentAllocator() throw () { }

    inline pointer adress(reference r) {
        return &r;
    }

    inline const_pointer adress(const_reference r) const {
        return &r;
    }

    inline pointer allocate(size_type n) {
        return (pointer)aligned_malloc(n * sizeof(value_type), N);
    }

    inline void deallocate(pointer p, size_type) {
        aligned_free(p);
    }

    inline void construct(pointer p, const value_type& wert) {
        new (p) value_type(wert);
    }

    inline void destroy(pointer p) {
        p->~value_type();
    }

    inline size_type max_size() const throw () {
        return size_type(-1) / sizeof(value_type);
    }

    template <typename T2>
    struct rebind {
        typedef AlignmentAllocator<T2, N> other;
    };

    bool operator!=(const AlignmentAllocator<T, N>& other) const {
        return !(*this == other);
    }

    // Returns true if and only if storage allocated from *this
    // can be deallocated from other, and vice versa.
    // Always returns true for stateless allocators.
    bool operator==(const AlignmentAllocator<T, N>& other) const {
        return true;
    }
};

template<class T>
std::vector<T> poly_remove_duplicate(const std::vector<T>& v, const float tollerance = 0.001)
{
    std::vector<T> ret;
    for (size_t i = 0; i < v.size(); i++)
    {
        //if (glm::distance2(v[i], v[(i + 1) % v.size()]) > tollerance)
        if (v[i] != v[(i + 1) % v.size()])
            ret.push_back(v[i]);
    }
    return ret;
}

template<>
std::vector<vertex_t> poly_remove_duplicate<vertex_t>(const std::vector<vertex_t>& v, const float tollerance);

template <typename T>
std::vector<std::string> split(const std::string& subject, T d, int max_split = 0)
{
    std::vector<std::string> ret;
    size_t start = 0;
    size_t n = subject.find_first_of(d);
    while (n != std::string::npos)
    {
        ret.push_back(subject.substr(start, n - start));
        start = n + 1;
        if (max_split && ret.size() == max_split)
            break;
        n = subject.find_first_of(d, start);
    }
    ret.push_back(subject.substr(start));
    return ret;
}

// from {origin, size} to {min, max}
glm::vec4 rect_to_box(const glm::vec4& rect);
// from {min, max} to {origin, size}
glm::vec4 box_to_rect(const glm::vec4& box);

// params {x, y} and {origin, size} form
bool point_in_rect(const glm::vec2& point, const glm::vec4& rect);
// params and returns {origin, size} form
glm::vec4 rect_intersection(glm::vec4 a, glm::vec4 b);
// params and returns {origin, size} form
glm::vec4 rect_union(glm::vec4 a, glm::vec4 b);
// return the are of the box {min, max}

float box_area(glm::vec4 b);
glm::vec2 box_size(glm::vec4 b);
// params and returns {min, max} form
glm::vec4 box_union(glm::vec4 a, glm::vec4 b);
// params and returns {min, max} form
glm::vec4 box_intersection(glm::vec4 a, glm::vec4 b);

bool ray_intersect(glm::vec3 ray_origin, glm::vec3 ray_dir, glm::vec3 plane_origin,
    glm::vec3 plane_normal, glm::vec3 plane_tangent, glm::vec3& out_hit, float& out_t);
float lines_distance(const glm::vec3& p0a, const glm::vec3& p0b,
    const glm::vec3& p1a, const glm::vec3& p1b);
bool segments_intersect_3d(const glm::vec3& p0a, const glm::vec3& p0b,
    const glm::vec3& p1a, const glm::vec3& p1b, glm::vec3& out_pt, glm::vec2& out_hit_uv);
bool segments_intersect(const glm::vec2& p0a, const glm::vec2& p0b,
    const glm::vec2& p1a, const glm::vec2& p1b, glm::vec2& out_pt, glm::vec2& out_hit_uv);
bool point_side(glm::vec2 a, glm::vec2 b, glm::vec2 p);

std::vector<vertex_t> poly_intersect(const vertex_t* poly_begin, const vertex_t* poly_end, const std::vector<glm::vec2>& clip);
std::vector<glm::vec2> poly_intersect(const std::vector<glm::vec2>& poly, const std::vector<glm::vec2>& clip);
std::vector<glm::vec3> poly_clip_near(const std::vector<glm::vec3>& poly, float near_plane_distance);
std::vector<vertex_t> triangulate(const std::vector<vertex_t>& points);
std::vector<vertex_t> triangulate(const std::vector<glm::vec2>& points);
std::vector<vertex_t> triangulate_simple(const std::vector<vertex_t>& vertices);

glm::vec4 rand_color();
glm::vec3 convert_long_rgb(uint32_t hex);
uint32_t convert_rgb_long(glm::vec3 rgb);
glm::vec3 convert_hsv2rgb(const glm::vec3 c);
glm::vec3 convert_rgb2hsv(const glm::vec3 c);

std::string unescape(const std::string& s);
std::wstring str2wstr(const std::string& str);
std::string wstr2str(const std::wstring& wstr);
bool str_iequals(const std::string& a, const std::string& b);
std::string str_replace(const std::string& string, const std::string& search, const std::string& replace);

size_t curl_data_handler(void *contents, size_t size, size_t nmemb, void *userp);
size_t curl_data_write(void *ptr, size_t size, size_t nmemb, FILE *stream);
void check_OpenGLError(const char* stmt, const char* fname, int line);
bool copy_framebuffer_to_texture_target(
    uint32_t texture_target,
    int destination_x,
    int destination_y,
    int source_x,
    int source_y,
    int width,
    int height,
    int level = 0) noexcept;
bool copy_framebuffer_to_texture_2d(
    int destination_x,
    int destination_y,
    int source_x,
    int source_y,
    int width,
    int height,
    int level = 0) noexcept;
inline glm::vec2 xy(const glm::vec4& v) { return glm::vec2(v.x, v.y); }
inline glm::vec3 xyz(const glm::vec4& v) { return glm::vec3(v.x, v.y, v.z); }
inline glm::vec2 zw(const glm::vec4& v) { return glm::vec2(v.z, v.w); }
inline glm::ivec2 xy(const glm::ivec4& v) { return glm::ivec2(v.x, v.y); }
inline glm::ivec3 xyz(const glm::ivec4& v) { return glm::ivec3(v.x, v.y, v.z); }
inline glm::ivec2 zw(const glm::ivec4& v) { return glm::ivec2(v.z, v.w); }
inline glm::vec2 xy(const glm::vec3& v) { return glm::vec2(v.x, v.y); }

void parallel_for(size_t nb_elements, std::function<void(size_t i)> functor, bool use_threads = true);

template<typename T> struct cbuffer
{
    std::unique_ptr<T[]> m_vec;
    int m_capacity = 0;
    int m_count = 0;
    int m_index = 0;
    cbuffer(int initial_capacity)
    {
        m_capacity = initial_capacity;
        m_index = 0;
        m_count = 0;
        m_vec = std::make_unique<T[]>(m_capacity);
    }
    void resize(int new_capacity)
    {
        m_capacity = new_capacity;
        m_vec = std::make_unique<T[]>(m_capacity);
        m_index = 0;
        m_count = 0;
    }
    void clear()
    {
        m_index = 0;
        m_count = 0;
    }
    const T& head() const
    {
        return m_index == 0 ? m_vec[m_count - 1] : m_vec[m_index - 1];
    }
    void add(const T& v)
    {
        m_vec[m_index] = v;
        m_index = (m_index + 1) % m_capacity;
        m_count = m_count < m_capacity ? m_count + 1 : m_count;
    }
    int count() const
    {
        return m_count;
    }
    template<typename T2 = T> T2 average() const
    {
        T2 tot{};
        if (m_count == 0)
            return tot;
        for (int i = 0; i < m_count; i++)
            tot += m_vec[i];
        return tot / (float)m_count;
    }
    template<typename T2 = T> T2 average_threshold(T threshold) const
    {
        T2 tot{};
        if (m_count == 0)
            return tot;
        int n = 0;
        for (int i = 0; i < m_count; i++)
            tot += glm::abs(m_vec[i] - head()) < threshold ? 0 : m_vec[i], n++;
        return tot / (float)n;
    }
};

template<typename T, int Max = 0>
class BlockingQueue
{
public:
    std::deque<std::pair<T, bool>> q;
    std::condition_variable post_cv;
    std::condition_variable get_cv;
    mutable std::mutex mutex;
    volatile bool unlocked = false;
    BlockingQueue() = default;
    BlockingQueue(const BlockingQueue& other) = delete;
    BlockingQueue& operator=(const BlockingQueue& other) = delete;
    BlockingQueue(BlockingQueue&& other) : q(std::move(q)) { }
    BlockingQueue& operator=(BlockingQueue&& other) { q = std::move(q); return *this; }
    void Post(T pkt)
    {
        std::unique_lock<std::mutex> lock(mutex);
        if (Max > 0)
        {
            post_cv.wait(lock, [&]() { return unlocked | (q.size() < Max); });
            if (q.size() >= Max) return;
        }
        q.push_back({ pkt, false });
        get_cv.notify_one();
    }
    void PostUnique(T pkt, bool top)
    {
        std::unique_lock<std::mutex> lock(mutex);
        if (Max > 0)
        {
            post_cv.wait(lock, [&]() { return unlocked | (q.size() < Max); });
            if (q.size() >= Max) return;
        }
        auto search = std::make_pair(pkt, top);
        if (std::find(q.begin(), q.end(), search) == q.end())
        {
            if (top)
            {
                // find the first low priority
                auto low = std::find_if(q.begin(), q.end(), [](auto const& x) { return x.second == false; });
                q.insert(low, { pkt, top });
            }
            else
            {
                q.push_back({ pkt, top });
            }
        }
        get_cv.notify_one();
    }
    void Remove(T pkt)
    {
        std::unique_lock<std::mutex> lock(mutex);
        auto it = std::find_if(q.begin(), q.end(), [&pkt](auto const& x) { return x.first == pkt; });
        if (it != q.end())
            q.erase(it);
        if (Max > 0) post_cv.notify_all();
    }
    T Get()
    {
        static T emptyT{};
        std::unique_lock<std::mutex> lock(mutex);
        get_cv.wait(lock, [&]() { return unlocked | (q.size() > 0); });
        if (q.empty())
            return std::move(emptyT);
        auto tmp = std::move(q.front());
        q.pop_front();
        if (Max > 0) post_cv.notify_all();
        return std::move(tmp.first);
    }
    void UnlockGetters()
    {
        unlocked = true;
        get_cv.notify_all();
        if (Max > 0) post_cv.notify_all();
    }
    int Size() const
    {
        std::lock_guard<std::mutex> lock(mutex);
        return (int)q.size();
    }
};

struct gl_state
{
    GLboolean blend;
    GLboolean depth_test;
    GLboolean scissor_test;
    GLint vp[4];
    GLfloat cc[4];
    GLint tex[10];
    GLint cube;
    GLint sampler[10];
    GLint program;
    GLint fbr, fbd;
    GLint active_tex;
    GLfloat line_width;
    void save();
    void restore();
};
