#include "iterators.hpp"

using namespace world::iterator;

std::unique_ptr<AbstractFill> world::iterator::Get(world::action::Shape shape, uint16_t radius) {
    switch (shape) {
    case world::action::Shape::Cube:
        return std::make_unique<Cube>(radius);
    case world::action::Shape::RawSphere:
        return std::make_unique<RawSphere>(radius);
    case world::action::Shape::SmoothSphere:
        return std::make_unique<SmoothSphere>(radius);
    default:
        return std::unique_ptr<AbstractFill>(nullptr);
    }
}
std::unique_ptr<AbstractBorder> world::iterator::GetBorder(world::action::Shape shape, uint16_t radius) {
    switch (shape) {
    case world::action::Shape::Cube:
        return std::make_unique<CubeBorder>(radius);
    case world::action::Shape::RawSphere:
    case world::action::Shape::SmoothSphere:
        return std::make_unique<SphereBorder>(radius);
    default:
        return std::unique_ptr<AbstractBorder>(nullptr);
    }
}

bool Cube::next(pair& out) {
    if (pos.z > radius)
        return false;

    out.first = pos;
    out.second = 1;

    // MAYBE: use linear idx to make branch less
    if (pos.x < radius) {
        pos.x++;
    } else {
        pos.x = -radius;
        if (pos.y < radius) {
            pos.y++;
        } else {
            pos.y = -radius;
            pos.z++;
        }
    }
    return true;
}
bool CubeBorder::next(glm::ivec3& out) {
    if (face > 5)
        return false;

    out = pos;

    const auto getAxis = [&](glm::ivec3 &in, int offset) -> int& {
        const auto v = (face / 2 + offset) % 3;
        return v == 0 ? in.x : (v == 1 ? in.y : in.z);
    };
    const auto r0_max = radius - (face > 1);
    if (auto& r0 = getAxis(pos, 0); r0 < r0_max) {
        r0++;
    } else {
        r0 = -r0_max;
        const auto r1_max = radius - (face > 3);
        if (auto &r1 = getAxis(pos, 1); r1 < r1_max) {
            r1++;
        } else {
            r1 = -r1_max;
            getAxis(pos, 2) = -radius;
            face++;
            getAxis(pos, 2) = (face % 2 == 0) ? -radius : radius;
        }
    }
    return true;
}

bool RawSphere::next(pair& out) {
    const auto delta = [](int radius, int pos) -> int {
        const auto rad = radius + .5;
        return floor(sqrt(rad * rad - pos * pos));
    };

    if (pos.z < dz) {
        pos.z++;
    } else {
        if (pos.y < dy) {
            pos.y++;
        } else {
            if (pos.x < radius) {
                pos.x++;
                dy = delta(radius, pos.x);
                pos.y = -dy;
            } else {
                return false;
            }
        }
        dz = delta(dy, pos.y);
        pos.z = -dz;
    }

    out.first = pos;
    out.second = 1;
    return true;
}

bool SmoothSphere::next(pair& out) {
    const auto delta = [](double radius, int pos) {
        return sqrt(radius * radius - pos * pos);
    };

    if (pos.z < dz) {
        const int idz = floor(dz);
        out.second = pos.z < idz ? 1 : dz - idz;
        pos.z++;
    } else {
        if (pos.y < dy) {
            pos.y++;
        } else {
            if (pos.x < radius) {
                pos.x++;
                dy = delta(radius, pos.x);
                pos.y = -ceil(dy);
            } else {
                return false;
            }
        }
        dz = delta(dy, pos.y);
        out.second = dz - floor(dz);
        pos.z = -ceil(dz);
    }

    out.first = pos;
    return true;
}

constexpr float PI = 3.14159;
bool SphereBorder::next(glm::ivec3& out) {
    if (i >= max_i)
        return false;

    const float si = sin(PI * i / (float)max_i);
    const float sj = sin(2 * PI * j / (float)max_j);
    const float ci = cos(PI * i / (float)max_i);
    const float cj = cos(2 * PI * j / (float)max_j);
    out = glm::vec3(si * cj, si * sj, ci) * (radius + .5f);

    if (j < max_j - 2) {
        j++;
    } else {
        j = 0;
        i++;
    }
    return true;
}