/*
 * Copyright (C) 2016 Google, Inc.
 *
 * 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.
 */

#include <cassert>
#include <cmath>
#include <cstring>
#include <array>
#include <unordered_map>

#include "Helpers.h"
#include "Meshes.h"

namespace {

class Mesh {
public:
    struct Position {
        float x;
        float y;
        float z;
    };

    struct Normal {
        float x;
        float y;
        float z;
    };

    struct Face {
        int v0;
        int v1;
        int v2;
    };

    static uint32_t vertex_stride()
    {
        // Position + Normal
        const int comp_count = 6;

        return sizeof(float) * comp_count;
    }

    static VkVertexInputBindingDescription vertex_input_binding()
    {
        VkVertexInputBindingDescription vi_binding = {};
        vi_binding.binding = 0;
        vi_binding.stride = vertex_stride();
        vi_binding.inputRate = VK_VERTEX_INPUT_RATE_VERTEX;

        return vi_binding;
    }

    static std::vector<VkVertexInputAttributeDescription> vertex_input_attributes()
    {
        std::vector<VkVertexInputAttributeDescription> vi_attrs(2);
        // Position
        vi_attrs[0].location = 0;
        vi_attrs[0].binding = 0;
        vi_attrs[0].format = VK_FORMAT_R32G32B32_SFLOAT;
        vi_attrs[0].offset = 0;
        // Normal
        vi_attrs[1].location = 1;
        vi_attrs[1].binding = 0;
        vi_attrs[1].format = VK_FORMAT_R32G32B32_SFLOAT;
        vi_attrs[1].offset = sizeof(float) * 3;

        return vi_attrs;
    }

    static VkIndexType index_type()
    {
        return VK_INDEX_TYPE_UINT32;
    }

    static VkPipelineInputAssemblyStateCreateInfo input_assembly_state()
    {
        VkPipelineInputAssemblyStateCreateInfo ia_info = {};
        ia_info.sType = VK_STRUCTURE_TYPE_PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO;
        ia_info.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST;
        ia_info.primitiveRestartEnable = false;
        return ia_info;
    }

    void build(const std::vector<std::array<float, 6>> &vertices, const std::vector<std::array<int, 3>> &faces)
    {
        positions_.reserve(vertices.size());
        normals_.reserve(vertices.size());
        for (const auto &v : vertices) {
            positions_.emplace_back(Position{ v[0], v[1], v[2] });
            normals_.emplace_back(Normal{ v[3], v[4], v[5] });
        }

        faces_.reserve(faces.size());
        for (const auto &f : faces)
            faces_.emplace_back(Face{ f[0], f[1], f[2] });
    }

    uint32_t vertex_count() const
    {
        return static_cast<uint32_t>(positions_.size());
    }

    VkDeviceSize vertex_buffer_size() const
    {
        return vertex_stride() * vertex_count();
    }

    void vertex_buffer_write(void *data) const
    {
        float *dst = reinterpret_cast<float *>(data);
        for (size_t i = 0; i < positions_.size(); i++) {
            const Position &pos = positions_[i];
            const Normal &normal = normals_[i];
            dst[0] = pos.x;
            dst[1] = pos.y;
            dst[2] = pos.z;
            dst[3] = normal.x;
            dst[4] = normal.y;
            dst[5] = normal.z;
            dst += 6;
        }
    }

    uint32_t index_count() const
    {
        return static_cast<uint32_t>(faces_.size()) * 3;
    }

    VkDeviceSize index_buffer_size() const
    {
        return sizeof(uint32_t) * index_count();
    }

    void index_buffer_write(void *data) const
    {
        uint32_t *dst = reinterpret_cast<uint32_t *>(data);
        for (const auto &face : faces_) {
            dst[0] = face.v0;
            dst[1] = face.v1;
            dst[2] = face.v2;
            dst += 3;
        }
    }

