#include "shape.hpp"

#include "geometry/polar.hpp"
#include "geometry/basic.hpp"
#include "mesh.hpp"

#include <vector>
#include <initializer_list>
#include <array>
#include <cmath>


using namespace frog;
using frog::geo::vec3;
using frog::gl::mesh;


mesh frog::gl::shape::paddle(int layers,
                             int pieces,
                             float radius,
                             float alpha,
                             float beta)
{
    using namespace geo;

    auto vertices = std::vector<float>{};
    auto normals = std::vector<float>{};
    auto tex_coords = std::vector<float>{};
    auto indices = std::vector<uint32_t>{};

    // TODO
    throw 0;
}


mesh frog::gl::shape::polygon_2D(int sides, float radius)
{
    using namespace geo;

    auto vertices = std::vector<float>{};
    auto normals = std::vector<float>{};
    auto tex_coords = std::vector<float>{};
    auto indices = std::vector<uint32_t>{};

    float angle = 2 * geo::Pi / sides;

    // add middle
    vertices.push_back(0);
    vertices.push_back(0);
    vertices.push_back(0);
    normals.push_back(0);
    normals.push_back(1);
    normals.push_back(0);
    tex_coords.push_back(0.5);
    tex_coords.push_back(0.5);

    // add vertices
    for (int i = 0; i < sides; i++)
    {
        vec2 pt = geo::to_cartesian(polar2{ radius, angle * i });
        vertices.push_back(pt.x());
        vertices.push_back(0);
        vertices.push_back(pt.y());
        normals.push_back(0);
        normals.push_back(1);
        normals.push_back(0);
        tex_coords.push_back(0.5 + 0.5 * pt.x() / radius);
        tex_coords.push_back(0.5 + 0.5 * pt.y() / radius);
    }

    // add indices
    for (int i = 0; i < sides; i++)
    {
        indices.push_back((i + 2) % sides + 1);
        indices.push_back((i + 1) % sides + 1);
        indices.push_back(0);
    }

    return mesh(std::move(vertices)
              , std::move(normals)
              , std::move(tex_coords)
              , std::move(indices));
}


mesh frog::gl::shape::sphere(int rings, int sides, float radius)
{
    using namespace geo;

    auto vertices = std::vector<float>{};
    auto normals = std::vector<float>{};
    auto tex_coords = std::vector<float>{};
    auto indices = std::vector<uint32_t>{};

    float y_angle = 2 * geo::Pi / static_cast<float>(sides);

    int point_idx = 0;
    auto add = [&](vec3 pt)
    {
        vertices.push_back(pt.x());
        vertices.push_back(pt.y());
        vertices.push_back(pt.z());
        normals.push_back(pt.x() / radius);
        normals.push_back(pt.y() / radius);
        normals.push_back(pt.z() / radius);
        // TODO
        tex_coords.push_back(0);
        tex_coords.push_back(0);
        indices.push_back(point_idx);
        ++point_idx;
    };

    auto add_cyl = [&](cylindrical3 cyl)
    {
        add(to_cartesian(cyl));
    };

    for (int s = 0; s < sides; s++)
    {
        float ry = s * y_angle;
        float ry_next = (s + 1) * y_angle;

        for (int r = 0; r < rings; r++)
        {
            float y = radius * std::cos(r * Pi / rings);
            float y_next = radius * std::cos((r + 1) * Pi / rings);

            float r1 = std::sqrt(radius * radius - y * y);
            float r_next = std::sqrt(radius * radius - y_next * y_next);

            add_cyl({ r_next, ry_next, y_next });
            add_cyl({ r_next, ry, y_next });
            add_cyl({ r1, ry, y });

            add_cyl({ r1, ry_next, y });
            add_cyl({ r_next, ry_next, y_next });
            add_cyl({ r1, ry, y });
        }
    }

    return mesh(std::move(vertices)
              , std::move(normals)
              , std::move(tex_coords)
              , std::move(indices));
}


mesh frog::gl::shape::cuboid(vec3 size)
{
    auto vertices = std::vector<float>{};
    auto normals = std::vector<float>{};
    auto tex_coords = std::vector<float>{};
    auto indices = std::vector<uint32_t>{};

    using std::initializer_list, std::pair, std::array;
    static const auto side = initializer_list<pair<int, int>>
    {
        { -1, -1 },
        {  1, -1 },
        { -1,  1 },
        {  1,  1 },
    };

    static const auto triangles = array<initializer_list<initializer_list<int>>, 2>
    {
        {
            /* one side */
            { { 2, 1, 0 }, { 2, 3, 1 } },

            /* opposite side
            * (reverse order because of face culling) */
            { { 0, 1, 2 }, { 1, 3, 2 } },
        }
    };

    int vert_idx = -1;
    for (int axis = 0; axis < 3; axis++)
    {
        int a = (axis + 1) % 3;
        int b = (axis + 2) % 3;

        for (int sgn : { -1, 1 })
        {
            for (auto[sgn_a, sgn_b] : side)
            {
                vec3 pt;
                pt[a]    = sgn_a * size[a]    / 2;
                pt[b]    = sgn_b * size[b]    / 2;
                pt[axis] = sgn   * size[axis] / 2;

                vec3 n;
                n[a] = 0;
                n[b] = 0;
                n[axis] = sgn;

                // add new vertex
                ++vert_idx;
                vertices.push_back(pt.x());
                vertices.push_back(pt.y());
                vertices.push_back(pt.z());
                normals.push_back(n.x());
                normals.push_back(n.y());
                normals.push_back(n.z());
                tex_coords.push_back((sgn_a + 1) / 2);
                tex_coords.push_back((sgn_b + 1) / 2);
            }
            // add this side's vertices to the indices
            for (auto tri : triangles[(sgn + 1) / 2])
            {
                for (int idx : tri)
                {
                    indices.push_back(vert_idx - 3 + idx);
                }
            }
        }
    }

    // 6 sides, 4 vert each, 3 dimensions
    assert(vertices.size() == 4 * 6 * 3);

    return mesh(std::move(vertices)
              , std::move(normals)
              , std::move(tex_coords)
              , std::move(indices));
}
