//  ************************************************************************************************
//
//  BornAgain: simulate and fit reflection and scattering
//
//! @file      ff/Make.cpp
//! @brief     Implements factory functions in namespace make
//!
//! @homepage  https://jugit.fz-juelich.de/mlz/libformfactor
//! @license   GNU General Public License v3 or higher (see COPYING)
//! @copyright Forschungszentrum Jülich GmbH 2022
//! @authors   Scientific Computing Group at MLZ (see CITATION, AUTHORS)
//
//  ************************************************************************************************

#include "ff/Make.h"
#include "ff/Topology.h"
#include <numbers>

namespace {

constexpr double pi_2 = std::numbers::pi / 2;

} // namespace


namespace ff::make {

//  ************************************************************************************************
//  Prisms
//  ************************************************************************************************

ff::Box* Box(double length, double width, double height)
{
    if (length <= 0)
        throw std::runtime_error("Box: invalid parameter: length<=0");
    if (width <= 0)
        throw std::runtime_error("Box: invalid parameter: width<=0");
    if (height <= 0)
        throw std::runtime_error("Box: invalid parameter: height<=0");

    return new ff::Box(length, width, height);
}

ff::Prism* Prism3(double base_edge, double height)
{
    if (base_edge <= 0)
        throw std::runtime_error("Prism3: invalid parameter: base_edge<=0");
    if (height <= 0)
        throw std::runtime_error("Prism3: invalid parameter: height<=0");

    double a = base_edge;
    double as = a / 2;
    double ac = a / sqrt(3) / 2;
    double ah = a / sqrt(3);

    std::vector<R3> base_vertices{{-ac, as, 0.}, {-ac, -as, 0.}, {ah, 0., 0.}};

    return new ff::Prism(false, height, base_vertices);
}

ff::Prism* Prism6(double base_edge, double height)
{
    if (base_edge <= 0)
        throw std::runtime_error("Prism3: invalid parameter: base_edge<=0");
    if (height <= 0)
        throw std::runtime_error("Prism3: invalid parameter: height<=0");

    double a = base_edge;
    double as = a * sqrt(3) / 2;
    double ac = a / 2;
    std::vector<R3> base_vertices{{a, 0., 0.},  {ac, as, 0.},   {-ac, as, 0.},
                                  {-a, 0., 0.}, {-ac, -as, 0.}, {ac, -as, 0.}};

    return new ff::Prism(true, height, base_vertices);
}

//  ************************************************************************************************
//  Platonic solids
//  ************************************************************************************************

ff::Polyhedron* Tetrahedron(double edge)
{
    static const Topology topology{
        {{{2, 1, 0}, false}, {{0, 1, 3}, false}, {{1, 2, 3}, false}, {{2, 0, 3}, false}}, false};

    if (edge <= 0)
        throw std::runtime_error("Tetrahedron: invalid parameter: edge<=0");
    const double a = edge;
    const double as = a / 2;
    const double ac = a / sqrt(3) / 2;
    const double ah = a / sqrt(3);
    const double height = sqrt(2. / 3) * edge;
    const double zcom = height / 4; // center of mass

    const std::vector<R3> vertices(
        {{-ac, as, -zcom}, {-ac, -as, -zcom}, {ah, 0., -zcom}, {0, 0., height - zcom}});

    return new ff::Polyhedron(topology, vertices);
}

ff::Polyhedron* Octahedron(double edge)
{
    static const Topology topology{{{{0, 2, 1}, false},
                                    {{0, 3, 2}, false},
                                    {{0, 4, 3}, false},
                                    {{0, 1, 4}, false},
                                    {{2, 3, 5}, false},
                                    {{1, 2, 5}, false},
                                    {{4, 1, 5}, false},
                                    {{3, 4, 5}, false}},
                                   true};

    if (edge <= 0)
        throw std::runtime_error("Octahedron: invalid parameter: edge<=0");
    const double h = edge / sqrt(2);
    const double a = edge / 2;

    const std::vector<R3> vertices{{0, 0, -h}, {a, -a, 0},  {a, a, 0},
                                   {-a, a, 0}, {-a, -a, 0}, {0, 0, h}};

    return new ff::Polyhedron(topology, vertices);
}

ff::Polyhedron* Dodecahedron(double edge)
{
    static const Topology topology{{// bottom:
                                    {{0, 4, 3, 2, 1}, false},
                                    // lower ring:
                                    {{0, 5, 12, 9, 4}, false},
                                    {{4, 9, 11, 8, 3}, false},
                                    {{3, 8, 10, 7, 2}, false},
                                    {{2, 7, 14, 6, 1}, false},
                                    {{1, 6, 13, 5, 0}, false},
                                    // upper ring:
                                    {{8, 11, 16, 15, 10}, false},
                                    {{9, 12, 17, 16, 11}, false},
                                    {{5, 13, 18, 17, 12}, false},
                                    {{6, 14, 19, 18, 13}, false},
                                    {{7, 10, 15, 19, 14}, false},
                                    // top:
                                    {{15, 16, 17, 18, 19}, false}},
                                   true};

    if (edge <= 0)
        throw std::runtime_error("Dodecahedron: invalid parameter: edge<=0");
    const double r1 = 0.2628655560595668 * edge; // sqrt((5-sqrt(5)/40)
    const double r2 = 0.42532540417602 * edge;   // r1*phi
    const double r3 = 0.5 * edge;
    const double r4 = 0.6881909602355868 * edge; // r2*phi
    const double r5 = 0.8090169943749473 * edge; // r3*phi
    const double r6 = 0.8506508083520399 * edge; // r1 * 2 * phi
    const double r7 = 1.113516364411607 * edge;  // r4*phi
    const double r8 = 1.309016994374947 * edge;  // r5*phi
    const double r9 = 1.376381920471174 * edge;  // r6*phi

    const std::vector<R3> vertices(
        {{r6, 0, -r7}, {r1, r5, -r7},  {-r4, r3, -r7}, {-r4, -r3, -r7}, {r1, -r5, -r7},
         {r9, 0, -r1}, {r2, r8, -r1},  {-r7, r5, -r1}, {-r7, -r5, -r1}, {r2, -r8, -r1},
         {-r9, 0, r1}, {-r2, -r8, r1}, {r7, -r5, r1},  {r7, r5, r1},    {-r2, r8, r1},
         {-r6, 0, r7}, {-r1, -r5, r7}, {r4, -r3, r7},  {r4, r3, r7},    {-r1, r5, r7}});

    return new ff::Polyhedron(topology, vertices);
}

ff::Polyhedron* Icosahedron(double edge)
{
    static const Topology topology{{// bottom:
                                    {{0, 2, 1}, false},
                                    // 1st row:
                                    {{0, 5, 2}, false},
                                    {{2, 3, 1}, false},
                                    {{1, 4, 0}, false},
                                    // 2nd row:
                                    {{0, 6, 5}, false},
                                    {{2, 5, 8}, false},
                                    {{2, 8, 3}, false},
                                    {{1, 3, 7}, false},
                                    {{1, 7, 4}, false},
                                    {{0, 4, 6}, false},
                                    // 3rd row:
                                    {{3, 8, 9}, false},
                                    {{5, 11, 8}, false},
                                    {{5, 6, 11}, false},
                                    {{4, 10, 6}, false},
                                    {{4, 7, 10}, false},
                                    {{3, 9, 7}, false},
                                    // 4th row:
                                    {{8, 11, 9}, false},
                                    {{6, 10, 11}, false},
                                    {{7, 9, 10}, false},
                                    // top:
                                    {{9, 11, 10}, false}},
                                   true};

    if (edge <= 0)
        throw std::runtime_error("Icosahedron: invalid parameter: edge<=0");
    const double s1 = 0.1784110448865449 * edge; // 1/sqrt(6)/sqrt(3+sqrt(5))
    const double s2 = 0.288675134594813 * edge;  // s1 * phi
    const double s3 = 0.467086179481358 * edge;  // s2 * phi
    const double s4 = 0.5 * edge;
    const double s5 = 0.5773502691896258 * edge; // 2 * s2
    const double s6 = 0.7557613140761708 * edge; // s3 * phi
    const double s7 = 0.8090169943749473 * edge; // phi/2
    const double s8 = 0.9341723589627158 * edge; // s5 * phi

    const std::vector<R3> vertices{{s5, 0, -s6},   {-s2, s4, -s6}, {-s2, -s4, -s6}, {-s8, 0, -s1},
                                   {s3, s7, -s1},  {s3, -s7, -s1}, {s8, 0, s1},     {-s3, s7, s1},
                                   {-s3, -s7, s1}, {-s5, 0, s6},   {s2, s4, s6},    {s2, -s4, s6}};

    return new ff::Polyhedron(topology, vertices);
}

//  ************************************************************************************************
//  Pyramids
//  ************************************************************************************************

ff::Polyhedron* Pyramid2(double length, double width, double height, double alpha)
{
    static const ff::Topology topology = {{{{3, 2, 1, 0}, true},
                                           {{0, 1, 5, 4}, false},
                                           {{1, 2, 6, 5}, false},
                                           {{2, 3, 7, 6}, false},
                                           {{3, 0, 4, 7}, false},
                                           {{4, 5, 6, 7}, true}},
                                          false};

    if (length <= 0)
        throw std::runtime_error("Pyramid2: invalid parameter: length<=0");
    if (width <= 0)
        throw std::runtime_error("Pyramid2: invalid parameter: width<=0");
    if (height <= 0)
        throw std::runtime_error("Pyramid2: invalid parameter: height<=0");
    if (alpha <= 0)
        throw std::runtime_error("Pyramid2: invalid parameter: alpha<=0");
    if (alpha > pi_2)
        throw std::runtime_error("Pyramid2: invalid parameter: alpha>pi/2");

    const double D = length / 2;
    const double W = width / 2;
    const double R = height * tan(pi_2 - alpha);
    if (R > D)
        throw std::runtime_error("Pyramid2: invalid outcome: R > D");
    if (R > W)
        throw std::runtime_error("Pyramid2: invalid outcome: R > W");

    // center of mass
    double zcom = height / 2 * (6 * W * D - 4 * (W + D) * R + 3 * R * R)
                  / (6 * W * D - 3 * (W + D) * R + 2 * R * R);
    const double d = D - R;
    const double w = W - R;

    const std::vector<R3> vertices{// base:
                                   {-D, -W, -zcom},
                                   {D, -W, -zcom},
                                   {D, W, -zcom},
                                   {-D, W, -zcom},
                                   // top:
                                   {-d, -w, height - zcom},
                                   {d, -w, height - zcom},
                                   {d, w, height - zcom},
                                   {-d, w, height - zcom}};

    return new ff::Polyhedron(topology, vertices);
}

ff::Polyhedron* Pyramid3(double base_edge, double height, double alpha)
{
    static const ff::Topology topology = {{{{2, 1, 0}, false},
                                           {{0, 1, 4, 3}, false},
                                           {{1, 2, 5, 4}, false},
                                           {{2, 0, 3, 5}, false},
                                           {{3, 4, 5}, false}},
                                          false};

    if (base_edge <= 0)
        throw std::runtime_error("Pyramid3: invalid parameter: base_edge<=0");
    if (height <= 0)
        throw std::runtime_error("Pyramid3: invalid parameter: height<=0");
    if (alpha <= 0)
        throw std::runtime_error("Pyramid3: invalid parameter: alpha<=0");
    if (alpha > pi_2)
        throw std::runtime_error("Pyramid3: invalid parameter: alpha>pi/2");

    const double cot_alpha = tan(pi_2 - alpha);
    const double a = base_edge;
    const double b = base_edge - height * cot_alpha * 2 * sqrt(3.);
    if (b < 0)
        throw std::runtime_error("Pyramid3: invalid outcome: top_edge < 0");

    const double as = a / 2;
    const double ac = a * (1. / sqrt(3) / 2);
    const double ah = a * (1. / sqrt(3));
    const double bs = b / 2;
    const double bc = b * (1. / sqrt(3) / 2);
    const double bh = b * (1. / sqrt(3));

    // center of mass
    const double zcom =
        height * (1. / 4) * (a * a + 2 * a * b + 3 * b * b) / (a * a + a * b + b * b);

    const std::vector<R3> vertices{// base:
                                   {-ac, as, -zcom},
                                   {-ac, -as, -zcom},
                                   {ah, 0., -zcom},
                                   // top:
                                   {-bc, bs, height - zcom},
                                   {-bc, -bs, height - zcom},
                                   {bh, 0., height - zcom}};

    return new ff::Polyhedron(topology, vertices);
}

ff::Polyhedron* Pyramid4(double base_edge, double height, double alpha)
{
    static const ff::Topology topology = {{{{3, 2, 1, 0}, true},
                                           {{0, 1, 5, 4}, false},
                                           {{1, 2, 6, 5}, false},
                                           {{2, 3, 7, 6}, false},
                                           {{3, 0, 4, 7}, false},
                                           {{4, 5, 6, 7}, true}},
                                          false};

    if (base_edge <= 0)
        throw std::runtime_error("Pyramid4: invalid parameter: base_edge<=0");
    if (height <= 0)
        throw std::runtime_error("Pyramid4: invalid parameter: height<=0");
    if (alpha <= 0)
        throw std::runtime_error("Pyramid4: invalid parameter: alpha<=0");
    if (alpha > pi_2)
        throw std::runtime_error("Pyramid4: invalid parameter: alpha>pi/2");

    const double cot_alpha = tan(pi_2 - alpha);
    double a = base_edge / 2;
    double b = a - height * cot_alpha;
    if (b < 0)
        throw std::runtime_error("Pyramid4: invalid outcome: top_edge < 0");

    // center of mass
    double zcom = height / 4 * (a * a + 2 * a * b + 3 * b * b) / (a * a + a * b + b * b);

    const std::vector<R3> vertices{// base:
                                   {-a, -a, -zcom},
                                   {a, -a, -zcom},
                                   {a, a, -zcom},
                                   {-a, a, -zcom},
                                   // top:
                                   {-b, -b, height - zcom},
                                   {b, -b, height - zcom},
                                   {b, b, height - zcom},
                                   {-b, b, height - zcom}};

    return new ff::Polyhedron(topology, vertices);
}

ff::Polyhedron* Pyramid6(double base_edge, double height, double alpha)
{
    static const ff::Topology topology = {{{{5, 4, 3, 2, 1, 0}, true},
                                           {{0, 1, 7, 6}, false},
                                           {{1, 2, 8, 7}, false},
                                           {{2, 3, 9, 8}, false},
                                           {{3, 4, 10, 9}, false},
                                           {{4, 5, 11, 10}, false},
                                           {{5, 0, 6, 11}, false},
                                           {{6, 7, 8, 9, 10, 11}, true}},
                                          false};

    if (base_edge <= 0)
        throw std::runtime_error("Pyramid6: invalid parameter: base_edge<=0");
    if (height <= 0)
        throw std::runtime_error("Pyramid6: invalid parameter: height<=0");
    if (alpha <= 0)
        throw std::runtime_error("Pyramid6: invalid parameter: alpha<=0");
    if (alpha > pi_2)
        throw std::runtime_error("Pyramid6: invalid parameter: alpha>pi/2");

    const double cot_alpha = tan(pi_2 - alpha);
    const double a = base_edge;
    const double b = a - 2 / sqrt(3) * cot_alpha * height;
    if (b < 0)
        throw std::runtime_error("Pyramid6: invalid outcome: top_edge < 0");
    const double as = a / 2;
    const double ac = a * sqrt(3) / 2;
    const double bs = b / 2;
    const double bc = b * sqrt(3) / 2;

    // center of mass
    double zcom = height / 4 * (a * a + 2 * a * b + 3 * b * b) / (a * a + a * b + b * b);

    const std::vector<R3> vertices{// base:
                                   {a, 0., -zcom},
                                   {as, ac, -zcom},
                                   {-as, ac, -zcom},
                                   {-a, 0., -zcom},
                                   {-as, -ac, -zcom},
                                   {as, -ac, -zcom},
                                   // top:
                                   {b, 0., height - zcom},
                                   {bs, bc, height - zcom},
                                   {-bs, bc, height - zcom},
                                   {-b, 0., height - zcom},
                                   {-bs, -bc, height - zcom},
                                   {bs, -bc, height - zcom}};

    return new ff::Polyhedron(topology, vertices);
}

ff::Polyhedron* Bipyramid4(double length, double height, double height_ratio, double alpha)
{
    static const ff::Topology topology = {{{{3, 2, 1, 0}, true},
                                           {{0, 1, 5, 4}, false},
                                           {{1, 2, 6, 5}, false},
                                           {{2, 3, 7, 6}, false},
                                           {{3, 0, 4, 7}, false},
                                           {{4, 5, 9, 8}, false},
                                           {{5, 6, 10, 9}, false},
                                           {{6, 7, 11, 10}, false},
                                           {{7, 4, 8, 11}, false},
                                           {{8, 9, 10, 11}, true}},
                                          false};

    if (length <= 0)
        throw std::runtime_error("Bipyramid4: invalid parameter: length<=0");
    if (height <= 0)
        throw std::runtime_error("Bipyramid4: invalid parameter: height<=0");
    if (height_ratio < 0)
        throw std::runtime_error("Bipyramid4: invalid parameter: height_ratio<0");
    if (alpha <= 0)
        throw std::runtime_error("Bipyramid4: invalid parameter: alpha<=0");
    if (alpha > pi_2)
        throw std::runtime_error("Bipyramid4: invalid parameter: alpha>pi/2");

    const double cot_alpha = tan(pi_2 - alpha);
    const double x = height_ratio;
    const double r = cot_alpha * 2 * height / length;
    if (std::max(1., x) * r > 1)
        throw std::runtime_error(
            "Bipyramid4: "
            "parameters violate condition 2*height <= length*tan(alpha)*min(1,1/height_ratio)");

    const double a = length / 2 * (1 - r);
    const double b = length / 2;
    const double c = length / 2 * (1 - r * x);

    const double dzcom =
        height * ((x * x - 1) / 2 - 2 * r * (x * x * x - 1) / 3 + r * r * (x * x * x * x - 1) / 4)
        / ((x + 1) - r * (x * x + 1) + r * r * (x * x * x + 1) / 3);
    const double za = -dzcom - height;
    const double zb = -dzcom;
    const double zc = -dzcom + x * height;

    const std::vector<R3> vertices{// base:
                                   {-a, -a, za},
                                   {a, -a, za},
                                   {a, a, za},
                                   {-a, a, za},
                                   // middle
                                   {-b, -b, zb},
                                   {b, -b, zb},
                                   {b, b, zb},
                                   {-b, b, zb},
                                   // top
                                   {-c, -c, zc},
                                   {c, -c, zc},
                                   {c, c, zc},
                                   {-c, c, zc}};

    return new ff::Polyhedron(topology, vertices);
}

//  ************************************************************************************************
//  Others
//  ************************************************************************************************

ff::Polyhedron* CantellatedCube(double length, double removed_length)
{
    static const ff::Topology topology = {{
                                              /*  0 */ {{0, 1, 2, 3}, true},
                                              /*  1 */ {{0, 8, 5, 1}, true},
                                              /*  2 */ {{1, 9, 6, 2}, true},
                                              /*  3 */ {{2, 10, 7, 3}, true},
                                              /*  4 */ {{3, 11, 4, 0}, true},
                                              /*  5 */ {{0, 4, 8}, false},
                                              /*  6 */ {{1, 5, 9}, false},
                                              /*  7 */ {{2, 6, 10}, false},
                                              /*  8 */ {{3, 7, 11}, false},
                                              /*  9 */ {{4, 12, 16, 8}, true},
                                              /* 10 */ {{5, 13, 17, 9}, true},
                                              /* 11 */ {{4, 11, 19, 12}, true},
                                              /* 12 */ {{5, 8, 16, 13}, true},
                                              /* 13 */ {{7, 10, 18, 15}, true},
                                              /* 14 */ {{6, 9, 17, 14}, true},
                                              /* 15 */ {{7, 15, 19, 11}, true},
                                              /* 16 */ {{6, 14, 18, 10}, true},
                                              /* 17 */ {{13, 21, 17}, false},
                                              /* 18 */ {{12, 20, 16}, false},
                                              /* 19 */ {{15, 23, 19}, false},
                                              /* 20 */ {{14, 22, 18}, false},
                                              /* 21 */ {{14, 17, 21, 22}, true},
                                              /* 22 */ {{13, 16, 20, 21}, true},
                                              /* 23 */ {{12, 19, 23, 20}, true},
                                              /* 24 */ {{15, 18, 22, 23}, true},
                                              /* 25 */ {{20, 23, 22, 21}, true},
                                          },
                                          true};

    if (length <= 0)
        throw std::runtime_error("CantellatedCube: invalid parameter: length<=0");
    if (removed_length < 0)
        throw std::runtime_error("CantellatedCube: invalid parameter: removed_length<0");
    if (removed_length > length / 2)
        throw std::runtime_error("CantellatedCube: invalid parameter: removed_length<length/2");

    const double a = length / 2;
    const double c = a - removed_length;
    if (c < 0)
        throw std::runtime_error("CantellatedCube: invalid outcome: c<0");

    const std::vector<R3> vertices{
        {-c, -c, +a},                                           // point 0
        {+c, -c, +a}, {+c, +c, +a}, {-c, +c, +a}, {-a, -c, +c}, // point 4
        {+c, -a, +c}, {+a, +c, +c}, {-c, +a, +c}, {-c, -a, +c}, // point 8
        {+a, -c, +c}, {+c, +a, +c}, {-a, +c, +c}, {-a, -c, -c}, // point 12
        {+c, -a, -c}, {+a, +c, -c}, {-c, +a, -c}, {-c, -a, -c}, // point 16
        {+a, -c, -c}, {+c, +a, -c}, {-a, +c, -c}, {-c, -c, -a}, // point 20
        {+c, -c, -a}, {+c, +c, -a}, {-c, +c, -a}};

    return new ff::Polyhedron(topology, vertices);
}

ff::Polyhedron* TruncatedCube(double length, double removed_length)
{
    static const ff::Topology topology = {{{{0, 1, 7, 6, 9, 10, 4, 3}, true},
                                           {{0, 2, 1}, false},
                                           {{3, 4, 5}, false},
                                           {{9, 11, 10}, false},
                                           {{6, 7, 8}, false},
                                           {{0, 3, 5, 17, 15, 12, 14, 2}, true},
                                           {{4, 10, 11, 23, 22, 16, 17, 5}, true},
                                           {{1, 2, 14, 13, 19, 20, 8, 7}, true},
                                           {{6, 8, 20, 18, 21, 23, 11, 9}, true},
                                           {{15, 17, 16}, false},
                                           {{12, 13, 14}, false},
                                           {{18, 20, 19}, false},
                                           {{21, 22, 23}, false},
                                           {{12, 15, 16, 22, 21, 18, 19, 13}, true}},
                                          true};

    if (length <= 0)
        throw std::runtime_error("TruncatedCube: invalid parameter: length<=0");
    if (removed_length < 0)
        throw std::runtime_error("TruncatedCube: invalid parameter: removed_length<0");
    if (removed_length > length / 2)
        throw std::runtime_error("TruncatedCube: invalid parameter: removed_length<length/2");

    const double a = length / 2;
    const double c = a - removed_length;
    if (c < 0)
        throw std::runtime_error("TruncatedCube: invalid outcome: c<0");

    const std::vector<R3> vertices{
        {-c, -a, -a}, {-a, -c, -a}, {-a, -a, -c}, {c, -a, -a}, {a, -c, -a}, {a, -a, -c},
        {-c, a, -a},  {-a, c, -a},  {-a, a, -c},  {c, a, -a},  {a, c, -a},  {a, a, -c},
        {-c, -a, a},  {-a, -c, a},  {-a, -a, c},  {c, -a, a},  {a, -c, a},  {a, -a, c},
        {-c, a, a},   {-a, c, a},   {-a, a, c},   {c, a, a},   {a, c, a},   {a, a, c}};

    return new ff::Polyhedron(topology, vertices);
}

} // namespace ff::make
