
// Copyright 2019 Shlomi Steinberg
// 

#pragma once

#include <string>
#include <filesystem>
#include <iostream>
#include <fstream>
#include <map>

#include <tinyply.h>
#include "glm.hpp"

namespace renderer {

struct vert_t { double x,y,z,nx,ny,nz; };
struct model_t {
    std::vector<std::uint32_t> inds;
    std::vector<vert_t> verts;
};

namespace _utils {

inline std::vector<uint8_t> read_file(const std::string & pathToFile) {
    std::ifstream file(pathToFile);
    std::vector<uint8_t> fileBufferBytes;

    if (file.is_open()) {
        file.seekg(0, std::ios::end);
        size_t sizeBytes = file.tellg();
        file.seekg(0, std::ios::beg);
        fileBufferBytes.resize(sizeBytes);
        if (file.read((char*)fileBufferBytes.data(), sizeBytes)) return fileBufferBytes;
    }
    else throw std::runtime_error("could not open ifstream to path " + pathToFile);
    return fileBufferBytes;
}

struct memory_buffer : public std::streambuf {
    char * p_start {nullptr};
    char * p_end {nullptr};
    size_t size;

    memory_buffer(char const * first_elem, size_t size)
        : p_start(const_cast<char*>(first_elem)), p_end(p_start + size), size(size) {
        setg(p_start, p_start, p_end);
    }

    pos_type seekoff(off_type off, std::ios_base::seekdir dir, std::ios_base::openmode which) override {
        if (dir == std::ios_base::cur) gbump(static_cast<int>(off));
        else setg(p_start, (dir == std::ios_base::beg ? p_start : p_end) + off, p_end);
        return gptr() - p_start;
    }

    pos_type seekpos(pos_type pos, std::ios_base::openmode which) override {
        return seekoff(pos, std::ios_base::beg, which);
    }
};

struct memory_stream : virtual memory_buffer, public std::istream {
    memory_stream(char const * first_elem, size_t size)
        : memory_buffer(first_elem, size), std::istream(static_cast<std::streambuf*>(this)) {}
};

}

auto load_model(const std::filesystem::path &path, float rescale = 1.f) {    
    const auto byte_buffer = _utils::read_file(path);
    auto stream = _utils::memory_stream((char*)byte_buffer.data(), byte_buffer.size());
    if (stream.fail()) throw std::runtime_error("file_stream failed to open " + path.string());

    tinyply::PlyFile file;
    file.parse_header(stream);

    std::shared_ptr<tinyply::PlyData> vertices, faces;
    try { vertices = file.request_properties_from_element("vertex", { "x", "y", "z" }); }
    catch (const std::exception & e) { std::cerr << "tinyply exception: " << e.what() << std::endl; throw e; }
    try { faces = file.request_properties_from_element("face", { "vertex_indices" }, 3); }
    catch (const std::exception & e) { std::cerr << "tinyply exception: " << e.what() << std::endl; throw e; }

    if (vertices->t != tinyply::Type::FLOAT32) throw std::runtime_error("Unsupported " + path.string());
    if (faces->t != tinyply::Type::INT32 &&
        faces->t != tinyply::Type::UINT32)     throw std::runtime_error("Unsupported " + path.string());

    file.read(stream);

    model_t model;
    model.verts.resize(vertices->count);
    const auto vdata = reinterpret_cast<const float*>(vertices->buffer.get());
    for (std::size_t v=0; v<vertices->count; ++v) {
        model.verts[v].x = vdata[v*3+0] * rescale;
        model.verts[v].y = vdata[v*3+1] * rescale;
        model.verts[v].z = vdata[v*3+2] * rescale;
    }
    model.inds.resize(faces->count*3);
    const auto idata = reinterpret_cast<const std::uint32_t*>(faces->buffer.get());
    std::copy(idata, idata+faces->count*3, model.inds.data());

    // Generate normals
    std::unordered_map<std::uint32_t, std::pair<glm::vec3,int>> vertices_to_normals;
    for (std::size_t f=0; f<model.inds.size()/3; ++f) {
        glm::vec3 v0 = { model.verts[model.inds[f*3+0]].x, model.verts[model.inds[f*3+0]].y, model.verts[model.inds[f*3+0]].z };
        glm::vec3 v1 = { model.verts[model.inds[f*3+1]].x, model.verts[model.inds[f*3+1]].y, model.verts[model.inds[f*3+1]].z };
        glm::vec3 v2 = { model.verts[model.inds[f*3+2]].x, model.verts[model.inds[f*3+2]].y, model.verts[model.inds[f*3+2]].z };
        const auto n = glm::normalize(glm::cross(v1-v0,v2-v0));

        auto it0 = vertices_to_normals.try_emplace(model.inds[f*3+0], std::make_pair(n,1));
        auto it1 = vertices_to_normals.try_emplace(model.inds[f*3+1], std::make_pair(n,1));
        auto it2 = vertices_to_normals.try_emplace(model.inds[f*3+2], std::make_pair(n,1));
        if (!it0.second) { it0.first->second.first+=n; ++it0.first->second.second; }
        if (!it1.second) { it1.first->second.first+=n; ++it1.first->second.second; }
        if (!it2.second) { it2.first->second.first+=n; ++it2.first->second.second; }
    }
    for (auto &p : vertices_to_normals) {
        p.second.first /= static_cast<float>(p.second.second);
        model.verts[p.first].nx = p.second.first.x;
        model.verts[p.first].ny = p.second.first.y;
        model.verts[p.first].nz = p.second.first.z;
    }

    return model;
}

}