    std::vector<Position> positions_;
    std::vector<Normal> normals_;
    std::vector<Face> faces_;
};

class BuildPyramid {
public:
    BuildPyramid(Mesh &mesh)
    {
        const std::vector<std::array<float, 6>> vertices = {
            //      position                normal
            {  0.0f,  0.0f,  1.0f,    0.0f,  0.0f,  1.0f },
            { -1.0f, -1.0f, -1.0f,   -1.0f, -1.0f, -1.0f },
            {  1.0f, -1.0f, -1.0f,    1.0f, -1.0f, -1.0f },
            {  1.0f,  1.0f, -1.0f,    1.0f,  1.0f, -1.0f },
            { -1.0f,  1.0f, -1.0f,   -1.0f,  1.0f, -1.0f },
        };

        const std::vector<std::array<int, 3>> faces = {
            { 0, 1, 2 },
            { 0, 2, 3 },
            { 0, 3, 4 },
            { 0, 4, 1 },
            { 1, 4, 3 },
            { 1, 3, 2 },
        };

        mesh.build(vertices, faces);
    }
};

class BuildIcosphere {
public:
    BuildIcosphere(Mesh &mesh) : mesh_(mesh), radius_(1.0f)
    {
        const int tessellate_level = 2;

        build_icosahedron();
        for (int i = 0; i < tessellate_level; i++)
            tessellate();
    }

private:
    void build_icosahedron()
    {
        // https://en.wikipedia.org/wiki/Regular_icosahedron
        const float l1 = std::sqrt(2.0f / (5.0f + std::sqrt(5.0f))) * radius_;
        const float l2 = std::sqrt(2.0f / (5.0f - std::sqrt(5.0f))) * radius_;
        // vertices are from three golden rectangles
        const std::vector<std::array<float, 6>> icosahedron_vertices = {
            //   position           normal
            { -l1, -l2, 0.0f,   -l1, -l2, 0.0f, },
            {  l1, -l2, 0.0f,    l1, -l2, 0.0f, },
            {  l1,  l2, 0.0f,    l1,  l2, 0.0f, },
            { -l1,  l2, 0.0f,   -l1,  l2, 0.0f, },

            { -l2, 0.0f, -l1,   -l2, 0.0f, -l1, },
            {  l2, 0.0f, -l1,    l2, 0.0f, -l1, },
            {  l2, 0.0f,  l1,    l2, 0.0f,  l1, },
            { -l2, 0.0f,  l1,   -l2, 0.0f,  l1, },

            { 0.0f, -l1, -l2,   0.0f, -l1, -l2, },
            { 0.0f,  l1, -l2,   0.0f,  l1, -l2, },
            { 0.0f,  l1,  l2,   0.0f,  l1,  l2, },
            { 0.0f, -l1,  l2,   0.0f, -l1,  l2, },
        };
        const std::vector<std::array<int, 3>> icosahedron_faces = {
            // triangles sharing vertex 0
            {  0,  1, 11 },
            {  0, 11,  7 },
            {  0,  7,  4 },
            {  0,  4,  8 },
            {  0,  8,  1 },
            // adjacent triangles
            { 11,  1,  6 },
            {  7, 11, 10 },
            {  4,  7,  3 },
            {  8,  4,  9 },
            {  1,  8,  5 },
            // triangles sharing vertex 2
            {  2,  3, 10 },
            {  2, 10,  6 },
            {  2,  6,  5 },
            {  2,  5,  9 },
            {  2,  9,  3 },
            // adjacent triangles
            { 10,  3,  7 },
            {  6, 10, 11 },
            {  5,  6,  1 },
            {  9,  5,  8 },
            {  3,  9,  4 },
        };

        mesh_.build(icosahedron_vertices, icosahedron_faces);
    }

    void tessellate()
    {
        size_t middle_point_count = mesh_.faces_.size() * 3 / 2;
        size_t final_face_count = mesh_.faces_.size() * 4;

        std::vector<Mesh::Face> faces;
        faces.reserve(final_face_count);

        middle_points_.clear();
        middle_points_.reserve(middle_point_count);

        mesh_.positions_.reserve(mesh_.vertex_count() + middle_point_count);
        mesh_.normals_.reserve(mesh_.vertex_count() + middle_point_count);

        for (const auto &f : mesh_.faces_) {
            int v0 = f.v0;
            int v1 = f.v1;
            int v2 = f.v2;

            int v01 = add_middle_point(v0, v1);
            int v12 = add_middle_point(v1, v2);
            int v20 = add_middle_point(v2, v0);

            faces.emplace_back(Mesh::Face{ v0, v01, v20 });
            faces.emplace_back(Mesh::Face{ v1, v12, v01 });
            faces.emplace_back(Mesh::Face{ v2, v20, v12 });
            faces.emplace_back(Mesh::Face{ v01, v12, v20 });
        }

        mesh_.faces_.swap(faces);
    }

