//
// Created by Dan Crosby on 4/14/21.
//

#include "HexCubeUtils.hpp"

#include "src/main/cpp/net/eagle0/shardok/library/map/Coordinates.hpp"
#include "src/main/flatbuffer/net/eagle0/shardok/storage/coords.hpp"

namespace shardok {

auto OffsetToCube(const Coords &c) -> Cube {
    Cube cube{};

    cube.x = c.column() - (c.row() - (c.row() & 1)) / 2;
    cube.z = c.row();
    cube.y = -cube.x - cube.z;

    return cube;
}

auto CubeToOffset(const Cube &cube) -> Coords {
    const int row = cube.z;
    const int column = cube.x + (row - (row & 1)) / 2;

    return Coords(row, column);
}

auto CubesWithinDistance(const Cube &c1, const int distance) -> vector<Cube> {
    vector<Cube> cubes{};

    for (int x = -distance; x <= distance; x++) {
        for (int y = std::max(-distance, -x - distance); y <= std::min(distance, -x + distance);
             y++) {
            const int z = -x - y;
            cubes.emplace_back(c1.x + x, c1.y + y, c1.z + z);
        }
    }

    return cubes;
}

auto CubesWithExactDistance(const Cube &c1, const int distance) -> vector<Cube> {
    vector<Cube> cubes{};

    for (int x = -distance; x <= distance; x++) {
        for (int y = std::max(-distance, -x - distance); y <= std::min(distance, -x + distance);
             y++) {
            const int z = -x - y;
            if (abs(x) != distance && abs(y) != distance && abs(z) != distance) continue;
            cubes.emplace_back(c1.x + x, c1.y + y, c1.z + z);
        }
    }

    return cubes;
}

auto FixupCube(
        double newX,
        const double xDiff,
        double newY,
        const double yDiff,
        double newZ,
        const double zDiff) -> Cube {
    if (xDiff > yDiff && xDiff > zDiff) {
        newX = -newY - newZ;
    } else if (yDiff > zDiff) {
        newY = -newX - newZ;
    } else {
        newZ = -newY - newX;
    }

    return Cube(int16_t(newX), int16_t(newY), int16_t(newZ));
}

// Always rounds half up, instead of away from zero.
auto RoundHalfUp(const double d) -> double { return std::floor(d + 0.5); }

auto CubeLerp(const Cube &c1, const Cube &c2, const double t) -> vector<Cube> {
    const auto floatX = std::lerp(c1.x, c2.x, t);
    const auto newX = RoundHalfUp(floatX);
    const auto xDiff = abs(newX - floatX);

    const auto floatY = std::lerp(c1.y, c2.y, t);
    const auto newY = RoundHalfUp(floatY);
    const auto yDiff = abs(newY - floatY);

    const auto floatZ = std::lerp(c1.z, c2.z, t);
    const auto newZ = RoundHalfUp(floatZ);
    const auto zDiff = abs(newZ - floatZ);

    vector<Cube> cubes{};
    cubes.push_back(FixupCube(newX, xDiff, newY, yDiff, newZ, zDiff));

    // The 0.5 case represents a situation where the line passes exactly between two hexes. In that
    // case, for our obstruction checking purposes, we want to include *both* hexes as being on the
    // line.
    if (xDiff == 0.5) {
        cubes.push_back(FixupCube(floatX - 0.5, xDiff, newY, yDiff, newZ, zDiff));
    } else if (yDiff == 0.5) {
        cubes.push_back(FixupCube(newX, xDiff, floatY - 0.5, yDiff, newZ, zDiff));
    }

    return cubes;
}

// Slight variation of the method described in
// https://www.redblobgames.com/grids/hexagons/#line-drawing
// When rounding, we include *both* hexes for the in-between case.
auto CubeLineInclusive(const Cube &c1, const Cube &c2) -> vector<Cube> {
    vector<Cube> cubes;

    const auto dist = CubeDistance(c1, c2);
    const double reciprocal = 1.0 / dist;

    for (int i = 0; i <= dist; i++) {
        for (const auto &c : CubeLerp(c1, c2, i * reciprocal)) { cubes.push_back(c); }
    }

    return cubes;
}

}  // namespace shardok