#pragma once

#include <cmath>
#include <cstring>
#include <limits>
#include <utility>
#include <vector>

extern "C" {
#include <lauxlib.h>
#include <lua.h>
}

namespace wherigo {

// ── Unit conversion ───────────────────────────────────────────────────────────

inline double distanceToMeters(double value, const char *units) {
    if (!units || !strcmp(units,"meters") || !strcmp(units,"metres") || !strcmp(units,"m"))
        return value;
    if (!strcmp(units,"kilometers") || !strcmp(units,"kilometres") || !strcmp(units,"km"))
        return value * 1000.0;
    if (!strcmp(units,"feet") || !strcmp(units,"ft"))
        return value * 0.3048;
    if (!strcmp(units,"miles") || !strcmp(units,"mi"))
        return value * 1609.344;
    if (!strcmp(units,"nauticalmiles"))
        return value * 1852.0;
    return value;
}

inline double metersToUnit(double meters, const char *units) {
    if (!units || !strcmp(units,"meters") || !strcmp(units,"metres") || !strcmp(units,"m"))
        return meters;
    if (!strcmp(units,"kilometers") || !strcmp(units,"kilometres") || !strcmp(units,"km"))
        return meters / 1000.0;
    if (!strcmp(units,"feet") || !strcmp(units,"ft"))
        return meters / 0.3048;
    if (!strcmp(units,"miles") || !strcmp(units,"mi"))
        return meters / 1609.344;
    if (!strcmp(units,"nauticalmiles"))
        return meters / 1852.0;
    return meters;
}

// ── Geodesic calculations ─────────────────────────────────────────────────────

// Haversine formula → {distance_meters, bearing_degrees [0,360)}
inline std::pair<double,double> haversine(double lat1, double lon1,
                                          double lat2, double lon2) {
    constexpr double R = 6371000.0;
    constexpr double D2R = M_PI / 180.0;
    const double rl1 = lat1*D2R, rl2 = lat2*D2R;
    const double dl  = (lat2-lat1)*D2R, dl2 = (lon2-lon1)*D2R;
    const double a = std::sin(dl/2)*std::sin(dl/2) +
                     std::cos(rl1)*std::cos(rl2)*std::sin(dl2/2)*std::sin(dl2/2);
    const double dist = R * 2.0 * std::asin(std::min(1.0, std::sqrt(a)));
    const double brg = std::atan2(std::sin(dl2)*std::cos(rl2),
                                  std::cos(rl1)*std::sin(rl2) -
                                  std::sin(rl1)*std::cos(rl2)*std::cos(dl2));
    return {dist, std::fmod(brg*(180.0/M_PI)+360.0, 360.0)};
}

// Destination point given start, distance (m), bearing (deg)
inline std::pair<double,double> translatePoint(double lat, double lon,
                                               double distMeters, double bearingDeg) {
    constexpr double R = 6371000.0;
    constexpr double D2R = M_PI / 180.0;
    constexpr double R2D = 180.0 / M_PI;
    const double d  = distMeters / R;
    const double b  = bearingDeg * D2R;
    const double r1 = lat * D2R;
    const double r2 = std::asin(std::sin(r1)*std::cos(d) +
                                std::cos(r1)*std::sin(d)*std::cos(b));
    const double dl = std::atan2(std::sin(b)*std::sin(d)*std::cos(r1),
                                 std::cos(d) - std::sin(r1)*std::sin(r2));
    return {r2*R2D, lon + dl*R2D};
}

// Ray-casting point-in-polygon. pts = {lat, lon} pairs.
inline bool pointInPolygon(double playerLat, double playerLon,
                           const std::vector<std::pair<double,double>>& pts) {
    const int n = static_cast<int>(pts.size());
    if (n < 3) return false;
    bool inside = false;
    for (int i = 0, j = n-1; i < n; j = i++) {
        const double lati = pts[i].first, loni = pts[i].second;
        const double latj = pts[j].first, lonj = pts[j].second;
        if (((lati > playerLat) != (latj > playerLat)) &&
            (playerLon < (lonj-loni)*(playerLat-lati)/(latj-lati) + loni))
            inside = !inside;
    }
    return inside;
}

// Distance (m) from point to nearest point on segment; optionally returns that point
inline double distToSegment(double plat, double plon,
                            double lat1, double lon1,
                            double lat2, double lon2,
                            double *nearLatOut = nullptr,
                            double *nearLonOut = nullptr) {
    const double dlat = lat2-lat1, dlon = lon2-lon1;
    const double len2 = dlat*dlat + dlon*dlon;
    const double t = (len2 > 1e-12)
        ? std::max(0.0, std::min(1.0,
              ((plat-lat1)*dlat + (plon-lon1)*dlon) / len2))
        : 0.0;
    const double nLat = lat1 + t*dlat, nLon = lon1 + t*dlon;
    if (nearLatOut) *nearLatOut = nLat;
    if (nearLonOut) *nearLonOut = nLon;
    return haversine(plat, plon, nLat, nLon).first;
}

// Nearest edge of zone polygon → {distance_m, bearing_deg}
inline std::pair<double,double> nearestZoneEdge(
    double plat, double plon,
    const std::vector<std::pair<double,double>>& pts) {
    double minDist = std::numeric_limits<double>::max(), minBrg = 0.0;
    const int n = static_cast<int>(pts.size());
    for (int i = 0, j = n-1; i < n; j = i++) {
        double nLat, nLon;
        const double d = distToSegment(plat, plon,
            pts[j].first, pts[j].second, pts[i].first, pts[i].second,
            &nLat, &nLon);
        if (d < minDist) {
            minDist = d;
            minBrg = haversine(plat, plon, nLat, nLon).second;
        }
    }
    return {minDist, minBrg};
}

// ── Lua table helpers ─────────────────────────────────────────────────────────

// Push a Distance object (value in meters) with __call metatable
inline void pushDistance(lua_State *L, double meters) {
    lua_newtable(L);
    lua_pushnumber(L, meters);
    lua_setfield(L, -2, "value");
    lua_pushstring(L, "Distance");
    lua_setfield(L, -2, "_classname");
    luaL_getmetatable(L, "Wherigo.Distance");
    if (lua_istable(L, -1))
        lua_setmetatable(L, -2);
    else
        lua_pop(L, 1);
}

// Push a Bearing object (value in degrees [0,360)) with __call metatable
inline void pushBearing(lua_State *L, double degrees) {
    lua_newtable(L);
    lua_pushnumber(L, std::fmod(degrees+360.0, 360.0));
    lua_setfield(L, -2, "value");
    lua_pushstring(L, "Bearing");
    lua_setfield(L, -2, "_classname");
    luaL_getmetatable(L, "Wherigo.Bearing");
    if (lua_istable(L, -1))
        lua_setmetatable(L, -2);
    else
        lua_pop(L, 1);
}

// Read zone Points array from Lua table at absolute stack index
inline std::vector<std::pair<double,double>> readZonePoints(lua_State *L, int zoneIdx) {
    std::vector<std::pair<double,double>> pts;
    lua_getfield(L, zoneIdx, "Points");
    if (!lua_istable(L, -1)) { lua_pop(L, 1); return pts; }
    const int n = static_cast<int>(lua_objlen(L, -1));
    pts.reserve(n);
    for (int i = 1; i <= n; ++i) {
        lua_rawgeti(L, -1, i);
        if (lua_istable(L, -1)) {
            lua_getfield(L, -1, "latitude");  double lat = lua_tonumber(L,-1); lua_pop(L,1);
            lua_getfield(L, -1, "longitude"); double lon = lua_tonumber(L,-1); lua_pop(L,1);
            pts.push_back({lat, lon});
        }
        lua_pop(L, 1);
    }
    lua_pop(L, 1);
    return pts;
}

} // namespace wherigo