    int add_middle_point(int a, int b)
    {
        uint64_t key = (a < b) ? ((uint64_t) a << 32 | b) : ((uint64_t) b << 32 | a);
        auto it = middle_points_.find(key);
        if (it != middle_points_.end())
            return it->second;

        const Mesh::Position &pos_a = mesh_.positions_[a];
        const Mesh::Position &pos_b = mesh_.positions_[b];
        Mesh::Position pos_mid = {
            (pos_a.x + pos_b.x) / 2.0f,
            (pos_a.y + pos_b.y) / 2.0f,
            (pos_a.z + pos_b.z) / 2.0f,
        };
        float scale = radius_ / std::sqrt(pos_mid.x * pos_mid.x +
                                          pos_mid.y * pos_mid.y +
                                          pos_mid.z * pos_mid.z);
        pos_mid.x *= scale;
        pos_mid.y *= scale;
        pos_mid.z *= scale;

        Mesh::Normal normal_mid = { pos_mid.x, pos_mid.y, pos_mid.z };
        normal_mid.x /= radius_;
        normal_mid.y /= radius_;
        normal_mid.z /= radius_;

        mesh_.positions_.emplace_back(pos_mid);
        mesh_.normals_.emplace_back(normal_mid);

        int mid = mesh_.vertex_count() - 1;
        middle_points_.emplace(std::make_pair(key, mid));

        return mid;
    }

    Mesh &mesh_;
    const float radius_;
    std::unordered_map<uint64_t, uint32_t> middle_points_;
};

class BuildTeapot {
public:
    BuildTeapot(Mesh &mesh)
    {
#include "Meshes.teapot.h"
        const int position_count = sizeof(teapot_positions) / sizeof(teapot_positions[0]);
        const int index_count = sizeof(teapot_indices) / sizeof(teapot_indices[0]);
        assert(position_count % 3 == 0 && index_count % 3 == 0);

        Mesh::Position translate;
        float scale;
        get_transform(teapot_positions, position_count, translate, scale);

        for (int i = 0; i < position_count; i += 3) {
            mesh.positions_.emplace_back(Mesh::Position{
                (teapot_positions[i + 0] + translate.x) * scale,
                (teapot_positions[i + 1] + translate.y) * scale,
                (teapot_positions[i + 2] + translate.z) * scale,
            });

            mesh.normals_.emplace_back(Mesh::Normal{
                teapot_normals[i + 0],
                teapot_normals[i + 1],
                teapot_normals[i + 2],
            });
        }

        for (int i = 0; i < index_count; i += 3) {
            mesh.faces_.emplace_back(Mesh::Face{
                teapot_indices[i + 0],
                teapot_indices[i + 1],
                teapot_indices[i + 2]
            });
        }
    }

    void get_transform(const float *positions, int position_count,
                       Mesh::Position &translate, float &scale)
    {
        float min[3] = {
            positions[0],
            positions[1],
            positions[2],
        };
        float max[3] = {
            positions[0],
            positions[1],
            positions[2],
        };
        for (int i = 3; i < position_count; i += 3) {
            for (int j = 0; j < 3; j++) {
                if (min[j] > positions[i + j])
                    min[j] = positions[i + j];
                if (max[j] < positions[i + j])
                    max[j] = positions[i + j];
            }
        }

        translate.x = -(min[0] + max[0]) / 2.0f;
        translate.y = -(min[1] + max[1]) / 2.0f;
        translate.z = -(min[2] + max[2]) / 2.0f;

        float extents[3] = {
            max[0] + translate.x,
            max[1] + translate.y,
            max[2] + translate.z,
        };

        float max_extent = extents[0];
        if (max_extent < extents[1])
            max_extent = extents[1];
        if (max_extent < extents[2])
            max_extent = extents[2];

        scale = 1.0f / max_extent;
    }
};

void build_meshes(std::array<Mesh, Meshes::MESH_COUNT> &meshes)
{
    BuildPyramid build_pyramid(meshes[Meshes::MESH_PYRAMID]);
    BuildIcosphere build_icosphere(meshes[Meshes::MESH_ICOSPHERE]);
    BuildTeapot build_teapot(meshes[Meshes::MESH_TEAPOT]);
}

} // namespace

Meshes::Meshes(VkDevice dev, const std::vector<VkMemoryPropertyFlags> &mem_flags)
    : dev_(dev),
      vertex_input_binding_(Mesh::vertex_input_binding()),
      vertex_input_attrs_(Mesh::vertex_input_attributes()),
      vertex_input_state_(),
      input_assembly_state_(Mesh::input_assembly_state()),
      index_type_(Mesh::index_type())
{
    vertex_input_state_.sType = VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO;
    vertex_input_state_.vertexBindingDescriptionCount = 1;
    vertex_input_state_.pVertexBindingDescriptions = &vertex_input_binding_;
    vertex_input_state_.vertexAttributeDescriptionCount = static_cast<uint32_t>(vertex_input_attrs_.size());
    vertex_input_state_.pVertexAttributeDescriptions = vertex_input_attrs_.data();

    std::array<Mesh, MESH_COUNT> meshes;
    build_meshes(meshes);

    draw_commands_.reserve(meshes.size());
    uint32_t first_index = 0;
    int32_t vertex_offset = 0;
    VkDeviceSize vb_size = 0;
    VkDeviceSize ib_size = 0;
    for (const auto &mesh : meshes) {
        VkDrawIndexedIndirectCommand draw = {};
        draw.indexCount = mesh.index_count();
        draw.instanceCount = 1;
        draw.firstIndex = first_index;
        draw.vertexOffset = vertex_offset;
        draw.firstInstance = 0;

        draw_commands_.push_back(draw);

        first_index += mesh.index_count();
        vertex_offset += mesh.vertex_count();
        vb_size += mesh.vertex_buffer_size();
        ib_size += mesh.index_buffer_size();
    }

    allocate_resources(vb_size, ib_size, mem_flags);

    uint8_t *vb_data, *ib_data;
    vk::assert_success(vk::MapMemory(dev_, mem_, 0, VK_WHOLE_SIZE,
                0, reinterpret_cast<void **>(&vb_data)));
    ib_data = vb_data + ib_mem_offset_;

    for (const auto &mesh : meshes) {
        mesh.vertex_buffer_write(vb_data);
        mesh.index_buffer_write(ib_data);
        vb_data += mesh.vertex_buffer_size();
        ib_data += mesh.index_buffer_size();
    }

    vk::UnmapMemory(dev_, mem_);
}

Meshes::~Meshes()
{
    vk::FreeMemory(dev_, mem_, nullptr);
    vk::DestroyBuffer(dev_, vb_, nullptr);
    vk::DestroyBuffer(dev_, ib_, nullptr);
}

void Meshes::cmd_bind_buffers(VkCommandBuffer cmd) const
{
    const VkDeviceSize vb_offset = 0;
    vk::CmdBindVertexBuffers(cmd, 0, 1, &vb_, &vb_offset);

    vk::CmdBindIndexBuffer(cmd, ib_, 0, index_type_);
}

void Meshes::cmd_draw(VkCommandBuffer cmd, Type type) const
{
    const auto &draw = draw_commands_[type];
    vk::CmdDrawIndexed(cmd, draw.indexCount, draw.instanceCount,
            draw.firstIndex, draw.vertexOffset, draw.firstInstance);
}

void Meshes::allocate_resources(VkDeviceSize vb_size, VkDeviceSize ib_size, const std::vector<VkMemoryPropertyFlags> &mem_flags)
{
    VkBufferCreateInfo buf_info = {};
    buf_info.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO;
    buf_info.size = vb_size;
    buf_info.usage = VK_BUFFER_USAGE_VERTEX_BUFFER_BIT;
    buf_info.sharingMode = VK_SHARING_MODE_EXCLUSIVE;
    vk::CreateBuffer(dev_, &buf_info, nullptr, &vb_);

    buf_info.size = ib_size;
    buf_info.usage = VK_BUFFER_USAGE_INDEX_BUFFER_BIT;
    vk::CreateBuffer(dev_, &buf_info, nullptr, &ib_);

    VkMemoryRequirements vb_mem_reqs, ib_mem_reqs;
    vk::GetBufferMemoryRequirements(dev_, vb_, &vb_mem_reqs);
    vk::GetBufferMemoryRequirements(dev_, ib_, &ib_mem_reqs);

    // indices follow vertices
    ib_mem_offset_ = vb_mem_reqs.size +
        (ib_mem_reqs.alignment - (vb_mem_reqs.size % ib_mem_reqs.alignment));

    VkMemoryAllocateInfo mem_info = {};
    mem_info.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO;
    mem_info.allocationSize = ib_mem_offset_ + ib_mem_reqs.size;

    // find any supported and mappable memory type
    uint32_t mem_types = (vb_mem_reqs.memoryTypeBits & ib_mem_reqs.memoryTypeBits);
    for (uint32_t idx = 0; idx < mem_flags.size(); idx++) {
        if ((mem_types & (1 << idx)) &&
            (mem_flags[idx] & VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT) &&
            (mem_flags[idx] & VK_MEMORY_PROPERTY_HOST_COHERENT_BIT)) {
            // TODO this may not be reachable
            mem_info.memoryTypeIndex = idx;
            break;
        }
    }

    vk::AllocateMemory(dev_, &mem_info, nullptr, &mem_);

    vk::BindBufferMemory(dev_, vb_, mem_, 0);
    vk::BindBufferMemory(dev_, ib_, mem_, ib_mem_offset_);
}